1099 lines
31 KiB
Go
1099 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/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")
|
|
|
|
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(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")
|
|
}
|
|
}
|
|
|
|
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)
|
|
*/
|