diff --git a/.gitignore b/.gitignore index 804fcb3..caaa2bb 100644 --- a/.gitignore +++ b/.gitignore @@ -16,16 +16,20 @@ telebit-client-macos telebit-client-windows-debug.exe telebit-client-windows.exe -/mplexer/cmd/dnsclient/dnsclient -/mplexer/cmd/sqlstore/sqlstore -/mplexer/mgmt/cmd/mgmt/mgmt -/mplexer/cmd/signjwt/signjwt -/mplexer/cmd/telebit/telebit +/cmd/dnsclient/dnsclient +/cmd/sqlstore/sqlstore +/cmd/mgmt/mgmt +/cmd/signjwt/signjwt +/cmd/telebit/telebit /telebit /cmd/telebit/telebit /telebit-relay +/telebit-relay-linux +/telebit-relay-macos /cmd/telebit-relay/telebit-relay +/cmd/telebit-relay/telebit-relay-linux +/cmd/telebit-relay/telebit-relay-macos .*.sw* log.txt diff --git a/README.md b/README.md index f267258..e95abab 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ pushd mplexy/ 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=darwin GOARCH=amd64 go build -mod vendor -o mgmt-server-macos ./mgmt/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 -ldflags "-H windowsgui" -o mgmt-server-windows.exe ./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 ./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 ./cmd/mgmt/*.go ``` ### Example diff --git a/build.sh b/build-client.sh similarity index 100% rename from build.sh rename to build-client.sh diff --git a/build-mgmt.sh b/build-mgmt.sh index ab761ee..adff117 100644 --- a/build-mgmt.sh +++ b/build-mgmt.sh @@ -1,6 +1,6 @@ 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=darwin GOARCH=amd64 go build -mod vendor -o mgmt-server-macos ./mgmt/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 -ldflags "-H windowsgui" -o mgmt-server-windows.exe ./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 ./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 ./cmd/mgmt/*.go diff --git a/mplexer/cmd/dnsclient/dnsclient.go b/cmd/dnsclient/dnsclient.go similarity index 100% rename from mplexer/cmd/dnsclient/dnsclient.go rename to cmd/dnsclient/dnsclient.go diff --git a/mplexer/mgmt/cmd/mgmt/acmeroutes.go b/cmd/mgmt/acmeroutes.go similarity index 100% rename from mplexer/mgmt/cmd/mgmt/acmeroutes.go rename to cmd/mgmt/acmeroutes.go diff --git a/mplexer/mgmt/cmd/mgmt/devices.go b/cmd/mgmt/devices.go similarity index 98% rename from mplexer/mgmt/cmd/mgmt/devices.go rename to cmd/mgmt/devices.go index c4a9a27..cbbcb97 100644 --- a/mplexer/mgmt/cmd/mgmt/devices.go +++ b/cmd/mgmt/devices.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" + "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore" "github.com/go-chi/chi" ) diff --git a/mplexer/mgmt/cmd/mgmt/mgmt.go b/cmd/mgmt/mgmt.go similarity index 97% rename from mplexer/mgmt/cmd/mgmt/mgmt.go rename to cmd/mgmt/mgmt.go index 50d46b0..9ab082b 100644 --- a/mplexer/mgmt/cmd/mgmt/mgmt.go +++ b/cmd/mgmt/mgmt.go @@ -10,7 +10,7 @@ import ( "os" "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/providers/dns/duckdns" diff --git a/mplexer/mgmt/cmd/mgmt/route.go b/cmd/mgmt/route.go similarity index 99% rename from mplexer/mgmt/cmd/mgmt/route.go rename to cmd/mgmt/route.go index 015fef0..ef27653 100644 --- a/mplexer/mgmt/cmd/mgmt/route.go +++ b/cmd/mgmt/route.go @@ -9,7 +9,7 @@ import ( "strings" "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/go-chi/chi" "github.com/go-chi/chi/middleware" diff --git a/mplexer/cmd/signjwt/signjwt.go b/cmd/signjwt/signjwt.go similarity index 92% rename from mplexer/cmd/signjwt/signjwt.go rename to cmd/signjwt/signjwt.go index dcd297b..b56b284 100644 --- a/mplexer/cmd/signjwt/signjwt.go +++ b/cmd/signjwt/signjwt.go @@ -6,7 +6,7 @@ import ( "fmt" "os" - "git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" + "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore" "github.com/denisbrodbeck/machineid" _ "github.com/joho/godotenv/autoload" diff --git a/mplexer/cmd/sqlstore/sqlstore.go b/cmd/sqlstore/sqlstore.go similarity index 94% rename from mplexer/cmd/sqlstore/sqlstore.go rename to cmd/sqlstore/sqlstore.go index d5653e1..8ac2c4c 100644 --- a/mplexer/cmd/sqlstore/sqlstore.go +++ b/cmd/sqlstore/sqlstore.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" + "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore" ) func main() { diff --git a/cmd/telebit-relay/telebit-relay.go b/cmd/telebit-relay/telebit-relay.go index 2750536..e434266 100644 --- a/cmd/telebit-relay/telebit-relay.go +++ b/cmd/telebit-relay/telebit-relay.go @@ -16,8 +16,8 @@ import ( "strings" "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/mgmt" "git.coolaj86.com/coolaj86/go-telebitd/relay" "git.coolaj86.com/coolaj86/go-telebitd/relay/api" "git.coolaj86.com/coolaj86/go-telebitd/relay/mplexy" @@ -257,7 +257,7 @@ func main() { 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) { return []byte(secretKey), nil diff --git a/cmd/telebit/telebit.go b/cmd/telebit/telebit.go index 2a6e761..26f5449 100644 --- a/cmd/telebit/telebit.go +++ b/cmd/telebit/telebit.go @@ -1,356 +1,253 @@ +//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver + package main import ( "context" - "crypto/tls" + "encoding/base64" + "encoding/hex" "flag" "fmt" "log" + "net" + "net/url" "os" - "regexp" - "strconv" "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/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 httpRegexp = regexp.MustCompile(`(?i)^http`) -var locals string -var domains string -var insecure bool -var relay string -var secret string -var token string +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" +) -func init() { - flag.StringVar(&locals, "locals", "", "comma separated list of : or "+ - ":: to which matching incoming connections should forward. "+ - "Ex: smtps:8465,https:example.com:8443") - 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 - // or or : or :: - - 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: - // - proxies = append(proxies, proxy{"http", "*", port}) - proxies = append(proxies, proxy{"https", "*", port}) - case 2: - // : - // : - 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 := 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 - // or :: - - err := fmt.Errorf("invalid argument for --domains, use format or ::") - 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 +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 :") flag.Parse() - var err error - - if "" == locals { - locals = os.Getenv("LOCALS") - } - - 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) - } + if len(os.Args) >= 2 { + if "version" == os.Args[1] { + fmt.Printf("telebit %s %s %s", GitVersion, GitRev[:7], GitTimestamp) + os.Exit(0) } } - //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) - } + if "" != *acmeDirectory { + if *acmeStaging { + fmt.Fprintf(os.Stderr, "pick either acme-directory or acme-staging\n") + os.Exit(1) } } + if *acmeStaging { + *acmeDirectory = certmagic.LetsEncryptStagingCA + } - servicePorts := extractServicePorts(proxies) - domainMap := make(map[string]bool) - for _, p := range proxies { - if p.hostname != "" && p.hostname != "*" { - domainMap[p.hostname] = true + 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, + }) - if relay == "" { - relay = os.Getenv("RELAY") - } - if relay == "" { - fmt.Fprintf(os.Stderr, "must provide remote relay server to connect to\n") - os.Exit(1) - } - - if secret == "" { - secret = os.Getenv("SECRET") - } - - if secret != "" { - domains := make([]string, 0, len(domainMap)) - for name := range domainMap { - domains = append(domains, name) + // don't load wildcard into jwt domains + if "*" == domain { + continue } - tokenData := jwt.MapClaims{"domains": domains} - - secret := []byte(secret) - jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, tokenData) - if tokenStr, err := jwtToken.SignedString(secret); err != nil { - panic(err) - } else { - token = tokenStr - } - } else if token != "" { - fmt.Fprintf(os.Stderr, "must provide either token or secret\n") - os.Exit(1) + domains = append(domains, domain) } - ctx, quit := context.WithCancel(context.Background()) - 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}) + ppid, err := machineid.ProtectedID(fmt.Sprintf("%s|%s", *appID, *secret)) if nil != err { - fmt.Fprintf(os.Stderr, "failed to initialize certificate management (discovery url? local folder perms?): %s\n", err) + fmt.Fprintf(os.Stderr, "unauthorized device") os.Exit(1) } + ppidBytes, err := hex.DecodeString(ppid) + ppid = base64.RawURLEncoding.EncodeToString(ppidBytes) - tlsConfig := &tls.Config{ - GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { - return magic.GetCertificate(hello) - /* - if false { - _, _ = 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 - */ - }, + 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 } - config := client.Config{ - Insecure: insecure, - Server: relay, - Services: servicePorts, - Token: token, - TLSConfig: tlsConfig, + 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" } - fmt.Printf("config:\n%#v\n", config) - log.Fatal(client.Run(ctx, &config)) + 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)) } -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, - CA: directory, - Email: email, - Agreed: true, - DisableHTTPChallenge: true, - DisableTLSALPNChallenge: true, - // plus any other customizations you need - }) - return magic, nil +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 @@ -360,13 +257,58 @@ func newDuckDNSProvider(token string) (*duckdns.DNSProvider, error) { return duckdns.NewDNSProviderConfig(config) } -func stringSlice(csv string) []string { - list := []string{} - for _, item := range strings.Split(csv, ", ") { - if 0 == len(item) { - continue - } - list = append(list, item) - } - return list +// 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 } diff --git a/mplexer/mgmt/auth.go b/mgmt/auth.go similarity index 95% rename from mplexer/mgmt/auth.go rename to mgmt/auth.go index 7892835..3593e63 100644 --- a/mplexer/mgmt/auth.go +++ b/mgmt/auth.go @@ -6,8 +6,8 @@ import ( "fmt" "io/ioutil" + "git.coolaj86.com/coolaj86/go-telebitd/mgmt/authstore" telebit "git.coolaj86.com/coolaj86/go-telebitd/mplexer" - "git.coolaj86.com/coolaj86/go-telebitd/mplexer/mgmt/authstore" ) type SuccessResponse struct { diff --git a/mplexer/mgmt/authstore/authstore.go b/mgmt/authstore/authstore.go similarity index 100% rename from mplexer/mgmt/authstore/authstore.go rename to mgmt/authstore/authstore.go diff --git a/mplexer/mgmt/authstore/authstore_test.go b/mgmt/authstore/authstore_test.go similarity index 100% rename from mplexer/mgmt/authstore/authstore_test.go rename to mgmt/authstore/authstore_test.go diff --git a/mplexer/mgmt/authstore/init.sql b/mgmt/authstore/init.sql similarity index 100% rename from mplexer/mgmt/authstore/init.sql rename to mgmt/authstore/init.sql diff --git a/mplexer/mgmt/authstore/insert.sql b/mgmt/authstore/insert.sql similarity index 100% rename from mplexer/mgmt/authstore/insert.sql rename to mgmt/authstore/insert.sql diff --git a/mplexer/mgmt/authstore/postgresql.go b/mgmt/authstore/postgresql.go similarity index 100% rename from mplexer/mgmt/authstore/postgresql.go rename to mgmt/authstore/postgresql.go diff --git a/mplexer/cmd/telebit/telebit.go b/mplexer/cmd/telebit/telebit.go deleted file mode 100644 index 6b1bccf..0000000 --- a/mplexer/cmd/telebit/telebit.go +++ /dev/null @@ -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 :") - 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 -}