file reorganization

This commit is contained in:
AJ ONeal 2020-06-03 01:47:06 -06:00
parent 3502d67b06
commit 9b7c3f62db
20 changed files with 285 additions and 653 deletions

14
.gitignore vendored
View File

@ -16,16 +16,20 @@ telebit-client-macos
telebit-client-windows-debug.exe telebit-client-windows-debug.exe
telebit-client-windows.exe telebit-client-windows.exe
/mplexer/cmd/dnsclient/dnsclient /cmd/dnsclient/dnsclient
/mplexer/cmd/sqlstore/sqlstore /cmd/sqlstore/sqlstore
/mplexer/mgmt/cmd/mgmt/mgmt /cmd/mgmt/mgmt
/mplexer/cmd/signjwt/signjwt /cmd/signjwt/signjwt
/mplexer/cmd/telebit/telebit /cmd/telebit/telebit
/telebit /telebit
/cmd/telebit/telebit /cmd/telebit/telebit
/telebit-relay /telebit-relay
/telebit-relay-linux
/telebit-relay-macos
/cmd/telebit-relay/telebit-relay /cmd/telebit-relay/telebit-relay
/cmd/telebit-relay/telebit-relay-linux
/cmd/telebit-relay/telebit-relay-macos
.*.sw* .*.sw*
log.txt log.txt

View File

