aboutsummaryrefslogtreecommitdiff
path: root/cmd/relayd/main.go
diff options
context:
space:
mode:
authorXe Iaso <me@xeiaso.net>2025-04-19 16:09:19 -0400
committerXe Iaso <me@xeiaso.net>2025-04-19 16:09:19 -0400
commitb5533db3c24657875901863126f46adae488e703 (patch)
treeae1b7dbdfb8ea1f292995481f531b6dc628de264 /cmd/relayd/main.go
parentb2bc32ad7a215e257616bde979a6febcef1332e1 (diff)
downloadx-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.go126
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
}