telebit/cmd/telebit/telebit.go

1113 lines
31 KiB
Go

//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver/v2
package main
import (
"context"
"encoding/base64"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"git.rootprojects.org/root/telebit/internal/dbg"
"git.rootprojects.org/root/telebit/internal/dns01"
"git.rootprojects.org/root/telebit/internal/http01"
"git.rootprojects.org/root/telebit/internal/http01proxy"
"git.rootprojects.org/root/telebit/internal/iplist"
"git.rootprojects.org/root/telebit/internal/mgmt"
"git.rootprojects.org/root/telebit/internal/mgmt/authstore"
"git.rootprojects.org/root/telebit/internal/service"
"git.rootprojects.org/root/telebit/internal/telebit"
"git.rootprojects.org/root/telebit/internal/tunnel"
"github.com/coolaj86/certmagic"
"github.com/denisbrodbeck/machineid"
"github.com/go-acme/lego/v3/challenge"
legoDNS01 "github.com/go-acme/lego/v3/challenge/dns01"
"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"
"github.com/joho/godotenv"
"github.com/judwhite/go-svc/svc"
_ "github.com/joho/godotenv/autoload"
)
const (
// exitOk is for normal exits, such as a graceful disconnect or shutdown
exitOk = 0
// exitBadArguments is for positive failures as a result of arguments
exitBadArguments = 1
// exitBadConfig is for positive failures from an external service
exitBadConfig = 2
// exitRetry is for potentially false negative failures from temporary
// conditions such as a DNS resolution or network failure for which it would
// be reasonable to wait 10 seconds and try again
exitRetry = 29
)
var (
// commit refers to the abbreviated commit hash
commit = "0000000"
// version refers to the most recent tag, plus any commits made since then
version = "v0.0.0-pre0+0000000"
// GitTimestamp refers to the timestamp of the most recent commit
date = "0000-00-00T00:00:00+0000"
// serviceName is the service name
serviceName = "telebit"
// serviceDesc
serviceDesc = "Telebit Secure Proxy"
// defaultRelay should be set when compiled for the client
defaultRelay = "" //"https://telebit.app"
)
var bindAddrs []string
// Forward describes how to route a network connection
type Forward struct {
scheme string
pattern string
port string
localTLS bool
}
var isHostname = regexp.MustCompile(`^[A-Za-z0-9_\.\-]+$`).MatchString
// VendorID may be baked in, or supplied via ENVs or --args
var VendorID string
// ClientSecret may be baked in, or supplied via ENVs or --args
var ClientSecret string
// Config describes how to run
type Config struct {
acme *telebit.ACME
acmeRelay string
acmeDNS01Relay string
acmeHTTP01Relay string
enableHTTP01 bool
enableTLSALPN01 bool
forwards []Forward
portForwards []Forward
apiHostname string
authURL string
tunnelRelay string // api directory
wsTunnel string // ws tunnel
token string
leeway time.Duration
pairwiseSecret string // secret ppid
logPath string
}
var config Config
func ver() string {
return fmt.Sprintf("%s v%s (%s) %s", serviceName, version, commit[:7], date)
}
func main() {
parseFlagsAndENVs()
prg := program{}
defer func() {
if prg.LogFile != nil {
if closeErr := prg.LogFile.Close(); closeErr != nil {
log.Printf("error closing '%s': %v\n", prg.LogFile.Name(), closeErr)
}
}
}()
// call svc.Run to start your program/service
// svc.Run will call Init, Start, and Stop
if err := svc.Run(&prg); err != nil {
log.Fatal(err)
}
}
// implements svc.Service
type program struct {
LogFile *os.File
}
func (p *program) Init(env svc.Environment) error {
// write to "telebit.log" when running as a Windows Service
if env.IsWindowsService() && 0 == len(config.logPath) {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return err
}
config.logPath = filepath.Join(dir, "telebit.log")
}
if len(config.logPath) > 0 {
_ = os.MkdirAll(filepath.Dir(config.logPath), 0750)
f, err := os.OpenFile(config.logPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640)
if err != nil {
return err
}
os.Stdout = f
os.Stderr = f
dbg.OutFile = f
dbg.ErrFile = f
log.SetOutput(f)
}
return nil
}
var started bool
func (p *program) Start() error {
log.Printf("Starting...\n")
if !started {
started = true
go fetchDirectivesAndRun()
}
return nil
}
func (p *program) Stop() error {
log.Printf("Can't stop. Doing nothing instead.\n")
return nil
}
func parseFlagsAndENVs() {
if len(os.Args) >= 2 {
if "version" == strings.TrimLeft(os.Args[1], "-") {
fmt.Printf("%s\n", ver())
os.Exit(exitOk)
return
}
}
if len(os.Args) >= 2 {
if "install" == os.Args[1] {
if err := service.Install(serviceName, serviceDesc); nil != err {
fmt.Fprintf(os.Stderr, "%v", err)
}
return
}
}
var domains []string
var resolvers []string
var dnsPropagationDelay time.Duration
debug := flag.Bool("debug", false, "show debug output")
verbose := flag.Bool("verbose", false, "log excessively")
spfDomain := flag.String("spf-domain", "", "domain with SPF-like list of IP addresses which are allowed to connect to clients")
vendorID := flag.String("vendor-id", "", "a unique identifier for a deploy target environment")
envpath := flag.String("env", "", "path to .env file")
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")
clientSecret := flag.String("secret", "", "the same secret used by telebit-relay (used for JWT authentication)")
resolverList := flag.String("dns-resolvers", "", "a list of resolvers in the format 8.8.8.8:53,8.8.4.4:53")
proxyHTTP01 := flag.String("proxy-http-01", "", "listen on port 80 and forward .well-known/acme-challenge traffic to this url")
flag.DurationVar(&dnsPropagationDelay, "dns-01-delay", 0, "add an extra delay after dns self-check to allow DNS-01 challenges to propagate")
flag.BoolVar(&config.enableHTTP01, "acme-http-01", false, "enable HTTP-01 ACME challenges")
flag.BoolVar(&config.enableTLSALPN01, "acme-tls-alpn-01", false, "enable TLS-ALPN-01 ACME challenges")
flag.StringVar(&config.logPath, "outfile", "", "where to direct output (default system logger or OS stdout)")
flag.StringVar(&config.acmeRelay, "acme-relay-url", "", "the base url of the ACME relay, if different from relay's directives")
flag.StringVar(&config.acmeDNS01Relay, "acme-dns-01-relay-url", "", "the base url of the ACME DNS-01 relay, if different from ACME relay")
flag.StringVar(&config.acmeHTTP01Relay, "acme-http-01-relay-url", "", "the base url of the ACME HTTP-01 relay, if different from ACME relay")
flag.StringVar(&config.authURL, "auth-url", "", "the base url for authentication, if not the same as the tunnel relay")
flag.StringVar(&config.tunnelRelay, "tunnel-relay-url", "", "the websocket url at which to connect to the tunnel relay")
flag.StringVar(&config.apiHostname, "api-hostname", "", "the hostname used to manage clients")
flag.StringVar(&config.token, "token", "", "an auth token for the server (instead of generating --secret); use --token=false to ignore any $TOKEN in env")
flag.DurationVar(&config.leeway, "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 <from-domain>:<to-port>")
portToPorts := flag.String("port-forward", "", "a list of <from-port>:<to-port> for raw port-forwarding")
flag.Parse()
if len(*envpath) > 0 {
if err := godotenv.Load(*envpath); nil != err {
fmt.Fprintf(os.Stderr, "%v", err)
os.Exit(exitBadArguments)
return
}
}
dbg.Init()
if !dbg.Verbose {
if *verbose {
dbg.Verbose = true
dbg.Printf("--verbose: extra output enabled")
}
}
if !dbg.Debug {
if *debug {
dbg.Verbose = true
dbg.Debug = true
dbg.Printf("--debug: byte output will be printed in full as hex")
}
}
spfRecords := iplist.Init(*spfDomain)
if len(spfRecords) > 0 {
fmt.Println(
"Allow client connections from:",
strings.Join(spfRecords, " "),
)
}
if len(*acmeDirectory) > 0 {
if *acmeStaging {
fmt.Fprintf(os.Stderr, "pick either acme-directory or acme-staging\n")
os.Exit(exitBadArguments)
return
}
}
if *acmeStaging {
*acmeDirectory = certmagic.LetsEncryptStagingCA
}
if !*acmeAgree {
if "true" == os.Getenv("ACME_AGREE") {
*acmeAgree = true
}
}
if 0 == len(config.acmeRelay) {
config.acmeRelay = os.Getenv("ACME_RELAY_URL")
}
if 0 == len(config.acmeHTTP01Relay) {
config.acmeHTTP01Relay = os.Getenv("ACME_HTTP_01_RELAY_URL")
}
if 0 == len(config.acmeHTTP01Relay) {
config.acmeHTTP01Relay = config.acmeRelay
}
if 0 == len(config.acmeDNS01Relay) {
config.acmeDNS01Relay = os.Getenv("ACME_DNS_01_RELAY_URL")
}
if 0 == len(config.acmeDNS01Relay) {
config.acmeDNS01Relay = config.acmeRelay
}
if 0 == len(*email) {
*email = os.Getenv("ACME_EMAIL")
}
if 0 == len(*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]
}
config.forwards = append(config.forwards, Forward{
scheme: scheme,
pattern: domain,
port: port,
})
// don't load wildcard into jwt domains
if "*" == domain {
continue
}
domains = append(domains, domain)
}
if 0 == len(*tlsLocals) {
*tlsLocals = os.Getenv("TLS_LOCALS")
}
for _, cfg := range strings.Fields(strings.ReplaceAll(*tlsLocals, ",", " ")) {
parts := strings.Split(cfg, ":")
last := len(parts) - 1
port := parts[last]
domain := parts[last-1]
scheme := ""
if len(parts) > 2 {
scheme = parts[0]
}
config.forwards = append(config.forwards, Forward{
scheme: scheme,
pattern: domain,
port: port,
localTLS: true,
})
// don't load wildcard into jwt domains
if "*" == domain {
continue
}
domains = append(domains, domain)
}
var err error
if 0 == dnsPropagationDelay {
dnsPropagationDelay, err = time.ParseDuration(os.Getenv("DNS_01_DELAY"))
}
if 0 == dnsPropagationDelay {
dnsPropagationDelay = 5 * time.Second
}
if 0 == len(*resolverList) {
*resolverList = os.Getenv("DNS_RESOLVERS")
}
if len(*resolverList) > 0 {
for _, resolver := range strings.Fields(strings.ReplaceAll(*resolverList, ",", " ")) {
resolvers = append(resolvers, resolver)
}
legoDNS01.AddRecursiveNameservers(resolvers)
}
if 0 == len(*portToPorts) {
*portToPorts = os.Getenv("PORT_FORWARDS")
}
config.portForwards, err = parsePortForwards(portToPorts)
if nil != err {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(exitBadArguments)
return
}
if 0 == len(*bindAddrsStr) {
*bindAddrsStr = os.Getenv("LISTEN")
}
bindAddrs, err = parseBindAddrs(*bindAddrsStr)
if nil != err {
fmt.Fprintf(os.Stderr, "invalid bind address(es) given to --listen\n")
os.Exit(exitBadArguments)
return
}
if dbg.Debug {
fmt.Println("[debug] bindAddrs", bindAddrs, *bindAddrsStr)
}
// Baked-in takes precedence
if 0 == len(VendorID) {
VendorID = *vendorID
} else if 0 != len(*vendorID) {
if VendorID != *vendorID {
fmt.Fprintf(os.Stderr, "invalid --vendor-id\n")
os.Exit(exitBadArguments)
}
}
if 0 == len(VendorID) {
VendorID = os.Getenv("VENDOR_ID")
} else if 0 != len(os.Getenv("VENDOR_ID")) {
if VendorID != os.Getenv("VENDOR_ID") {
fmt.Fprintf(os.Stderr, "invalid VENDOR_ID\n")
os.Exit(exitBadArguments)
}
}
if 0 == len(ClientSecret) {
ClientSecret = *clientSecret
} else if 0 != len(*clientSecret) {
if ClientSecret != *clientSecret {
fmt.Fprintf(os.Stderr, "invalid --secret\n")
os.Exit(exitBadArguments)
}
}
if 0 == len(ClientSecret) {
ClientSecret = os.Getenv("SECRET")
} else if 0 != len(os.Getenv("SECRET")) {
if ClientSecret != os.Getenv("SECRET") {
fmt.Fprintf(os.Stderr, "invalid SECRET\n")
os.Exit(exitBadArguments)
}
}
config.pairwiseSecret, err = machineid.ProtectedID(fmt.Sprintf("%s|%s", VendorID, ClientSecret))
if nil != err {
fmt.Fprintf(os.Stderr, "unauthorized device\n")
os.Exit(exitBadConfig)
return
}
ppidBytes, _ := hex.DecodeString(config.pairwiseSecret)
config.pairwiseSecret = base64.RawURLEncoding.EncodeToString(ppidBytes)
if 0 == len(config.token) {
config.token = os.Getenv("TOKEN")
}
if "false" == config.token {
config.token = ""
}
if 0 == len(*proxyHTTP01) {
*proxyHTTP01 = os.Getenv("PROXY_HTTP_01")
}
if 0 == len(config.tunnelRelay) {
config.tunnelRelay = os.Getenv("TUNNEL_RELAY_URL") // "wss://example.com:443"
}
if 0 == len(config.tunnelRelay) {
config.tunnelRelay = defaultRelay
}
if 0 == len(config.tunnelRelay) {
if len(bindAddrs) > 0 {
fmt.Fprintf(os.Stderr, "Acting as Relay\n")
} else {
fmt.Fprintf(os.Stderr, "error: must provide Relay, or act as Relay\n")
os.Exit(exitBadArguments)
return
}
}
if 0 == len(config.authURL) {
config.authURL = os.Getenv("AUTH_URL")
}
fmt.Printf("Email: %q\n", *email)
config.acme = &telebit.ACME{
Email: *email,
StoragePath: *certpath,
Agree: *acmeAgree,
Directory: *acmeDirectory,
EnableTLSALPNChallenge: config.enableTLSALPN01,
}
//
// Telebit Relay Server
//
if 0 == len(config.apiHostname) {
config.apiHostname = os.Getenv("API_HOSTNAME")
}
// Proxy for HTTP-01 requests
// TODO needs to be limited to .well-known/acme-challenges
if len(*proxyHTTP01) > 0 {
go func() {
fmt.Printf("Proxying HTTP-01 on port 80 to %s\n", *proxyHTTP01)
log.Fatalf("%v", http01proxy.ListenAndServe(*proxyHTTP01, 10*time.Second))
}()
}
}
func tokener() string {
token := config.token
if 0 == len(token) {
var err error
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway)
if dbg.Debug {
fmt.Printf("[debug] app_id: %q\n", VendorID)
//fmt.Printf("[debug] client_secret: %q\n", ClientSecret)
//fmt.Printf("[debug] ppid: %q\n", ppid)
//fmt.Printf("[debug] ppid: [redacted]\n")
fmt.Printf("[debug] token: %q\n", token)
}
if nil != err {
fmt.Fprintf(os.Stderr, "neither client secret nor token provided\n")
os.Exit(exitBadArguments)
return ""
}
}
return token
}
func fetchDirectivesAndRun() {
token := tokener()
var grants *telebit.Grants
if len(config.tunnelRelay) > 0 {
grants = fetchDirectives(&config, token)
}
// TODO
// Blog about the stupidity of this typing
// var dns01Solver *dns01.Solver = nil
if len(config.acmeDNS01Relay) > 0 {
provider, err := getACMEDNS01Provider(config.acmeDNS01Relay, tokener)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
// it's possible for some providers this could be a failed network request,
// but I think in the case of what we specifically support it's bad arguments
os.Exit(exitBadArguments)
return
}
// TODO Use libdns DNS01Solver instead.
// See https://pkg.go.dev/github.com/caddyserver/certmagic#DNS01Solver
// DNS01Solver{ DNSProvider: libdnsprovider, PropagationTimeout: dnsPropagationDelay, Resolvesr: resolvers }
config.acme.DNS01Solver = dns01.NewSolver(provider)
fmt.Println("Using DNS-01 solver for ACME Challenges")
}
if config.enableHTTP01 {
config.acme.EnableHTTPChallenge = true
}
if len(config.acmeHTTP01Relay) > 0 {
config.acme.EnableHTTPChallenge = true
endpoint, err := url.Parse(config.acmeHTTP01Relay)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(exitBadArguments)
return
}
http01Solver, err := http01.NewSolver(&http01.Config{
Endpoint: endpoint,
Tokener: tokener,
})
config.acme.HTTP01Solver = http01Solver
fmt.Println("Using HTTP-01 solver for ACME Challenges")
}
if nil == config.acme.HTTP01Solver && nil == config.acme.DNS01Solver {
fmt.Fprintf(os.Stderr, "Neither ACME HTTP 01 nor DNS 01 proxy URL detected, nor supplied\n")
os.Exit(exitBadArguments)
return
}
mux := muxAll(config.portForwards, config.forwards, config.acme, config.apiHostname, config.authURL, grants)
done := make(chan error)
for _, addr := range bindAddrs {
go func(addr string) {
fmt.Printf("Listening on %s\n", addr)
ln, err := net.Listen("tcp", addr)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to bind to %q: %s", addr, err)
done <- err
return
}
if err := telebit.Serve(ln, mux); nil != err {
fmt.Fprintf(os.Stderr, "failed to bind to %q: %s", addr, err)
done <- err
return
}
}(addr)
}
//connected := make(chan net.Conn)
go func() {
if 0 == len(config.wsTunnel) {
return
}
timeoutCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
defer cancel()
tun, err := telebit.DialWebsocketTunnel(timeoutCtx, config.wsTunnel, 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", config.wsTunnel, err, msg)
os.Exit(exitRetry)
return
}
err = mgmt.Ping(config.authURL, token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
//os.Exit(exitRetry)
}
go func() {
for {
time.Sleep(10 * time.Minute)
if len(ClientSecret) > 0 {
// re-create token unless no secret was supplied
token, err = authstore.HMACToken(config.pairwiseSecret, config.leeway)
}
err = mgmt.Ping(config.authURL, token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to ping mgmt server: %s\n", err)
//os.Exit(exitRetry)
}
}
}()
//connected <- tun
//tun := <-connected
fmt.Printf("Listening through %s\n", config.wsTunnel)
err = telebit.ListenAndServe(tun, mux)
fmt.Fprintf(os.Stderr, "Closed server: %s\n", err)
os.Exit(exitRetry)
done <- err
}()
if err := <-done; nil != err {
os.Exit(exitRetry)
}
}
func fetchDirectives(config *Config, token string) *telebit.Grants {
var grants *telebit.Grants
directory, err := tunnel.Discover(config.tunnelRelay)
if nil != err {
fmt.Fprintf(os.Stderr, "Error: invalid Tunnel Relay URL %q: %s\n", config.tunnelRelay, err)
os.Exit(exitRetry)
}
fmt.Printf("[Directory] %s\n", config.tunnelRelay)
jsonb, _ := json.Marshal(directory)
fmt.Printf("\t%s\n", string(jsonb))
// TODO trimming this should no longer be necessary, but I need to double check
authBase := strings.TrimSuffix(directory.Authenticate.URL, "/inspect")
if "" == config.authURL {
config.authURL = authBase
} else {
fmt.Println("Suggested Auth URL:", authBase)
fmt.Println("--auth-url Auth URL:", config.authURL)
}
if "" == config.authURL {
fmt.Fprintf(os.Stderr, "Discovered Directory Endpoints: %+v\n", directory)
fmt.Fprintf(os.Stderr, "No Auth URL detected, nor supplied\n")
os.Exit(exitBadConfig)
return nil
}
fmt.Println("Auth URL", config.authURL)
acmeDNS01Relay := directory.DNS01Proxy.URL
if 0 == len(config.acmeDNS01Relay) {
config.acmeDNS01Relay = acmeDNS01Relay
} else {
fmt.Println("Suggested ACME DNS 01 Proxy URL:", acmeDNS01Relay)
fmt.Println("--acme-relay-url ACME DNS 01 Proxy URL:", config.acmeDNS01Relay)
}
acmeHTTP01Relay := directory.HTTP01Proxy.URL
if 0 == len(config.acmeHTTP01Relay) {
config.acmeHTTP01Relay = acmeHTTP01Relay
} else {
fmt.Println("Suggested ACME HTTP 01 Proxy URL:", acmeHTTP01Relay)
fmt.Println("--acme-http-01-relay-url ACME HTTP 01 Proxy URL:", config.acmeHTTP01Relay)
}
// backwards compat
if 0 == len(config.acmeRelay) {
if !config.enableHTTP01 && len(config.acmeDNS01Relay) > 0 {
config.acmeRelay = config.acmeDNS01Relay
}
}
grants, err = telebit.Inspect(config.authURL, token)
if nil != err {
if dbg.Debug {
fmt.Fprintf(os.Stderr, "failed to inspect token: %s\n", err)
}
_, err := mgmt.Register(config.authURL, ClientSecret, config.pairwiseSecret)
if nil != err {
if strings.Contains(err.Error(), `"E_NOT_FOUND"`) {
fmt.Fprintf(os.Stderr, "invalid client credentials: %s\n", err)
// the server confirmed that the client is bad
os.Exit(exitBadConfig)
} else {
// there may have been a network error
fmt.Fprintf(os.Stderr, "failed to register client: %s\n", err)
os.Exit(exitRetry)
}
return nil
}
grants, err = telebit.Inspect(config.authURL, token)
if nil != err {
fmt.Fprintf(os.Stderr, "failed to authenticate after registering client: %s\n", err)
// there was no error registering the client, yet there was one authenticating
// therefore this may be an error that will be resolved
os.Exit(exitRetry)
return nil
}
}
fmt.Printf("[Grants]\n\t%#v\n", grants)
config.wsTunnel = grants.Audience
return grants
}
func muxAll(
portForwards, forwards []Forward,
acme *telebit.ACME,
apiHostname, authURL string,
grants *telebit.Grants,
) *telebit.RouteMux {
//mux := telebit.NewRouteMux(acme)
mux := telebit.NewRouteMux()
// Port forward without TerminatingTLS
for _, fwd := range portForwards {
msg := fmt.Sprintf("Fwd: %s %s", fwd.pattern, fwd.port)
fmt.Println(msg)
mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, msg, "[Port Forward]")
}
//
// Telebit Relay Server
//
if len(config.apiHostname) > 0 {
// this is a generic net listener
r := chi.NewRouter()
r.Get("/version", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(ver() + "\n"))
})
telebit.RouteAdmin(config.authURL, r)
apiListener := tunnel.NewListener()
go func() {
httpsrv := &http.Server{Handler: r}
httpsrv.Serve(apiListener)
}()
fmt.Printf("Will respond to Websocket and API requests to %q\n", config.apiHostname)
mux.HandleTLS(config.apiHostname, acme, mux, "[Terminate TLS & Recurse] for "+config.apiHostname)
mux.HandleTCP(config.apiHostname, telebit.HandlerFunc(func(client net.Conn) error {
if dbg.Debug {
fmt.Printf("[debug] Accepting API or WebSocket client %q\n", config.apiHostname)
}
apiListener.Feed(client)
if dbg.Debug {
fmt.Printf("[debug] done with %q client\n", config.apiHostname)
}
// nil now means handler in-progress (go routine)
// EOF now means handler finished
return nil
}), "[Admin API & Server Relays]")
}
// TODO close connection on invalid hostname
mux.HandleTCP("*", telebit.HandlerFunc(routeSubscribersAndClients), "[Tun => Remote Servers]")
if nil != grants {
for i, domainname := range grants.Domains {
fmt.Printf("[%d] Will decrypt remote requests to %q\n", i, domainname)
mux.HandleTLS(domainname, acme, mux, "[Terminate TLS & Recurse] for (tunnel) "+domainname)
}
}
for i, fwd := range forwards {
fmt.Printf("[%d] Will decrypt local requests to \"%s://%s\"\n", i, fwd.scheme, fwd.pattern)
mux.HandleTLS(fwd.pattern, acme, mux, "[Terminate TLS & Recurse] for (local) "+fwd.pattern)
}
//mux.HandleTLSFunc(func (sni) bool {
// // do whatever
// return false
//}, acme, mux, "[Terminate TLS & Recurse]")
for _, fwd := range forwards {
//mux.ForwardTCP("*", "localhost:"+fwd.port, 120*time.Second)
if "https" == fwd.scheme {
if fwd.localTLS {
// this doesn't make much sense, but... security theatre
mux.ReverseProxyHTTPS(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, "[Servername Reverse Proxy TLS]")
} else {
mux.ReverseProxyHTTP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, "[Servername Reverse Proxy]")
}
}
mux.ForwardTCP(fwd.pattern, "localhost:"+fwd.port, 120*time.Second, "[Servername Forward]")
}
return mux
}
func routeSubscribersAndClients(client net.Conn) error {
var wconn *telebit.ConnWrap
switch conn := client.(type) {
case *telebit.ConnWrap:
wconn = conn
default:
panic("routeSubscribersAndClients is special in that it must receive &ConnWrap{ Conn: conn }")
}
// We know this to be two parts "ip:port"
dstParts := strings.Split(client.LocalAddr().String(), ":")
//dstAddr := dstParts[0]
dstPort, _ := strconv.Atoi(dstParts[1])
if dbg.Debug {
fmt.Printf("[debug] wconn.LocalAddr() %+v\n", wconn.LocalAddr())
fmt.Printf("[debug] wconn.RemoteAddr() %+v\n", wconn.RemoteAddr())
}
if 80 != dstPort && 443 != dstPort {
// TODO handle by port without peeking at Servername / Hostname
// if tryToServePort(client.LocalAddr().String(), wconn) {
// return io.EOF
// }
}
// TODO hostname for plain http?
servername := strings.ToLower(wconn.Servername())
if "" != servername && !isHostname(servername) {
_ = client.Close()
if dbg.Debug {
fmt.Println("[debug] invalid servername")
}
return fmt.Errorf("invalid servername")
}
if dbg.Debug {
fmt.Printf("[debug] wconn.Servername() %+v\n", servername)
}
// Match full servername "sub.domain.example.com"
if tryToServeName(servername, wconn) {
// TODO better non-error
return nil
}
// Match wild names
// - "*.domain.example.com"
// - "*.example.com"
// - (skip)
labels := strings.Split(servername, ".")
n := len(labels)
if n < 3 {
// skip
return telebit.ErrNotHandled
}
for i := 1; i < n-1; i++ {
wildname := "*." + strings.Join(labels[i:], ".")
if tryToServeName(wildname, wconn) {
return io.EOF
}
}
// skip
return telebit.ErrNotHandled
}
// tryToServeName picks the server tunnel with the least connections, if any
func tryToServeName(servername string, wconn *telebit.ConnWrap) bool {
srv, ok := telebit.GetServer(servername)
if !ok || nil == srv {
if ok {
// TODO BUG: Sometimes srv=nil & ok=true, which should not be possible
fmt.Println("[bug] found 'srv=nil'", servername, srv)
}
if dbg.Debug {
fmt.Println("[debug] no server to server", servername)
}
return false
}
// Note: timing can reveal if the client exists
if allowAll, _ := iplist.IsAllowed(nil); !allowAll {
addr := wconn.RemoteAddr()
if nil == addr {
// handled by denial
wconn.Close()
return true
}
// 192.168.1.100:2345
// [::fe12]:2345
remoteIP := addr.String()
index := strings.LastIndex(remoteIP, ":")
if index < 1 {
// TODO how to handle unexpected invalid address?
wconn.Close()
return true
}
remoteIP = remoteIP[:index]
fmt.Println("remote addr:", remoteIP)
if "127.0.0.1" != remoteIP &&
"::1" != remoteIP &&
"localhost" != remoteIP {
ipAddr := net.ParseIP(remoteIP)
if nil == ipAddr {
wconn.Close()
return true
}
if ok, err := iplist.IsAllowed(ipAddr); !ok || nil != err {
wconn.Close()
return true
}
}
}
// async so that the call stack can complete and be released
//srv.clients.Store(wconn.LocalAddr().String(), wconn)
go func() {
if dbg.Debug {
fmt.Printf("[debug] found server to handle client:\n%#v\n", srv)
}
err := srv.Serve(wconn)
if dbg.Debug {
fmt.Printf("[debug] a browser client stream is done: %v\n", err)
}
}()
return true
}
func parsePortForwards(portToPorts *string) ([]Forward, error) {
var portForwards []Forward
for _, cfg := range strings.Fields(strings.ReplaceAll(*portToPorts, ",", " ")) {
parts := strings.Split(cfg, ":")
if 2 != len(parts) {
return nil, fmt.Errorf("--port-forward should be in the format 1234:5678, not %q", cfg)
}
if _, err := strconv.Atoi(parts[0]); nil != err {
return nil, fmt.Errorf("couldn't parse port %q of %q", parts[0], cfg)
}
if _, err := strconv.Atoi(parts[1]); nil != err {
return nil, fmt.Errorf("couldn't parse port %q of %q", parts[1], cfg)
}
portForwards = append(portForwards, Forward{
pattern: ":" + parts[0],
port: parts[1],
})
}
return portForwards, nil
}
func parseBindAddrs(bindAddrsStr string) ([]string, error) {
bindAddrs := []string{}
for _, addr := range strings.Fields(strings.ReplaceAll(bindAddrsStr, ",", " ")) {
parts := strings.Split(addr, ":")
if len(parts) > 2 {
return nil, fmt.Errorf("too many colons (:) in bind address %s", addr)
}
if "" == addr {
continue
}
var hostname, port string
if 2 == len(parts) {
hostname = parts[0]
port = parts[1]
} else {
port = parts[0]
}
if _, err := strconv.Atoi(port); nil != err {
return nil, fmt.Errorf("couldn't parse port of %q", addr)
}
bindAddrs = append(bindAddrs, hostname+":"+port)
}
return bindAddrs, nil
}
func getACMEDNS01Provider(acmeRelay string, token func() string) (challenge.Provider, error) {
var err error
var provider challenge.Provider = nil
if "" != os.Getenv("GODADDY_API_KEY") {
id := os.Getenv("GODADDY_API_KEY")
apiSecret := os.Getenv("GODADDY_API_SECRET")
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
}
} else {
if "" == acmeRelay {
return nil, fmt.Errorf("No relay for ACME DNS-01 challenges given to --acme-relay-url")
}
endpoint := acmeRelay
if !strings.HasSuffix(endpoint, "/") {
endpoint += "/"
}
/*
if strings.HasSuffix(endpoint, "/") {
endpoint = endpoint[:len(endpoint)-1]
}
endpoint += "/api/dns/"
*/
if provider, err = newAPIDNSProvider(endpoint, tokener); nil != err {
return nil, err
}
}
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()
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, tokener func() string) (*dns01.DNSProvider, error) {
config := dns01.NewDefaultConfig()
config.Tokener = tokener
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)
*/