From ad9422000710b4bbae44b5b5020484628d82b8b4 Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Tue, 30 Dec 2025 21:53:06 +0100 Subject: [PATCH 1/9] chore: added go-proxyproto --- go.mod | 1 + go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/go.mod b/go.mod index ac48abd98..c9f5eee0a 100644 --- a/go.mod +++ b/go.mod @@ -146,6 +146,7 @@ require ( github.com/natefinch/atomic v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pjbgf/sha1cd v0.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect diff --git a/go.sum b/go.sum index e8a4615bf..f6fa175a6 100644 --- a/go.sum +++ b/go.sum @@ -328,6 +328,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0= +github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY= github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= From cfd0528df1f0dc79fd89748b35731311a95e795e Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Wed, 31 Dec 2025 07:15:15 +0100 Subject: [PATCH 2/9] feat: added proxy protocol options, Listenerwrapper, ConnContext and HttpHandler --- cmd/anubis/main.go | 161 ++++++++++++++++++++++---------------- internal/headers.go | 33 ++++++++ internal/proxyprotocol.go | 42 ++++++++++ lib/config.go | 52 ++++++------ 4 files changed, 197 insertions(+), 91 deletions(-) create mode 100644 internal/proxyprotocol.go diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index e21eeca1d..f5dd57ff5 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -37,53 +37,56 @@ import ( "github.com/TecharoHQ/anubis/web" "github.com/facebookgo/flagenv" _ "github.com/joho/godotenv/autoload" + "github.com/pires/go-proxyproto" "github.com/prometheus/client_golang/prometheus/promhttp" healthv1 "google.golang.org/grpc/health/grpc_health_v1" ) var ( - basePrefix = flag.String("base-prefix", "", "base prefix (root URL) the application is served under e.g. /myapp") - bind = flag.String("bind", ":8923", "network address to bind HTTP to") - bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp") - challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge") - cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for") - cookieDynamicDomain = flag.Bool("cookie-dynamic-domain", false, "if set, automatically set the cookie Domain value based on the request domain") - cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") - cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis") - cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") - difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims") - useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.") - forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header") - hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") - cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies") - cookieSameSite = flag.String("cookie-same-site", "None", "sets the same site option on Anubis cookies, will auto-downgrade None to Lax if cookie-secure is false. Valid values are None, Lax, Strict, and Default.") - ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned") - ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex") - metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to") - metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to") - socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.") - robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots") - policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)") - redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.") - slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)") - stripBasePrefix = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server") - target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request") - targetSNI = flag.String("target-sni", "", "if set, TLS handshake hostname when forwarding requests to the target, if set to auto, use Host header") - targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target") - targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend") - targetDisableKeepAlive = flag.Bool("target-disable-keepalive", false, "if true, disables HTTP keep-alive for the backend") - healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis") - useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal") - debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate") - ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough") - ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time") - ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache") - extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder") - webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals") - versionFlag = flag.Bool("version", false, "print Anubis version") - publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).") - xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For") - customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)") + basePrefix = flag.String("base-prefix", "", "base prefix (root URL) the application is served under e.g. /myapp") + bind = flag.String("bind", ":8923", "network address to bind HTTP to") + bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp") + challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge") + cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for") + cookieDynamicDomain = flag.Bool("cookie-dynamic-domain", false, "if set, automatically set the cookie Domain value based on the request domain") + cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") + cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis") + cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") + difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims") + useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.") + forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header") + hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") + cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies") + cookieSameSite = flag.String("cookie-same-site", "None", "sets the same site option on Anubis cookies, will auto-downgrade None to Lax if cookie-secure is false. Valid values are None, Lax, Strict, and Default.") + ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned") + ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex") + metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to") + metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to") + socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.") + robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots") + policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)") + redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.") + slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)") + stripBasePrefix = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server") + target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request") + targetSNI = flag.String("target-sni", "", "if set, TLS handshake hostname when forwarding requests to the target, if set to auto, use Host header") + targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target") + targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend") + targetDisableKeepAlive = flag.Bool("target-disable-keepalive", false, "if true, disables HTTP keep-alive for the backend") + healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis") + useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal") + useProxyProtocol = flag.Bool("use-proxy-protocol", false, "read the client's IP address from the proxy protocol header of an incoming connection (see https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)") + proxyProtocolAllowedCIDRs = flag.String("proxy-protocol-allowed-cidrs", "", "List of CIDRs or IPs seperated by commas from which to accept Proxy Headers to fill in the servers information about the requests source address") + debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate") + ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough") + ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time") + ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache") + extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder") + webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals") + versionFlag = flag.Bool("version", false, "print Anubis version") + publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).") + xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For") + customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)") thothInsecure = flag.Bool("thoth-insecure", false, "if set, connect to Thoth over plain HTTP/2, don't enable this unless support told you to") thothURL = flag.String("thoth-url", "", "if set, URL for Thoth, the IP reputation database for Anubis") @@ -430,6 +433,18 @@ func main() { lg.Warn("REDIRECT_DOMAINS is not set, Anubis will only redirect to the same domain a request is coming from, see https://anubis.techaro.lol/docs/admin/configuration/redirect-domains") } + var proxyProtocolAllowedCIDRsList []string + if *proxyProtocolAllowedCIDRs != "" { + cidrs := strings.Split(*proxyProtocolAllowedCIDRs, ",") + for _, cidr := range cidrs { + _, _, err = net.ParseCIDR(cidr) + if err != nil { + log.Fatalf("cannot parse cidr %q: %s", cidr, err.Error()) + } + proxyProtocolAllowedCIDRsList = append(proxyProtocolAllowedCIDRsList, strings.TrimSpace(cidr)) + } + } + anubis.CookieName = *cookiePrefix + "-auth" anubis.TestCookieName = *cookiePrefix + "-cookie-verification" anubis.ForcedLanguage = *forcedLanguage @@ -445,30 +460,31 @@ func main() { } s, err := libanubis.New(libanubis.Options{ - BasePrefix: *basePrefix, - StripBasePrefix: *stripBasePrefix, - Next: rp, - Policy: policy, - TargetHost: *targetHost, - TargetSNI: *targetSNI, - TargetInsecureSkipVerify: *targetInsecureSkipVerify, - ServeRobotsTXT: *robotsTxt, - ED25519PrivateKey: ed25519Priv, - HS512Secret: []byte(*hs512Secret), - CookieDomain: *cookieDomain, - CookieDynamicDomain: *cookieDynamicDomain, - CookieExpiration: *cookieExpiration, - CookiePartitioned: *cookiePartitioned, - RedirectDomains: redirectDomainsList, - Target: *target, - WebmasterEmail: *webmasterEmail, - OpenGraph: policy.OpenGraph, - CookieSecure: *cookieSecure, - CookieSameSite: parseSameSite(*cookieSameSite), - PublicUrl: *publicUrl, - JWTRestrictionHeader: *jwtRestrictionHeader, - Logger: policy.Logger.With("subsystem", "anubis"), - DifficultyInJWT: *difficultyInJWT, + BasePrefix: *basePrefix, + StripBasePrefix: *stripBasePrefix, + Next: rp, + Policy: policy, + TargetHost: *targetHost, + TargetSNI: *targetSNI, + TargetInsecureSkipVerify: *targetInsecureSkipVerify, + ServeRobotsTXT: *robotsTxt, + ED25519PrivateKey: ed25519Priv, + HS512Secret: []byte(*hs512Secret), + CookieDomain: *cookieDomain, + CookieDynamicDomain: *cookieDynamicDomain, + CookieExpiration: *cookieExpiration, + CookiePartitioned: *cookiePartitioned, + RedirectDomains: redirectDomainsList, + ProxyProtocolAllowedCIDRs: proxyProtocolAllowedCIDRsList, + Target: *target, + WebmasterEmail: *webmasterEmail, + OpenGraph: policy.OpenGraph, + CookieSecure: *cookieSecure, + CookieSameSite: parseSameSite(*cookieSameSite), + PublicUrl: *publicUrl, + JWTRestrictionHeader: *jwtRestrictionHeader, + Logger: policy.Logger.With("subsystem", "anubis"), + DifficultyInJWT: *difficultyInJWT, }) if err != nil { log.Fatalf("can't construct libanubis.Server: %v", err) @@ -481,9 +497,21 @@ func main() { h = internal.XForwardedForToXRealIP(h) h = internal.XForwardedForUpdate(*xffStripPrivate, h) h = internal.JA4H(h) + h = internal.ProxyProtocol(*useProxyProtocol, h) srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()} listener, listenerUrl := setupListener(*bindNetwork, *bind) + + if *useProxyProtocol { + srv.ConnContext = internal.ProxyProtoConnContext() + var policyFunc proxyproto.PolicyFunc + policyFunc = proxyproto.MustStrictWhiteListPolicy(proxyProtocolAllowedCIDRsList) + listener = &proxyproto.Listener{ + Listener: listener, + Policy: policyFunc, + } + } + lg.Info( "listening", "url", listenerUrl, @@ -492,6 +520,7 @@ func main() { "target", *target, "version", anubis.Version, "use-remote-address", *useRemoteAddress, + "use-proxy-protocol", *useProxyProtocol, "debug-benchmark-js", *debugBenchmarkJS, "og-passthrough", *ogPassthrough, "og-expiry-time", *ogTimeToLive, diff --git a/internal/headers.go b/internal/headers.go index 5293cfbd6..c4276383d 100644 --- a/internal/headers.go +++ b/internal/headers.go @@ -62,6 +62,39 @@ func CustomRealIPHeader(customRealIPHeaderValue string, next http.Handler) http. }) } +// ProxyProtocol sets the X-Real-Ip header to the request's real IP set from Proxy Protocol Headers +func ProxyProtocol(useProxyProtocol bool, next http.Handler) http.Handler { + if !useProxyProtocol { + slog.Debug("skipping middleware, useProxyProtocol is empty") + return next + } + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !ProxyProtocolUsed(r.Context()) { + next.ServeHTTP(w, r) + return + } + + hdr, ok := ProxyProtocolHeader(r.Context()) + if !ok || hdr.SourceAddr == nil { + next.ServeHTTP(w, r) + return + } + + host, _, err := net.SplitHostPort(hdr.SourceAddr.String()) + if err == nil { + r.Header.Set("X-Real-Ip", host) + if addr, err := netip.ParseAddr(host); err == nil { + r = r.WithContext( + context.WithValue(r.Context(), realIPKey{}, addr), + ) + } + } + + next.ServeHTTP(w, r) + }) +} + // RemoteXRealIP sets the X-Real-Ip header to the request's real IP if // the setting is enabled by the user. func RemoteXRealIP(useRemoteAddress bool, bindNetwork string, next http.Handler) http.Handler { diff --git a/internal/proxyprotocol.go b/internal/proxyprotocol.go new file mode 100644 index 000000000..b50d336e8 --- /dev/null +++ b/internal/proxyprotocol.go @@ -0,0 +1,42 @@ +package internal + +import ( + "context" + "net" + + "github.com/pires/go-proxyproto" +) + +type proxyProtocolUsedKey struct{} + +func ProxyProtocolUsed(ctx context.Context) bool { + v, ok := ctx.Value(proxyProtocolUsedKey{}).(bool) + return ok && v +} + +type proxyProtocolHeaderKey struct{} + +func ProxyProtocolHeader(ctx context.Context) (proxyproto.Header, bool) { + v, ok := ctx.Value(proxyProtocolUsedKey{}).(proxyproto.Header) + return v, ok +} + +func ProxyProtoConnContext() func(ctx context.Context, c net.Conn) context.Context { + return func(ctx context.Context, c net.Conn) context.Context { + ppConn, ok := c.(*proxyproto.Conn) + if !ok { + return ctx + } + + hdr := ppConn.ProxyHeader() + if hdr == nil { + return context.WithValue(ctx, proxyProtocolUsedKey{}, false) + } + + ctx = context.WithValue(ctx, proxyProtocolUsedKey{}, true) + + ctx = context.WithValue(ctx, proxyProtocolHeaderKey{}, hdr) + + return ctx + } +} diff --git a/lib/config.go b/lib/config.go index cb98e8a0d..fb4f61cc6 100644 --- a/lib/config.go +++ b/lib/config.go @@ -28,31 +28,33 @@ import ( ) type Options struct { - Next http.Handler - Policy *policy.ParsedConfig - Target string - TargetHost string - TargetSNI string - TargetInsecureSkipVerify bool - CookieDynamicDomain bool - CookieDomain string - CookieExpiration time.Duration - CookiePartitioned bool - BasePrefix string - WebmasterEmail string - RedirectDomains []string - ED25519PrivateKey ed25519.PrivateKey - HS512Secret []byte - StripBasePrefix bool - OpenGraph config.OpenGraph - ServeRobotsTXT bool - CookieSecure bool - CookieSameSite http.SameSite - Logger *slog.Logger - LogLevel string - PublicUrl string - JWTRestrictionHeader string - DifficultyInJWT bool + Next http.Handler + Policy *policy.ParsedConfig + Target string + TargetHost string + TargetSNI string + TargetInsecureSkipVerify bool + CookieDynamicDomain bool + CookieDomain string + CookieExpiration time.Duration + CookiePartitioned bool + BasePrefix string + WebmasterEmail string + RedirectDomains []string + ED25519PrivateKey ed25519.PrivateKey + HS512Secret []byte + StripBasePrefix bool + OpenGraph config.OpenGraph + ServeRobotsTXT bool + CookieSecure bool + CookieSameSite http.SameSite + Logger *slog.Logger + LogLevel string + PublicUrl string + JWTRestrictionHeader string + DifficultyInJWT bool + ProxyProtocolTimeout time.Duration + ProxyProtocolAllowedCIDRs []string } func LoadPoliciesOrDefault(ctx context.Context, fname string, defaultDifficulty int, logLevel string) (*policy.ParsedConfig, error) { From 32f9920ef8506fc231342c6a9e804db466add7c6 Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Wed, 31 Dec 2025 08:05:48 +0100 Subject: [PATCH 3/9] fix: wrong return type --- internal/proxyprotocol.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/proxyprotocol.go b/internal/proxyprotocol.go index b50d336e8..366cf83c7 100644 --- a/internal/proxyprotocol.go +++ b/internal/proxyprotocol.go @@ -16,9 +16,9 @@ func ProxyProtocolUsed(ctx context.Context) bool { type proxyProtocolHeaderKey struct{} -func ProxyProtocolHeader(ctx context.Context) (proxyproto.Header, bool) { - v, ok := ctx.Value(proxyProtocolUsedKey{}).(proxyproto.Header) - return v, ok +func ProxyProtocolHeader(ctx context.Context) (*proxyproto.Header, bool) { + h, ok := ctx.Value(proxyProtocolHeaderKey{}).(*proxyproto.Header) + return h, ok } func ProxyProtoConnContext() func(ctx context.Context, c net.Conn) context.Context { @@ -37,6 +37,8 @@ func ProxyProtoConnContext() func(ctx context.Context, c net.Conn) context.Conte ctx = context.WithValue(ctx, proxyProtocolHeaderKey{}, hdr) + v, ok := ctx.Value(proxyProtocolHeaderKey{}).(proxyproto.Header) + println(v.DestinationAddr) return ctx } } From ddd5ba0fcf15705e85135acc7965c2f36fde54fb Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Wed, 31 Dec 2025 16:37:07 +0100 Subject: [PATCH 4/9] feat: added proxyProtocolPolicy and proxyProtocolReadHeaderTimeout options --- cmd/anubis/main.go | 106 +++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index f5dd57ff5..31b23dc71 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -43,50 +43,52 @@ import ( ) var ( - basePrefix = flag.String("base-prefix", "", "base prefix (root URL) the application is served under e.g. /myapp") - bind = flag.String("bind", ":8923", "network address to bind HTTP to") - bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp") - challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge") - cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for") - cookieDynamicDomain = flag.Bool("cookie-dynamic-domain", false, "if set, automatically set the cookie Domain value based on the request domain") - cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") - cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis") - cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") - difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims") - useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.") - forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header") - hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") - cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies") - cookieSameSite = flag.String("cookie-same-site", "None", "sets the same site option on Anubis cookies, will auto-downgrade None to Lax if cookie-secure is false. Valid values are None, Lax, Strict, and Default.") - ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned") - ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex") - metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to") - metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to") - socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.") - robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots") - policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)") - redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.") - slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)") - stripBasePrefix = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server") - target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request") - targetSNI = flag.String("target-sni", "", "if set, TLS handshake hostname when forwarding requests to the target, if set to auto, use Host header") - targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target") - targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend") - targetDisableKeepAlive = flag.Bool("target-disable-keepalive", false, "if true, disables HTTP keep-alive for the backend") - healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis") - useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal") - useProxyProtocol = flag.Bool("use-proxy-protocol", false, "read the client's IP address from the proxy protocol header of an incoming connection (see https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)") - proxyProtocolAllowedCIDRs = flag.String("proxy-protocol-allowed-cidrs", "", "List of CIDRs or IPs seperated by commas from which to accept Proxy Headers to fill in the servers information about the requests source address") - debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate") - ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough") - ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time") - ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache") - extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder") - webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals") - versionFlag = flag.Bool("version", false, "print Anubis version") - publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).") - xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For") - customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)") + basePrefix = flag.String("base-prefix", "", "base prefix (root URL) the application is served under e.g. /myapp") + bind = flag.String("bind", ":8923", "network address to bind HTTP to") + bindNetwork = flag.String("bind-network", "tcp", "network family to bind HTTP to, e.g. unix, tcp") + challengeDifficulty = flag.Int("difficulty", anubis.DefaultDifficulty, "difficulty of the challenge") + cookieDomain = flag.String("cookie-domain", "", "if set, the top-level domain that the Anubis cookie will be valid for") + cookieDynamicDomain = flag.Bool("cookie-dynamic-domain", false, "if set, automatically set the cookie Domain value based on the request domain") + cookieExpiration = flag.Duration("cookie-expiration-time", anubis.CookieDefaultExpirationTime, "The amount of time the authorization cookie is valid for") + cookiePrefix = flag.String("cookie-prefix", anubis.CookieName, "prefix for browser cookies created by Anubis") + cookiePartitioned = flag.Bool("cookie-partitioned", false, "if true, sets the partitioned flag on Anubis cookies, enabling CHIPS support") + difficultyInJWT = flag.Bool("difficulty-in-jwt", false, "if true, adds a difficulty field in the JWT claims") + useSimplifiedExplanation = flag.Bool("use-simplified-explanation", false, "if true, replaces the text when clicking \"Why am I seeing this?\" with a more simplified text for a non-tech-savvy audience.") + forcedLanguage = flag.String("forced-language", "", "if set, this language is being used instead of the one from the request's Accept-Language header") + hs512Secret = flag.String("hs512-secret", "", "secret used to sign JWTs, uses ed25519 if not set") + cookieSecure = flag.Bool("cookie-secure", true, "if true, sets the secure flag on Anubis cookies") + cookieSameSite = flag.String("cookie-same-site", "None", "sets the same site option on Anubis cookies, will auto-downgrade None to Lax if cookie-secure is false. Valid values are None, Lax, Strict, and Default.") + ed25519PrivateKeyHex = flag.String("ed25519-private-key-hex", "", "private key used to sign JWTs, if not set a random one will be assigned") + ed25519PrivateKeyHexFile = flag.String("ed25519-private-key-hex-file", "", "file name containing value for ed25519-private-key-hex") + metricsBind = flag.String("metrics-bind", ":9090", "network address to bind metrics to") + metricsBindNetwork = flag.String("metrics-bind-network", "tcp", "network family for the metrics server to bind to") + socketMode = flag.String("socket-mode", "0770", "socket mode (permissions) for unix domain sockets.") + robotsTxt = flag.Bool("serve-robots-txt", false, "serve a robots.txt file that disallows all robots") + policyFname = flag.String("policy-fname", "", "full path to anubis policy document (defaults to a sensible built-in policy)") + redirectDomains = flag.String("redirect-domains", "", "list of domains separated by commas which anubis is allowed to redirect to. Leaving this unset allows any domain.") + slogLevel = flag.String("slog-level", "INFO", "logging level (see https://pkg.go.dev/log/slog#hdr-Levels)") + stripBasePrefix = flag.Bool("strip-base-prefix", false, "if true, strips the base prefix from requests forwarded to the target server") + target = flag.String("target", "http://localhost:3923", "target to reverse proxy to, set to an empty string to disable proxying when only using auth request") + targetSNI = flag.String("target-sni", "", "if set, TLS handshake hostname when forwarding requests to the target, if set to auto, use Host header") + targetHost = flag.String("target-host", "", "if set, the value of the Host header when forwarding requests to the target") + targetInsecureSkipVerify = flag.Bool("target-insecure-skip-verify", false, "if true, skips TLS validation for the backend") + targetDisableKeepAlive = flag.Bool("target-disable-keepalive", false, "if true, disables HTTP keep-alive for the backend") + healthcheck = flag.Bool("healthcheck", false, "run a health check against Anubis") + useRemoteAddress = flag.Bool("use-remote-address", false, "read the client's IP address from the network request, useful for debugging and running Anubis on bare metal") + useProxyProtocol = flag.Bool("use-proxy-protocol", false, "read the client's IP address from the proxy protocol header of an incoming connection (see https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)") + proxyProtocolAllowedCIDRs = flag.String("proxy-protocol-allowed-cidrs", "", "List of CIDRs or IPs seperated by commas from which to accept Proxy Headers to fill in the servers information about the requests source address") + proxyProtocolPolicy = flag.String("proxy-protocol-policy", "", "The Policy to be used if the Connection doesn't come from allowed cidrs, possible: IGNORE, REJECT, IGNORE ignores the proxy headers, REJECT rejects the whole connection") + proxyProtocolReadHeaderTimeout = flag.Duration("proxy-protocol-read-header-timeout", 5*time.Second, "The Duration in which the ProxyHeaders should be received, if Proxy Protocol IGNORE is set to let traffic without Proxy Protcol Headers pass through this should be set lower") + debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate") + ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough") + ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time") + ogCacheConsiderHost = flag.Bool("og-cache-consider-host", false, "enable or disable the use of the host in the Open Graph tag cache") + extractResources = flag.String("extract-resources", "", "if set, extract the static resources to the specified folder") + webmasterEmail = flag.String("webmaster-email", "", "if set, displays webmaster's email on the reject page for appeals") + versionFlag = flag.Bool("version", false, "print Anubis version") + publicUrl = flag.String("public-url", "", "the externally accessible URL for this Anubis instance, used for constructing redirect URLs (e.g., for forwardAuth).") + xffStripPrivate = flag.Bool("xff-strip-private", true, "if set, strip private addresses from X-Forwarded-For") + customRealIPHeader = flag.String("custom-real-ip-header", "", "if set, read remote IP from header of this name (in case your environment doesn't set X-Real-IP header)") thothInsecure = flag.Bool("thoth-insecure", false, "if set, connect to Thoth over plain HTTP/2, don't enable this unless support told you to") thothURL = flag.String("thoth-url", "", "if set, URL for Thoth, the IP reputation database for Anubis") @@ -504,11 +506,21 @@ func main() { if *useProxyProtocol { srv.ConnContext = internal.ProxyProtoConnContext() + // This is pretty rudamentary, a better solution would be to have an object like [[[10.0.2.0/4, 192.168.178.20/32], IGNORE],[[10.0.2.0/4], USE]](same values just for demonstration) see https://github.com/pires/go-proxyproto/blob/eef9d7ef24f63315b173a5438e7b4c7849bc26b0/policy.go#L32 + // also refactor into setupListener var policyFunc proxyproto.PolicyFunc - policyFunc = proxyproto.MustStrictWhiteListPolicy(proxyProtocolAllowedCIDRsList) + switch *proxyProtocolPolicy { + case "IGNORE": + policyFunc = proxyproto.MustLaxWhiteListPolicy(proxyProtocolAllowedCIDRsList) + case "REJECT": + policyFunc = proxyproto.MustStrictWhiteListPolicy(proxyProtocolAllowedCIDRsList) + default: + log.Fatalf("invalid Proxy Protocol Policy: %s, valid values are IGNORE, REJECT", *proxyProtocolPolicy) + } listener = &proxyproto.Listener{ - Listener: listener, - Policy: policyFunc, + Listener: listener, + Policy: policyFunc, + ReadHeaderTimeout: *proxyProtocolReadHeaderTimeout, } } From 96eca701ffe362e358af0a36e6f4a3f9a783a04f Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Wed, 31 Dec 2025 16:41:09 +0100 Subject: [PATCH 5/9] chore: removed dbg line --- internal/proxyprotocol.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/proxyprotocol.go b/internal/proxyprotocol.go index 366cf83c7..be46e948e 100644 --- a/internal/proxyprotocol.go +++ b/internal/proxyprotocol.go @@ -38,7 +38,6 @@ func ProxyProtoConnContext() func(ctx context.Context, c net.Conn) context.Conte ctx = context.WithValue(ctx, proxyProtocolHeaderKey{}, hdr) v, ok := ctx.Value(proxyProtocolHeaderKey{}).(proxyproto.Header) - println(v.DestinationAddr) return ctx } } From 04672638d1cac5850cec024597fd2ef2c9edcd8c Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Wed, 31 Dec 2025 16:41:45 +0100 Subject: [PATCH 6/9] chore: see previous --- internal/proxyprotocol.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/proxyprotocol.go b/internal/proxyprotocol.go index be46e948e..c50c27192 100644 --- a/internal/proxyprotocol.go +++ b/internal/proxyprotocol.go @@ -34,10 +34,8 @@ func ProxyProtoConnContext() func(ctx context.Context, c net.Conn) context.Conte } ctx = context.WithValue(ctx, proxyProtocolUsedKey{}, true) - ctx = context.WithValue(ctx, proxyProtocolHeaderKey{}, hdr) - v, ok := ctx.Value(proxyProtocolHeaderKey{}).(proxyproto.Header) return ctx } } From 1b019a15c5c648412c93eebb9ee3788330c3b48e Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Wed, 31 Dec 2025 18:42:28 +0100 Subject: [PATCH 7/9] feat: add sendProxyProtocolHeaderVersion option and implemented functionality --- cmd/anubis/main.go | 3 +- internal/headers.go | 31 +++++++++++++++--- internal/proxyprotocol.go | 67 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 5 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index 31b23dc71..ce71a29d7 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -79,6 +79,7 @@ var ( proxyProtocolAllowedCIDRs = flag.String("proxy-protocol-allowed-cidrs", "", "List of CIDRs or IPs seperated by commas from which to accept Proxy Headers to fill in the servers information about the requests source address") proxyProtocolPolicy = flag.String("proxy-protocol-policy", "", "The Policy to be used if the Connection doesn't come from allowed cidrs, possible: IGNORE, REJECT, IGNORE ignores the proxy headers, REJECT rejects the whole connection") proxyProtocolReadHeaderTimeout = flag.Duration("proxy-protocol-read-header-timeout", 5*time.Second, "The Duration in which the ProxyHeaders should be received, if Proxy Protocol IGNORE is set to let traffic without Proxy Protcol Headers pass through this should be set lower") + proxyProtocolSendVersion = flag.String("proxy-protocol-send-version", "", "The Version of Proxy Protocol and if to send to the target, possible values: v1, v2") debugBenchmarkJS = flag.Bool("debug-benchmark-js", false, "respond to every request with a challenge for benchmarking hashrate") ogPassthrough = flag.Bool("og-passthrough", false, "enable Open Graph tag passthrough") ogTimeToLive = flag.Duration("og-expiry-time", 24*time.Hour, "Open Graph tag cache expiration time") @@ -499,7 +500,7 @@ func main() { h = internal.XForwardedForToXRealIP(h) h = internal.XForwardedForUpdate(*xffStripPrivate, h) h = internal.JA4H(h) - h = internal.ProxyProtocol(*useProxyProtocol, h) + h = internal.ProxyProtocol(*useProxyProtocol, *proxyProtocolSendVersion, h) srv := http.Server{Handler: h, ErrorLog: internal.GetFilteredHTTPLogger()} listener, listenerUrl := setupListener(*bindNetwork, *bind) diff --git a/internal/headers.go b/internal/headers.go index c4276383d..c70997bea 100644 --- a/internal/headers.go +++ b/internal/headers.go @@ -62,8 +62,8 @@ func CustomRealIPHeader(customRealIPHeaderValue string, next http.Handler) http. }) } -// ProxyProtocol sets the X-Real-Ip header to the request's real IP set from Proxy Protocol Headers -func ProxyProtocol(useProxyProtocol bool, next http.Handler) http.Handler { +// ProxyProtocol sets the X-Real-Ip header to the request's real IP set from Proxy Protocol Headers also it sets the request context with proxyProtocolInfo to send Proxy Protocol Heaaders +func ProxyProtocol(useProxyProtocol bool, sendProxyProtocol string, next http.Handler) http.Handler { if !useProxyProtocol { slog.Debug("skipping middleware, useProxyProtocol is empty") return next @@ -84,11 +84,34 @@ func ProxyProtocol(useProxyProtocol bool, next http.Handler) http.Handler { host, _, err := net.SplitHostPort(hdr.SourceAddr.String()) if err == nil { r.Header.Set("X-Real-Ip", host) - if addr, err := netip.ParseAddr(host); err == nil { + if addr, err := netip.ParseAddrPort(host); err == nil { r = r.WithContext( - context.WithValue(r.Context(), realIPKey{}, addr), + context.WithValue(r.Context(), proxyProtocolInfoKey{}, ProxyProtocolInfo{ + AddrPort: addr, + }), ) + + } + } + // stolen from caddyserver :) + if sendProxyProtocol != "" { + address := r.Header.Get("X-Real-Ip") + addrPort, err := netip.ParseAddrPort(address) + if err != nil { + // OK; probably didn't have a port + addr, err := netip.ParseAddr(address) + if err != nil { + // Doesn't seem like a valid ip address at all + } else { + // Ok, only the port was missing + addrPort = netip.AddrPortFrom(addr, 0) + } } + r = r.WithContext( + context.WithValue(r.Context(), proxyProtocolInfoKey{}, ProxyProtocolInfo{ + AddrPort: addrPort, + }), + ) } next.ServeHTTP(w, r) diff --git a/internal/proxyprotocol.go b/internal/proxyprotocol.go index c50c27192..b75994b05 100644 --- a/internal/proxyprotocol.go +++ b/internal/proxyprotocol.go @@ -2,7 +2,9 @@ package internal import ( "context" + "fmt" "net" + "net/netip" "github.com/pires/go-proxyproto" ) @@ -21,6 +23,17 @@ func ProxyProtocolHeader(ctx context.Context) (*proxyproto.Header, bool) { return h, ok } +type proxyProtocolInfoKey struct{} + +func proxyProtocolInfo(ctx context.Context) (ProxyProtocolInfo, bool) { + h, ok := ctx.Value(proxyProtocolInfoKey{}).(ProxyProtocolInfo) + return h, ok +} + +type ProxyProtocolInfo struct { + AddrPort netip.AddrPort +} + func ProxyProtoConnContext() func(ctx context.Context, c net.Conn) context.Context { return func(ctx context.Context, c net.Conn) context.Context { ppConn, ok := c.(*proxyproto.Conn) @@ -39,3 +52,57 @@ func ProxyProtoConnContext() func(ctx context.Context, c net.Conn) context.Conte return ctx } } + +func SendProxyProtocolDialer(dialer *net.Dialer, proxyProtocolVersion string) func(ctx context.Context, network, addr string) (net.Conn, error) { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := dialer.DialContext(ctx, network, addr) + if err != nil { + return nil, fmt.Errorf("dial error: %w", err) + } + // stolen from caddyserver :) + proxyProtocolInfo, ok := proxyProtocolInfo(ctx) + if !ok { + return nil, fmt.Errorf("failed to get proxy protocol info from context") + } + var proxyv byte + switch proxyProtocolVersion { + case "v1": + proxyv = 1 + case "v2": + proxyv = 2 + default: + return nil, fmt.Errorf("unexpected proxy protocol version") + } + + // The src and dst have to be of the same address family. As we don't know the original + // dst address (it's kind of impossible to know) and this address is generally of very + // little interest, we just set it to all zeros. + var destAddr net.Addr + switch { + case proxyProtocolInfo.AddrPort.Addr().Is4(): + destAddr = &net.TCPAddr{ + IP: net.IPv4zero, + } + case proxyProtocolInfo.AddrPort.Addr().Is6(): + destAddr = &net.TCPAddr{ + IP: net.IPv6zero, + } + default: + return nil, fmt.Errorf("unexpected remote addr type in proxy protocol info") + } + sourceAddr := &net.TCPAddr{ + IP: proxyProtocolInfo.AddrPort.Addr().AsSlice(), + Port: int(proxyProtocolInfo.AddrPort.Port()), + Zone: proxyProtocolInfo.AddrPort.Addr().Zone(), + } + header := proxyproto.HeaderProxyFromAddrs(proxyv, sourceAddr, destAddr) + + _, err = header.WriteTo(conn) + + if err != nil { + return nil, err + } + + return conn, nil + } +} From 54f015b474c3fefdfd47c83ce0e6917da093e828 Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Wed, 31 Dec 2025 18:52:39 +0100 Subject: [PATCH 8/9] fix: add DialCOntext to Transport --- cmd/anubis/main.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index ce71a29d7..cb56c40a8 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -214,7 +214,7 @@ func setupListener(network string, address string) (net.Listener, string) { return listener, formattedAddress } -func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool) (http.Handler, error) { +func makeReverseProxy(target string, targetSNI string, targetHost string, insecureSkipVerify bool, targetDisableKeepAlive bool, proxyProtocolVersion string) (http.Handler, error) { targetUri, err := url.Parse(target) if err != nil { return nil, fmt.Errorf("failed to parse target URL: %w", err) @@ -233,13 +233,18 @@ func makeReverseProxy(target string, targetSNI string, targetHost string, insecu targetUri.Path = "" // tell transport how to dial unix sockets transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { - dialer := net.Dialer{} + dialer := &net.Dialer{} return dialer.DialContext(ctx, "unix", addr) } // tell transport how to handle the unix url scheme transport.RegisterProtocol("unix", libanubis.UnixRoundTripper{Transport: transport}) } + if proxyProtocolVersion != "" { + dialer := &net.Dialer{} + transport.DialContext = internal.SendProxyProtocolDialer(dialer, proxyProtocolVersion) + } + if insecureSkipVerify || targetSNI != "" { transport.TLSClientConfig = &tls.Config{} } @@ -318,7 +323,7 @@ func main() { // when using anubis via Systemd and environment variables, then it is not possible to set targe to an empty string but only to space if strings.TrimSpace(*target) != "" { var err error - rp, err = makeReverseProxy(*target, *targetSNI, *targetHost, *targetInsecureSkipVerify, *targetDisableKeepAlive) + rp, err = makeReverseProxy(*target, *targetSNI, *targetHost, *targetInsecureSkipVerify, *targetDisableKeepAlive, *proxyProtocolSendVersion) if err != nil { log.Fatalf("can't make reverse proxy: %v", err) } From 804daf61013b8ede106a279872fef1e43f0526c7 Mon Sep 17 00:00:00 2001 From: Joshua Siwek Date: Fri, 2 Jan 2026 08:58:38 +0100 Subject: [PATCH 9/9] chore: hints --- internal/headers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/headers.go b/internal/headers.go index c70997bea..399cbed69 100644 --- a/internal/headers.go +++ b/internal/headers.go @@ -95,8 +95,10 @@ func ProxyProtocol(useProxyProtocol bool, sendProxyProtocol string, next http.Ha } // stolen from caddyserver :) if sendProxyProtocol != "" { + // this will not work correctly because when we dont get the infromation via the proxy protocol headers but instead trough x-real-ip we never have a port for the address and always will set it to 0 address := r.Header.Get("X-Real-Ip") addrPort, err := netip.ParseAddrPort(address) + //this check would only make sense if there would be a port if err != nil { // OK; probably didn't have a port addr, err := netip.ParseAddr(address)