Compare commits

...

14 Commits

Author SHA1 Message Date
wenyifan 5b1e38e2c0 新增dnst隧道 2026-07-04 13:48:22 +08:00
wenyifan 74fef8838e 新增dnst隧道 2026-07-04 13:01:21 +08:00
wenyifan bf85966600 切换为默认的dns解析;增加resolver=system的支持 2026-07-02 20:50:24 +08:00
wenyifan a24d79fd45 fix android dns query 2026-06-28 15:01:58 +08:00
wenyifan cb5d5c590b 增加connect host伪装 2026-06-28 12:41:23 +08:00
wenyifan 3993c95700 UTLS兼容 2026-06-28 12:08:30 +08:00
ginuerzh f63f36805b fix(forwarder): enforce match pattern as content filter when rewriter is set
When a rewrite rule has both match and rewriter set, the body is only
sent to the rewriter plugin if it matches the pattern. A nil pattern
(non-empty match) means unconditional rewrite via the plugin.

Coincidence: fix indentation drift in the previous commit's rewrite
condition blocks (tab alignment was off by one level).
2026-06-27 23:19:54 +08:00
ginuerzh 1b07475bf3 feat(forwarder): support plugin-based rewriter in HTTP body rewrite rules
Each rewriteBody/rewriteResponseBody/rewriteRequestBody rule can now
optionally reference a named rewriter plugin (HTTP/gRPC backend) via
the rewriter field. When set, body rewriting delegates to the plugin
rather than applying pattern/replacement, while content-type filtering
still applies.

Coincident fixes:
- rewriter/plugin/grpc: return nil when conn is nil (avoid nil-ptr panic)
- rewriter/plugin/http: drain response body in defer to prevent leaks
- config/parsing/rewriter: pass TimeoutOption to gRPC plugin
- config/parsing/service: warn when referenced rewriter is not registered
2026-06-27 22:52:17 +08:00
ginuerzh 3b25e0317f feat(rewriter): implement plugin-based Rewriter module with gRPC and HTTP backends
Add the first implementation of core/rewriter.Rewriter as a plugin-only
component following the bypass/admission pattern (single-value, no
RewriterObject). Includes:

- plugin/rewriter/proto/: protobuf definition with Rewrite RPC returning
  transformed data in the reply
- x/rewriter/plugin/: gRPC and HTTP plugin clients
- x/registry/rewriter.go: hot-reload-safe RewriterRegistry with wrapper
- x/config/config.go: RewriterConfig, Config.Rewriters, ServiceConfig.Rewriter
- x/config/parsing/rewriter/parse.go: config parser (plugin backends only)
- core/handler/option.go: Rewriter field + RewriterOption on handler.Options
- x/config/loader/loader.go: rewriter registration during startup
- x/config/parsing/service/parse.go: inject rewriter into handler options
2026-06-27 21:44:13 +08:00
ginuerzh fb0cf72446 fix(forwarder): record post-rewrite URI in HTTP recorder
After RewriteURL mutates req.URL.Path, ro.HTTP.URI still held the
pre-rewrite value because the recorder object was initialized before
the rewrite loop. Update ro.HTTP.URI to match the rewritten path so
the recorded HTTP request reflects the URI actually sent upstream.
2026-06-27 18:50:46 +08:00
ginuerzh ee7b1e9462 fix(handler/socks5): resolve UDP domains via configured resolver to stop DNS leak
SOCKS5 UDP datagrams carrying ATYP=DOMAINNAME were force-resolved via
net.ResolveUDPAddr in udpConn.ReadFrom, which always used the system
resolver and ignored any configured handler resolver (resolver=1.1.1.1),
leaking the query locally. The premature domain→IP conversion also
stripped the hostname before it reached the relay chain, so the exit
node could not resolve DNS itself.

udpConn.ReadFrom now returns a domainAddr for domain targets, letting
the name flow untouched through udp.Relay into the upstream WriteTo.
Chain-backed PacketConns (udpTunConn, udpRelayConn) encode it as
ATYP=Domain and forward to the exit. Direct (no-chain) associations
yield a raw *net.UDPConn that cannot consume a domainAddr, so they are
wrapped with resolvePacketConn, which resolves via hostMapper →
configured resolver → system DNS.
2026-06-27 16:12:36 +08:00
ginuerzh 143438a30e 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'
2026-06-27 14:33:40 +08:00
ginuerzh cd97e5e746 fix(hop): skip priority short-circuit when multiple nodes share the same priority
When two or more nodes have an identical matcher rule they receive
identical default priority (rule string length). The priority shortcut
assumed the top-priority node was the single authoritative choice and
returned it directly, bypassing the selector (FailFilter, BackupFilter,
strategy) — so weight, round-robin, and hash were silently ignored.

Add a third condition: the top priority must be strictly greater than
the second-highest. When multiple nodes share the same highest priority
the selector applies normally for load balancing.

Add doc comments on NodeMatcherConfig.Priority explaining the three
semantic ranges (0=auto, negative=always-selector, positive=shortcut).
2026-06-26 22:53:17 +08:00
ginuerzh c1d954d689 fix(handler/auto): sniff SOCKS4/SOCKS5 with 1-byte peek before 5-byte Sniff()
Sniff() calls Peek(5) which blocks on SOCKS5 greeting (3 bytes:
VER, NMETHODS, METHODS).  The SOCKS5 connector sends those 3 bytes
and then waits for the server's method-selection reply — deadlock.

Restore the original 1-byte peek for SOCKS4 (0x04) / SOCKS5 (0x05)
detection first, routing them to their handlers immediately.  Only
call Sniff() for non-SOCKS traffic (TLS, HTTP, SSH) where 5 bytes
are guaranteed.

