diff options
| author | Xe Iaso <me@xeiaso.net> | 2025-04-19 16:09:19 -0400 |
|---|---|---|
| committer | Xe Iaso <me@xeiaso.net> | 2025-04-19 16:09:19 -0400 |
| commit | b5533db3c24657875901863126f46adae488e703 (patch) | |
| tree | ae1b7dbdfb8ea1f292995481f531b6dc628de264 /cmd/relayd/main.go | |
| parent | b2bc32ad7a215e257616bde979a6febcef1332e1 (diff) | |
| download | x-b5533db3c24657875901863126f46adae488e703.tar.xz x-b5533db3c24657875901863126f46adae488e703.zip | |
cmd/relayd: automagically reload TLS certificates, JA3N/JA4 fingerprints
Signed-off-by: Xe Iaso <me@xeiaso.net>
Diffstat (limited to 'cmd/relayd/main.go')
| -rw-r--r-- | cmd/relayd/main.go | 126 |
1 files changed, 118 insertions, 8 deletions
diff --git a/cmd/relayd/main.go b/cmd/relayd/main.go index 18d94cc..4ee685a 100644 --- a/cmd/relayd/main.go +++ b/cmd/relayd/main.go @@ -1,14 +1,20 @@ package main import ( + "crypto/tls" "flag" + "fmt" "log" "log/slog" + "net" "net/http" "net/http/httputil" "net/url" "os" + "os/signal" "path/filepath" + "sync" + "syscall" "time" "within.website/x/internal" @@ -68,12 +74,116 @@ func main() { log.Fatal(err) } - log.Fatal( - http.ListenAndServeTLS( - *bind, - cert, - key, - httputil.NewSingleHostReverseProxy(u), - ), - ) + kpr, err := NewKeypairReloader(cert, key) + if err != nil { + log.Fatal(err) + } + + h := httputil.NewSingleHostReverseProxy(u) + oldDirector := h.Director + + h.Director = func(req *http.Request) { + oldDirector(req) + + host, _, _ := net.SplitHostPort(req.RemoteAddr) + if host != "" { + req.Header.Set("X-Real-Ip", host) + } + + fp := GetTLSFingerprint(req) + if fp != nil { + req.Header.Set("JA3N-Fingerprint", fp.JA3N().String()) + req.Header.Set("JA4-Fingerprint", fp.JA4().String()) + } + } + + srv := &http.Server{ + Addr: *bind, + Handler: h, + TLSConfig: &tls.Config{ + GetCertificate: kpr.GetCertificate, + }, + } + + applyTLSFingerprinter(srv) + + log.Fatal(srv.ListenAndServeTLS("", "")) +} + +type keypairReloader struct { + certMu sync.RWMutex + cert *tls.Certificate + certPath string + keyPath string + modTime time.Time +} + +func NewKeypairReloader(certPath, keyPath string) (*keypairReloader, error) { + result := &keypairReloader{ + certPath: certPath, + keyPath: keyPath, + } + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, err + } + result.cert = &cert + + st, err := os.Stat(certPath) + if err != nil { + return nil, err + } + result.modTime = st.ModTime() + + go func() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP) + for range c { + slog.Info("got SIGHUP") + if err := result.maybeReload(); err != nil { + slog.Error("can't load tls cert", "err", err) + } + } + }() + return result, nil +} + +func (kpr *keypairReloader) maybeReload() error { + slog.Info("loading new keypair", "cert", kpr.certPath, "key", kpr.keyPath) + newCert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath) + if err != nil { + return err + } + + st, err := os.Stat(kpr.certPath) + if err != nil { + return err + } + + kpr.certMu.Lock() + defer kpr.certMu.Unlock() + kpr.cert = &newCert + kpr.modTime = st.ModTime() + + return nil +} + +func (kpr *keypairReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { + kpr.certMu.RLock() + defer kpr.certMu.RUnlock() + + st, err := os.Stat(kpr.certPath) + if err != nil { + return nil, fmt.Errorf("internal error: stat(%q): %q", kpr.certPath, err) + } + + if st.ModTime().After(kpr.modTime) { + kpr.certMu.RUnlock() + if err := kpr.maybeReload(); err != nil { + return nil, fmt.Errorf("can't reload cert: %w", err) + } + kpr.certMu.RLock() + } + + return kpr.cert, nil } |
