Compare commits
10 Commits
d4e9450bad
...
cb5d5c590b
| Author | SHA1 | Date | |
|---|---|---|---|
| cb5d5c590b | |||
| 3993c95700 | |||
| f63f36805b | |||
| 1b07475bf3 | |||
| 3b25e0317f | |||
| fb0cf72446 | |||
| ee7b1e9462 | |||
| 143438a30e | |||
| cd97e5e746 | |||
| c1d954d689 |
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+20
-2
@@ -316,6 +316,11 @@ type RecorderObject struct {
|
||||
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type RewriterConfig struct {
|
||||
Name string `json:"name"`
|
||||
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
|
||||
}
|
||||
|
||||
type LimiterConfig struct {
|
||||
Name string `json:"name"`
|
||||
Limits []string `yaml:",omitempty" json:"limits,omitempty"`
|
||||
@@ -397,6 +402,8 @@ type HTTPBodyRewriteConfig struct {
|
||||
Type string
|
||||
Match string
|
||||
Replacement string
|
||||
// name of the rewriter plugin (via registry)
|
||||
Rewriter string
|
||||
}
|
||||
|
||||
type NodeFilterConfig struct {
|
||||
@@ -405,6 +412,15 @@ type NodeFilterConfig struct {
|
||||
Path string `yaml:",omitempty" json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// NodeMatcherConfig defines a routing-rule matcher for a hop node.
|
||||
//
|
||||
// Priority controls election among multiple matching nodes:
|
||||
// - 0 (default): auto-computed from the rule string length — longer rules
|
||||
// (more specific) get higher priority.
|
||||
// - negative: the node participates in matching but priority short-circuit
|
||||
// is disabled; the selector (round-robin, random, hash, etc.) always applies.
|
||||
// - positive: explicit priority; when a single node has strictly higher
|
||||
// priority than all others, it wins directly, bypassing the selector.
|
||||
type NodeMatcherConfig struct {
|
||||
Rule string `yaml:",omitempty" json:"rule,omitempty"`
|
||||
Priority int `yaml:",omitempty" json:"priority,omitempty"`
|
||||
@@ -478,8 +494,9 @@ type ServiceConfig struct {
|
||||
Logger string `yaml:",omitempty" json:"logger,omitempty"`
|
||||
Loggers []string `yaml:",omitempty" json:"loggers,omitempty"`
|
||||
Observer string `yaml:",omitempty" json:"observer,omitempty"`
|
||||
Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"`
|
||||
Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"`
|
||||
Rewriter string `yaml:",omitempty" json:"rewriter,omitempty"`
|
||||
Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"`
|
||||
Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"`
|
||||
Listener *ListenerConfig `yaml:",omitempty" json:"listener,omitempty"`
|
||||
Forwarder *ForwarderConfig `yaml:",omitempty" json:"forwarder,omitempty"`
|
||||
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
|
||||
@@ -628,6 +645,7 @@ type Config struct {
|
||||
Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"`
|
||||
SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"`
|
||||
Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"`
|
||||
Rewriters []*RewriterConfig `yaml:",omitempty" json:"rewriters,omitempty"`
|
||||
Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"`
|
||||
Quotas []*QuotaConfig `yaml:",omitempty" json:"quotas,omitempty"`
|
||||
CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"`
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
reg "github.com/go-gost/core/registry"
|
||||
"github.com/go-gost/core/resolver"
|
||||
"github.com/go-gost/core/router"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/parsing"
|
||||
@@ -38,6 +39,7 @@ import (
|
||||
quota_parser "github.com/go-gost/x/config/parsing/quota"
|
||||
recorder_parser "github.com/go-gost/x/config/parsing/recorder"
|
||||
resolver_parser "github.com/go-gost/x/config/parsing/resolver"
|
||||
rewriter_parser "github.com/go-gost/x/config/parsing/rewriter"
|
||||
router_parser "github.com/go-gost/x/config/parsing/router"
|
||||
sd_parser "github.com/go-gost/x/config/parsing/sd"
|
||||
service_parser "github.com/go-gost/x/config/parsing/service"
|
||||
@@ -244,6 +246,16 @@ func register(cfg *config.Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var entries []named[rewriter.Rewriter]
|
||||
for _, c := range cfg.Rewriters {
|
||||
entries = append(entries, named[rewriter.Rewriter]{c.Name, rewriter_parser.ParseRewriter(c)})
|
||||
}
|
||||
if err := registerGroup(entries, registry.RewriterRegistry()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var entries []named[traffic.TrafficLimiter]
|
||||
for _, c := range cfg.Limiters {
|
||||
|
||||
@@ -29,6 +29,29 @@ import (
|
||||
// metadata-driven options (so_mark, interface, netns, proxy protocol), sets up
|
||||
// bypass rules, node filters, HTTP settings, and TLS node settings. The hop
|
||||
// parameter is used only for logging context.
|
||||
|
||||
func parseBodyRewrites(vs []config.HTTPBodyRewriteConfig, log logger.Logger) []chain.HTTPBodyRewriteSettings {
|
||||
var out []chain.HTTPBodyRewriteSettings
|
||||
for _, v := range vs {
|
||||
pattern, _ := regexp.Compile(v.Match)
|
||||
rw := chain.HTTPBodyRewriteSettings{
|
||||
Type: v.Type,
|
||||
Pattern: pattern,
|
||||
Replacement: []byte(v.Replacement),
|
||||
}
|
||||
if v.Rewriter != "" {
|
||||
if !registry.RewriterRegistry().IsRegistered(v.Rewriter) {
|
||||
log.Warnf("rewriter %q not found in registry for rewrite rule", v.Rewriter)
|
||||
}
|
||||
rw.Rewriter = registry.RewriterRegistry().Get(v.Rewriter)
|
||||
}
|
||||
if pattern != nil || rw.Rewriter != nil {
|
||||
out = append(out, rw)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.Node, error) {
|
||||
if cfg == nil {
|
||||
return nil, nil
|
||||
@@ -176,6 +199,10 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No
|
||||
if rule := strings.TrimSpace(cfg.Matcher.Rule); rule != "" {
|
||||
if matcher, err := routing.NewMatcher(rule); err == nil {
|
||||
log.Debugf("new matcher for node %s with rule %s", cfg.Name, cfg.Matcher.Rule)
|
||||
// Priority 0 means "use default": automatically set to the
|
||||
// rule length so longer (more specific) rules outrank shorter
|
||||
// ones. Use a negative priority to opt out of this behavior
|
||||
// and always go through the selector.
|
||||
if priority == 0 {
|
||||
priority = len(cfg.Matcher.Rule)
|
||||
}
|
||||
@@ -222,33 +249,9 @@ func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.No
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, v := range cfg.HTTP.RewriteBody {
|
||||
if pattern, _ := regexp.Compile(v.Match); pattern != nil {
|
||||
settings.RewriteResponseBody = append(settings.RewriteResponseBody, chain.HTTPBodyRewriteSettings{
|
||||
Type: v.Type,
|
||||
Pattern: pattern,
|
||||
Replacement: []byte(v.Replacement),
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, v := range cfg.HTTP.RewriteResponseBody {
|
||||
if pattern, _ := regexp.Compile(v.Match); pattern != nil {
|
||||
settings.RewriteResponseBody = append(settings.RewriteResponseBody, chain.HTTPBodyRewriteSettings{
|
||||
Type: v.Type,
|
||||
Pattern: pattern,
|
||||
Replacement: []byte(v.Replacement),
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, v := range cfg.HTTP.RewriteRequestBody {
|
||||
if pattern, _ := regexp.Compile(v.Match); pattern != nil {
|
||||
settings.RewriteRequestBody = append(settings.RewriteRequestBody, chain.HTTPBodyRewriteSettings{
|
||||
Type: v.Type,
|
||||
Pattern: pattern,
|
||||
Replacement: []byte(v.Replacement),
|
||||
})
|
||||
}
|
||||
}
|
||||
settings.RewriteResponseBody = append(settings.RewriteResponseBody, parseBodyRewrites(cfg.HTTP.RewriteBody, log)...)
|
||||
settings.RewriteResponseBody = append(settings.RewriteResponseBody, parseBodyRewrites(cfg.HTTP.RewriteResponseBody, log)...)
|
||||
settings.RewriteRequestBody = append(settings.RewriteRequestBody, parseBodyRewrites(cfg.HTTP.RewriteRequestBody, log)...)
|
||||
opts = append(opts, chain.HTTPNodeOption(settings))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package rewriter
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"strings"
|
||||
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
rewriter_plugin "github.com/go-gost/x/rewriter/plugin"
|
||||
)
|
||||
|
||||
// ParseRewriter converts a RewriterConfig into a rewriter.Rewriter.
|
||||
// It currently supports plugin backends only (HTTP or gRPC).
|
||||
// Returns nil when cfg is nil or no backend is configured.
|
||||
func ParseRewriter(cfg *config.RewriterConfig) rewriter.Rewriter {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cfg.Plugin != nil {
|
||||
var tlsCfg *tls.Config
|
||||
if cfg.Plugin.TLS != nil {
|
||||
tlsCfg = &tls.Config{
|
||||
ServerName: cfg.Plugin.TLS.ServerName,
|
||||
InsecureSkipVerify: !cfg.Plugin.TLS.Secure,
|
||||
}
|
||||
}
|
||||
switch strings.ToLower(cfg.Plugin.Type) {
|
||||
case "http":
|
||||
return rewriter_plugin.NewHTTPPlugin(
|
||||
cfg.Name, cfg.Plugin.Addr,
|
||||
plugin.TokenOption(cfg.Plugin.Token),
|
||||
plugin.TLSConfigOption(tlsCfg),
|
||||
plugin.TimeoutOption(cfg.Plugin.Timeout),
|
||||
)
|
||||
default:
|
||||
return rewriter_plugin.NewGRPCPlugin(
|
||||
cfg.Name, cfg.Plugin.Addr,
|
||||
plugin.TokenOption(cfg.Plugin.Token),
|
||||
plugin.TLSConfigOption(tlsCfg),
|
||||
plugin.TimeoutOption(cfg.Plugin.Timeout),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/hop"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/observer/stats"
|
||||
"github.com/go-gost/core/recorder"
|
||||
@@ -303,6 +304,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)
|
||||
@@ -327,6 +354,14 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
})
|
||||
}
|
||||
|
||||
var rew rewriter.Rewriter
|
||||
if cfg.Rewriter != "" {
|
||||
if !registry.RewriterRegistry().IsRegistered(cfg.Rewriter) {
|
||||
serviceLogger.Warnf("rewriter %q not found in registry", cfg.Rewriter)
|
||||
}
|
||||
rew = registry.RewriterRegistry().Get(cfg.Rewriter)
|
||||
}
|
||||
|
||||
routerOpts = []chain.RouterOption{
|
||||
chain.RetriesRouterOption(cfg.Handler.Retries),
|
||||
chain.TimeoutRouterOption(dialTimeout),
|
||||
@@ -356,6 +391,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
||||
handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)),
|
||||
handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)),
|
||||
handler.RecordersOption(recorders...),
|
||||
handler.RewriterOption(rew),
|
||||
handler.LoggerOption(handlerLogger),
|
||||
handler.ServiceOption(cfg.Name),
|
||||
handler.NetnsOption(netnsIn),
|
||||
|
||||
@@ -2,9 +2,12 @@ package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -53,10 +56,16 @@ func (c *httpConnector) Connect(ctx context.Context, conn net.Conn, network, add
|
||||
})
|
||||
log.Debugf("connect %s/%s", address, network)
|
||||
|
||||
connectHost := address
|
||||
if c.md.host != "" {
|
||||
// 伪装的地址
|
||||
connectHost = c.md.host
|
||||
}
|
||||
|
||||
req := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: address},
|
||||
Host: address,
|
||||
URL: &url.URL{Host: connectHost},
|
||||
Host: connectHost,
|
||||
ProtoMajor: 1,
|
||||
ProtoMinor: 1,
|
||||
Header: c.md.header,
|
||||
@@ -67,6 +76,12 @@ func (c *httpConnector) Connect(ctx context.Context, conn net.Conn, network, add
|
||||
}
|
||||
req.Header.Set("Proxy-Connection", "keep-alive")
|
||||
|
||||
// 真实连接的地址
|
||||
if c.md.host != "" {
|
||||
targetAddr := encodeServerName(address)
|
||||
req.Header.Set("Gost-Target", targetAddr)
|
||||
}
|
||||
|
||||
if user := c.options.Auth; user != nil {
|
||||
u := user.Username()
|
||||
p, _ := user.Password()
|
||||
@@ -129,3 +144,10 @@ func (c *httpConnector) Connect(ctx context.Context, conn net.Conn, network, add
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func encodeServerName(name string) string {
|
||||
buf := &bytes.Buffer{}
|
||||
binary.Write(buf, binary.BigEndian, crc32.ChecksumIEEE([]byte(name)))
|
||||
buf.WriteString(base64.RawURLEncoding.EncodeToString([]byte(name)))
|
||||
return base64.RawURLEncoding.EncodeToString(buf.Bytes())
|
||||
}
|
||||
|
||||
@@ -11,15 +11,19 @@ import (
|
||||
type metadata struct {
|
||||
connectTimeout time.Duration
|
||||
header http.Header
|
||||
//伪装混淆用host
|
||||
host string
|
||||
}
|
||||
|
||||
func (c *httpConnector) parseMetadata(md mdata.Metadata) (err error) {
|
||||
const (
|
||||
connectTimeout = "timeout"
|
||||
header = "header"
|
||||
host = "host"
|
||||
)
|
||||
|
||||
c.md.connectTimeout = mdutil.GetDuration(md, connectTimeout)
|
||||
c.md.host = mdutil.GetString(md, host)
|
||||
|
||||
if mm := mdutil.GetStringMapString(md, header); len(mm) > 0 {
|
||||
hd := http.Header{}
|
||||
|
||||
@@ -2,7 +2,6 @@ package mtls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
"github.com/go-gost/x/internal/net/proxyproto"
|
||||
"github.com/go-gost/x/internal/util/mux"
|
||||
"github.com/go-gost/x/registry"
|
||||
"github.com/go-gost/x/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -141,7 +141,7 @@ func (d *mtlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...di
|
||||
}
|
||||
|
||||
func (d *mtlsDialer) initSession(ctx context.Context, conn net.Conn) (*muxSession, error) {
|
||||
tlsConn := tls.Client(conn, d.options.TLSConfig)
|
||||
tlsConn := util.NewUTLSChromeClient(conn, d.options.TLSConfig)
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+18
-4
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/go-gost/x/internal/util/mux"
|
||||
ws_util "github.com/go-gost/x/internal/util/ws"
|
||||
"github.com/go-gost/x/registry"
|
||||
"github.com/go-gost/x/util"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
@@ -172,17 +173,30 @@ func (d *mwsDialer) initSession(ctx context.Context, host string, conn net.Conn,
|
||||
},
|
||||
}
|
||||
|
||||
url := url.URL{Scheme: "ws", Host: host, Path: d.md.path}
|
||||
urlObj := url.URL{Scheme: "ws", Host: host, Path: d.md.path}
|
||||
if d.tlsEnabled {
|
||||
url.Scheme = "wss"
|
||||
urlObj.Scheme = "wss"
|
||||
dialer.TLSClientConfig = d.options.TLSConfig
|
||||
dialer.NetDialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
client, err := util.NewUTLSWebSocketClient(conn, d.options.TLSConfig, d.md.useH2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
if d.md.handshakeTimeout > 0 {
|
||||
conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout))
|
||||
}
|
||||
|
||||
c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header)
|
||||
urlStr, errUnescape := url.QueryUnescape(urlObj.String())
|
||||
if errUnescape != nil {
|
||||
d.options.Logger.Debugf("[mws] URL QueryUnescape Error URL.String() -> %s", urlObj.String())
|
||||
}
|
||||
c, resp, err := dialer.DialContext(ctx, urlStr, d.md.header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
mdata "github.com/go-gost/core/metadata"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
"github.com/go-gost/x/internal/util/mux"
|
||||
mdutil "github.com/go-gost/x/metadata/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,6 +18,8 @@ type metadata struct {
|
||||
host string
|
||||
path string
|
||||
|
||||
useH2 bool
|
||||
|
||||
handshakeTimeout time.Duration
|
||||
readHeaderTimeout time.Duration
|
||||
readBufferSize int
|
||||
@@ -77,5 +79,7 @@ func (d *mwsDialer) parseMetadata(md mdata.Metadata) (err error) {
|
||||
d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval")
|
||||
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
|
||||
|
||||
d.md.useH2 = mdutil.GetBool(md, "h2")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
"github.com/go-gost/x/internal/net/proxyproto"
|
||||
"github.com/go-gost/x/registry"
|
||||
"github.com/go-gost/x/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -77,7 +77,7 @@ func (d *tlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dia
|
||||
defer conn.SetDeadline(time.Time{})
|
||||
}
|
||||
|
||||
tlsConn := tls.Client(conn, d.options.TLSConfig)
|
||||
tlsConn := util.NewUTLSChromeClient(conn, d.options.TLSConfig)
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-gost/core/dialer"
|
||||
"github.com/go-gost/core/logger"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
xnet "github.com/go-gost/x/internal/net"
|
||||
"github.com/go-gost/x/internal/net/proxyproto"
|
||||
"github.com/go-gost/x/registry"
|
||||
"github.com/go-gost/x/util"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
@@ -82,7 +82,7 @@ func (d *utlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...di
|
||||
clientHelloID, ok := GetClientHelloID(d.md.fingerprint)
|
||||
if ok {
|
||||
d.log.Debugf("utls handshake with fingerprint: %s", d.md.fingerprint)
|
||||
utlsCfg := (*utls.Config)(unsafe.Pointer(d.options.TLSConfig))
|
||||
utlsCfg := util.NewUTLSConfig(d.options.TLSConfig)
|
||||
uconn := utls.UClient(conn, utlsCfg, clientHelloID)
|
||||
if err := uconn.HandshakeContext(ctx); err != nil {
|
||||
conn.Close()
|
||||
|
||||
+18
-4
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/go-gost/x/internal/net/proxyproto"
|
||||
ws_util "github.com/go-gost/x/internal/util/ws"
|
||||
"github.com/go-gost/x/registry"
|
||||
"github.com/go-gost/x/util"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
@@ -110,13 +111,26 @@ func (d *wsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dial
|
||||
},
|
||||
}
|
||||
|
||||
url := url.URL{Scheme: "ws", Host: host, Path: d.md.path}
|
||||
urlObj := url.URL{Scheme: "ws", Host: host, Path: d.md.path}
|
||||
if d.tlsEnabled {
|
||||
url.Scheme = "wss"
|
||||
urlObj.Scheme = "wss"
|
||||
dialer.TLSClientConfig = d.options.TLSConfig
|
||||
dialer.NetDialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
client, err := util.NewUTLSWebSocketClient(conn, d.options.TLSConfig, d.md.useH2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.Handshake(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header)
|
||||
urlStr, errUnescape := url.QueryUnescape(urlObj.String())
|
||||
if errUnescape != nil {
|
||||
d.options.Logger.Debugf("[ws] URL QueryUnescape Error URL.String() -> %s", urlObj.String())
|
||||
}
|
||||
c, resp, err := dialer.DialContext(ctx, urlStr, d.md.header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ const (
|
||||
)
|
||||
|
||||
type metadata struct {
|
||||
host string
|
||||
path string
|
||||
host string
|
||||
path string
|
||||
useH2 bool
|
||||
|
||||
handshakeTimeout time.Duration
|
||||
readHeaderTimeout time.Duration
|
||||
@@ -66,5 +67,6 @@ func (d *wsDialer) parseMetadata(md mdata.Metadata) (err error) {
|
||||
d.md.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval")
|
||||
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
|
||||
|
||||
d.md.useH2 = mdutil.GetBool(md, "h2")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
github.com/gin-contrib/cors v1.7.2
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/go-gost/core v0.4.3
|
||||
github.com/go-gost/core v0.5.1
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3
|
||||
github.com/go-gost/gosocks4 v0.1.0
|
||||
github.com/go-gost/gosocks5 v0.5.0
|
||||
github.com/go-gost/plugin v0.3.0
|
||||
github.com/go-gost/plugin v0.4.0
|
||||
github.com/go-gost/relay v0.6.1
|
||||
github.com/go-gost/tls-dissector v0.2.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
|
||||
@@ -51,16 +51,18 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gost/core v0.4.3 h1:9VEHRzuSq/MVdDZYrp/TtVG0+0r6NVGUQDXsJozrvJs=
|
||||
github.com/go-gost/core v0.4.3/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/core v0.5.0 h1:3SkPojjqw2Av9l5OSyRVlBpRvWhaXgU5BWMXB9aR4SM=
|
||||
github.com/go-gost/core v0.5.0/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/core v0.5.1 h1:HbIn3naEOC661Z4SwzaSYUzffWSRbU0P6gkxiHmuG0I=
|
||||
github.com/go-gost/core v0.5.1/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3 h1:6CUZLp+mTWXnKP2aK8/Z9ZP+ERMX9gSbywmPu4kGX/A=
|
||||
github.com/go-gost/go-shadowsocks2 v0.1.3/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys=
|
||||
github.com/go-gost/gosocks4 v0.1.0 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk=
|
||||
github.com/go-gost/gosocks4 v0.1.0/go.mod h1:hzVjwijJuZR1pp3GqpTj+AKcSGrx68RlWTrQMFMYBP0=
|
||||
github.com/go-gost/gosocks5 v0.5.0 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yic=
|
||||
github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
|
||||
github.com/go-gost/plugin v0.3.0 h1:pmll8nNd9PX92BWMB5+b2y2SEkBAWJLxD2ANfX+WHuw=
|
||||
github.com/go-gost/plugin v0.3.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts=
|
||||
github.com/go-gost/plugin v0.4.0 h1:M7MR5PL7QAFCrdWfcXVX+5iLNTVIZlhl8FfdV/K8dZY=
|
||||
github.com/go-gost/plugin v0.4.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts=
|
||||
github.com/go-gost/relay v0.6.1 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc=
|
||||
github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
|
||||
github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg=
|
||||
|
||||
+26
-15
@@ -98,16 +98,13 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
}
|
||||
|
||||
br := bufio.NewReader(conn)
|
||||
proto, _ := sniffing.Sniff(ctx, br)
|
||||
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
|
||||
if proto == sniffing.ProtoTLS {
|
||||
return h.handleTLS(ctx, conn, log)
|
||||
}
|
||||
|
||||
// Fall back to 1-byte peek for SOCKS4 (0x04) vs SOCKS5 (0x05) vs HTTP.
|
||||
// Sniff() already read 5 bytes, but the buffered reader exposes them.
|
||||
// Peek 1 byte first to detect small protocols (SOCKS4/SOCKS5)
|
||||
// that send fewer bytes than the 5-byte TLS record header that
|
||||
// Sniff() needs. If we called Sniff() first, its Peek(5) would
|
||||
// block indefinitely on a SOCKS5 greeting (3 bytes: VER, NMETHODS,
|
||||
// METHODS), deadlocking with the client that is itself waiting for
|
||||
// the server's method-selection reply.
|
||||
b, err := br.Peek(1)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
@@ -116,18 +113,32 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
}
|
||||
|
||||
switch b[0] {
|
||||
case gosocks4.Ver4: // socks4
|
||||
case gosocks4.Ver4:
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
if h.socks4Handler != nil {
|
||||
return h.socks4Handler.Handle(ctx, conn)
|
||||
}
|
||||
case gosocks5.Ver5: // socks5
|
||||
return nil
|
||||
case gosocks5.Ver5:
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
if h.socks5Handler != nil {
|
||||
return h.socks5Handler.Handle(ctx, conn)
|
||||
}
|
||||
default: // http
|
||||
if h.httpHandler != nil {
|
||||
return h.httpHandler.Handle(ctx, conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Not SOCKS — sniff for TLS, HTTP, or SSH.
|
||||
proto, _ := sniffing.Sniff(ctx, br)
|
||||
|
||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||
|
||||
if proto == sniffing.ProtoTLS {
|
||||
return h.handleTLS(ctx, conn, log)
|
||||
}
|
||||
|
||||
// Default to HTTP.
|
||||
if h.httpHandler != nil {
|
||||
return h.httpHandler.Handle(ctx, conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -93,6 +93,21 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st
|
||||
return err
|
||||
}
|
||||
|
||||
// A direct (no-chain) UDP association yields a raw *net.UDPConn, which
|
||||
// cannot consume the domainAddr returned by udpConn.ReadFrom for
|
||||
// ATYP=DOMAINNAME datagrams. Wrap it so domains are resolved through the
|
||||
// configured resolver (hostMapper → resolver → system DNS) instead of the
|
||||
// previous hardcoded net.ResolveUDPAddr in the SOCKS5 decode path.
|
||||
// Chain-backed PacketConns (udpTunConn, udpRelayConn, ...) encode domains
|
||||
// as ATYP=Domain themselves and are left untouched.
|
||||
if _, isDirect := pc.(*net.UDPConn); isDirect {
|
||||
pc = &resolvePacketConn{
|
||||
PacketConn: pc,
|
||||
resolver: h.options.Router.Options().Resolver,
|
||||
hostMapper: h.options.Router.Options().HostMapper,
|
||||
}
|
||||
}
|
||||
|
||||
saddr := gosocks5.Addr{}
|
||||
saddr.ParseFrom(cc.LocalAddr().String())
|
||||
|
||||
@@ -203,6 +218,55 @@ func (f *filteredPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error
|
||||
}
|
||||
}
|
||||
|
||||
// resolvePacketConn wraps the upstream net.PacketConn for a direct (no-chain)
|
||||
// UDP association and resolves domain addresses to IPs in WriteTo calls.
|
||||
//
|
||||
// udpConn.ReadFrom now returns a domainAddr for ATYP=DOMAINNAME datagrams so
|
||||
// the domain survives to the chain; a raw *net.UDPConn cannot consume a
|
||||
// domainAddr (WriteTo needs *net.UDPAddr), so direct connections are wrapped
|
||||
// here. Resolution order is hostMapper → configured resolver → system DNS, so a
|
||||
// configured resolver (e.g. resolver=1.1.1.1) is honored instead of leaking
|
||||
// through the system resolver.
|
||||
type resolvePacketConn struct {
|
||||
net.PacketConn
|
||||
resolver resolver.Resolver
|
||||
hostMapper hosts.HostMapper
|
||||
}
|
||||
|
||||
func (c *resolvePacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
host, portStr, err := net.SplitHostPort(addr.String())
|
||||
if err != nil {
|
||||
return c.PacketConn.WriteTo(b, addr)
|
||||
}
|
||||
if net.ParseIP(host) != nil {
|
||||
return c.PacketConn.WriteTo(b, addr)
|
||||
}
|
||||
|
||||
var ips []net.IP
|
||||
if c.hostMapper != nil {
|
||||
ips, _ = c.hostMapper.Lookup(context.Background(), "ip", host)
|
||||
}
|
||||
if len(ips) == 0 && c.resolver != nil {
|
||||
ips, _ = c.resolver.Resolve(context.Background(), "ip", host)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
ips, _ = net.LookupIP(host)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return 0, fmt.Errorf("socks5 udp: cannot resolve %s", host)
|
||||
}
|
||||
|
||||
ip := ips[0]
|
||||
for _, candidate := range ips {
|
||||
if candidate.To4() != nil {
|
||||
ip = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
return c.PacketConn.WriteTo(b, &net.UDPAddr{IP: ip, Port: port})
|
||||
}
|
||||
|
||||
// domainResolvePacketConn wraps a net.PacketConn and resolves domain
|
||||
// addresses to IPs in WriteTo calls. This guarantees that SOCKS5 UDP
|
||||
// response datagrams never carry ATYP=Domain (0x03), which some clients
|
||||
|
||||
+7
-3
@@ -208,13 +208,17 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
|
||||
})
|
||||
|
||||
if nodes[0].Options().Priority > 0 &&
|
||||
!anyBackupNode(nodes) {
|
||||
!anyBackupNode(nodes) &&
|
||||
nodes[0].Options().Priority > nodes[1].Options().Priority {
|
||||
// Priority short-circuit: highest-priority non-backup node wins.
|
||||
// Conditions: (1) top priority > 0 means a matcher indicated routing
|
||||
// specificity, so the top node is authoritative for this request;
|
||||
// (2) no backup node is present, otherwise BackupFilter would be
|
||||
// silently bypassed. When both hold the selector (FailFilter,
|
||||
// BackupFilter, strategy) is skipped and the best node wins directly.
|
||||
// silently bypassed; (3) the top priority is strictly higher than
|
||||
// the second-highest — when multiple nodes share the same matcher
|
||||
// rule they have equal priority and the selector (FailFilter,
|
||||
// BackupFilter, strategy) should still apply for load balancing.
|
||||
// When all three hold the selector is skipped and the best node wins directly.
|
||||
p.logger.Debugf("priority shortcut: node %s selected", nodes[0].Name)
|
||||
return nodes[0]
|
||||
}
|
||||
|
||||
@@ -522,6 +522,31 @@ func TestSelect_WithSelector(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect_EqualPriorityDoesNotShortcut(t *testing.T) {
|
||||
// Two nodes with identical matchers get the same default priority.
|
||||
// The priority shortcut must NOT trigger when multiple nodes share
|
||||
// the highest priority — the selector should still apply.
|
||||
n1 := chain.NewNode("n1", "127.0.0.1:8080",
|
||||
chain.MatcherNodeOption(&testMatcher{match: true}),
|
||||
chain.PriorityNodeOption(50),
|
||||
)
|
||||
n2 := chain.NewNode("n2", "127.0.0.1:9090",
|
||||
chain.MatcherNodeOption(&testMatcher{match: true}),
|
||||
chain.PriorityNodeOption(50),
|
||||
)
|
||||
sel := &testNodeSelector{selectedIdx: 1} // picks n2
|
||||
h := newTestHop(NodeOption(n1, n2), SelectorOption(sel))
|
||||
defer h.Close()
|
||||
|
||||
node := h.Select(context.Background())
|
||||
if node == nil {
|
||||
t.Fatal("expected node, got nil")
|
||||
}
|
||||
if node.Name != "n2" {
|
||||
t.Errorf("expected 'n2' (selected by selector, not shortcut), got %q", node.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelect_NilNodesSkipped(t *testing.T) {
|
||||
h := newTestHop()
|
||||
defer h.Close()
|
||||
|
||||
@@ -356,6 +356,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
if re.Pattern.MatchString(req.URL.Path) {
|
||||
if s := re.Pattern.ReplaceAllString(req.URL.Path, re.Replacement); s != "" {
|
||||
req.URL.Path = s
|
||||
ro.HTTP.URI = req.URL.RequestURI()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -368,7 +369,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
|
||||
// Rewrite request body before wrapping for recording,
|
||||
// so the recorder sees the rewritten content.
|
||||
if err = rewriteReqBody(req, reqBodyRewrites...); err != nil {
|
||||
if err = rewriteReqBody(ctx, req, reqBodyRewrites...); err != nil {
|
||||
log.Errorf("rewrite request body: %v", err)
|
||||
return
|
||||
}
|
||||
@@ -445,7 +446,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
||||
resp.Header.Set("Connection", "close")
|
||||
}
|
||||
|
||||
if err = rewriteRespBody(resp, respBodyRewrites...); err != nil {
|
||||
if err = rewriteRespBody(ctx, resp, respBodyRewrites...); err != nil {
|
||||
log.Errorf("rewrite body: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func (h *Sniffer) handleUpgradeResponse(ctx context.Context, rw, cc io.ReadWrite
|
||||
return xnet.Pipe(ctx, rw, cc)
|
||||
}
|
||||
|
||||
func rewriteRespBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
func rewriteRespBody(ctx context.Context, resp *http.Response, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
if resp == nil || len(rewrites) == 0 || resp.ContentLength <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -65,7 +65,14 @@ func rewriteRespBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSetti
|
||||
continue
|
||||
}
|
||||
|
||||
if rewrite.Pattern != nil {
|
||||
if rewrite.Rewriter != nil {
|
||||
if rewrite.Pattern == nil || rewrite.Pattern.Match(body) {
|
||||
body, err = rewrite.Rewriter.Rewrite(ctx, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if rewrite.Pattern != nil {
|
||||
body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +97,7 @@ func drainBody(b io.ReadCloser) (body []byte, err error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func rewriteReqBody(req *http.Request, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
func rewriteReqBody(ctx context.Context, req *http.Request, rewrites ...chain.HTTPBodyRewriteSettings) error {
|
||||
if req == nil || len(rewrites) == 0 || req.Body == nil || req.ContentLength <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -114,7 +121,14 @@ func rewriteReqBody(req *http.Request, rewrites ...chain.HTTPBodyRewriteSettings
|
||||
continue
|
||||
}
|
||||
|
||||
if rewrite.Pattern != nil {
|
||||
if rewrite.Rewriter != nil {
|
||||
if rewrite.Pattern == nil || rewrite.Pattern.Match(body) {
|
||||
body, err = rewrite.Rewriter.Rewrite(ctx, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if rewrite.Pattern != nil {
|
||||
body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/hop"
|
||||
"github.com/go-gost/core/recorder"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
xrecorder "github.com/go-gost/x/recorder"
|
||||
)
|
||||
@@ -164,7 +165,7 @@ func TestRewriteReqBody(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.req != nil {
|
||||
_ = rewriteReqBody(tt.req, tt.rewrites...)
|
||||
_ = rewriteReqBody(context.Background(), tt.req, tt.rewrites...)
|
||||
if tt.req.Body != nil {
|
||||
body, _ := io.ReadAll(tt.req.Body)
|
||||
tt.req.Body.Close()
|
||||
@@ -178,7 +179,7 @@ func TestRewriteReqBody(t *testing.T) {
|
||||
t.Errorf("ContentLength = %d, want %d", tt.req.ContentLength, tt.wantCL)
|
||||
}
|
||||
} else {
|
||||
_ = rewriteReqBody(nil) // should not panic
|
||||
_ = rewriteReqBody(context.Background(), nil) // should not panic
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -344,8 +345,8 @@ func TestDrainBody(t *testing.T) {
|
||||
|
||||
func TestRewriteRespBody(t *testing.T) {
|
||||
t.Run("nil response", func(t *testing.T) {
|
||||
if err := rewriteRespBody(nil); err != nil {
|
||||
t.Errorf("rewriteRespBody(nil) = %v, want nil", err)
|
||||
if err := rewriteRespBody(context.Background(), nil); err != nil {
|
||||
t.Errorf("rewriteRespBody(context.Background(), nil) = %v, want nil", err)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -354,7 +355,7 @@ func TestRewriteRespBody(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp)
|
||||
_ = rewriteRespBody(context.Background(), resp)
|
||||
// Body unchanged
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "original" {
|
||||
@@ -367,7 +368,7 @@ func TestRewriteRespBody(t *testing.T) {
|
||||
ContentLength: 0,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile("."),
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
@@ -385,7 +386,7 @@ func TestRewriteRespBody(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
})
|
||||
@@ -413,7 +414,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, makeRewrite("text/html", "replaced"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("text/html", "replaced"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "replaced" {
|
||||
t.Errorf("body = %q, want %q", string(got), "replaced")
|
||||
@@ -426,7 +427,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, makeRewrite("*", "world"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("*", "world"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "world" {
|
||||
t.Errorf("body = %q, want %q", string(got), "world")
|
||||
@@ -439,7 +440,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
_ = rewriteRespBody(resp, makeRewrite("text/html", "replaced"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("text/html", "replaced"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "original" {
|
||||
t.Errorf("body = %q, want %q (unchanged)", string(got), "original")
|
||||
@@ -454,7 +455,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
||||
}
|
||||
// Empty type defaults to "text/html", and response has no Content-Type -> "" containing "text/html"? No.
|
||||
// strings.Contains("text/html", "") is always true, so rewrite should happen.
|
||||
_ = rewriteRespBody(resp, makeRewrite("", "replaced"))
|
||||
_ = rewriteRespBody(context.Background(), resp, makeRewrite("", "replaced"))
|
||||
got, _ := io.ReadAll(resp.Body)
|
||||
if string(got) != "replaced" {
|
||||
t.Errorf("body = %q, want %q (should be rewritten)", string(got), "replaced")
|
||||
@@ -1146,7 +1147,7 @@ func TestRewriteRespBody_PatternReplacement(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("<title>Old Title</title>")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile("Old"),
|
||||
Type: "text/html",
|
||||
Replacement: []byte("New"),
|
||||
@@ -1169,7 +1170,7 @@ func TestRewriteRespBody_MultipleRewrites(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello")),
|
||||
}
|
||||
err := rewriteRespBody(resp,
|
||||
err := rewriteRespBody(context.Background(), resp,
|
||||
chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile("h.*o"),
|
||||
Type: "text/html",
|
||||
@@ -1196,7 +1197,7 @@ func TestRewriteRespBody_NilPattern(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: nil,
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
@@ -1215,7 +1216,7 @@ func TestRewriteRespBody_EmptyContentTypeUsesDefault(t *testing.T) {
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile(".*"),
|
||||
Type: "",
|
||||
Replacement: []byte("rewritten"),
|
||||
@@ -1235,7 +1236,7 @@ func TestRewriteRespBody_NegativeContentLength(t *testing.T) {
|
||||
ContentLength: -1,
|
||||
Body: io.NopCloser(strings.NewReader("original")),
|
||||
}
|
||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Pattern: regexp.MustCompile(".*"),
|
||||
Type: "*",
|
||||
Replacement: []byte("replaced"),
|
||||
@@ -1250,9 +1251,209 @@ func TestRewriteRespBody_NegativeContentLength(t *testing.T) {
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// copyWebsocketFrame Additional Tests
|
||||
// Rewriter Plugin Tests
|
||||
// =============================================================================
|
||||
|
||||
// mockRewriter is a simple rewriter.Rewriter for testing.
|
||||
type mockRewriter struct {
|
||||
cb func(b []byte) []byte
|
||||
called bool
|
||||
}
|
||||
|
||||
func (m *mockRewriter) Rewrite(_ context.Context, b []byte, _ ...rewriter.RewriteOption) ([]byte, error) {
|
||||
m.called = true
|
||||
if m.cb != nil {
|
||||
return m.cb(b), nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func TestRewriteRespBody_RewriterNilPattern(t *testing.T) {
|
||||
rw := &mockRewriter{}
|
||||
resp := &http.Response{
|
||||
Header: http.Header{"Content-Type": {"text/html"}},
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello")),
|
||||
}
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Type: "*",
|
||||
Rewriter: rw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !rw.called {
|
||||
t.Error("rewriter was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteRespBody_RewriterMatchPass(t *testing.T) {
|
||||
rw := &mockRewriter{
|
||||
cb: func(b []byte) []byte {
|
||||
return []byte("rewritten")
|
||||
},
|
||||
}
|
||||
resp := &http.Response{
|
||||
Header: http.Header{"Content-Type": {"text/html"}},
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello world")),
|
||||
}
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Type: "*",
|
||||
Pattern: regexp.MustCompile("hello"),
|
||||
Rewriter: rw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !rw.called {
|
||||
t.Fatal("rewriter was not called")
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != "rewritten" {
|
||||
t.Errorf("body = %q, want %q", string(body), "rewritten")
|
||||
}
|
||||
if resp.ContentLength != int64(len("rewritten")) {
|
||||
t.Errorf("ContentLength = %d, want %d", resp.ContentLength, len("rewritten"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteRespBody_RewriterMatchSkip(t *testing.T) {
|
||||
rw := &mockRewriter{}
|
||||
resp := &http.Response{
|
||||
Header: http.Header{"Content-Type": {"text/html"}},
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello world")),
|
||||
}
|
||||
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Type: "*",
|
||||
Pattern: regexp.MustCompile("xyz"),
|
||||
Rewriter: rw,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rw.called {
|
||||
t.Fatal("rewriter should not have been called: body did not match pattern")
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != "hello world" {
|
||||
t.Errorf("body = %q, want %q (unchanged)", string(body), "hello world")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteRespBody_RewriterContentTypeFilter(t *testing.T) {
|
||||
rw := &mockRewriter{
|
||||
cb: func(b []byte) []byte { return []byte("rewritten") },
|
||||
}
|
||||
|
||||
t.Run("type matches calls rewriter", func(t *testing.T) {
|
||||
rw.called = false
|
||||
resp := &http.Response{
|
||||
Header: http.Header{"Content-Type": {"application/json"}},
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader(`{"key":"value"}`)),
|
||||
}
|
||||
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Type: "application/json",
|
||||
Rewriter: rw,
|
||||
})
|
||||
if !rw.called {
|
||||
t.Error("rewriter should have been called")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("type mismatch skips rewriter", func(t *testing.T) {
|
||||
rw.called = false
|
||||
resp := &http.Response{
|
||||
Header: http.Header{"Content-Type": {"text/plain"}},
|
||||
ContentLength: 100,
|
||||
Body: io.NopCloser(strings.NewReader("hello")),
|
||||
}
|
||||
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||
Type: "application/json",
|
||||
Rewriter: rw,
|
||||
})
|
||||
if rw.called {
|
||||
t.Error("rewriter should not have been called: content type mismatch")
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if string(body) != "hello" {
|
||||
t.Errorf("body = %q, want %q (unchanged)", string(body), "hello")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRewriteReqBody_Rewriter(t *testing.T) {
|
||||
rw := &mockRewriter{
|
||||
cb: func(b []byte) []byte {
|
||||
return []byte("rewritten")
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("nil pattern calls rewriter", func(t *testing.T) {
|
||||
rw.called = false
|
||||
req := &http.Request{
|
||||
Body: io.NopCloser(strings.NewReader("hello")),
|
||||
ContentLength: 5,
|
||||
Header: http.Header{"Content-Type": {"text/plain"}},
|
||||
}
|
||||
_ = rewriteReqBody(context.Background(), req, chain.HTTPBodyRewriteSettings{
|
||||
Type: "*",
|
||||
Rewriter: rw,
|
||||
})
|
||||
if !rw.called {
|
||||
t.Fatal("rewriter was not called")
|
||||
}
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
if string(body) != "rewritten" {
|
||||
t.Errorf("body = %q, want %q", string(body), "rewritten")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("match passes through to rewriter", func(t *testing.T) {
|
||||
rw.called = false
|
||||
req := &http.Request{
|
||||
Body: io.NopCloser(strings.NewReader("hello world")),
|
||||
ContentLength: 11,
|
||||
Header: http.Header{"Content-Type": {"text/html"}},
|
||||
}
|
||||
_ = rewriteReqBody(context.Background(), req, chain.HTTPBodyRewriteSettings{
|
||||
Type: "text/html",
|
||||
Pattern: regexp.MustCompile("hello"),
|
||||
Rewriter: rw,
|
||||
})
|
||||
if !rw.called {
|
||||
t.Fatal("rewriter was not called")
|
||||
}
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
if string(body) != "rewritten" {
|
||||
t.Errorf("body = %q, want %q", string(body), "rewritten")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-match skips rewriter", func(t *testing.T) {
|
||||
rw.called = false
|
||||
req := &http.Request{
|
||||
Body: io.NopCloser(strings.NewReader("hello world")),
|
||||
ContentLength: 11,
|
||||
Header: http.Header{"Content-Type": {"text/html"}},
|
||||
}
|
||||
_ = rewriteReqBody(context.Background(), req, chain.HTTPBodyRewriteSettings{
|
||||
Type: "text/html",
|
||||
Pattern: regexp.MustCompile("xyz"),
|
||||
Rewriter: rw,
|
||||
})
|
||||
if rw.called {
|
||||
t.Fatal("rewriter should not have been called: body did not match pattern")
|
||||
}
|
||||
body, _ := io.ReadAll(req.Body)
|
||||
if string(body) != "hello world" {
|
||||
t.Errorf("body = %q, want %q (unchanged)", string(body), "hello world")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCopyWebsocketFrame_EmptyPayload(t *testing.T) {
|
||||
h := &Sniffer{
|
||||
Recorder: &noopRecorder{},
|
||||
|
||||
@@ -3,6 +3,7 @@ package socks
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-gost/core/common/bufpool"
|
||||
"github.com/go-gost/gosocks5"
|
||||
@@ -106,6 +107,20 @@ func UDPConn(c net.PacketConn, bufferSize int) net.PacketConn {
|
||||
}
|
||||
}
|
||||
|
||||
// domainAddr is a net.Addr that preserves a domain name without resolving it.
|
||||
// Returned by udpConn.ReadFrom when a SOCKS5 UDP datagram carries
|
||||
// ATYP=DOMAINNAME, so that downstream consumers (e.g. relay-chain WriteTo) can
|
||||
// forward the domain verbatim instead of forcing a local DNS lookup that would
|
||||
// leak the query through the system resolver and bypass any configured resolver.
|
||||
type domainAddr struct {
|
||||
network string
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
func (a *domainAddr) Network() string { return a.network }
|
||||
func (a *domainAddr) String() string { return net.JoinHostPort(a.host, strconv.Itoa(a.port)) }
|
||||
|
||||
// ReadFrom reads an UDP datagram.
|
||||
// NOTE: for server side,
|
||||
// the returned addr is the target address the client want to relay to.
|
||||
@@ -128,7 +143,11 @@ func (c *udpConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
|
||||
}
|
||||
n = copy(b, buf[hlen:n])
|
||||
|
||||
addr, err = net.ResolveUDPAddr("udp", socksAddr.String())
|
||||
if net.ParseIP(socksAddr.Host) != nil {
|
||||
addr, err = net.ResolveUDPAddr("udp", socksAddr.String())
|
||||
} else {
|
||||
addr = &domainAddr{network: "udp", host: socksAddr.Host, port: int(socksAddr.Port)}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
reg "github.com/go-gost/core/registry"
|
||||
"github.com/go-gost/core/resolver"
|
||||
"github.com/go-gost/core/router"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/core/sd"
|
||||
"github.com/go-gost/core/service"
|
||||
"github.com/go-gost/x/limiter/quota"
|
||||
@@ -46,6 +47,7 @@ var (
|
||||
resolverReg reg.Registry[resolver.Resolver] = new(resolverRegistry)
|
||||
hostsReg reg.Registry[hosts.HostMapper] = new(hostsRegistry)
|
||||
recorderReg reg.Registry[recorder.Recorder] = new(recorderRegistry)
|
||||
rewriterReg reg.Registry[rewriter.Rewriter] = new(rewriterRegistry)
|
||||
|
||||
trafficLimiterReg reg.Registry[traffic.TrafficLimiter] = new(trafficLimiterRegistry)
|
||||
connLimiterReg reg.Registry[conn.ConnLimiter] = new(connLimiterRegistry)
|
||||
@@ -187,6 +189,11 @@ func RecorderRegistry() reg.Registry[recorder.Recorder] {
|
||||
return recorderReg
|
||||
}
|
||||
|
||||
// RewriterRegistry returns the global registry of rewriter instances.
|
||||
func RewriterRegistry() reg.Registry[rewriter.Rewriter] {
|
||||
return rewriterReg
|
||||
}
|
||||
|
||||
// TrafficLimiterRegistry returns the global registry of traffic limiter instances.
|
||||
func TrafficLimiterRegistry() reg.Registry[traffic.TrafficLimiter] {
|
||||
return trafficLimiterReg
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-gost/core/rewriter"
|
||||
)
|
||||
|
||||
// rewriterRegistry implements a hot-reload-safe registry for rewriter.Rewriter.
|
||||
type rewriterRegistry struct {
|
||||
registry[rewriter.Rewriter]
|
||||
}
|
||||
|
||||
// Register stores a Rewriter under the given name.
|
||||
func (r *rewriterRegistry) Register(name string, v rewriter.Rewriter) error {
|
||||
return r.registry.Register(name, v)
|
||||
}
|
||||
|
||||
// Get returns a wrapper that delegates to the currently registered Rewriter.
|
||||
// Returns nil if name is empty.
|
||||
func (r *rewriterRegistry) Get(name string) rewriter.Rewriter {
|
||||
if name != "" {
|
||||
return &rewriterWrapper{name: name, r: r}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rewriterRegistry) get(name string) rewriter.Rewriter {
|
||||
return r.registry.Get(name)
|
||||
}
|
||||
|
||||
type rewriterWrapper struct {
|
||||
name string
|
||||
r *rewriterRegistry
|
||||
}
|
||||
|
||||
func (w *rewriterWrapper) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) {
|
||||
v := w.r.get(w.name)
|
||||
if v == nil {
|
||||
return b, nil
|
||||
}
|
||||
return v.Rewrite(ctx, b, opts...)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package rewriter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/plugin/rewriter/proto"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type grpcPlugin struct {
|
||||
conn grpc.ClientConnInterface
|
||||
client proto.RewriterClient
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewGRPCPlugin creates a Rewriter plugin based on gRPC.
|
||||
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) rewriter.Rewriter {
|
||||
var options plugin.Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{
|
||||
"kind": "rewriter",
|
||||
"rewriter": name,
|
||||
})
|
||||
conn, err := plugin.NewGRPCConn(addr, &options)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
if conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := &grpcPlugin{
|
||||
conn: conn,
|
||||
log: log,
|
||||
}
|
||||
p.client = proto.NewRewriterClient(conn)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *grpcPlugin) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) {
|
||||
if p.client == nil {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
var options rewriter.RewriteOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
md, err := json.Marshal(options.Metadata)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
reply, err := p.client.Rewrite(ctx,
|
||||
&proto.RewriteRequest{
|
||||
Data: b,
|
||||
Metadata: md,
|
||||
})
|
||||
if err != nil {
|
||||
p.log.Error(err)
|
||||
return b, err
|
||||
}
|
||||
if reply == nil || !reply.Ok {
|
||||
return b, nil
|
||||
}
|
||||
if reply.Data != nil {
|
||||
return reply.Data, nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (p *grpcPlugin) Close() error {
|
||||
if p.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if closer, ok := p.conn.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package rewriter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/rewriter"
|
||||
"github.com/go-gost/x/internal/plugin"
|
||||
)
|
||||
|
||||
type httpPluginRequest struct {
|
||||
Data []byte `json:"data"`
|
||||
Metadata []byte `json:"metadata"`
|
||||
}
|
||||
|
||||
type httpPluginResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
type httpPlugin struct {
|
||||
url string
|
||||
client *http.Client
|
||||
header http.Header
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewHTTPPlugin creates a Rewriter plugin based on HTTP.
|
||||
func NewHTTPPlugin(name string, url string, opts ...plugin.Option) rewriter.Rewriter {
|
||||
var options plugin.Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
return &httpPlugin{
|
||||
url: url,
|
||||
client: plugin.NewHTTPClient(&options),
|
||||
header: options.Header,
|
||||
log: logger.Default().WithFields(map[string]any{
|
||||
"kind": "rewriter",
|
||||
"rewriter": name,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *httpPlugin) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) {
|
||||
if p.client == nil {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
var options rewriter.RewriteOptions
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
md, err := json.Marshal(options.Metadata)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
rb := httpPluginRequest{
|
||||
Data: b,
|
||||
Metadata: md,
|
||||
}
|
||||
v, err := json.Marshal(&rb)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v))
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
if p.header != nil {
|
||||
req.Header = p.header.Clone()
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := p.client.Do(req)
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer io.Copy(io.Discard, resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return b, nil
|
||||
}
|
||||
|
||||
res := httpPluginResponse{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
if !res.OK {
|
||||
return b, nil
|
||||
}
|
||||
if res.Data != nil {
|
||||
return res.Data, nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
stdtls "crypto/tls"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
func NewUTLSClient(conn net.Conn, config *stdtls.Config, clientHelloID tls.ClientHelloID) *tls.UConn {
|
||||
return tls.UClient(conn, NewUTLSConfig(config), clientHelloID)
|
||||
}
|
||||
|
||||
func NewUTLSChromeClient(conn net.Conn, config *stdtls.Config) *tls.UConn {
|
||||
return NewUTLSClient(conn, config, tls.HelloChrome_Auto)
|
||||
}
|
||||
|
||||
func NewUTLSWebSocketClient(conn net.Conn, config *stdtls.Config, useH2 bool) (*tls.UConn, error) {
|
||||
utlsConfig := NewLegacyUTLSConfig(config)
|
||||
if useH2 {
|
||||
return tls.UClient(conn, utlsConfig, tls.HelloChrome_Auto), nil
|
||||
}
|
||||
|
||||
client := tls.UClient(conn, utlsConfig, tls.HelloCustom)
|
||||
spec, err := NewWebSocketClientHelloSpec()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.ApplyPreset(spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func NewLegacyUTLSConfig(config *stdtls.Config) *tls.Config {
|
||||
if config == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
|
||||
cfg := &tls.Config{
|
||||
InsecureSkipVerify: config.InsecureSkipVerify,
|
||||
ServerName: config.ServerName,
|
||||
ClientAuth: tls.ClientAuthType(config.ClientAuth),
|
||||
ClientCAs: config.ClientCAs,
|
||||
RootCAs: config.RootCAs,
|
||||
}
|
||||
|
||||
cfg.Certificates = make([]tls.Certificate, 0, len(config.Certificates))
|
||||
for _, cert := range config.Certificates {
|
||||
cfg.Certificates = append(cfg.Certificates, tls.Certificate{
|
||||
Certificate: cert.Certificate,
|
||||
PrivateKey: cert.PrivateKey,
|
||||
OCSPStaple: cert.OCSPStaple,
|
||||
SignedCertificateTimestamps: cert.SignedCertificateTimestamps,
|
||||
Leaf: cert.Leaf,
|
||||
})
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func NewUTLSConfig(config *stdtls.Config) *tls.Config {
|
||||
if config == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
|
||||
cfg := &tls.Config{
|
||||
Rand: config.Rand,
|
||||
Time: config.Time,
|
||||
RootCAs: config.RootCAs,
|
||||
NextProtos: append([]string(nil), config.NextProtos...),
|
||||
ServerName: config.ServerName,
|
||||
ClientAuth: tls.ClientAuthType(config.ClientAuth),
|
||||
ClientCAs: config.ClientCAs,
|
||||
InsecureSkipVerify: config.InsecureSkipVerify,
|
||||
CipherSuites: append([]uint16(nil), config.CipherSuites...),
|
||||
SessionTicketsDisabled: config.SessionTicketsDisabled,
|
||||
MinVersion: config.MinVersion,
|
||||
MaxVersion: config.MaxVersion,
|
||||
DynamicRecordSizingDisabled: config.DynamicRecordSizingDisabled,
|
||||
Renegotiation: tls.RenegotiationSupport(config.Renegotiation),
|
||||
KeyLogWriter: config.KeyLogWriter,
|
||||
VerifyPeerCertificate: config.VerifyPeerCertificate,
|
||||
}
|
||||
|
||||
cfg.Certificates = make([]tls.Certificate, 0, len(config.Certificates))
|
||||
for _, cert := range config.Certificates {
|
||||
cfg.Certificates = append(cfg.Certificates, toUTLSCertificate(cert))
|
||||
}
|
||||
|
||||
cfg.CurvePreferences = make([]tls.CurveID, 0, len(config.CurvePreferences))
|
||||
for _, curve := range config.CurvePreferences {
|
||||
cfg.CurvePreferences = append(cfg.CurvePreferences, tls.CurveID(curve))
|
||||
}
|
||||
|
||||
if config.VerifyConnection != nil {
|
||||
cfg.VerifyConnection = func(state tls.ConnectionState) error {
|
||||
return config.VerifyConnection(toStdTLSConnectionState(state))
|
||||
}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func toUTLSCertificate(cert stdtls.Certificate) tls.Certificate {
|
||||
signatureAlgorithms := make([]tls.SignatureScheme, 0, len(cert.SupportedSignatureAlgorithms))
|
||||
for _, sig := range cert.SupportedSignatureAlgorithms {
|
||||
signatureAlgorithms = append(signatureAlgorithms, tls.SignatureScheme(sig))
|
||||
}
|
||||
|
||||
return tls.Certificate{
|
||||
Certificate: cert.Certificate,
|
||||
PrivateKey: cert.PrivateKey,
|
||||
SupportedSignatureAlgorithms: signatureAlgorithms,
|
||||
OCSPStaple: cert.OCSPStaple,
|
||||
SignedCertificateTimestamps: cert.SignedCertificateTimestamps,
|
||||
Leaf: cert.Leaf,
|
||||
}
|
||||
}
|
||||
|
||||
func toStdTLSConnectionState(state tls.ConnectionState) stdtls.ConnectionState {
|
||||
return stdtls.ConnectionState{
|
||||
Version: state.Version,
|
||||
HandshakeComplete: state.HandshakeComplete,
|
||||
DidResume: state.DidResume,
|
||||
CipherSuite: state.CipherSuite,
|
||||
NegotiatedProtocol: state.NegotiatedProtocol,
|
||||
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
||||
ServerName: state.ServerName,
|
||||
PeerCertificates: state.PeerCertificates,
|
||||
VerifiedChains: state.VerifiedChains,
|
||||
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
||||
OCSPResponse: state.OCSPResponse,
|
||||
TLSUnique: state.TLSUnique,
|
||||
ECHAccepted: state.ECHAccepted,
|
||||
}
|
||||
}
|
||||
|
||||
func NewWebSocketClientHelloSpec() (*tls.ClientHelloSpec, error) {
|
||||
fingerprinter := &tls.Fingerprinter{}
|
||||
rawCapturedClientHelloBytes, _ := hex.DecodeString("16030107b8010007b403036252c641762f4a4f55d93dcbe1a01e62839ba1b48e9082896737ca23dc6bd02120c719a58f491c56be1f21fcd68bd097f2ee5a370c576a7c512aba46b7ed73841a0020eaea130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f00350100074baaaa0000000a000c000a5a5a11ec001d00170018000d0012001004030804040105030805050108060601ff0100010000170000002b0007065a5a03040303001b00030200020010000b000908687474702f312e31000b00020100002d00020101fe0d00ba0000010001be0020a5fb3e1a8aa228913bdf2dc9c52670860d2032a04d71140d6dc08f4cb41b2a1c0090420727529f82e9a8b097215adb52126184c638a98f0a6f8121b04edb939a7ff2a9564b44a917160ceb0a41f7a2dc12b89c622bc6687a21759c813858c81fe2e9024b02c6501c4b4c9e87ed2bd1538a420b6045a0785a66b044e53260e19b6f09042b903fa6758ce571dff16b91e95a19dbe5da0a46cab946036f48ad1394534866ee874f8dc75b3e4efa444be660262f003304ef04ed5a5a00010011ec04c098b01e12861f0d1738ebdb83b5fb876e7a1382f50149279a62479352b110f9791d1710c99cd988348175a20884650682e5e6a48bd2b27bb52360882bd84c6f97b6bc142c300ba7ab420bbec1872c13ba6970501e2b473f5c325da09a1dda800060d3c14454a95e6a67ddf28be60140cc3b07d393826b8babaf9a074b97bfa6fb8a2da784e98c5a332c62d7a60b7ac5b85e6606d128a4e9d89056680c6ff3ad5c3a3018218006db0fd81843e4ccb3f3e42ec858554be50aaa5b9afb613a1c07a631551706c4840bdb11a41564db56cdd2e58d1c619fc34567b74a2c54755f4174a0d589bf5938388a0976456c2a2aca2c08b2af11a2b550da9b22e4a39e2a7759c887e884b362b0b0e8394a149b44e82b6f5e680cd280cd2167953c94b111c20c4d028b2fc0926ac1a0454977fdf4a6876a555048a0c8b23531215f8543b5af5a2a69fc4151a870f481204e3a1535ec35321697d0ea0a470239e7b40b99e9b7ee01205bf567c42479c137c346b8af4dd825d4324c6d34875dd50b1841a78c42b79075af259a0f45252f311997f39902337088ee6817565b2e63858924a2354d86b1ba77bfa324339ec4a72663134db7227e652aea43700f751f7f519c5a66a036e0274a48c58716a3eb8a6de4730d19d534dd52ae83b9a7f07ccbc8d63b073a5127e3170dc58031524b81b611c6c4cf2aaba01fc55747b643fb417c85c0a21aa823709a037e418fc8e9c16c51a0c105c07d7c2f56dbcd4516a17a10036531768fb04300f0cdcc7c5135517b1643c3204b47575b97d0a513287985a0353523d93d511b694d6bb5ecc0698823855a606b83c77723808dd6737c9129a5c50457e71a063f927299214e902861e89a81423715b60717e8551b29126f857499810a0328d7182b538276d15517aa21b828ba7afcc13c1cc4cd1574a1c97bcfe4490f766d0e95a0c2911bc0d7a47161bcf5c344558314e7a1ab6c4227950b76c3319acaa53a7776a91e3b62ecd38f0732504674247947855a95892cc43b7f56a09f0c4281976e2b73820ff48180f054f482638078b08cf01d2b221a35214002b5cc8e394a414230a4f562af739e70ec540c260c651009a0616c5830658e0cbb2d2a2f94dab9f539a9ef19750f4925f7106a3a9c0a68e4a7d442202678beeef25b92c8709d93133c08728603ac900719db6615d294ad1aa965546a0c655a6891ab8e9fea31b7262dbba5af94372262934a4feac79ad72a3277aa7e97ab48f9a06f6857d26a2d48306b80a4bb600785f5b26f00202d2eb6af0226774e93615a484fa42a91bfe6034e890f6642c95a979d8df8a39ab10977846b1dc1998b51a8dde62975dc54bf92ada8dc0d7c0a020f429d4ff0b11ba95b6e076ec0443d01f0cb963441ddca9244631b776a029f874b799534c9c14808863c756b2d8a0309a0fab26bc37db05601cd5741acf9acc4c84a9e6b76b1e8282908a8d2268562e72109c3c8cb93507e1accf48723a056bd4acab4317771d4e432216a0a39377792469757a8c186d8085078aa2a8bce201628bad8c7ce98bb41dc56d6715a1dc3cb1cab48cb5a8f18d04a651b9efdd751bd199c266aac08726177bb432ecba334b06e516c12449846dcf90a4080248e2b15cb435e021ee8c4a8c17c095babf8011d67e0a5f16e25dc7357fc1c78e5884fe32598695589294e9c431c94ac2f5d2053757f9a905df36b001d002022b1fc0ef89cbfbd8f197c3e230879a2e71c155a1a6cb1bc33136cf93380c41d00000011000f00000c7373682e6576616e2e72756e0012000000230000000500050100000000baba0001000029010b00e600e0d1ae9857a5731dd0d5ca9d726bf57748ff9f57a7bb61ee7cc84e0f06358291fd2694914ab4ab3abc95fec3a1b393f458cca49eddd4dc24c896429eba5464d54ed59c562be84fe5660c8f74263db3c5eefbd36f2c667b9bd8219e20d84162a545978844bce25f2dc0f1c56f032bcee7f0bcd9fc762cfa115f368d0fe3b2e03fce32673bfdafd419eb8bc36bcf9c4a0d49c8d750ea7373e41c2ac13eb97f8fdb6c619742e98951c6447b76b263e0040ae874c7bd486421af99541b3b40e5b6eb3c0ae555dc1dfa7448aee19f05e54f70eb6655a91eb03eb93015a45eb29adc73d82437927e002120022bd91157ac53b55c75c03be16f0bc0040f38fa0085949f0157f8e7db794cc5")
|
||||
spec, err := fingerprinter.FingerprintClientHello(rawCapturedClientHelloBytes)
|
||||
return spec, err
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
stdtls "crypto/tls"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
tls "github.com/refraction-networking/utls"
|
||||
)
|
||||
|
||||
func TestNewUTLSConfigPreservesClientSettings(t *testing.T) {
|
||||
called := false
|
||||
cfg := NewUTLSConfig(&stdtls.Config{
|
||||
ServerName: "example.com",
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
MinVersion: stdtls.VersionTLS12,
|
||||
MaxVersion: stdtls.VersionTLS13,
|
||||
CipherSuites: []uint16{stdtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
|
||||
CurvePreferences: []stdtls.CurveID{stdtls.X25519, stdtls.CurveP256},
|
||||
DynamicRecordSizingDisabled: true,
|
||||
Renegotiation: stdtls.RenegotiateOnceAsClient,
|
||||
VerifyConnection: func(state stdtls.ConnectionState) error {
|
||||
called = true
|
||||
if state.ServerName != "example.com" {
|
||||
t.Fatalf("ServerName = %q, want example.com", state.ServerName)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
if cfg.ServerName != "example.com" {
|
||||
t.Fatalf("ServerName = %q, want example.com", cfg.ServerName)
|
||||
}
|
||||
if !cfg.InsecureSkipVerify {
|
||||
t.Fatal("InsecureSkipVerify was not preserved")
|
||||
}
|
||||
if len(cfg.NextProtos) != 2 || cfg.NextProtos[0] != "h2" || cfg.NextProtos[1] != "http/1.1" {
|
||||
t.Fatalf("NextProtos = %v", cfg.NextProtos)
|
||||
}
|
||||
if cfg.MinVersion != tls.VersionTLS12 || cfg.MaxVersion != tls.VersionTLS13 {
|
||||
t.Fatalf("version range = %x-%x", cfg.MinVersion, cfg.MaxVersion)
|
||||
}
|
||||
if len(cfg.CipherSuites) != 1 || cfg.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 {
|
||||
t.Fatalf("CipherSuites = %v", cfg.CipherSuites)
|
||||
}
|
||||
if len(cfg.CurvePreferences) != 2 || cfg.CurvePreferences[0] != tls.X25519 || cfg.CurvePreferences[1] != tls.CurveP256 {
|
||||
t.Fatalf("CurvePreferences = %v", cfg.CurvePreferences)
|
||||
}
|
||||
if !cfg.DynamicRecordSizingDisabled {
|
||||
t.Fatal("DynamicRecordSizingDisabled was not preserved")
|
||||
}
|
||||
if cfg.Renegotiation != tls.RenegotiateOnceAsClient {
|
||||
t.Fatalf("Renegotiation = %v", cfg.Renegotiation)
|
||||
}
|
||||
if cfg.VerifyConnection == nil {
|
||||
t.Fatal("VerifyConnection was not preserved")
|
||||
}
|
||||
if err := cfg.VerifyConnection(tls.ConnectionState{ServerName: "example.com"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("VerifyConnection wrapper was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUTLSChromeClientBuildsChromeHello(t *testing.T) {
|
||||
c1, c2 := net.Pipe()
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
client := NewUTLSChromeClient(c1, &stdtls.Config{ServerName: "example.com"})
|
||||
if client.ClientHelloID != tls.HelloChrome_Auto {
|
||||
t.Fatalf("ClientHelloID = %+v, want %+v", client.ClientHelloID, tls.HelloChrome_Auto)
|
||||
}
|
||||
if err := client.BuildHandshakeStateWithoutSession(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(client.Extensions) == 0 {
|
||||
t.Fatal("Chrome ClientHello has no extensions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUTLSWebSocketClientAppliesCustomSpec(t *testing.T) {
|
||||
c1, c2 := net.Pipe()
|
||||
defer c1.Close()
|
||||
defer c2.Close()
|
||||
|
||||
client, err := NewUTLSWebSocketClient(c1, &stdtls.Config{
|
||||
ServerName: "example.com",
|
||||
NextProtos: []string{"h2"},
|
||||
MinVersion: stdtls.VersionTLS13,
|
||||
}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if client.ClientHelloID != tls.HelloCustom {
|
||||
t.Fatalf("ClientHelloID = %+v, want %+v", client.ClientHelloID, tls.HelloCustom)
|
||||
}
|
||||
if len(client.Extensions) == 0 {
|
||||
t.Fatal("custom WebSocket ClientHello preset has no extensions")
|
||||
}
|
||||
if err := client.BuildHandshakeStateWithoutSession(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(client.HandshakeState.Hello.AlpnProtocols) != 1 || client.HandshakeState.Hello.AlpnProtocols[0] != "http/1.1" {
|
||||
t.Fatalf("ALPN = %v, want [http/1.1]", client.HandshakeState.Hello.AlpnProtocols)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user