From 8c8d5b150fcbc461ce2ff6f0f4aeb005a93afdd2 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Thu, 12 Nov 2020 06:30:52 -0700 Subject: [PATCH] bugfix HTTP-01 relay and update docs --- .gitignore | 1 + README.md | 2 +- cmd/mgmt/README.md | 29 +++++++++ cmd/mgmt/acmeroutes.go | 3 + cmd/mgmt/mgmt.go | 102 +++++++++++++++++++++-------- cmd/mgmt/route.go | 3 +- cmd/signjwt/signjwt.go | 8 ++- cmd/telebit/README.md | 0 cmd/telebit/admin.go | 8 +++ cmd/telebit/telebit.go | 126 ++++++++++++++++++++++-------------- examples/client.env | 26 +++++++- examples/mgmt.env | 34 ++++++++-- examples/relay.env | 45 ++++++++++--- go.sum | 1 + internal/dns01/dns01.go | 8 ++- internal/http01/http01.go | 5 +- mgmt/authstore/authstore.go | 3 +- telebit.go | 6 +- tunnel/discover.go | 4 +- vendor/modules.txt | 5 ++ 20 files changed, 315 insertions(+), 104 deletions(-) create mode 100644 cmd/mgmt/README.md create mode 100644 cmd/telebit/README.md diff --git a/.gitignore b/.gitignore index a5d4630..b9c235c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ telebit-client-windows.exe /cmd/wsconnect/wsconnect /cmd/mgmt/mgmt /cmd/signjwt/signjwt +/signjwt /cmd/telebit/telebit /telebit diff --git a/README.md b/README.md index d7dbb68..5894490 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ go build -mod=vendor -ldflags "-s -w" -o signjwt cmd/signjwt/*.go To generate an `admin` token: ```bash -VERDOR_ID="test-id" +VENDOR_ID="test-id" SECRET="xxxxxxxxxxx" TOKEN=$(./signjwt \ --expires-in 15m \ diff --git a/cmd/mgmt/README.md b/cmd/mgmt/README.md new file mode 100644 index 0000000..b4b283c --- /dev/null +++ b/cmd/mgmt/README.md @@ -0,0 +1,29 @@ +# MGMT Server + +# Config + +```bash +VERBOSE= + +PORT=6468 + +# JWT Verification Secret +#SECRET=XxxxxxxxxxxxxxxX + +DB_URL=postgres://postgres:postgres@localhost:5432/postgres +DOMAIN=mgmt.example.com +TUNNEL_DOMAIN=tunnel.example.com + +NAMECOM_USERNAME=johndoe +NAMECOM_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +# Build + +```bash +go generate -mod vendor ./... + +pushd cmd/mgmt + go build -mod vendor -o telebit-mgmt +popd +``` diff --git a/cmd/mgmt/acmeroutes.go b/cmd/mgmt/acmeroutes.go index bbb944e..5de7543 100644 --- a/cmd/mgmt/acmeroutes.go +++ b/cmd/mgmt/acmeroutes.go @@ -58,6 +58,9 @@ func handleDNSRoutes(r chi.Router) { r.Delete("/{domain}/{token}/{keyAuth}/{challengeType}", deleteChallenge) } + // TODO pick one and stick with it + r.Route("/acme-relay", handleACMEChallenges) + r.Route("/acme-solver", handleACMEChallenges) r.Route("/dns", handleACMEChallenges) r.Route("/http", handleACMEChallenges) } diff --git a/cmd/mgmt/mgmt.go b/cmd/mgmt/mgmt.go index 5f8e264..c2e9aa3 100644 --- a/cmd/mgmt/mgmt.go +++ b/cmd/mgmt/mgmt.go @@ -15,6 +15,7 @@ import ( "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/go-acme/lego/v3/providers/dns/namedotcom" _ "github.com/joho/godotenv/autoload" ) @@ -43,51 +44,89 @@ func help() { func main() { var err error - addr := flag.String("address", "", "IPv4 or IPv6 bind address") - port := flag.String("port", "3000", "port to listen to") - challengesPort := flag.String("challenges-port", "80", "port to use to respond to .well-known/acme-challenge tokens") - dbURL := flag.String( - "db-url", - "postgres://postgres:postgres@localhost/postgres", - "database (postgres) connection url", - ) - flag.StringVar(&secret, "secret", "", "a >= 16-character random string for JWT key signing") - flag.StringVar(&primaryDomain, "domain", "", "the base domain to use for all clients") - flag.StringVar(&relayDomain, "tunnel-domain", "", "the domain name of the tunnel relay service, if different from base domain") + var port string + var lnAddr string + var dbURL string + var challengesPort string + + flag.StringVar(&port, "port", "", + "port to listen to (default localhost 3000)") + flag.StringVar(&lnAddr, "listen", "", + "IPv4 or IPv6 bind address + port (instead of --port)") + flag.StringVar(&challengesPort, "challenges-port", "80", + "port to use to respond to .well-known/acme-challenge tokens") + flag.StringVar(&dbURL, "db-url", "postgres://postgres:postgres@localhost:5432/postgres", + "database (postgres) connection url") + flag.StringVar(&secret, "secret", "", + "a >= 16-character random string for JWT key signing") + flag.StringVar(&primaryDomain, "domain", "", + "the base domain to use for all clients") + flag.StringVar(&relayDomain, "tunnel-domain", "", + "the domain name of the tunnel relay service, if different from base domain") + flag.Parse() - if "" == primaryDomain { - help() - os.Exit(1) + if 0 == len(primaryDomain) { + primaryDomain = os.Getenv("DOMAIN") } - if "" == relayDomain { + + if 0 == len(relayDomain) { + relayDomain = os.Getenv("TUNNEL_DOMAIN") + } + if 0 == len(relayDomain) { relayDomain = primaryDomain } - if "" != os.Getenv("GODADDY_API_KEY") { + if 0 == len(dbURL) { + dbURL = os.Getenv("DB_URL") + } + + if 0 == len(secret) { + secret = os.Getenv("SECRET") + } + + // prefer --listen (with address) over --port (localhost only) + if 0 == len(lnAddr) { + lnAddr = os.Getenv("LISTEN") + } + if 0 == len(lnAddr) { + if 0 == len(port) { + port = os.Getenv("PORT") + } + if 0 == len(port) { + port = "3000" + } + lnAddr = "localhost:" + port + } + + if len(os.Getenv("GODADDY_API_KEY")) > 0 { id := os.Getenv("GODADDY_API_KEY") apiSecret := os.Getenv("GODADDY_API_SECRET") if provider, err = newGoDaddyDNSProvider(id, apiSecret); nil != err { panic(err) } - } else if "" != os.Getenv("DUCKDNS_TOKEN") { + } else if len(os.Getenv("DUCKDNS_TOKEN")) > 0 { if provider, err = newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN")); nil != err { panic(err) } + } else if len(os.Getenv("NAMECOM_API_TOKEN")) > 0 { + if provider, err = newNameDotComDNSProvider( + os.Getenv("NAMECOM_USERNAME"), + os.Getenv("NAMECOM_API_TOKEN"), + ); nil != err { + panic(err) + } } else { - panic("Must provide either DUCKDNS or GODADDY credentials") + fmt.Println("DNS-01 relay disabled") } - if "" == secret { - secret = os.Getenv("SECRET") - } - if "" == secret { + if 0 == len(primaryDomain) || 0 == len(secret) || 0 == len(dbURL) { help() os.Exit(1) return } - connStr := *dbURL + connStr := dbURL // TODO url.Parse if strings.Contains(connStr, "@localhost/") || strings.Contains(connStr, "@localhost:") { connStr += "?sslmode=disable" @@ -104,16 +143,23 @@ func main() { defer store.Close() go func() { - fmt.Println("Listening for ACME challenges on :" + *challengesPort) - if err := http.ListenAndServe(":"+*challengesPort, routeStatic()); nil != err { + fmt.Println("Listening for ACME challenges on :" + challengesPort) + if err := http.ListenAndServe(":"+challengesPort, routeStatic()); nil != err { log.Fatal(err) os.Exit(1) } }() - bind := *addr + ":" + *port - fmt.Println("Listening on", bind) - fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(bind, routeAll())) + fmt.Println("Listening on", lnAddr) + fmt.Fprintf(os.Stderr, "failed: %s", http.ListenAndServe(lnAddr, routeAll())) +} + +// newNameDotComDNSProvider is for the sake of demoing the tunnel +func newNameDotComDNSProvider(username, apitoken string) (*namedotcom.DNSProvider, error) { + config := namedotcom.NewDefaultConfig() + config.Username = username + config.APIToken = apitoken + return namedotcom.NewDNSProviderConfig(config) } // newDuckDNSProvider is for the sake of demoing the tunnel diff --git a/cmd/mgmt/route.go b/cmd/mgmt/route.go index 763586d..41c87e4 100644 --- a/cmd/mgmt/route.go +++ b/cmd/mgmt/route.go @@ -18,6 +18,7 @@ import ( "github.com/go-chi/chi/middleware" ) +// MgmtClaims includes a Slug, for backwards compatibility type MgmtClaims struct { Slug string `json:"slug"` jwt.StandardClaims @@ -197,7 +198,7 @@ func routeAll() chi.Router { http.Error(w, msg, http.StatusNotFound) return } - if "" != original.MachinePPID { + if len(original.MachinePPID) > 0 { msg := `{"error":"the presented key has already been used", "code":"E_EXIST"}` log.Printf("/api/register-device/\n") log.Println(err) diff --git a/cmd/signjwt/signjwt.go b/cmd/signjwt/signjwt.go index 275bccf..d1019d5 100644 --- a/cmd/signjwt/signjwt.go +++ b/cmd/signjwt/signjwt.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "strconv" + "time" telebit "git.rootprojects.org/root/telebit" "git.rootprojects.org/root/telebit/dbg" @@ -30,6 +31,7 @@ var durAbbrs = map[byte]bool{ func main() { var secret, clientSecret, relaySecret string + debug := flag.Bool("debug", true, "show more debug output") machinePPID := flag.String("machine-ppid", "", "spoof the machine ppid") machineID := flag.String("machine-id", "", "spoof the raw machine id") vendorID := flag.String("vendor-id", "", "a unique identifier for a deploy target environment") @@ -39,6 +41,10 @@ func main() { flag.StringVar(&secret, "secret", "", "either the remote server or the tunnel relay secret (used for JWT authentication)") flag.Parse() + if *debug { + dbg.Debug = *debug + } + if 0 == len(*authURL) { *authURL = os.Getenv("AUTH_URL") } @@ -145,7 +151,7 @@ func main() { fmt.Fprintf(os.Stderr, "[debug] pub = %s\n", pub) } - tok, err := authstore.HMACToken(ppid, expNum) + tok, err := authstore.HMACToken(ppid, 15*time.Minute, expNum) if nil != err { fmt.Fprintf(os.Stderr, "signing error: %s\n", err) os.Exit(1) diff --git a/cmd/telebit/README.md b/cmd/telebit/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cmd/telebit/admin.go b/cmd/telebit/admin.go index ad360f1..d21f80a 100644 --- a/cmd/telebit/admin.go +++ b/cmd/telebit/admin.go @@ -66,6 +66,14 @@ func InitAdmin(authURL string) { // Proxy mgmt server ACME DNS 01 Challenges r.Get("/api/dns/*", proxyHandleFunc) + r.Post("/api/dns/*", proxyHandleFunc) + r.Delete("/api/dns/*", proxyHandleFunc) + r.Get("/api/http/*", proxyHandleFunc) + r.Post("/api/http/*", proxyHandleFunc) + r.Delete("/api/http/*", proxyHandleFunc) + r.Get("/api/acme-relay/*", proxyHandleFunc) + r.Post("/api/acme-relay/*", proxyHandleFunc) + r.Delete("/api/acme-relay/*", proxyHandleFunc) r.Route("/api", func(r chi.Router) { // TODO token needs a globally unique subject diff --git a/cmd/telebit/telebit.go b/cmd/telebit/telebit.go index 66a4415..a9e7838 100644 --- a/cmd/telebit/telebit.go +++ b/cmd/telebit/telebit.go @@ -18,9 +18,9 @@ import ( "strings" "time" - telebit "git.rootprojects.org/root/telebit" + "git.rootprojects.org/root/telebit" "git.rootprojects.org/root/telebit/dbg" - tbDns01 "git.rootprojects.org/root/telebit/internal/dns01" + "git.rootprojects.org/root/telebit/internal/dns01" "git.rootprojects.org/root/telebit/internal/http01" "git.rootprojects.org/root/telebit/internal/service" "git.rootprojects.org/root/telebit/iplist" @@ -28,7 +28,7 @@ import ( "git.rootprojects.org/root/telebit/mgmt/authstore" "git.rootprojects.org/root/telebit/table" "git.rootprojects.org/root/telebit/tunnel" - legoDns01 "github.com/go-acme/lego/v3/challenge/dns01" + legoDNS01 "github.com/go-acme/lego/v3/challenge/dns01" "github.com/coolaj86/certmagic" "github.com/denisbrodbeck/machineid" @@ -36,6 +36,7 @@ import ( "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/go-acme/lego/v3/providers/dns/namedotcom" "github.com/joho/godotenv" _ "github.com/joho/godotenv/autoload" ) @@ -112,6 +113,7 @@ func main() { var portForwards []Forward var resolvers []string + debug := flag.Bool("debug", true, "show debug output") spfDomain := flag.String("spf-domain", "", "domain with SPF-like list of IP addresses which are allowed to connect to clients") // TODO replace the websocket connection with a mock server vendorID := flag.String("vendor-id", "", "a unique identifier for a deploy target environment") @@ -133,7 +135,7 @@ func main() { apiHostname := flag.String("api-hostname", "", "the hostname used to manage clients") secret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)") token := flag.String("token", "", "an auth token for the server (instead of generating --secret); use --token=false to ignore any $TOKEN in env") - _ = flag.String("leeway", "", "(reserved for future use) allow for time drift / skew (hard-coded to 15 minutes)") + leeway := flag.Duration("leeway", 15*time.Minute, "allow for time drift / skew (hard-coded to 15 minutes)") bindAddrsStr := flag.String("listen", "", "list of bind addresses on which to listen, such as localhost:80, or :443") tlsLocals := flag.String("tls-locals", "", "like --locals, but TLS will be used to connect to the local port") locals := flag.String("locals", "", "a list of :") @@ -142,7 +144,7 @@ func main() { flag.Parse() if !dbg.Debug { - dbg.Debug = *verbose + dbg.Debug = *verbose || *debug } if len(*envpath) > 0 { @@ -253,7 +255,7 @@ func main() { for _, resolver := range strings.Fields(strings.ReplaceAll(*resolverList, ",", " ")) { resolvers = append(resolvers, resolver) } - legoDns01.AddRecursiveNameservers(resolvers) + legoDNS01.AddRecursiveNameservers(resolvers) } if 0 == len(*portToPorts) { @@ -325,7 +327,7 @@ func main() { *token = "" } if 0 == len(*token) { - *token, err = authstore.HMACToken(ppid) + *token, err = authstore.HMACToken(ppid, *leeway) if dbg.Debug { fmt.Printf("[debug] app_id: %q\n", VendorID) //fmt.Printf("[debug] client_secret: %q\n", ClientSecret) @@ -384,19 +386,18 @@ func main() { authorizer = NewAuthorizer(*authURL) dns01Base := directory.DNS01Proxy.URL - if "" == *acmeRelay { + if 0 == len(*acmeRelay) { *acmeRelay = dns01Base } else { fmt.Println("Suggested ACME DNS 01 Proxy URL:", dns01Base) fmt.Println("--acme-relay-url ACME DNS 01 Proxy URL:", *acmeRelay) } - if "" == *acmeRelay { - fmt.Fprintf(os.Stderr, "Discovered Directory Endpoints: %+v\n", directory) - fmt.Fprintf(os.Stderr, "No ACME DNS 01 Proxy URL detected, nor supplied\n") - os.Exit(exitBadConfig) - return + if 0 == len(*acmeHTTP01Relay) { + *acmeHTTP01Relay = directory.HTTP01Proxy.URL + } else { + fmt.Println("Suggested ACME HTTP 01 Proxy URL:", dns01Base) + fmt.Println("--acme-http-01-relay-url ACME DNS 01 Proxy URL:", *acmeRelay) } - fmt.Println("DNS 01 URL", *acmeRelay) grants, err = telebit.Inspect(*authURL, *token) if nil != err { @@ -429,7 +430,19 @@ func main() { } authorizer = NewAuthorizer(*authURL) - var dns01Solver *tbDns01.Solver + fmt.Printf("Email: %q\n", *email) + + acme := &telebit.ACME{ + Email: *email, + StoragePath: *certpath, + Agree: *acmeAgree, + Directory: *acmeDirectory, + EnableTLSALPNChallenge: *enableTLSALPN01, + } + + // TODO + // Blog about the stupidity of this typing + // var dns01Solver *dns01.Solver = nil if len(*acmeRelay) > 0 { provider, err := getACMEProvider(acmeRelay, token) if nil != err { @@ -439,33 +452,8 @@ func main() { os.Exit(exitBadArguments) return } - dns01Solver = tbDns01.NewSolver(provider) - } - - var http01Solver *http01.Solver - if len(*acmeHTTP01Relay) > 0 { - endpoint, err := url.Parse(*acmeHTTP01Relay) - if nil != err { - fmt.Fprintf(os.Stderr, "%s\n", err) - os.Exit(exitBadArguments) - return - } - http01Solver, err = http01.NewSolver(&http01.Config{ - Endpoint: endpoint, - Token: *token, - }) - } - - fmt.Printf("Email: %q\n", *email) - - acme := &telebit.ACME{ - Email: *email, - StoragePath: *certpath, - Agree: *acmeAgree, - Directory: *acmeDirectory, - DNS01Solver: dns01Solver, /* - options: legoDns01.WrapPreCheck(func(domain, fqdn, value string, orig legoDns01.PreCheckFunc) (bool, error) { + options: legoDNS01.WrapPreCheck(func(domain, fqdn, value string, orig legoDNS01.PreCheckFunc) (bool, error) { ok, err := orig(fqdn, value) if ok && dnsPropagationDelay > 0 { fmt.Printf("[Telebit-ACME-DNS] sleeping an additional %s\n", dnsPropagationDelay) @@ -474,10 +462,35 @@ func main() { return ok, err }), */ - HTTP01Solver: http01Solver, - //DNSChallengeOption: legoDns01.DNSProviderOption, - EnableHTTPChallenge: *enableHTTP01, - EnableTLSALPNChallenge: *enableTLSALPN01, + //DNSChallengeOption: legoDNS01.DNSProviderOption, + acme.DNS01Solver = dns01.NewSolver(provider) + fmt.Println("Using DNS-01 solver for ACME Challenges") + } + + if *enableHTTP01 { + acme.EnableHTTPChallenge = true + } + if len(*acmeHTTP01Relay) > 0 { + acme.EnableHTTPChallenge = true + endpoint, err := url.Parse(*acmeHTTP01Relay) + if nil != err { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(exitBadArguments) + return + } + http01Solver, err := http01.NewSolver(&http01.Config{ + Endpoint: endpoint, + Token: *token, + }) + + acme.HTTP01Solver = http01Solver + fmt.Println("Using HTTP-01 solver for ACME Challenges") + } + + if nil == acme.HTTP01Solver && nil == acme.DNS01Solver { + fmt.Fprintf(os.Stderr, "Neither ACME HTTP 01 nor DNS 01 proxy URL detected, nor supplied\n") + os.Exit(1) + return } mux := muxAll(portForwards, forwards, acme, apiHostname, authURL, grants) @@ -533,7 +546,7 @@ func main() { time.Sleep(10 * time.Minute) if "" != ClientSecret { // re-create token unless no secret was supplied - *token, err = authstore.HMACToken(ppid) + *token, err = authstore.HMACToken(ppid, *leeway) } err = mgmt.Ping(*authURL, *token) if nil != err { @@ -831,6 +844,13 @@ func getACMEProvider(acmeRelay, token *string) (challenge.Provider, error) { if provider, err = newGoDaddyDNSProvider(id, apiSecret); nil != err { return nil, err } + } else if "" != os.Getenv("NAMECOM_API_TOKEN") { + if provider, err = newNameDotComDNSProvider( + os.Getenv("NAMECOM_USERNAME"), + os.Getenv("NAMECOM_API_TOKEN"), + ); nil != err { + return nil, err + } } else if "" != os.Getenv("DUCKDNS_TOKEN") { if provider, err = newDuckDNSProvider(os.Getenv("DUCKDNS_TOKEN")); nil != err { return nil, err @@ -857,6 +877,14 @@ func getACMEProvider(acmeRelay, token *string) (challenge.Provider, error) { return provider, nil } +// newNameDotComDNSProvider is for the sake of demoing the tunnel +func newNameDotComDNSProvider(username, apitoken string) (*namedotcom.DNSProvider, error) { + config := namedotcom.NewDefaultConfig() + config.Username = username + config.APIToken = apitoken + return namedotcom.NewDNSProviderConfig(config) +} + // newDuckDNSProvider is for the sake of demoing the tunnel func newDuckDNSProvider(token string) (*duckdns.DNSProvider, error) { config := duckdns.NewDefaultConfig() @@ -873,15 +901,15 @@ func newGoDaddyDNSProvider(id, secret string) (*godaddy.DNSProvider, error) { } // newAPIDNSProvider is for the sake of demoing the tunnel -func newAPIDNSProvider(baseURL string, token string) (*tbDns01.DNSProvider, error) { - config := tbDns01.NewDefaultConfig() +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 tbDns01.NewDNSProviderConfig(config) + return dns01.NewDNSProviderConfig(config) } /* diff --git a/examples/client.env b/examples/client.env index e53b292..934bd4d 100644 --- a/examples/client.env +++ b/examples/client.env @@ -1,7 +1,31 @@ -CLIENT_SUBJECT=newbie +# TUNNEL_RELAY_URL +# The URL of the Telebit Relay, of course. +# Note that many client configuration details can be preassigned at +# https://devices.example.com/.well-known/telebit.app/index.json TUNNEL_RELAY_URL=https://devices.example.com/ + +# VENDOR_ID +# Used to distinguish between different white-labeled Telebit binaries. +# It's just as well to generate a random ID for your organization. +VENDOR_ID= + +# Used for Let's Encrypt registration +# ACME_AGREE +ACME_AGREE=true +# ACME_EMAIL +ACME_EMAIL=johndoe@example.com + +# CLIENT_SUBJECT (optional) +# NOT used by Telebit. +# This is for the Device Management & Authentication server. +CLIENT_SUBJECT=newbie CLIENT_SECRET=xxxxxxxxxxxxxxxx LOCALS=https:$CLIENT_SUBJECT.devices.example.com:3000,https:*.$CLIENT_SUBJECT.devices.example.com:3000 #ACME_HTTP_01_RELAY_URL=http://localhost:4200/api/http #PORT_FORWARDS=3443:3001,8443:3002 #DUCKDNS_TOKEN=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# AUTH_URL +# The client may receive this as `.authn.url` +# through `https://$API_DOMAIN/.well-known/telebit.app/index.json` +# Setting the ENV AUTH_URL or the flag --auth-url overrides the discovery endpoint diff --git a/examples/mgmt.env b/examples/mgmt.env index 314ef46..c27e954 100644 --- a/examples/mgmt.env +++ b/examples/mgmt.env @@ -1,8 +1,32 @@ -# For bash tests -MGMT_SECRET=xxxxxxxxxxxxxxxx -MGMT_URL=https://devices.example.com +# DOMAIN +# This is the base domain from which all devices +# will be given a subdomain (ex: foobar.devices.example.com). +DOMAIN=devices.example.com + +# TUNNEL_DOMAIN +# This is the domain that will be used for the wss:// connection URL. +TUNNEL_DOMAIN=new.telebit.cloud # For mgmt server itself +#SECRET=XxxxxxxxxxxxxxxX +DB_URL=postgres://postgres:postgres@localhost:5432/postgres + +# PORT +# the localhost port on which to listen +PORT=6468 + +# LISTEN +# alternative to PORT, including address +#LISTEN=localhost:6468 + DUCKDNS_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX -GODADDY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -GODADDY_API_SECRET=XXXXXXXXXXXXXXXXXXXXXX + +#NAMECOM_USERNAME=johndoe +#NAMECOM_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +#GODADDY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +#GODADDY_API_SECRET=XXXXXXXXXXXXXXXXXXXXXX + +# For bash tests +MGMT_SECRET=XxxxxxxxxxxxxxxX +MGMT_URL=https://devices.example.com diff --git a/examples/relay.env b/examples/relay.env index 2cc5c1a..249b4dd 100644 --- a/examples/relay.env +++ b/examples/relay.env @@ -1,21 +1,46 @@ # For Tunnel Relay Service + +# SECRET +# This should be the same as the MGMT server secret +# It is used for JWT token creation and verification +SECRET=xxxxxxxxxxxxxxxx + +# VERBOSE=true +# This will cause more verbose logs VERBOSE=true + +# API_HOSTNAME +# This is the domain name that should be captured for the API +# (as opposed to being routed downstream) +# If this is not set, the relay will not be active. API_HOSTNAME=devices.example.com + +# LISTEN +# This is the addr:port combo to which telebit should bind and listen. +# Note: a tunnel client can itself still be a relay through the tunnel. LISTEN=":443" -# To proxy incoming requests for 'https://mgmt.devices.example.com' to localhost:3010 -LOCALS=https:mgmt.devices.example.com:3010 +# LOCALS +# Act as a reverse proxy for matching incoming requests +# LOCALS=:: +# Example: 'https://mgmt.devices.example.com' to localhost:6468 +LOCALS=https:mgmt.devices.example.com:6468 -# For Device Management & Authorization Server -AUTH_URL=http://localhost:4200/api +# AUTH_URL +# Telebit is narrowly scoped to handle network connections +# The concerns of Device Management & Authorization should +# be handled per each specific use case. +AUTH_URL=http://localhost:6468/api -# For Let's Encrypt ACME registration +# For Let's Encrypt ACME registration of the API_HOSTNAME +# and LOCALS (reverse-proxied traffic). +# This is NOT for the remote telebit clients! ACME_AGREE=true ACME_EMAIL=jon.doe@example.com - -# For Let's Encrypt ACME Challenges (pick one) -ACME_RELAY_URL=http://localhost:4200/api/dns -SECRET=xxxxxxxxxxxxxxxx -#DUCKDNS_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +ACME_RELAY_URL=http://localhost:6468/api/acme-relay +# (pick ONLY ONE DNS-01 provider) +DUCKDNS_TOKEN=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX +#NAMECOM_USERNAME=johndoe +#NAMECOM_API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx #GODADDY_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx #GODADDY_API_SECRET=XXXXXXXXXXXXXXXXXXXXXX diff --git a/go.sum b/go.sum index 900887d..502c84d 100644 --- a/go.sum +++ b/go.sum @@ -239,6 +239,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= diff --git a/internal/dns01/dns01.go b/internal/dns01/dns01.go index d0bc41d..9f86866 100644 --- a/internal/dns01/dns01.go +++ b/internal/dns01/dns01.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "io/ioutil" + "log" "net/http" "net/url" "path" @@ -202,11 +203,13 @@ type Solver struct { // Present creates a DNS-01 Challenge Token func (s *Solver) Present(ctx context.Context, ch acme.Challenge) error { + log.Println("Present DNS-01 challenge solution for", ch.Identifier.Value) return s.provider.Present(ch.Identifier.Value, ch.Token, ch.KeyAuthorization) } // CleanUp deletes a DNS-01 Challenge Token func (s *Solver) CleanUp(ctx context.Context, ch acme.Challenge) error { + log.Println("CleanUp DNS-01 challenge solution for", ch.Identifier.Value) c := make(chan error) go func() { c <- s.provider.CleanUp(ch.Identifier.Value, ch.Token, ch.KeyAuthorization) @@ -222,6 +225,7 @@ func (s *Solver) CleanUp(ctx context.Context, ch acme.Challenge) error { // Wait blocks until the TXT record created in Present() appears in // authoritative lookups, i.e. until it has propagated, or until // timeout, whichever is first. -func (s *Solver) Wait(ctx context.Context, challenge acme.Challenge) error { - return s.dnsChecker.Wait(ctx, challenge) +func (s *Solver) Wait(ctx context.Context, ch acme.Challenge) error { + log.Println("Wait on DNS-01 challenge self-verification for", ch.Identifier.Value) + return s.dnsChecker.Wait(ctx, ch) } diff --git a/internal/http01/http01.go b/internal/http01/http01.go index 400b903..158551c 100644 --- a/internal/http01/http01.go +++ b/internal/http01/http01.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io/ioutil" + "log" "net/http" "net/url" "path" @@ -60,8 +61,9 @@ func NewSolver(config *Config) (*Solver, error) { return &Solver{config: config}, nil } -// Present creates a DNS-01 Challenge Token +// Present creates a HTTP-01 Challenge Token func (s *Solver) Present(ctx context.Context, ch acme.Challenge) error { + log.Println("Present HTTP-01 challenge solution for", ch.Identifier.Value) msg := &Challenge{ Type: "http-01", Token: ch.Token, @@ -81,6 +83,7 @@ func (s *Solver) Present(ctx context.Context, ch acme.Challenge) error { // CleanUp deletes an HTTP-01 Challenge Token func (s *Solver) CleanUp(ctx context.Context, ch acme.Challenge) error { + log.Println("CleanUp HTTP-01 challenge solution for", ch.Identifier.Value) msg := &Challenge{ Type: "http-01", Token: ch.Token, diff --git a/mgmt/authstore/authstore.go b/mgmt/authstore/authstore.go index 6e767a6..961b3e5 100644 --- a/mgmt/authstore/authstore.go +++ b/mgmt/authstore/authstore.go @@ -52,7 +52,7 @@ func ToPublicKeyString(secret string) string { return pub } -func HMACToken(secret string, maybeExp ...int) (token string, err error) { +func HMACToken(secret string, leeway time.Duration, maybeExp ...int) (token string, err error) { keyID := ToPublicKeyString(secret) if dbg.Debug { fmt.Fprintf(os.Stderr, "[debug] keyID=%s\n", keyID) @@ -67,7 +67,6 @@ func HMACToken(secret string, maybeExp ...int) (token string, err error) { b := make([]byte, 16) _, _ = rand.Read(b) - leeway := 15 * time.Minute claims := &jwt.StandardClaims{ Id: base64.RawURLEncoding.EncodeToString(b), Subject: "", // TODO diff --git a/telebit.go b/telebit.go index b70bfe7..0ab9439 100644 --- a/telebit.go +++ b/telebit.go @@ -443,7 +443,7 @@ func NewCertMagic(acme *ACME) (*certmagic.Config, error) { }) // yes, a circular reference, passing `magic` to its own Issuer fmt.Printf("ACME Email: %q\n", acme.Email) - magic.Issuer = certmagic.NewACMEManager(magic, certmagic.ACMEManager{ + manager := certmagic.ACMEManager{ DNS01Solver: acme.DNS01Solver, HTTP01Solver: acme.HTTP01Solver, CA: acme.Directory, @@ -452,7 +452,9 @@ func NewCertMagic(acme *ACME) (*certmagic.Config, error) { DisableHTTPChallenge: !acme.EnableHTTPChallenge, DisableTLSALPNChallenge: !acme.EnableTLSALPNChallenge, // plus any other customizations you need - }) + } + + magic.Issuer = certmagic.NewACMEManager(magic, manager) return magic, nil } diff --git a/tunnel/discover.go b/tunnel/discover.go index b88e65c..1127dc8 100644 --- a/tunnel/discover.go +++ b/tunnel/discover.go @@ -82,7 +82,9 @@ func Discover(relay string) (*Endpoints, error) { if len(directives.ChallengeProxy.Pathname) > 0 { directives.ChallengeProxy.URL = endpointToURLString(directives.APIHost, directives.ChallengeProxy) } - directives.DNS01Proxy.URL = endpointToURLString(directives.APIHost, directives.DNS01Proxy) + if len(directives.DNS01Proxy.Pathname) > 0 { + directives.DNS01Proxy.URL = endpointToURLString(directives.APIHost, directives.DNS01Proxy) + } if len(directives.HTTP01Proxy.Pathname) > 0 { directives.HTTP01Proxy.URL = endpointToURLString(directives.APIHost, directives.HTTP01Proxy) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 0a2b060..6a8c0cc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -34,6 +34,7 @@ github.com/go-acme/lego/v3/platform/tester github.com/go-acme/lego/v3/platform/wait github.com/go-acme/lego/v3/providers/dns/duckdns github.com/go-acme/lego/v3/providers/dns/godaddy +github.com/go-acme/lego/v3/providers/dns/namedotcom # github.com/go-chi/chi v4.1.1+incompatible ## explicit github.com/go-chi/chi @@ -116,8 +117,12 @@ github.com/mholt/acmez github.com/mholt/acmez/acme # github.com/miekg/dns v1.1.30 github.com/miekg/dns +# github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 +github.com/namedotcom/go/namecom # github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e ## explicit +# github.com/pkg/errors v0.9.1 +github.com/pkg/errors # github.com/pmezard/go-difflib v1.0.0 github.com/pmezard/go-difflib/difflib # github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749