Fixes go-gost/gost#879
2026-06-26 20:17:49 +08:00
47 changed files with 2597 additions and 117 deletions
+95
View File
@@ -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)
}
}
+61
View File
@@ -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
}
+4 -1
View File
@@ -95,7 +95,9 @@ func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error)
resolverCfg.Nameservers = append(
resolverCfg.Nameservers,
&config.NameserverConfig{
Addr: rs,
Addr: rs,
Prefer: mdutil.GetString(md, "prefer"),
Only: mdutil.GetString(md, "only"),
},
)
}
@@ -272,6 +274,7 @@ func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error)
&config.NameserverConfig{
Addr: rs,
Prefer: mdutil.GetString(md, "prefer"),
Only: mdutil.GetString(md, "only"),
},
)
}
+71 -19
View File
@@ -211,13 +211,13 @@ func TestNorm(t *testing.T) {
wantErr bool
}{
{
name: "empty string",
input: "",
name: "empty string",
input: "",
wantErr: true,
},
{
name: "whitespace only",
input: " ",
name: "whitespace only",
input: " ",
wantErr: true,
},
{
@@ -326,12 +326,12 @@ func TestCutHost(t *testing.T) {
func TestParseSelector(t *testing.T) {
tests := []struct {
name string
input map[string]any
wantNil bool
wantStrat string
wantFails int
wantDurGt bool // failTimeout > 0
name string
input map[string]any
wantNil bool
wantStrat string
wantFails int
wantDurGt bool // failTimeout > 0
}{
{
name: "no params",
@@ -359,9 +359,9 @@ func TestParseSelector(t *testing.T) {
{
name: "all params with failTimeout",
input: map[string]any{
"strategy": "hash",
"maxFails": 5,
"failTimeout": "10s",
"strategy": "hash",
"maxFails": 5,
"failTimeout": "10s",
},
wantStrat: "hash",
wantFails: 5,
@@ -455,8 +455,8 @@ func TestCopyConnectorConfig(t *testing.T) {
t.Run("non-nil deep copy", func(t *testing.T) {
orig := &config.ConnectorConfig{
Type: "http",
Auth: &config.AuthConfig{Username: "u", Password: "p"},
Type: "http",
Auth: &config.AuthConfig{Username: "u", Password: "p"},
Metadata: map[string]any{"k": "v"},
}
c := copyConnectorConfig(orig)
@@ -487,9 +487,9 @@ func TestCopyDialerConfig(t *testing.T) {
t.Run("non-nil deep copy", func(t *testing.T) {
orig := &config.DialerConfig{
Type: "tls",
TLS: &config.TLSConfig{ServerName: "example.com"},
Auth: &config.AuthConfig{Username: "u"},
Type: "tls",
TLS: &config.TLSConfig{ServerName: "example.com"},
Auth: &config.AuthConfig{Username: "u"},
Metadata: map[string]any{"k": "v"},
}
c := copyDialerConfig(orig)
@@ -1068,6 +1068,31 @@ func TestBuildConfigFromCmd_NodeWithResolver(t *testing.T) {
}
}
func TestBuildConfigFromCmd_NodeResolverFamilyOptions(t *testing.T) {
cfg, err := BuildConfigFromCmd(
[]string{":8080"},
[]string{"http://proxy:3128?resolver=auto&prefer=ipv6&only=ipv4"},
)
if err != nil {
t.Fatalf("BuildConfigFromCmd: %v", err)
}
rs := cfg.Resolvers[0]
if len(rs.Nameservers) != 1 {
t.Fatalf("len(Nameservers) = %d, want 1", len(rs.Nameservers))
}
ns := rs.Nameservers[0]
if ns.Addr != "auto" {
t.Errorf("Addr = %q, want auto", ns.Addr)
}
if ns.Prefer != "ipv6" {
t.Errorf("Prefer = %q, want ipv6", ns.Prefer)
}
if ns.Only != "ipv4" {
t.Errorf("Only = %q, want ipv4", ns.Only)
}
}
func TestBuildConfigFromCmd_NodeWithHosts(t *testing.T) {
cfg, err := BuildConfigFromCmd(
[]string{":8080"},
@@ -1171,6 +1196,34 @@ func TestBuildConfigFromCmd_ServiceWithResolver(t *testing.T) {
}
}
func TestBuildConfigFromCmd_ServiceSystemResolverOnlyIPv4(t *testing.T) {
cfg, err := BuildConfigFromCmd(
[]string{"http://:8080?resolver=system&only=ipv4"},
nil,
)
if err != nil {
t.Fatalf("BuildConfigFromCmd: %v", err)
}
if len(cfg.Resolvers) != 1 {
t.Fatalf("len(Resolvers) = %d, want 1", len(cfg.Resolvers))
}
rs := cfg.Resolvers[0]
if len(rs.Nameservers) != 1 {
t.Fatalf("len(Nameservers) = %d, want 1", len(rs.Nameservers))
}
ns := rs.Nameservers[0]
if ns.Addr != "system" {
t.Errorf("Addr = %q, want system", ns.Addr)
}
if ns.Only != "ipv4" {
t.Errorf("Only = %q, want ipv4", ns.Only)
}
if cfg.Services[0].Resolver != rs.Name {
t.Errorf("Service.Resolver = %q, want %q", cfg.Services[0].Resolver, rs.Name)
}
}
func TestBuildConfigFromCmd_ServiceWithHosts(t *testing.T) {
cfg, err := BuildConfigFromCmd(
[]string{":8080?hosts=example.com:1.2.3.4"},
@@ -1896,4 +1949,3 @@ func TestBuildNodeConfig_TLSOnlySecure(t *testing.T) {
t.Error("TLS.Secure should be true")
}
}
+20 -2
View File
@@ -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"`
+12
View File
@@ -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 {
+30 -27
View File
@@ -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))
}
+48
View File
@@ -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
}
+36
View File
@@ -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),
+24 -2
View File
@@ -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())
}
+4
View File
@@ -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{}
+54
View File
@@ -0,0 +1,54 @@
package dns
import (
"context"
"net"
"time"
"github.com/quic-go/quic-go"
)
type quicSession struct {
session *quic.Conn
}
func (session *quicSession) GetConn() (*quicConn, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
stream, err := session.session.OpenStreamSync(ctx)
if err != nil {
return nil, err
}
return &quicConn{
Stream: stream,
laddr: session.session.LocalAddr(),
raddr: session.session.RemoteAddr(),
}, nil
}
func (session *quicSession) IsClosed() bool {
select {
case <-session.session.Context().Done():
return true
default:
return false
}
}
func (session *quicSession) Close() error {
return session.session.CloseWithError(quic.ApplicationErrorCode(0), "closed")
}
type quicConn struct {
*quic.Stream
laddr net.Addr
raddr net.Addr
}
func (c *quicConn) LocalAddr() net.Addr {
return c.laddr
}
func (c *quicConn) RemoteAddr() net.Addr {
return c.raddr
}
+153
View File
@@ -0,0 +1,153 @@
package dns
import (
"context"
"crypto/tls"
"errors"
"net"
"sync"
"github.com/go-gost/core/dialer"
"github.com/go-gost/core/logger"
md "github.com/go-gost/core/metadata"
dns_util "github.com/go-gost/x/internal/util/dns"
"github.com/go-gost/x/registry"
"github.com/quic-go/quic-go"
)
func init() {
registry.DialerRegistry().Register("dns", NewDialer)
registry.DialerRegistry().Register("dns-tunnel", NewDialer)
registry.DialerRegistry().Register("dnst", NewDialer)
}
type dnsDialer struct {
sessions map[string]*quicSession
sessionMutex sync.Mutex
logger logger.Logger
md metadata
options dialer.Options
}
func NewDialer(opts ...dialer.Option) dialer.Dialer {
options := dialer.Options{}
for _, opt := range opts {
opt(&options)
}
return &dnsDialer{
sessions: make(map[string]*quicSession),
logger: options.Logger,
options: options,
}
}
func (d *dnsDialer) Init(md md.Metadata) (err error) {
if err = d.parseMetadata(md); err != nil {
return
}
return nil
}
func (d *dnsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (conn net.Conn, err error) {
if _, _, err := net.SplitHostPort(addr); err != nil {
addr = net.JoinHostPort(addr, "0")
}
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
d.sessionMutex.Lock()
session, ok := d.sessions[addr]
if session != nil && session.IsClosed() {
delete(d.sessions, addr)
ok = false
}
if !ok {
options := &dialer.DialOptions{}
for _, opt := range opts {
opt(options)
}
c, err := options.Dialer.Dial(ctx, "udp", "")
if err != nil {
d.sessionMutex.Unlock()
return nil, err
}
pc, ok := c.(net.PacketConn)
if !ok {
c.Close()
d.sessionMutex.Unlock()
return nil, errors.New("dns: wrong connection type")
}
pc = dns_util.ClientConn(
pc,
dns_util.DomainOption(d.md.domain),
dns_util.UDPSizeOption(d.md.udpSize),
)
session, err = d.initSession(ctx, udpAddr, pc)
if err != nil {
d.logger.Error(err)
pc.Close()
d.sessionMutex.Unlock()
return nil, err
}
d.sessions[addr] = session
}
d.sessionMutex.Unlock()
conn, err = session.GetConn()
if err != nil {
d.sessionMutex.Lock()
if d.sessions[addr] == session {
delete(d.sessions, addr)
}
session.Close()
d.sessionMutex.Unlock()
return nil, err
}
return
}
func (d *dnsDialer) initSession(ctx context.Context, addr net.Addr, conn net.PacketConn) (*quicSession, error) {
quicConfig := &quic.Config{
KeepAlivePeriod: d.md.keepAlivePeriod,
HandshakeIdleTimeout: d.md.handshakeTimeout,
MaxIdleTimeout: d.md.maxIdleTimeout,
Versions: []quic.Version{
quic.Version1,
quic.Version2,
},
MaxIncomingStreams: int64(d.md.maxStreams),
EnableDatagrams: d.md.enableDatagram,
}
tlsCfg := d.options.TLSConfig
if tlsCfg == nil {
tlsCfg = &tls.Config{}
} else {
tlsCfg = tlsCfg.Clone()
}
if len(tlsCfg.NextProtos) == 0 {
tlsCfg.NextProtos = []string{"h3", "quic/v1"}
}
session, err := quic.DialEarly(ctx, conn, addr, tlsCfg, quicConfig)
if err != nil {
return nil, err
}
return &quicSession{session: session}, nil
}
// Multiplex implements dialer.Multiplexer interface.
func (d *dnsDialer) Multiplex() bool {
return true
}
+48
View File
@@ -0,0 +1,48 @@
package dns
import (
"time"
mdata "github.com/go-gost/core/metadata"
mdutil "github.com/go-gost/x/metadata/util"
)
type metadata struct {
keepAlivePeriod time.Duration
maxIdleTimeout time.Duration
handshakeTimeout time.Duration
maxStreams int
enableDatagram bool
domain string
udpSize int
}
func (d *dnsDialer) parseMetadata(md mdata.Metadata) (err error) {
const (
keepAlive = "keepAlive"
keepAlivePeriod = "ttl"
handshakeTimeout = "handshakeTimeout"
maxIdleTimeout = "maxIdleTimeout"
maxStreams = "maxStreams"
domain = "domain"
udpSize = "udpSize"
)
if md == nil || !md.IsExists(keepAlive) || mdutil.GetBool(md, keepAlive) {
d.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod)
if d.md.keepAlivePeriod <= 0 {
d.md.keepAlivePeriod = 10 * time.Second
}
}
d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout)
d.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout)
d.md.maxStreams = mdutil.GetInt(md, maxStreams)
d.md.enableDatagram = mdutil.GetBool(md, "quic.enableDatagram", "enableDatagram")
d.md.domain = mdutil.GetString(md, domain, "dns.domain")
d.md.udpSize = mdutil.GetInt(md, udpSize, "dns.udpSize")
return
}
+2 -2
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -2
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
}
+4 -2
View File
@@ -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
}
+2 -2
View File
@@ -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
+6 -4
View File
@@ -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
View File
@@ -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
}
+64
View File
@@ -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
View File
@@ -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]
}
+25
View File
@@ -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()
+5 -1
View File
@@ -145,8 +145,12 @@ func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, i
default:
return nil, fmt.Errorf("dial: unsupported network %s", network)
}
var resolver *net.Resolver
//if runtime.GOOS != "android" {
// resolver = &net.Resolver{PreferGo: true}
//}
netd := net.Dialer{
Resolver: &net.Resolver{PreferGo: true},
Resolver: resolver,
LocalAddr: ifAddr,
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
+318
View File
@@ -0,0 +1,318 @@
package dns
import (
"errors"
"net"
"sync"
"sync/atomic"
"github.com/go-gost/core/common/bufpool"
mdns "github.com/miekg/dns"
)
const (
defaultDomain = "gost.local."
defaultUDPSize = 4096
readBufferSize = 65535
edns0PayloadOption uint16 = mdns.EDNS0LOCALSTART
)
var (
ErrInvalidPacket = errors.New("dns: invalid packet")
ErrShortBuffer = errors.New("dns: short buffer")
ErrUnsupported = errors.New("dns: unsupported operation")
)
type options struct {
domain string
udpSize uint16
}
type setBuffer interface {
SetReadBuffer(int) error
SetWriteBuffer(int) error
}
// Option configures a DNS packet wrapper.
type Option func(*options)
// DomainOption sets the DNS question name used by wrapped packets.
func DomainOption(domain string) Option {
return func(o *options) {
if domain != "" {
o.domain = mdns.Fqdn(domain)
}
}
}
// UDPSizeOption sets the EDNS0 UDP payload size advertised in wrapped packets.
func UDPSizeOption(size int) Option {
return func(o *options) {
if size > 0 && size <= readBufferSize {
o.udpSize = uint16(size)
}
}
}
func parseOptions(opts ...Option) options {
o := options{
domain: defaultDomain,
udpSize: defaultUDPSize,
}
for _, opt := range opts {
opt(&o)
}
return o
}
type clientConn struct {
net.PacketConn
id uint32
options options
}
// ClientConn wraps outbound payloads as DNS queries and unwraps DNS responses.
func ClientConn(conn net.PacketConn, opts ...Option) net.PacketConn {
return &clientConn{
PacketConn: conn,
options: parseOptions(opts...),
}
}
func (c *clientConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
buf := bufpool.Get(readBufferSize)
defer bufpool.Put(buf)
for {
n, addr, err = c.PacketConn.ReadFrom(buf)
if err != nil {
return
}
m := mdns.Msg{}
if err = m.Unpack(buf[:n]); err != nil {
return 0, addr, err
}
if !m.Response {
continue
}
payload, ok := payloadFromMessage(&m)
if !ok {
continue
}
if len(b) < len(payload) {
return 0, addr, ErrShortBuffer
}
n = copy(b, payload)
return
}
}
func (c *clientConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
id := uint16(atomic.AddUint32(&c.id, 1))
wb, err := packQuery(id, c.options.domain, c.options.udpSize, b)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(wb, addr)
if err != nil {
return 0, err
}
return len(b), nil
}
func (c *clientConn) SetReadBuffer(n int) error {
if pc, ok := c.PacketConn.(setBuffer); ok {
return pc.SetReadBuffer(n)
}
return ErrUnsupported
}
func (c *clientConn) SetWriteBuffer(n int) error {
if pc, ok := c.PacketConn.(setBuffer); ok {
return pc.SetWriteBuffer(n)
}
return ErrUnsupported
}
type queryState struct {
id uint16
question []mdns.Question
}
type serverConn struct {
net.PacketConn
mu sync.RWMutex
queries map[string]queryState
options options
}
// ServerConn unwraps DNS queries as inbound payloads and wraps outbound
// payloads as DNS responses.
func ServerConn(conn net.PacketConn, opts ...Option) net.PacketConn {
return &serverConn{
PacketConn: conn,
queries: make(map[string]queryState),
options: parseOptions(opts...),
}
}
func (c *serverConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
buf := bufpool.Get(readBufferSize)
defer bufpool.Put(buf)
for {
n, addr, err = c.PacketConn.ReadFrom(buf)
if err != nil {
return
}
m := mdns.Msg{}
if err = m.Unpack(buf[:n]); err != nil {
return 0, addr, err
}
if m.Response || !validQuestion(&m, c.options.domain) {
continue
}
payload, ok := payloadFromMessage(&m)
if !ok {
continue
}
if len(b) < len(payload) {
return 0, addr, ErrShortBuffer
}
c.storeQuery(addr, &m)
n = copy(b, payload)
return
}
}
func (c *serverConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
state := c.loadQuery(addr)
wb, err := packResponse(state.id, state.question, c.options.domain, c.options.udpSize, b)
if err != nil {
return 0, err
}
_, err = c.PacketConn.WriteTo(wb, addr)
if err != nil {
return 0, err
}
return len(b), nil
}
func (c *serverConn) SetReadBuffer(n int) error {
if pc, ok := c.PacketConn.(setBuffer); ok {
return pc.SetReadBuffer(n)
}
return ErrUnsupported
}
func (c *serverConn) SetWriteBuffer(n int) error {
if pc, ok := c.PacketConn.(setBuffer); ok {
return pc.SetWriteBuffer(n)
}
return ErrUnsupported
}
func (c *serverConn) storeQuery(addr net.Addr, m *mdns.Msg) {
if addr == nil {
return
}
state := queryState{
id: m.Id,
question: append([]mdns.Question(nil), m.Question...),
}
c.mu.Lock()
c.queries[addr.String()] = state
c.mu.Unlock()
}
func (c *serverConn) loadQuery(addr net.Addr) queryState {
if addr == nil {
return queryState{}
}
c.mu.RLock()
state := c.queries[addr.String()]
c.mu.RUnlock()
return state
}
func packQuery(id uint16, domain string, udpSize uint16, payload []byte) ([]byte, error) {
m := mdns.Msg{}
m.SetQuestion(domain, mdns.TypeNULL)
m.Id = id
m.RecursionDesired = false
addPayloadOpt(&m, udpSize, payload)
return m.Pack()
}
func packResponse(id uint16, question []mdns.Question, domain string, udpSize uint16, payload []byte) ([]byte, error) {
m := mdns.Msg{}
m.Id = id
m.Response = true
m.Authoritative = true
m.Rcode = mdns.RcodeSuccess
if len(question) > 0 {
m.Question = append([]mdns.Question(nil), question...)
} else {
m.SetQuestion(domain, mdns.TypeNULL)
m.Id = id
m.Response = true
m.Authoritative = true
}
addPayloadOpt(&m, udpSize, payload)
return m.Pack()
}
func addPayloadOpt(m *mdns.Msg, udpSize uint16, payload []byte) {
opt := &mdns.OPT{
Hdr: mdns.RR_Header{
Name: ".",
Rrtype: mdns.TypeOPT,
Class: udpSize,
},
Option: []mdns.EDNS0{
&mdns.EDNS0_LOCAL{
Code: edns0PayloadOption,
Data: append([]byte(nil), payload...),
},
},
}
m.Extra = append(m.Extra, opt)
}
func payloadFromMessage(m *mdns.Msg) ([]byte, bool) {
for _, rr := range m.Extra {
opt, ok := rr.(*mdns.OPT)
if !ok {
continue
}
for _, option := range opt.Option {
local, ok := option.(*mdns.EDNS0_LOCAL)
if !ok || local.Code != edns0PayloadOption {
continue
}
return append([]byte(nil), local.Data...), true
}
}
return nil, false
}
func validQuestion(m *mdns.Msg, domain string) bool {
if len(m.Question) == 0 {
return false
}
for _, q := range m.Question {
if q.Qtype == mdns.TypeNULL && mdns.CanonicalName(q.Name) == mdns.CanonicalName(domain) {
return true
}
}
return false
}
+109
View File
@@ -0,0 +1,109 @@
package dns
import (
"bytes"
"net"
"testing"
"time"
)
func TestPacketConnRoundTrip(t *testing.T) {
serverPC, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer serverPC.Close()
clientPC, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer clientPC.Close()
server := ServerConn(serverPC, DomainOption("tunnel.example."))
client := ClientConn(clientPC, DomainOption("tunnel.example."))
_ = server.SetDeadline(time.Now().Add(3 * time.Second))
_ = client.SetDeadline(time.Now().Add(3 * time.Second))
requestPayload := []byte("hello over dns")
if n, err := client.WriteTo(requestPayload, server.LocalAddr()); err != nil {
t.Fatal(err)
} else if n != len(requestPayload) {
t.Fatalf("client WriteTo wrote %d bytes, want %d", n, len(requestPayload))
}
buf := make([]byte, 1500)
n, clientAddr, err := server.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if got := buf[:n]; !bytes.Equal(got, requestPayload) {
t.Fatalf("server ReadFrom got %q, want %q", got, requestPayload)
}
responsePayload := []byte("world over dns")
if n, err := server.WriteTo(responsePayload, clientAddr); err != nil {
t.Fatal(err)
} else if n != len(responsePayload) {
t.Fatalf("server WriteTo wrote %d bytes, want %d", n, len(responsePayload))
}
n, serverAddr, err := client.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if serverAddr.String() != server.LocalAddr().String() {
t.Fatalf("response addr = %v, want %v", serverAddr, server.LocalAddr())
}
if got := buf[:n]; !bytes.Equal(got, responsePayload) {
t.Fatalf("client ReadFrom got %q, want %q", got, responsePayload)
}
}
func TestPacketConnSetBuffer(t *testing.T) {
base := &bufferPacketConn{}
client := ClientConn(base)
if err := client.(setBuffer).SetReadBuffer(1024); err != nil {
t.Fatal(err)
}
if err := client.(setBuffer).SetWriteBuffer(2048); err != nil {
t.Fatal(err)
}
if base.readBuffer != 1024 {
t.Fatalf("read buffer = %d, want 1024", base.readBuffer)
}
if base.writeBuffer != 2048 {
t.Fatalf("write buffer = %d, want 2048", base.writeBuffer)
}
server := ServerConn(base)
if err := server.(setBuffer).SetReadBuffer(4096); err != nil {
t.Fatal(err)
}
if err := server.(setBuffer).SetWriteBuffer(8192); err != nil {
t.Fatal(err)
}
if base.readBuffer != 4096 {
t.Fatalf("read buffer = %d, want 4096", base.readBuffer)
}
if base.writeBuffer != 8192 {
t.Fatalf("write buffer = %d, want 8192", base.writeBuffer)
}
}
type bufferPacketConn struct {
net.PacketConn
readBuffer int
writeBuffer int
}
func (c *bufferPacketConn) SetReadBuffer(n int) error {
c.readBuffer = n
return nil
}
func (c *bufferPacketConn) SetWriteBuffer(n int) error {
c.writeBuffer = n
return nil
}
+3 -2
View File
@@ -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
}
+18 -4
View File
@@ -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)
}
}
+218 -17
View File
@@ -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{},
+20 -1
View File
@@ -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
}
+36
View File
@@ -27,11 +27,14 @@ import (
func init() {
registry.ListenerRegistry().Register("dns", NewListener)
registry.ListenerRegistry().Register("dns-tunnel", NewTunnelListener)
registry.ListenerRegistry().Register("dnst", NewTunnelListener)
}
type dnsListener struct {
addr net.Addr
server Server
tunnel listener.Listener
cqueue chan net.Conn
errChan chan error
logger logger.Logger
@@ -55,6 +58,29 @@ func (l *dnsListener) Init(md md.Metadata) (err error) {
return
}
if isTunnelMode(l.md.mode) {
tunnel := NewTunnelListener(
listener.AddrOption(l.options.Addr),
listener.RouterOption(l.options.Router),
listener.AutherOption(l.options.Auther),
listener.AuthOption(l.options.Auth),
listener.TLSConfigOption(l.options.TLSConfig),
listener.AdmissionOption(l.options.Admission),
listener.TrafficLimiterOption(l.options.TrafficLimiter),
listener.ConnLimiterOption(l.options.ConnLimiter),
listener.ServiceOption(l.options.Service),
listener.ProxyProtocolOption(l.options.ProxyProtocol),
listener.StatsOption(l.options.Stats),
listener.NetnsOption(l.options.Netns),
listener.LoggerOption(l.options.Logger),
)
if err = tunnel.Init(md); err != nil {
return
}
l.tunnel = tunnel
return nil
}
switch strings.ToLower(l.md.mode) {
case "tcp":
l.addr, err = net.ResolveTCPAddr("tcp", l.options.Addr)
@@ -230,6 +256,10 @@ func (l *dnsListener) Init(md md.Metadata) (err error) {
}
func (l *dnsListener) Accept() (conn net.Conn, err error) {
if l.tunnel != nil {
return l.tunnel.Accept()
}
var ok bool
select {
case conn = <-l.cqueue:
@@ -254,10 +284,16 @@ func (l *dnsListener) Accept() (conn net.Conn, err error) {
}
func (l *dnsListener) Close() error {
if l.tunnel != nil {
return l.tunnel.Close()
}
return l.server.Shutdown()
}
func (l *dnsListener) Addr() net.Addr {
if l.tunnel != nil {
return l.tunnel.Addr()
}
return l.addr
}
+10
View File
@@ -1,6 +1,7 @@
package dns
import (
"strings"
"time"
mdata "github.com/go-gost/core/metadata"
@@ -20,6 +21,15 @@ type metadata struct {
mptcp bool
}
func isTunnelMode(mode string) bool {
switch strings.ToLower(mode) {
case "tunnel", "quic", "packet":
return true
default:
return false
}
}
func (l *dnsListener) parseMetadata(md mdata.Metadata) (err error) {
const (
backlog = "backlog"
+230
View File
@@ -0,0 +1,230 @@
package dns
import (
"context"
"crypto/tls"
"errors"
"net"
"strings"
"github.com/go-gost/core/limiter"
"github.com/go-gost/core/listener"
"github.com/go-gost/core/logger"
md "github.com/go-gost/core/metadata"
admission "github.com/go-gost/x/admission/wrapper"
xnet "github.com/go-gost/x/internal/net"
dns_util "github.com/go-gost/x/internal/util/dns"
traffic_limiter "github.com/go-gost/x/limiter/traffic"
limiter_wrapper "github.com/go-gost/x/limiter/traffic/wrapper"
metrics "github.com/go-gost/x/metrics/wrapper"
stats "github.com/go-gost/x/observer/stats/wrapper"
"github.com/quic-go/quic-go"
)
type dnsTunnelListener struct {
ln quic.EarlyListener
cqueue chan net.Conn
errChan chan error
logger logger.Logger
md tunnelMetadata
options listener.Options
}
func NewTunnelListener(opts ...listener.Option) listener.Listener {
options := listener.Options{}
for _, opt := range opts {
opt(&options)
}
return &dnsTunnelListener{
logger: options.Logger,
options: options,
}
}
func (l *dnsTunnelListener) Init(md md.Metadata) (err error) {
if err = l.parseMetadata(md); err != nil {
return
}
addr := l.options.Addr
if _, _, err := net.SplitHostPort(addr); err != nil {
addr = net.JoinHostPort(strings.Trim(addr, "[]"), "0")
}
network := "udp"
if xnet.IsIPv4(addr) {
network = "udp4"
}
laddr, err := net.ResolveUDPAddr(network, addr)
if err != nil {
return
}
lc := net.ListenConfig{}
var conn net.PacketConn
conn, err = lc.ListenPacket(context.Background(), network, laddr.String())
if err != nil {
return
}
bufferConn := conn
conn = dns_util.ServerConn(
conn,
dns_util.DomainOption(l.md.domain),
dns_util.UDPSizeOption(l.md.udpSize),
)
conn = metrics.WrapPacketConn(l.options.Service, conn)
conn = stats.WrapPacketConn(conn, l.options.Stats)
conn = admission.WrapPacketConn(l.options.Admission, conn)
conn = limiter_wrapper.WrapPacketConn(
conn,
l.options.TrafficLimiter,
traffic_limiter.ServiceLimitKey,
limiter.ScopeOption(limiter.ScopeService),
limiter.ServiceOption(l.options.Service),
limiter.NetworkOption(conn.LocalAddr().Network()),
)
conn = &setBufferPacketConn{
PacketConn: conn,
bufferConn: bufferConn,
}
config := &quic.Config{
KeepAlivePeriod: l.md.keepAlivePeriod,
HandshakeIdleTimeout: l.md.handshakeTimeout,
MaxIdleTimeout: l.md.maxIdleTimeout,
Versions: []quic.Version{
quic.Version1,
quic.Version2,
},
MaxIncomingStreams: int64(l.md.maxStreams),
EnableDatagrams: l.md.enableDatagram,
}
tlsCfg := l.options.TLSConfig
if tlsCfg == nil {
tlsCfg = &tls.Config{}
} else {
tlsCfg = tlsCfg.Clone()
}
if len(tlsCfg.NextProtos) == 0 {
tlsCfg.NextProtos = []string{"h3", "quic/v1"}
}
ln, err := quic.ListenEarly(conn, tlsCfg, config)
if err != nil {
_ = conn.Close()
return
}
l.ln = *ln
l.cqueue = make(chan net.Conn, l.md.backlog)
l.errChan = make(chan error, 1)
go l.listenLoop()
return
}
func (l *dnsTunnelListener) Accept() (conn net.Conn, err error) {
var ok bool
select {
case conn = <-l.cqueue:
conn = limiter_wrapper.WrapConn(
conn,
l.options.TrafficLimiter,
conn.RemoteAddr().String(),
limiter.ScopeOption(limiter.ScopeConn),
limiter.ServiceOption(l.options.Service),
limiter.NetworkOption(conn.LocalAddr().Network()),
limiter.SrcOption(conn.RemoteAddr().String()),
)
case err, ok = <-l.errChan:
if !ok {
err = listener.ErrClosed
}
}
return
}
func (l *dnsTunnelListener) Close() error {
return l.ln.Close()
}
func (l *dnsTunnelListener) Addr() net.Addr {
return l.ln.Addr()
}
func (l *dnsTunnelListener) listenLoop() {
for {
ctx := context.Background()
session, err := l.ln.Accept(ctx)
if err != nil {
l.logger.Error("accept: ", err)
l.errChan <- err
close(l.errChan)
return
}
l.logger.Infof("new client session: %v", session.RemoteAddr())
go l.mux(ctx, session)
}
}
func (l *dnsTunnelListener) mux(ctx context.Context, session *quic.Conn) {
defer session.CloseWithError(0, "closed")
for {
stream, err := session.AcceptStream(ctx)
if err != nil {
l.logger.Error("accept stream: ", err)
return
}
conn := &dnsTunnelConn{
Stream: stream,
laddr: session.LocalAddr(),
raddr: session.RemoteAddr(),
}
select {
case l.cqueue <- conn:
case <-stream.Context().Done():
stream.Close()
default:
stream.Close()
l.logger.Warnf("connection queue is full, client %s discarded", session.RemoteAddr())
}
}
}
type dnsTunnelConn struct {
*quic.Stream
laddr net.Addr
raddr net.Addr
}
func (c *dnsTunnelConn) LocalAddr() net.Addr {
return c.laddr
}
func (c *dnsTunnelConn) RemoteAddr() net.Addr {
return c.raddr
}
type setBufferPacketConn struct {
net.PacketConn
bufferConn net.PacketConn
}
func (c *setBufferPacketConn) SetReadBuffer(n int) error {
if pc, ok := c.bufferConn.(interface{ SetReadBuffer(int) error }); ok {
return pc.SetReadBuffer(n)
}
return errors.New("dns: set read buffer unsupported")
}
func (c *setBufferPacketConn) SetWriteBuffer(n int) error {
if pc, ok := c.bufferConn.(interface{ SetWriteBuffer(int) error }); ok {
return pc.SetWriteBuffer(n)
}
return errors.New("dns: set write buffer unsupported")
}
+55
View File
@@ -0,0 +1,55 @@
package dns
import (
"time"
mdata "github.com/go-gost/core/metadata"
mdutil "github.com/go-gost/x/metadata/util"
)
type tunnelMetadata struct {
keepAlivePeriod time.Duration
handshakeTimeout time.Duration
maxIdleTimeout time.Duration
maxStreams int
enableDatagram bool
backlog int
domain string
udpSize int
}
func (l *dnsTunnelListener) parseMetadata(md mdata.Metadata) (err error) {
const (
keepAlive = "keepAlive"
keepAlivePeriod = "ttl"
handshakeTimeout = "handshakeTimeout"
maxIdleTimeout = "maxIdleTimeout"
maxStreams = "maxStreams"
backlog = "backlog"
domain = "domain"
udpSize = "udpSize"
)
l.md.backlog = mdutil.GetInt(md, backlog)
if l.md.backlog <= 0 {
l.md.backlog = defaultBacklog
}
if mdutil.GetBool(md, keepAlive) {
l.md.keepAlivePeriod = mdutil.GetDuration(md, keepAlivePeriod)
if l.md.keepAlivePeriod <= 0 {
l.md.keepAlivePeriod = 10 * time.Second
}
}
l.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout)
l.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout)
l.md.maxStreams = mdutil.GetInt(md, maxStreams)
l.md.enableDatagram = mdutil.GetBool(md, "quic.enableDatagram", "enableDatagram")
l.md.domain = mdutil.GetString(md, domain, "dns.domain")
l.md.udpSize = mdutil.GetInt(md, udpSize, "dns.udpSize")
return
}
+7
View File
@@ -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
+43
View File
@@ -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...)
}
+14
View File
@@ -82,6 +82,11 @@ func NewExchanger(addr string, opts ...Option) (Exchanger, error) {
opt(&options)
}
addr = strings.TrimSpace(addr)
if isSystemResolverAddr(addr) {
return newSystemExchanger(addr, options), nil
}
if !strings.Contains(addr, "://") {
addr = "udp://" + addr
}
@@ -143,6 +148,15 @@ func NewExchanger(addr string, opts ...Option) (Exchanger, error) {
return ex, nil
}
func isSystemResolverAddr(addr string) bool {
switch strings.ToLower(strings.TrimSpace(addr)) {
case "system", "auto", "system://", "auto://":
return true
default:
return false
}
}
func (ex *exchanger) Exchange(ctx context.Context, msg []byte) ([]byte, error) {
if ex.network == "https" {
return ex.dohExchange(ctx, msg)
+112
View File
@@ -10,6 +10,7 @@ import (
"github.com/go-gost/core/chain"
xlogger "github.com/go-gost/x/logger"
"github.com/miekg/dns"
)
func TestNewExchanger_UDP(t *testing.T) {
@@ -175,6 +176,117 @@ func TestNewExchanger_String(t *testing.T) {
}
}
func TestNewExchanger_System(t *testing.T) {
ex, err := NewExchanger("system", LoggerOption(xlogger.Nop()))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, ok := ex.(*systemExchanger); !ok {
t.Fatalf("NewExchanger(system) = %T, want *systemExchanger", ex)
}
if ex.String() != "system" {
t.Errorf("String() = %q, want system", ex.String())
}
}
func TestNewExchanger_AutoSystem(t *testing.T) {
ex, err := NewExchanger("auto", LoggerOption(xlogger.Nop()))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, ok := ex.(*systemExchanger); !ok {
t.Fatalf("NewExchanger(auto) = %T, want *systemExchanger", ex)
}
}
func TestSystemExchanger_ExchangeA(t *testing.T) {
ex := &systemExchanger{
rawAddr: "system",
lookupIP: func(_ context.Context, network, host string) ([]net.IP, error) {
if network != "ip4" {
t.Errorf("network = %q, want ip4", network)
}
if host != "example.com" {
t.Errorf("host = %q, want example.com", host)
}
return []net.IP{
net.ParseIP("192.0.2.1"),
net.ParseIP("2001:db8::1"),
}, nil
},
}
mq := new(dns.Msg)
mq.SetQuestion(dns.Fqdn("example.com"), dns.TypeA)
packed, err := mq.Pack()
if err != nil {
t.Fatal(err)
}
reply, err := ex.Exchange(context.Background(), packed)
if err != nil {
t.Fatalf("Exchange: %v", err)
}
mr := new(dns.Msg)
if err := mr.Unpack(reply); err != nil {
t.Fatalf("Unpack: %v", err)
}
if len(mr.Answer) != 1 {
t.Fatalf("len(Answer) = %d, want 1", len(mr.Answer))
}
a, ok := mr.Answer[0].(*dns.A)
if !ok {
t.Fatalf("Answer[0] = %T, want *dns.A", mr.Answer[0])
}
if !a.A.Equal(net.ParseIP("192.0.2.1")) {
t.Errorf("A = %s, want 192.0.2.1", a.A)
}
}
func TestSystemExchanger_ExchangeAAAA(t *testing.T) {
ex := &systemExchanger{
rawAddr: "system",
lookupIP: func(_ context.Context, network, host string) ([]net.IP, error) {
if network != "ip6" {
t.Errorf("network = %q, want ip6", network)
}
if host != "example.com" {
t.Errorf("host = %q, want example.com", host)
}
return []net.IP{
net.ParseIP("192.0.2.1"),
net.ParseIP("2001:db8::1"),
}, nil
},
}
mq := new(dns.Msg)
mq.SetQuestion(dns.Fqdn("example.com"), dns.TypeAAAA)
packed, err := mq.Pack()
if err != nil {
t.Fatal(err)
}
reply, err := ex.Exchange(context.Background(), packed)
if err != nil {
t.Fatalf("Exchange: %v", err)
}
mr := new(dns.Msg)
if err := mr.Unpack(reply); err != nil {
t.Fatalf("Unpack: %v", err)
}
if len(mr.Answer) != 1 {
t.Fatalf("len(Answer) = %d, want 1", len(mr.Answer))
}
aaaa, ok := mr.Answer[0].(*dns.AAAA)
if !ok {
t.Fatalf("Answer[0] = %T, want *dns.AAAA", mr.Answer[0])
}
if !aaaa.AAAA.Equal(net.ParseIP("2001:db8::1")) {
t.Errorf("AAAA = %s, want 2001:db8::1", aaaa.AAAA)
}
}
func TestExchange_DialFailure(t *testing.T) {
ex, err := NewExchanger("udp://127.0.0.1:1", TimeoutOption(100*time.Millisecond), LoggerOption(xlogger.Nop()))
if err != nil {
+108
View File
@@ -0,0 +1,108 @@
package exchanger
import (
"context"
"errors"
"net"
"strings"
"time"
"github.com/miekg/dns"
)
type systemExchanger struct {
rawAddr string
timeout time.Duration
lookupIP func(ctx context.Context, network, host string) ([]net.IP, error)
}
func newSystemExchanger(rawAddr string, opts Options) Exchanger {
if opts.timeout <= 0 {
opts.timeout = 5 * time.Second
}
ex := &systemExchanger{
rawAddr: rawAddr,
timeout: opts.timeout,
}
ex.lookupIP = net.DefaultResolver.LookupIP
return ex
}
func (ex *systemExchanger) Exchange(ctx context.Context, msg []byte) ([]byte, error) {
mq := new(dns.Msg)
if err := mq.Unpack(msg); err != nil {
return nil, err
}
mr := new(dns.Msg)
mr.SetReply(mq)
mr.RecursionAvailable = true
if len(mq.Question) == 0 {
return mr.Pack()
}
q := mq.Question[0]
network := "ip"
switch q.Qtype {
case dns.TypeA:
network = "ip4"
case dns.TypeAAAA:
network = "ip6"
default:
return mr.Pack()
}
if ex.timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, ex.timeout)
defer cancel()
}
host := strings.TrimSuffix(q.Name, ".")
ips, err := ex.lookupIP(ctx, network, host)
if err != nil {
var dnsErr *net.DNSError
if !errors.As(err, &dnsErr) || !dnsErr.IsNotFound {
return nil, err
}
}
for _, ip := range ips {
switch q.Qtype {
case dns.TypeA:
if ip4 := ip.To4(); ip4 != nil {
mr.Answer = append(mr.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: q.Name,
Rrtype: dns.TypeA,
Class: q.Qclass,
},
A: ip4,
})
}
case dns.TypeAAAA:
if ip.To4() == nil {
if ip16 := ip.To16(); ip16 != nil {
mr.Answer = append(mr.Answer, &dns.AAAA{
Hdr: dns.RR_Header{
Name: q.Name,
Rrtype: dns.TypeAAAA,
Class: q.Qclass,
},
AAAA: ip16,
})
}
}
}
}
return mr.Pack()
}
func (ex *systemExchanger) String() string {
if ex.rawAddr == "" {
return "system"
}
return ex.rawAddr
}
+90
View File
@@ -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
}
+106
View File
@@ -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
View File
@@ -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
}
+109
View File
@@ -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)
}
}