// tunneld is a single-node, self-hosted OpenTunnel server: it speaks the // wire protocol from docs/protocol.md, so the official opentunnel CLI and // SDK work against it unchanged. package main import ( "crypto/tls" "flag" "log" "net" "net/http" "os" "os/signal" "path/filepath" "syscall" "time" "gitea.tzgyn.dev/TZGyn/tunnel/internal/server" ) func envOr(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func defaultDataDir() string { if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { return filepath.Join(xdg, "tunneld") } if home, err := os.UserHomeDir(); err == nil { return filepath.Join(home, ".local", "share", "tunneld") } return ".local" } func main() { dataDefault := filepath.Join(defaultDataDir(), "data.json") domain := flag.String("domain", envOr("OPENTUNNEL_DOMAIN", "localhost"), "API domain; tunnels are .") tlsListen := flag.String("tls-listen", envOr("TLS_LISTEN", "127.0.0.1:8443"), "public TLS listen address (SNI router)") httpListen := flag.String("http-listen", envOr("HTTP_LISTEN", "127.0.0.1:8080"), `HTTP listen address ("off" disables)`) httpMode := flag.String("http-mode", envOr("HTTP_MODE", "serve"), `"serve" runs the whole app on plain HTTP, "redirect" 301s to the domain`) dataPath := flag.String("data", envOr("TUNNELD_DATA", dataDefault), "tunnel store file (created 0600)") caFile := flag.String("ca-file", envOr("LOCAL_CA_FILE", ""), "write the local CA certificate here for clients to trust") flag.Parse() if *caFile == "" { *caFile = filepath.Join(filepath.Dir(*dataPath), "ca.pem") } store, err := server.OpenStore(*dataPath) if err != nil { log.Fatalf("open store: %v", err) } if err := os.MkdirAll(filepath.Dir(*caFile), 0o700); err != nil { log.Fatalf("ca file directory: %v", err) } if err := os.WriteFile(*caFile, []byte(store.CACertPEM()), 0o644); err != nil { log.Fatalf("write CA file: %v", err) } certPEM, keyPEM, err := store.ServerCertificate(*domain) if err != nil { log.Fatalf("server certificate: %v", err) } serverCert, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { log.Fatalf("parse server certificate: %v", err) } hub := server.NewHub(store) api := &server.API{Store: store, Hub: hub, Domain: *domain} // Public TLS listener: SNI routing, with API terminations handed to the // shared HTTP server. ln, err := net.Listen("tcp", *tlsListen) if err != nil { log.Fatalf("tls listen: %v", err) } apiListener := server.NewChanListener(64, ln.Addr()) relay := &server.Relay{ Domain: *domain, Store: store, Hub: hub, Server: &serverCert, APIConns: apiListener.Conns(), } go relay.Serve(ln) go func() { if err := http.Serve(apiListener, api); err != nil { log.Printf("api over tls: %v", err) } }() log.Printf("tls: listening on %s (domain %s)", ln.Addr(), *domain) var httpLn net.Listener if *httpListen != "off" { httpLn, err = net.Listen("tcp", *httpListen) if err != nil { log.Fatalf("http listen: %v", err) } var handler http.Handler = api if *httpMode == "redirect" { handler = server.RedirectHandler(*domain) } go func() { if err := http.Serve(httpLn, handler); err != nil { log.Printf("http: %v", err) } }() log.Printf("http: listening on %s (mode %s)", httpLn.Addr(), *httpMode) } signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) sig := <-signals log.Printf("received %s; draining bridges", sig) hub.DrainAll() ln.Close() if httpLn != nil { httpLn.Close() } time.Sleep(500 * time.Millisecond) log.Printf("bye") }