diff options
| author | Xe Iaso <me@xeiaso.net> | 2025-04-27 14:03:42 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2025-04-27 14:05:04 -0400 |
| commit | c9136cc167ca0bbabce1196f88cfc1b302350f0a (patch) | |
| tree | ca839dde02b33cf5a4d8eca4666c9e94234791e0 | |
| parent | 1d9fa34fa84cc125c68ab486d8bbc2dbe7a51f0e (diff) | |
| download | x-c9136cc167ca0bbabce1196f88cfc1b302350f0a.tar.xz x-c9136cc167ca0bbabce1196f88cfc1b302350f0a.zip | |
feat(relayd): autocert support for automatic TLS cert minting
Signed-off-by: Xe Iaso <me@xeiaso.net>
| -rw-r--r-- | cmd/relayd/main.go | 138 | ||||
| -rw-r--r-- | cmd/relayd/relayd.service | 1 |
2 files changed, 64 insertions, 75 deletions
diff --git a/cmd/relayd/main.go b/cmd/relayd/main.go index a658673..8101778 100644 --- a/cmd/relayd/main.go +++ b/cmd/relayd/main.go @@ -21,20 +21,25 @@ import ( "syscall" "time" - "github.com/avct/uasurfer" "github.com/google/uuid" - "github.com/jackc/puddle/v2" + "golang.org/x/crypto/acme/autocert" _ "modernc.org/sqlite" "within.website/x/internal" + "within.website/x/xess" ) var ( - bind = flag.String("bind", ":3004", "port to listen on") - certDir = flag.String("cert-dir", "/xe/pki", "where to read mounted certificates from") - certFname = flag.String("cert-fname", "tls.crt", "certificate filename") - fpDatabase = flag.String("fp-database", "", "location of fingerprint database") - keyFname = flag.String("key-fname", "tls.key", "key filename") - proxyTo = flag.String("proxy-to", "http://localhost:5000", "where to reverse proxy to") + useAutocert = flag.Bool("use-autocert", false, "if true, provision certs with autocert") + autocertCacheDir = flag.String("autocert-cache-dir", "", "location to store cached certificates and ACME account details") + autocertDomainNames = flag.String("autocert-domain-names", "", "comma-separated list of TLS hostnames to allow") + autocertEmail = flag.String("autocert-email", "", "ACME account contact email") + bind = flag.String("bind", ":3004", "port to listen on") + certDir = flag.String("cert-dir", "/xe/pki", "where to read mounted certificates from") + certFname = flag.String("cert-fname", "tls.crt", "certificate filename") + fpDatabase = flag.String("fp-database", "", "location of fingerprint database") + keyFname = flag.String("key-fname", "tls.key", "key filename") + httpBind = flag.String("http-bind", "", "if set, plain HTTP port to listen on to forward requests to https") + proxyTo = flag.String("proxy-to", "http://localhost:5000", "where to reverse proxy to") ) func main() { @@ -47,44 +52,64 @@ func main() { "fp-database", *fpDatabase, "key-fname", *keyFname, "proxy-to", *proxyTo, + "use-autocert", *useAutocert, + "autocert-cache-dir", *autocertCacheDir, + "autocert-domain-names", *autocertDomainNames, + "autocert-email", *autocertEmail, ) - cert := filepath.Join(*certDir, *certFname) - key := filepath.Join(*certDir, *keyFname) + var tc *tls.Config - st, err := os.Stat(cert) + switch *useAutocert { + case true: + slog.Info("using autocert") + var fail bool + if *autocertCacheDir == "" { + fmt.Fprintln(os.Stderr, "cannot use --autocert without --autocert-cache-dir") + fail = true + } + if *autocertDomainNames == "" { + fmt.Fprintln(os.Stderr, "cannot use --autocert without --autocert-domain-names") + fail = true + } + if *autocertEmail == "" { + fmt.Fprintln(os.Stderr, "cannot use --autocert without --autocert-email") + fail = true + } + if *httpBind != ":80" { + fmt.Fprintln(os.Stderr, "cannot use --autocert without --http-bind=:80") + fail = true + } - if err != nil { - slog.Error("can't stat cert file", "certFname", cert) - os.Exit(1) - } + if fail { + log.Fatal("autocert configuration errors") + } - lastModified := st.ModTime() + m := &autocert.Manager{ + Cache: autocert.DirCache(*autocertCacheDir), + Prompt: autocert.AcceptTOS, + Email: *autocertEmail, + HostPolicy: autocert.HostWhitelist(strings.Split(*autocertDomainNames, ",")...), + } - go func(lm time.Time) { - t := time.NewTicker(time.Hour) - defer t.Stop() + go func() { + slog.Info("listening for plain HTTP", "http-bind", *httpBind) + log.Fatal(http.ListenAndServe(*httpBind, m.HTTPHandler(http.HandlerFunc(xess.NotFound)))) + }() - for range t.C { - st, err := os.Stat(cert) - if err != nil { - slog.Error("can't stat file", "fname", cert, "err", err) - continue - } + tc = m.TLSConfig() + case false: + cert := filepath.Join(*certDir, *certFname) + key := filepath.Join(*certDir, *keyFname) - if st.ModTime().After(lm) { - slog.Info("new cert detected", "oldTime", lm.Format(time.RFC3339), "newTime", st.ModTime().Format(time.RFC3339)) - os.Exit(0) - } + kpr, err := NewKeypairReloader(cert, key) + if err != nil { + log.Fatal(err) } - }(lastModified) - - u, err := url.Parse(*proxyTo) - if err != nil { - log.Fatal(err) + tc = &tls.Config{GetCertificate: kpr.GetCertificate} } - kpr, err := NewKeypairReloader(cert, key) + u, err := url.Parse(*proxyTo) if err != nil { log.Fatal(err) } @@ -102,11 +127,6 @@ func main() { } } - uaParser, err := uaParserPool() - if err != nil { - log.Fatal(err) - } - h := httputil.NewSingleHostReverseProxy(u) oldDirector := h.Director @@ -142,19 +162,6 @@ func main() { // req.Header.Set("X-TCP-Fingerprint-JA4T", tcpFP.String()) // } - if uap, err := uaParser.Acquire(req.Context()); err == nil { - defer uap.Release() - - uasurfer.ParseUserAgent(req.UserAgent(), uap.Value()) - - vers := uap.Value().Browser.Version - - req.Header.Set("Xe-X-Relayd-UserAgent-Browser", uap.Value().Browser.Name.StringTrimPrefix()) - req.Header.Set("Xe-X-Relayd-UserAgent-Browser-Version", fmt.Sprintf("%d.%d.%d", vers.Major, vers.Minor, vers.Patch)) - req.Header.Set("Xe-X-Relayd-UserAgent-OS", uap.Value().OS.Name.StringTrimPrefix()) - req.Header.Set("Xe-X-Relayd-UserAgent-Platform", uap.Value().OS.Platform.StringTrimPrefix()) - } - if ja4 := req.Header.Get("X-TLS-Fingerprint-JA4"); db != nil && ja4 != "" { var application, userAgent, notes sql.NullString if err := db.QueryRowContext(req.Context(), "SELECT application, user_agent_string, notes FROM fingerprints WHERE ja4_fingerprint = ?", ja4).Scan(&application, &userAgent, ¬es); err == nil { @@ -192,11 +199,9 @@ func main() { } srv := &http.Server{ - Addr: *bind, - Handler: h, - TLSConfig: &tls.Config{ - GetCertificate: kpr.GetCertificate, - }, + Addr: *bind, + Handler: h, + TLSConfig: tc, } applyTLSFingerprinter(srv) @@ -281,20 +286,3 @@ func (kpr *keypairReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certifica return kpr.cert, nil } - -func uaParserPool() (*puddle.Pool[*uasurfer.UserAgent], error) { - cons := func(context.Context) (*uasurfer.UserAgent, error) { - return &uasurfer.UserAgent{}, nil - } - des := func(ua *uasurfer.UserAgent) { - ua.Reset() - } - - pool, err := puddle.NewPool(&puddle.Config[*uasurfer.UserAgent]{ - Constructor: cons, - Destructor: des, - MaxSize: 512, - }) - - return pool, err -} diff --git a/cmd/relayd/relayd.service b/cmd/relayd/relayd.service index 519d36a..f0f904c 100644 --- a/cmd/relayd/relayd.service +++ b/cmd/relayd/relayd.service @@ -13,6 +13,7 @@ CacheDirectoryMode=0755 StateDirectory=relayd StateDirectoryMode=0755 ReadWritePaths=/run +AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target
\ No newline at end of file |
