From 143438a30e79e9d4171ecde4421a0795a3dbeaa3 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sat, 27 Jun 2026 14:33:40 +0800 Subject: [PATCH] feat(auth): add skipauth metadata option to bypass auth for whitelisted client IPs Introduces WhitelistedAuthenticator that wraps an auth.Authenticator to skip authentication when the client IP matches configured CIDR ranges or exact IPs. The wrapping happens at service-parse time, transparently covering all handler types (HTTP, SOCKS4/5, relay, http2, SSH). Usage: gost -L 'user:pass@:5999?skipauth=192.168.0.0/24,172.22.22.22/32' --- auth/auth_test.go | 95 +++++++++++++++++++++++++++++++++ auth/whitelist.go | 61 +++++++++++++++++++++ config/parsing/service/parse.go | 26 +++++++++ 3 files changed, 182 insertions(+) create mode 100644 auth/whitelist.go diff --git a/auth/auth_test.go b/auth/auth_test.go index a13ceced..6abb3e0c 100644 --- a/auth/auth_test.go +++ b/auth/auth_test.go @@ -4,11 +4,13 @@ import ( "context" "errors" "io" + "net" "strings" "testing" "time" "github.com/go-gost/core/auth" + xctx "github.com/go-gost/x/ctx" "github.com/go-gost/x/internal/loader" xlogger "github.com/go-gost/x/logger" ) @@ -1003,3 +1005,96 @@ type staticAuther struct { func (s *staticAuther) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) { return s.id, s.ok } + +// --- whitelist tests --- + +func TestWhitelistedAuthenticator_IPMatch(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "" || !ok { + t.Fatalf("expected (\"\", true) for matched IP, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_IPNoMatch(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "test" || !ok { + t.Fatalf("expected (\"test\", true) for non-matched IP, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_CIDRMatch(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"10.0.0.0/8"}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.1.2.3"), Port: 9999}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "" || !ok { + t.Fatalf("expected (\"\", true) for matched CIDR, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_CIDRNoMatch(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"10.0.0.0/8"}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 8080}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "test" || !ok { + t.Fatalf("expected (\"test\", true) for non-matched CIDR, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_EmptyPatterns(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "test" || !ok { + t.Fatalf("expected (\"test\", true) for empty patterns, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_NoSrcAddr(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"}) + id, ok := w.Authenticate(context.Background(), "user", "pass") + if id != "test" || !ok { + t.Fatalf("expected (\"test\", true) with no src addr in context, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_NilAuther(t *testing.T) { + // When IP matches, the whitelist returns OK regardless of nil auther. + w := WhitelistedAuthenticator(nil, []string{"192.168.1.100"}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "" || !ok { + t.Fatalf("expected (\"\", true) for matched whitelist even with nil auther, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_NilAutherNoMatch(t *testing.T) { + // When IP does not match and auther is nil, it falls through to ("", false). + w := WhitelistedAuthenticator(nil, []string{"192.168.1.100"}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 54321}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "" || ok { + t.Fatalf("expected (\"\", false) for nil auther with non-matching IP, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_UnderlyingDenies(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: false}, []string{"10.0.0.0/8"}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 8080}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "test" || ok { + t.Fatalf("expected (\"test\", false) when underlying auther denies, got (%q, %v)", id, ok) + } +} + +func TestWhitelistedAuthenticator_InvalidPatterns(t *testing.T) { + w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"not-an-ip", "also-not-cidr/", ""}) + ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321}) + id, ok := w.Authenticate(ctx, "user", "pass") + if id != "test" || !ok { + t.Fatalf("expected (\"test\", true) for all-invalid patterns, got (%q, %v)", id, ok) + } +} diff --git a/auth/whitelist.go b/auth/whitelist.go new file mode 100644 index 00000000..0815e410 --- /dev/null +++ b/auth/whitelist.go @@ -0,0 +1,61 @@ +package auth + +import ( + "context" + "net" + "strings" + + "github.com/go-gost/core/auth" + xctx "github.com/go-gost/x/ctx" + "github.com/go-gost/x/internal/matcher" +) + +// whitelistedAuthenticator wraps an Authenticator to skip authentication +// for client IPs matching the configured whitelist patterns. +type whitelistedAuthenticator struct { + auther auth.Authenticator + ipMatcher matcher.Matcher + cidrMatcher matcher.Matcher +} + +// WhitelistedAuthenticator returns an Authenticator that skips the underlying +// auther when the client's IP address matches one of the given patterns. +// Patterns may be IP addresses (e.g. "192.168.1.1") or CIDR ranges +// (e.g. "10.0.0.0/8"). Invalid patterns are silently ignored. +func WhitelistedAuthenticator(auther auth.Authenticator, patterns []string) auth.Authenticator { + var ips []net.IP + var inets []*net.IPNet + for _, pattern := range patterns { + pattern = strings.TrimSpace(pattern) + if pattern == "" { + continue + } + if ip := net.ParseIP(pattern); ip != nil { + ips = append(ips, ip) + continue + } + if _, inet, err := net.ParseCIDR(pattern); err == nil { + inets = append(inets, inet) + } + } + return &whitelistedAuthenticator{ + auther: auther, + ipMatcher: matcher.IPMatcher(ips), + cidrMatcher: matcher.CIDRMatcher(inets), + } +} + +func (w *whitelistedAuthenticator) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) { + if srcAddr := xctx.SrcAddrFromContext(ctx); srcAddr != nil { + host, _, err := net.SplitHostPort(srcAddr.String()) + if err == nil && host != "" { + if w.ipMatcher.Match(host) || w.cidrMatcher.Match(host) { + return "", true + } + } + } + if w.auther != nil { + return w.auther.Authenticate(ctx, user, password, opts...) + } + return "", false +} diff --git a/config/parsing/service/parse.go b/config/parsing/service/parse.go index e9658338..315691d1 100644 --- a/config/parsing/service/parse.go +++ b/config/parsing/service/parse.go @@ -303,6 +303,32 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) { auther = xauth.AuthenticatorGroup(authers...) } + // Parse skipauth from handler metadata to whitelist client IPs that + // should bypass authentication. Accepts both YAML arrays + // (skipauth: ["10.0.0.0/8"]) and comma-separated URL query values + // (?skipauth=10.0.0.0/8,192.168.1.1). + if cfg.Handler.Metadata != nil { + hmd := metadata.NewMetadata(cfg.Handler.Metadata) + skipauth := mdutil.GetStrings(hmd, "skipauth") + if len(skipauth) == 0 { + if s := mdutil.GetString(hmd, "skipauth"); s != "" { + for _, p := range strings.Split(s, ",") { + if p = strings.TrimSpace(p); p != "" { + skipauth = append(skipauth, p) + } + } + } + } + if len(skipauth) > 0 { + if auther != nil { + auther = xauth.WhitelistedAuthenticator(auther, skipauth) + handlerLogger.Debugf("skipauth whitelist applied: %v", skipauth) + } else { + handlerLogger.Warnf("skipauth configured but no auther set — authentication is already disabled for all clients") + } + } + } + var recorders []recorder.RecorderObject for _, r := range cfg.Recorders { md := metadata.NewMetadata(r.Metadata)