feat(osiris): add TCP and TLS fingerprinting

Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
Xe Iaso
2025-07-18 20:38:19 +00:00
parent 1eafebedbc
commit d9c4e37978
10 changed files with 773 additions and 11 deletions

View File

@@ -2,6 +2,7 @@ package entrypoint
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"net"
@@ -12,6 +13,7 @@ import (
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/fingerprint"
"github.com/hashicorp/hcl/v2/hclsimple"
"github.com/prometheus/client_golang/prometheus/promhttp"
"golang.org/x/sync/errgroup"
@@ -57,7 +59,7 @@ func Main(opts Options) error {
ln.Close()
}(gCtx)
slog.Info("listening for HTTP", "bind", cfg.Bind.HTTP)
slog.Info("listening", "for", "http", "bind", cfg.Bind.HTTP)
srv := http.Server{Handler: rtr, ErrorLog: internal.GetFilteredHTTPLogger()}
@@ -65,6 +67,35 @@ func Main(opts Options) error {
})
// HTTPS
g.Go(func() error {
ln, err := net.Listen("tcp", cfg.Bind.HTTPS)
if err != nil {
return fmt.Errorf("(https) can't bind to tcp %s: %w", cfg.Bind.HTTPS, err)
}
defer ln.Close()
go func(ctx context.Context) {
<-ctx.Done()
ln.Close()
}(gCtx)
tc := &tls.Config{
GetCertificate: rtr.GetCertificate,
}
srv := &http.Server{
Addr: cfg.Bind.HTTPS,
Handler: rtr,
ErrorLog: internal.GetFilteredHTTPLogger(),
TLSConfig: tc,
}
fingerprint.ApplyTLSFingerprinter(srv)
slog.Info("listening", "for", "https", "bind", cfg.Bind.HTTPS)
return srv.ServeTLS(ln, "", "")
})
// Metrics
g.Go(func() error {
@@ -101,12 +132,18 @@ func Main(opts Options) error {
}
})
slog.Info("listening for Metrics", "bind", cfg.Bind.Metrics)
slog.Info("listening", "for", "metrics", "bind", cfg.Bind.Metrics)
srv := http.Server{Handler: mux, ErrorLog: internal.GetFilteredHTTPLogger()}
srv := http.Server{
Addr: cfg.Bind.Metrics,
Handler: mux,
ErrorLog: internal.GetFilteredHTTPLogger(),
}
return srv.Serve(ln)
})
internal.SetHealth("osiris", healthv1.HealthCheckResponse_SERVING)
return g.Wait()
}

View File

@@ -2,6 +2,7 @@ package entrypoint
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
@@ -13,14 +14,17 @@ import (
"sync"
"github.com/TecharoHQ/anubis/cmd/osiris/internal/config"
"github.com/TecharoHQ/anubis/internal/fingerprint"
"github.com/lum8rjack/go-ja4h"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
ErrTargetInvalid = errors.New("[unexpected] target invalid")
ErrNoHandler = errors.New("[unexpected] no handler for domain")
ErrTargetInvalid = errors.New("[unexpected] target invalid")
ErrNoHandler = errors.New("[unexpected] no handler for domain")
ErrInvalidTLSKeypair = errors.New("[unexpected] invalid TLS keypair")
ErrNoCert = errors.New("this server does not have a certificate for that domain")
requestsPerDomain = promauto.NewGaugeVec(prometheus.GaugeOpts{
Namespace: "techaro",
@@ -36,13 +40,15 @@ var (
)
type Router struct {
lock sync.RWMutex
routes map[string]http.Handler
lock sync.RWMutex
routes map[string]http.Handler
tlsCerts map[string]*tls.Certificate
}
func (rtr *Router) setConfig(c config.Toplevel) error {
var errs []error
newMap := map[string]http.Handler{}
newCerts := map[string]*tls.Certificate{}
for _, d := range c.Domains {
var domainErrs []error
@@ -75,6 +81,13 @@ func (rtr *Router) setConfig(c config.Toplevel) error {
newMap[d.Name] = h
cert, err := tls.LoadX509KeyPair(d.TLS.Cert, d.TLS.Key)
if err != nil {
domainErrs = append(domainErrs, fmt.Errorf("%w: %w", ErrInvalidTLSKeypair, err))
}
newCerts[d.Name] = &cert
if len(domainErrs) != 0 {
errs = append(errs, fmt.Errorf("invalid domain %s: %w", d.Name, errors.Join(domainErrs...)))
}
@@ -86,11 +99,24 @@ func (rtr *Router) setConfig(c config.Toplevel) error {
rtr.lock.Lock()
rtr.routes = newMap
rtr.tlsCerts = newCerts
rtr.lock.Unlock()
return nil
}
func (rtr *Router) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
rtr.lock.RLock()
cert, ok := rtr.tlsCerts[hello.ServerName]
rtr.lock.RUnlock()
if !ok {
return nil, ErrNoCert
}
return cert, nil
}
func NewRouter(c config.Toplevel) (*Router, error) {
result := &Router{
routes: map[string]http.Handler{},
@@ -104,17 +130,23 @@ func NewRouter(c config.Toplevel) (*Router, error) {
}
func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestsPerDomain.WithLabelValues(r.Host).Inc()
var host = r.Host
if strings.Contains(host, ":") {
host, _, _ = net.SplitHostPort(host)
}
requestsPerDomain.WithLabelValues(host).Inc()
var h http.Handler
var ok bool
ja4hFP := ja4h.JA4H(r)
slog.Info("got request", "method", r.Method, "host", r.Host, "path", r.URL.Path)
slog.Info("got request", "method", r.Method, "host", host, "path", r.URL.Path)
rtr.lock.RLock()
h, ok = rtr.routes[r.Host]
h, ok = rtr.routes[host]
rtr.lock.RUnlock()
if !ok {
@@ -125,5 +157,18 @@ func (rtr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.Header.Set("X-Http-Ja4h-Fingerprint", ja4hFP)
if fp := fingerprint.GetTLSFingerprint(r); fp != nil {
if ja3n := fp.JA3N(); ja3n != nil {
r.Header.Set("X-Tls-Ja3n-Fingerprint", ja3n.String())
}
if ja4 := fp.JA4(); ja4 != nil {
r.Header.Set("X-Tls-Ja4-Fingerprint", ja4.String())
}
}
if tcpFP := fingerprint.GetTCPFingerprint(r); tcpFP != nil {
r.Header.Set("X-Tcp-Ja4t-Fingerprint", tcpFP.String())
}
h.ServeHTTP(w, r)
}

View File

@@ -4,7 +4,7 @@ bind {
metrics = ":9091"
}
domain "anubis.techaro.lol" {
domain "osiris.local.cetacean.club" {
tls {
cert = "./internal/config/testdata/tls/selfsigned.crt"
key = "./internal/config/testdata/tls/selfsigned.key"