@ -50,10 +50,10 @@ pushd mplexy/
go generate ./... go generate ./...
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod vendor -o mgmt-server-linux ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod vendor -o mgmt-server-linux ./cmd/mgmt/*.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -mod vendor -o mgmt-server-macos ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -mod vendor -o mgmt-server-macos ./cmd/mgmt/*.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -o mgmt-server-windows-debug.exe ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -o mgmt-server-windows-debug.exe ./cmd/mgmt/*.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -ldflags "-H windowsgui" -o mgmt-server-windows.exe ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -ldflags "-H windowsgui" -o mgmt-server-windows.exe ./cmd/mgmt/*.go
``` ```
### Example ### Example

View File

@ -1,6 +1,6 @@
go generate ./... go generate ./...
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod vendor -o mgmt-server-linux ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -mod vendor -o mgmt-server-linux ./cmd/mgmt/*.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -mod vendor -o mgmt-server-macos ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -mod vendor -o mgmt-server-macos ./cmd/mgmt/*.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -o mgmt-server-windows-debug.exe ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -o mgmt-server-windows-debug.exe ./cmd/mgmt/*.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -ldflags "-H windowsgui" -o mgmt-server-windows.exe ./mgmt/cmd/mgmt/*.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -mod vendor -ldflags "-H windowsgui" -o mgmt-server-windows.exe ./cmd/mgmt/*.go

View File

@ -10,7 +10,7 @@ import (
"strings" "strings"
"time" "time"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore"
"github.com/go-chi/chi" "github.com/go-chi/chi"
) )

View File

@ -10,7 +10,7 @@ import (
"os" "os"
"strings" "strings"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore"
"github.com/go-acme/lego/v3/challenge" "github.com/go-acme/lego/v3/challenge"
"github.com/go-acme/lego/v3/providers/dns/duckdns" "github.com/go-acme/lego/v3/providers/dns/duckdns"

View File

@ -9,7 +9,7 @@ import (
"strings" "strings"
"time" "time"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore"
"github.com/dgrijalva/jwt-go" "github.com/dgrijalva/jwt-go"
"github.com/go-chi/chi" "github.com/go-chi/chi"
"github.com/go-chi/chi/middleware" "github.com/go-chi/chi/middleware"

View File

@ -6,7 +6,7 @@ import (
"fmt" "fmt"
"os" "os"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore"
"github.com/denisbrodbeck/machineid" "github.com/denisbrodbeck/machineid"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"

View File

@ -5,7 +5,7 @@ import (
"log" "log"
"strings" "strings"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore"
) )
func main() { func main() {

View File

@ -16,8 +16,8 @@ import (
"strings" "strings"
"git.coolaj86.com/coolaj86/go-telebitd/log" "git.coolaj86.com/coolaj86/go-telebitd/log"
telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01" "git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt"
"git.coolaj86.com/coolaj86/go-telebitd/relay" "git.coolaj86.com/coolaj86/go-telebitd/relay"
"git.coolaj86.com/coolaj86/go-telebitd/relay/api" "git.coolaj86.com/coolaj86/go-telebitd/relay/api"
"git.coolaj86.com/coolaj86/go-telebitd/relay/mplexy" "git.coolaj86.com/coolaj86/go-telebitd/relay/mplexy"
@ -257,7 +257,7 @@ func main() {
tokenString = r.URL.Query().Get("access_token") tokenString = r.URL.Query().Get("access_token")
} }
grants, err := mgmt.Inspect(authURL, tokenString) grants, err := telebit.Inspect(authURL, tokenString)
/* /*
tok, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { tok, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(secretKey), nil return []byte(secretKey), nil

View File

@ -1,356 +1,253 @@
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
package main package main
import ( import (
"context" "context"
"crypto/tls" "encoding/base64"
"encoding/hex"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"net"
"net/url"
"os" "os"
"regexp"
"strconv"
"strings" "strings"
"time"
"git.coolaj86.com/coolaj86/go-telebitd/client" "git.coolaj86.com/coolaj86/go-telebitd/mgmt"
"git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore"
telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01"
"github.com/caddyserver/certmagic" "github.com/caddyserver/certmagic"
"github.com/denisbrodbeck/machineid"
jwt "github.com/dgrijalva/jwt-go" jwt "github.com/dgrijalva/jwt-go"
"github.com/go-acme/lego/v3/challenge"
"github.com/go-acme/lego/v3/providers/dns/duckdns" "github.com/go-acme/lego/v3/providers/dns/duckdns"
"github.com/go-acme/lego/v3/providers/dns/godaddy"
_ "github.com/joho/godotenv/autoload" _ "github.com/joho/godotenv/autoload"
) )
var httpRegexp = regexp.MustCompile(`(?i)^http`) var (
var locals string // GitRev refers to the abbreviated commit hash
var domains string GitRev = "0000000"
var insecure bool // GitVersion refers to the most recent tag, plus any commits made since then
var relay string GitVersion = "v0.0.0-pre0+0000000"
var secret string // GitTimestamp refers to the timestamp of the most recent commit
var token string GitTimestamp = "0000-00-00T00:00:00+0000"
)
func init() { type Forward struct {
flag.StringVar(&locals, "locals", "", "comma separated list of <proto>:<port> or "+ scheme string
"<proto>:<hostname>:<port> to which matching incoming connections should forward. "+ pattern string
"Ex: smtps:8465,https:example.com:8443") port string
flag.StringVar(&domains, "domains", "", "comma separated list of domain names to set to the tunnel")
flag.BoolVar(&insecure, "insecure", false, "Allow TLS connections to telebit-relay without valid certs")
flag.BoolVar(&insecure, "k", false, "alias of --insecure")
flag.StringVar(&relay, "relay", "", "the domain (or ip address) at which the relay server is running")
flag.StringVar(&secret, "secret", "", "the same secret used by telebit-relay (used for JWT authentication)")
flag.StringVar(&token, "token", "", "a pre-generated token to give the server (instead of generating one with --secret)")
}
type proxy struct {
protocol string
hostname string
port int
}
func addLocals(proxies []proxy, location string) ([]proxy, error) {
parts := strings.Split(location, ":")
if len(parts) > 3 || "" == parts[0] {
return nil, fmt.Errorf("provided invalid --locals %q", location)
}
// Format can be any of
// <hostname> or <port> or <proto>:<port> or <proto>:<hostname>:<port>
n := len(parts)
i := n - 1
last := parts[i]
port, err := strconv.Atoi(last)
if nil != err {
// The last item is the hostname,
// which means it should be the only item
if n > 1 {
return nil, fmt.Errorf("provided invalid --locals %q", location)
}
// accepting all defaults
// If all that was provided as a "local" is the domain name we assume that domain
last = strings.ToLower(strings.Trim(last, "/"))
proxies = append(proxies, proxy{"http", last, 80})
proxies = append(proxies, proxy{"https", last, 443})
return proxies, nil
}
// the last item is the port, and it must be a valid port
if port <= 0 || port > 65535 {
return nil, fmt.Errorf("local port forward must be between 1 and 65535, not %d", port)
}
switch n {
case 1:
// <port>
proxies = append(proxies, proxy{"http", "*", port})
proxies = append(proxies, proxy{"https", "*", port})
case 2:
// <hostname>:<port>
// <scheme>:<port>
parts[0] = strings.ToLower(strings.Trim(parts[0], "/"))
if strings.Contains(parts[0], ".") {
hostname := parts[0]
proxies = append(proxies, proxy{"http", hostname, port})
proxies = append(proxies, proxy{"https", hostname, port})
} else {
scheme := parts[0]
proxies = append(proxies, proxy{scheme, "*", port})
}
case 3:
// <scheme>:<hostname>:<port>
scheme := strings.ToLower(strings.Trim(parts[0], "/"))
hostname := strings.ToLower(strings.Trim(parts[1], "/"))
proxies = append(proxies, proxy{scheme, hostname, port})
}
return proxies, nil
}
func addDomains(proxies []proxy, location string) ([]proxy, error) {
parts := strings.Split(location, ":")
if len(parts) > 3 || "" == parts[0] {
return nil, fmt.Errorf("provided invalid --domains %q", location)
}
// Format is limited to
// <hostname> or <proto>:<hostname>:<port>
err := fmt.Errorf("invalid argument for --domains, use format <domainname> or <scheme>:<domainname>:<local-port>")
switch len(parts) {
case 1:
// TODO test that it's a valid pattern for a domain
hostname := parts[0]
if !strings.Contains(hostname, ".") {
return nil, err
}
proxies = append(proxies, proxy{"http", hostname, 80})
proxies = append(proxies, proxy{"https", hostname, 443})
case 2:
return nil, err
case 3:
scheme := parts[0]
hostname := parts[1]
if "" == scheme {
return nil, err
}
if !strings.Contains(hostname, ".") {
return nil, err
}
port, _ := strconv.Atoi(parts[2])
if port <= 0 || port > 65535 {
return nil, err
}
proxies = append(proxies, proxy{scheme, hostname, port})
}
return proxies, nil
}
func extractServicePorts(proxies []proxy) client.RouteMap {
result := make(client.RouteMap, 2)
for _, p := range proxies {
if p.protocol != "" && p.port != 0 {
hostPorts := result[p.protocol]
if hostPorts == nil {
result[p.protocol] = make(map[client.DomainName]*client.TerminalConfig)
hostPorts = result[p.protocol]
}
// Only HTTP and HTTPS allow us to determine the hostname from the request, so only
// those protocols support different ports for the same service.
if !httpRegexp.MatchString(p.protocol) || p.hostname == "" {
p.hostname = "*"
}
if port, ok := hostPorts[p.hostname]; ok && port.Port != p.port {
panic(fmt.Sprintf("duplicate ports for %s://%s", p.protocol, p.hostname))
}
hostPorts[p.hostname] = &client.TerminalConfig{
Port: p.port,
}
}
}
// Make sure we have defaults for HTTPS and HTTP.
if result["https"] == nil {
result["https"] = make(map[client.DomainName]*client.TerminalConfig, 1)
}
if result["https"]["*"] == nil {
result["https"]["*"] = &client.TerminalConfig{}
}
if result["https"]["*"].Port == 0 {
result["https"]["*"].Port = 8443
}
if result["http"] == nil {
result["http"] = make(map[client.DomainName]*client.TerminalConfig, 1)
}
if result["http"]["*"] == nil {
result["http"]["*"] = &client.TerminalConfig{}
}
if result["http"]["*"].Port == 0 {
result["http"]["*"] = result["https"]["*"]
}
return result
} }
func main() { func main() {
var err error
var provider challenge.Provider = nil
var domains []string
var forwards []Forward
// TODO replace the websocket connection with a mock server
appID := flag.String("app-id", "telebit.io", "a unique identifier for a deploy target environment")
email := flag.String("acme-email", "", "email to use for Let's Encrypt / ACME registration")
certpath := flag.String("acme-storage", "./acme.d/", "path to ACME storage directory")
acmeAgree := flag.Bool("acme-agree", false, "agree to the terms of the ACME service provider (required)")
acmeStaging := flag.Bool("acme-staging", false, "get fake certificates for testing")
acmeDirectory := flag.String("acme-directory", "", "ACME Directory URL")
enableHTTP01 := flag.Bool("acme-http-01", false, "enable HTTP-01 ACME challenges")
enableTLSALPN01 := flag.Bool("acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges")
acmeRelay := flag.String("acme-relay", "", "the base url of the ACME DNS-01 relay, if not the same as the tunnel relay")
authURL := flag.String("auth-url", "", "the base url for authentication, if not the same as the tunnel relay")
relay := flag.String("relay", "", "the domain (or ip address) at which the relay server is running")
secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)")
token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)")
locals := flag.String("locals", "", "a list of <from-domain>:<to-port>")
flag.Parse() flag.Parse()
var err error if len(os.Args) >= 2 {
if "version" == os.Args[1] {
if "" == locals { fmt.Printf("telebit %s %s %s", GitVersion, GitRev[:7], GitTimestamp)
locals = os.Getenv("LOCALS") os.Exit(0)
}
proxies := make([]proxy, 0)
for _, option := range stringSlice(locals) {
for _, location := range strings.Split(option, ",") {
//fmt.Println("locals", location)
proxies, err = addLocals(proxies, location)
if nil != err {
panic(err)
}
}
}
//fmt.Println("proxies:")
//fmt.Printf("%+v\n\n", proxies)
for _, option := range stringSlice(domains) {
for _, location := range strings.Split(option, ",") {
proxies, err = addDomains(proxies, location)
if nil != err {
panic(err)
}
}
}
servicePorts := extractServicePorts(proxies)
domainMap := make(map[string]bool)
for _, p := range proxies {
if p.hostname != "" && p.hostname != "*" {
domainMap[p.hostname] = true
} }
} }
if relay == "" { if "" != *acmeDirectory {
relay = os.Getenv("RELAY") if *acmeStaging {
} fmt.Fprintf(os.Stderr, "pick either acme-directory or acme-staging\n")
if relay == "" {
fmt.Fprintf(os.Stderr, "must provide remote relay server to connect to\n")
os.Exit(1) os.Exit(1)
} }
}
if secret == "" { if *acmeStaging {
secret = os.Getenv("SECRET") *acmeDirectory = certmagic.LetsEncryptStagingCA
} }
if secret != "" { if "" == *locals {
domains := make([]string, 0, len(domainMap)) *locals = os.Getenv("LOCALS")
for name := range domainMap {
domains = append(domains, name)
} }
tokenData := jwt.MapClaims{"domains": domains} for _, cfg := range strings.Fields(strings.ReplaceAll(*locals, ",", " ")) {
parts := strings.Split(cfg, ":")
last := len(parts) - 1
port := parts[last]
domain := parts[last-1]
scheme := ""
if len(parts) > 2 {
scheme = parts[0]
}
forwards = append(forwards, Forward{
scheme: scheme,
pattern: domain,
port: port,
})
secret := []byte(secret) // don't load wildcard into jwt domains
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData) if "*" == domain {
if tokenStr, err := jwtToken.SignedString(secret); err != nil { continue
}
domains = append(domains, domain)
}
ppid, err := machineid.ProtectedID(fmt.Sprintf("%s|%s", *appID, *secret))
if nil != err {
fmt.Fprintf(os.Stderr, "unauthorized device")
os.Exit(1)
}
ppidBytes, err := hex.DecodeString(ppid)
ppid = base64.RawURLEncoding.EncodeToString(ppidBytes)
if "" == *token {
if "" == *secret {
*secret = os.Getenv("SECRET")
}
*token, err = authstore.HMACToken(ppid)
}
if nil != err {
fmt.Fprintf(os.Stderr, "neither secret nor token provided")
os.Exit(1)
return
}
if "" == *relay {
*relay = os.Getenv("RELAY") // "wss://example.com:443"
}
if "" == *relay {
fmt.Fprintf(os.Stderr, "Missing relay url")
os.Exit(1)
return
}
if "" == *acmeRelay {
*acmeRelay = strings.Replace(*relay, "ws", "http", 1) // "https://example.com:443"
}
if "" == *authURL {
*authURL = strings.Replace(*relay, "ws", "http", 1) // "https://example.com:443"
}
if "" != os.Getenv("GODADDY_API_KEY") {
id := os.Getenv("GODADDY_API_KEY")
secret := os.Getenv("GODADDY_API_SECRET")
if provider, err = newGoDaddyDNSProvider(id, secret); nil != err {
panic(err) panic(err)
}
} else if "" != os.Getenv("DUCKDNS_TOKEN") {
if provider, err = newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN")); nil != err {
panic(err)
}
} else { } else {
token = tokenStr endpoint := *acmeRelay
if strings.HasSuffix(endpoint, "/") {
endpoint = endpoint[:len(endpoint)-1]
}
//endpoint += "/api/dns/"
if provider, err = newAPIDNSProvider(endpoint, *token); nil != err {
panic(err)
} }
} else if token != "" {
fmt.Fprintf(os.Stderr, "must provide either token or secret\n")
os.Exit(1)
} }
ctx, quit := context.WithCancel(context.Background()) grants, err := telebit.Inspect(*authURL, *token)
defer quit()
acmeStorage := "./acme.d/"
acmeEmail := ""
acmeStaging := false
//
// CertMagic is Greenlock for Go
//
directory := certmagic.LetsEncryptProductionCA
if acmeStaging {
directory = certmagic.LetsEncryptStagingCA
}
magic, err := newCertMagic(directory, acmeEmail, &certmagic.FileStorage{Path: acmeStorage})
if nil != err { if nil != err {
fmt.Fprintf(os.Stderr, "failed to initialize certificate management (discovery url? local folder perms?): %s\n", err) _, err := mgmt.Register(*authURL, *secret, ppid)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to register client: %s", err)
os.Exit(1) os.Exit(1)
} }
grants, err = telebit.Inspect(*authURL, *token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to authenticate after registering client: %s", err)
os.Exit(1)
}
}
fmt.Println("grants", grants)
tlsConfig := &tls.Config{ acme := &telebit.ACME{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { Email: *email,
return magic.GetCertificate(hello) StoragePath: *certpath,
/* Agree: *acmeAgree,
if false { Directory: *acmeDirectory,
_, _ = magic.GetCertificate(hello)
}
// TODO
// 1. call out to greenlock for validation
// 2. push challenges through http channel
// 3. receive certificates (or don't)
certbundleT, err := tls.LoadX509KeyPair("certs/fullchain.pem", "certs/privkey.pem")
certbundle := &certbundleT
if err != nil {
return nil, err
}
return certbundle, nil
*/
},
}
config := client.Config{
Insecure: insecure,
Server: relay,
Services: servicePorts,
Token: token,
TLSConfig: tlsConfig,
}
fmt.Printf("config:\n%#v\n", config)
log.Fatal(client.Run(ctx, &config))
}
func newCertMagic(directory string, email string, storage certmagic.Storage) (*certmagic.Config, error) {
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
// do whatever you need to do to get the right
// configuration for this certificate; keep in
// mind that this config value is used as a
// template, and will be completed with any
// defaults that are set in the Default config
return &certmagic.Config{}, nil
},
})
provider, err := newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN"))
if err != nil {
return nil, err
}
magic := certmagic.New(cache, certmagic.Config{
Storage: storage,
OnDemand: &certmagic.OnDemandConfig{
DecisionFunc: func(name string) error {
return nil
},
},
})
// Ummm... just a little confusing
magic.Issuer = certmagic.NewACMEManager(magic, certmagic.ACMEManager{
DNSProvider: provider, DNSProvider: provider,
CA: directory, EnableHTTPChallenge: *enableHTTP01,
Email: email, EnableTLSALPNChallenge: *enableTLSALPN01,
Agreed: true, }
DisableHTTPChallenge: true,
DisableTLSALPNChallenge: true, mux := telebit.NewRouteMux()
// plus any other customizations you need mux.HandleTLS("*", acme, mux)
}) for _, fwd := range forwards {
return magic, nil mux.ForwardTCP("*", "localhost:"+fwd.port, 120*time.Second)
//mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second)
}
connected := make(chan net.Conn)
go func() {
timeoutCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
defer cancel()
tun, err := telebit.DialWebsocketTunnel(timeoutCtx, *relay, *token)
if nil != err {
msg := ""
if strings.Contains(err.Error(), "bad handshake") {
msg = " (may be auth related)"
}
fmt.Fprintf(os.Stderr, "Error connecting to %s: %s%s\n", *relay, err, msg)
os.Exit(1)
return
}
err = mgmt.Ping(*authURL, *token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s", err)
//os.Exit(1)
}
connected <- tun
}()
go func() {
for {
time.Sleep(10 * time.Minute)
err = mgmt.Ping(*authURL, *token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s", err)
//os.Exit(1)
}
}
}()
tun := <-connected
fmt.Printf("Listening at %s\n", *relay)
log.Fatal("Closed server: ", telebit.ListenAndServe(tun, mux))
}
type ACMEProvider struct {
BaseURL string
provider challenge.Provider
}
func (p *ACMEProvider) Present(domain, token, keyAuth string) error {
return p.provider.Present(domain, token, keyAuth)
}
func (p *ACMEProvider) CleanUp(domain, token, keyAuth string) error {
return p.provider.CleanUp(domain, token, keyAuth)
} }
// newDuckDNSProvider is for the sake of demoing the tunnel // newDuckDNSProvider is for the sake of demoing the tunnel
@ -360,13 +257,58 @@ func newDuckDNSProvider(token string) (*duckdns.DNSProvider, error) {
return duckdns.NewDNSProviderConfig(config) return duckdns.NewDNSProviderConfig(config)
} }
func stringSlice(csv string) []string { // newGoDaddyDNSProvider is for the sake of demoing the tunnel
list := []string{} func newGoDaddyDNSProvider(id, secret string) (*godaddy.DNSProvider, error) {
for _, item := range strings.Split(csv, ", ") { config := godaddy.NewDefaultConfig()
if 0 == len(item) { config.APIKey = id
continue config.APISecret = secret
return godaddy.NewDNSProviderConfig(config)
} }
list = append(list, item)
// newAPIDNSProvider is for the sake of demoing the tunnel
func newAPIDNSProvider(baseURL string, token string) (*dns01.DNSProvider, error) {
config := dns01.NewDefaultConfig()
config.Token = token
endpoint, err := url.Parse(baseURL)
if nil != err {
return nil, err
} }
return list config.Endpoint = endpoint
return dns01.NewDNSProviderConfig(config)
}
/*
// TODO for http proxy
return mplexer.TargetOptions {
Hostname // default localhost
Termination // default TLS
XFWD // default... no?
Port // default 0
Conn // should be dialed beforehand
}, nil
*/
/*
t := telebit.New(token)
mux := telebit.RouteMux{}
mux.HandleTLS("*", mux) // go back to itself
mux.HandleProxy("example.com", "localhost:3000")
mux.HandleTCP("example.com", func (c *telebit.Conn) {
return httpmux.Serve()
})
l := t.Listen("wss://example.com")
conn := l.Accept()
telebit.Serve(listener, mux)
t.ListenAndServe("wss://example.com", mux)
*/
func getToken(secret string, domains []string) (token string, err error) {
tokenData := jwt.MapClaims{"domains": domains}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData)
if token, err = jwtToken.SignedString([]byte(secret)); err != nil {
return "", err
}
return token, nil
} }

