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'
This commit is contained in:
@@ -4,11 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-gost/core/auth"
|
"github.com/go-gost/core/auth"
|
||||||
|
xctx "github.com/go-gost/x/ctx"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
xlogger "github.com/go-gost/x/logger"
|
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) {
|
func (s *staticAuther) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
|
||||||
return s.id, s.ok
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -303,6 +303,32 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
|||||||
auther = xauth.AuthenticatorGroup(authers...)
|
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
|
var recorders []recorder.RecorderObject
|
||||||
for _, r := range cfg.Recorders {
|
for _, r := range cfg.Recorders {
|
||||||
md := metadata.NewMetadata(r.Metadata)
|
md := metadata.NewMetadata(r.Metadata)
|
||||||
|
|||||||
Reference in New Issue
Block a user