View File

@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore"
telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer" telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore"
) )
type SuccessResponse struct { type SuccessResponse struct {

View File

@ -1,314 +0,0 @@
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
package main
import (
"context"
"encoding/base64"
"encoding/hex"
"flag"
"fmt"
"log"
"net"
"net/url"
"os"
"strings"
"time"
telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/dns01"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt"
"git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore"
"github.com/caddyserver/certmagic"
"github.com/denisbrodbeck/machineid"
jwt "github.com/dgrijalva/jwt-go"
"github.com/go-acme/lego/v3/challenge"
"github.com/go-acme/lego/v3/providers/dns/duckdns"
"github.com/go-acme/lego/v3/providers/dns/godaddy"
_ "github.com/joho/godotenv/autoload"
)
var (
// GitRev refers to the abbreviated commit hash
GitRev = "0000000"
// GitVersion refers to the most recent tag, plus any commits made since then
GitVersion = "v0.0.0-pre0+0000000"
// GitTimestamp refers to the timestamp of the most recent commit
GitTimestamp = "0000-00-00T00:00:00+0000"
)
type Forward struct {
scheme string
pattern string
port string
}
func main() {
var err error
var provider challenge.Provider = nil
var domains []string
var forwards []Forward
// TODO replace the websocket connection with a mock server
appID := flag.String("app-id", "telebit.io", "a unique identifier for a deploy target environment")
email := flag.String("acme-email", "", "email to use for Let's Encrypt / ACME registration")
certpath := flag.String("acme-storage", "./acme.d/", "path to ACME storage directory")
acmeAgree := flag.Bool("acme-agree", false, "agree to the terms of the ACME service provider (required)")
acmeStaging := flag.Bool("acme-staging", false, "get fake certificates for testing")
acmeDirectory := flag.String("acme-directory", "", "ACME Directory URL")
enableHTTP01 := flag.Bool("acme-http-01", false, "enable HTTP-01 ACME challenges")
enableTLSALPN01 := flag.Bool("acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges")
acmeRelay := flag.String("acme-relay", "", "the base url of the ACME DNS-01 relay, if not the same as the tunnel relay")
authURL := flag.String("auth-url", "", "the base url for authentication, if not the same as the tunnel relay")
relay := flag.String("relay", "", "the domain (or ip address) at which the relay server is running")
secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)")
token := flag.String("token", "", "a pre-generated token to give the server (instead of generating one with --secret)")
locals := flag.String("locals", "", "a list of <from-domain>:<to-port>")
flag.Parse()
if len(os.Args) >= 2 {
if "version" == os.Args[1] {
fmt.Printf("telebit %s %s %s", GitVersion, GitRev[:7], GitTimestamp)
os.Exit(0)
}
}
if "" != *acmeDirectory {
if *acmeStaging {
fmt.Fprintf(os.Stderr, "pick either acme-directory or acme-staging\n")
os.Exit(1)
}
}
if *acmeStaging {
*acmeDirectory = certmagic.LetsEncryptStagingCA
}
if "" == *locals {
*locals = os.Getenv("LOCALS")
}
for _, cfg := range strings.Fields(strings.ReplaceAll(*locals, ",", " ")) {
parts := strings.Split(cfg, ":")
last := len(parts) - 1
port := parts[last]
domain := parts[last-1]
scheme := ""
if len(parts) > 2 {
scheme = parts[0]
}
forwards = append(forwards, Forward{
scheme: scheme,
pattern: domain,
port: port,
})
// don't load wildcard into jwt domains
if "*" == domain {
continue
}
domains = append(domains, domain)
}
ppid, err := machineid.ProtectedID(fmt.Sprintf("%s|%s", *appID, *secret))
if nil != err {
fmt.Fprintf(os.Stderr, "unauthorized device")
os.Exit(1)
}
ppidBytes, err := hex.DecodeString(ppid)
ppid = base64.RawURLEncoding.EncodeToString(ppidBytes)
if "" == *token {
if "" == *secret {
*secret = os.Getenv("SECRET")
}
*token, err = authstore.HMACToken(ppid)
}
if nil != err {
fmt.Fprintf(os.Stderr, "neither secret nor token provided")
os.Exit(1)
return
}
if "" == *relay {
*relay = os.Getenv("RELAY") // "wss://example.com:443"
}
if "" == *relay {
fmt.Fprintf(os.Stderr, "Missing relay url")
os.Exit(1)
return
}
if "" == *acmeRelay {
*acmeRelay = strings.Replace(*relay, "ws", "http", 1) // "https://example.com:443"
}
if "" == *authURL {
*authURL = strings.Replace(*relay, "ws", "http", 1) // "https://example.com:443"
}
if "" != os.Getenv("GODADDY_API_KEY") {
id := os.Getenv("GODADDY_API_KEY")
secret := os.Getenv("GODADDY_API_SECRET")
if provider, err = newGoDaddyDNSProvider(id, secret); nil != err {
panic(err)
}
} else if "" != os.Getenv("DUCKDNS_TOKEN") {
if provider, err = newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN")); nil != err {
panic(err)
}
} else {
endpoint := *acmeRelay
if strings.HasSuffix(endpoint, "/") {
endpoint = endpoint[:len(endpoint)-1]
}
//endpoint += "/api/dns/"
if provider, err = newAPIDNSProvider(endpoint, *token); nil != err {
panic(err)
}
}
grants, err := telebit.Inspect(*authURL, *token)
if nil != err {
_, err := mgmt.Register(*authURL, *secret, ppid)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to register client: %s", err)
os.Exit(1)
}
grants, err = telebit.Inspect(*authURL, *token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to authenticate after registering client: %s", err)
os.Exit(1)
}
}
fmt.Println("grants", grants)
acme := &telebit.ACME{
Email: *email,
StoragePath: *certpath,
Agree: *acmeAgree,
Directory: *acmeDirectory,
DNSProvider: provider,
EnableHTTPChallenge: *enableHTTP01,
EnableTLSALPNChallenge: *enableTLSALPN01,
}
mux := telebit.NewRouteMux()
mux.HandleTLS("*", acme, mux)
for _, fwd := range forwards {
mux.ForwardTCP("*", "localhost:"+fwd.port, 120*time.Second)
//mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second)
}
connected := make(chan net.Conn)
go func() {
timeoutCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
defer cancel()
tun, err := telebit.DialWebsocketTunnel(timeoutCtx, *relay, *token)
if nil != err {
msg := ""
if strings.Contains(err.Error(), "bad handshake") {
msg = " (may be auth related)"
}
fmt.Fprintf(os.Stderr, "Error connecting to %s: %s%s\n", *relay, err, msg)
os.Exit(1)
return
}
err = mgmt.Ping(*authURL, *token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s", err)
//os.Exit(1)
}
connected <- tun
}()
go func() {
for {
time.Sleep(10 * time.Minute)
err = mgmt.Ping(*authURL, *token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s", err)
//os.Exit(1)
}
}
}()
tun := <-connected
fmt.Printf("Listening at %s\n", *relay)
log.Fatal("Closed server: ", telebit.ListenAndServe(tun, mux))
}
type ACMEProvider struct {
BaseURL string
provider challenge.Provider
}
func (p *ACMEProvider) Present(domain, token, keyAuth string) error {
return p.provider.Present(domain, token, keyAuth)
}
func (p *ACMEProvider) CleanUp(domain, token, keyAuth string) error {
return p.provider.CleanUp(domain, token, keyAuth)
}
// newDuckDNSProvider is for the sake of demoing the tunnel
func newDuckDNSProvider(token string) (*duckdns.DNSProvider, error) {
config := duckdns.NewDefaultConfig()
config.Token = token
return duckdns.NewDNSProviderConfig(config)
}
// newGoDaddyDNSProvider is for the sake of demoing the tunnel
func newGoDaddyDNSProvider(id, secret string) (*godaddy.DNSProvider, error) {
config := godaddy.NewDefaultConfig()
config.APIKey = id
config.APISecret = secret
return godaddy.NewDNSProviderConfig(config)
}
// newAPIDNSProvider is for the sake of demoing the tunnel
func newAPIDNSProvider(baseURL string, token string) (*dns01.DNSProvider, error) {
config := dns01.NewDefaultConfig()
config.Token = token
endpoint, err := url.Parse(baseURL)
if nil != err {
return nil, err
}
config.Endpoint = endpoint
return dns01.NewDNSProviderConfig(config)
}
/*
// TODO for http proxy
return mplexer.TargetOptions {
Hostname // default localhost
Termination // default TLS
XFWD // default... no?
Port // default 0
Conn // should be dialed beforehand
}, nil
*/
/*
t := telebit.New(token)
mux := telebit.RouteMux{}
mux.HandleTLS("*", mux) // go back to itself
mux.HandleProxy("example.com", "localhost:3000")
mux.HandleTCP("example.com", func (c *telebit.Conn) {
return httpmux.Serve()
})
l := t.Listen("wss://example.com")
conn := l.Accept()
telebit.Serve(listener, mux)
t.ListenAndServe("wss://example.com", mux)
*/
func getToken(secret string, domains []string) (token string, err error) {
tokenData := jwt.MapClaims{"domains": domains}
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData)
if token, err = jwtToken.SignedString([]byte(secret)); err != nil {
return "", err
}
return token, nil
}