Compare commits
14 Commits
d4e9450bad
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b1e38e2c0 | |||
| 74fef8838e | |||
| bf85966600 | |||
| a24d79fd45 | |||
| cb5d5c590b | |||
| 3993c95700 | |||
| f63f36805b | |||
| 1b07475bf3 | |||
| 3b25e0317f | |||
| fb0cf72446 | |||
| ee7b1e9462 | |||
| 143438a30e | |||
| cd97e5e746 | |||
| c1d954d689 |
@@ -4,11 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-gost/core/auth"
|
"github.com/go-gost/core/auth"
|
||||||
|
xctx "github.com/go-gost/x/ctx"
|
||||||
"github.com/go-gost/x/internal/loader"
|
"github.com/go-gost/x/internal/loader"
|
||||||
xlogger "github.com/go-gost/x/logger"
|
xlogger "github.com/go-gost/x/logger"
|
||||||
)
|
)
|
||||||
@@ -1003,3 +1005,96 @@ type staticAuther struct {
|
|||||||
func (s *staticAuther) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
|
func (s *staticAuther) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
|
||||||
return s.id, s.ok
|
return s.id, s.ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- whitelist tests ---
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_IPMatch(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "" || !ok {
|
||||||
|
t.Fatalf("expected (\"\", true) for matched IP, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_IPNoMatch(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 12345})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "test" || !ok {
|
||||||
|
t.Fatalf("expected (\"test\", true) for non-matched IP, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_CIDRMatch(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"10.0.0.0/8"})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.1.2.3"), Port: 9999})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "" || !ok {
|
||||||
|
t.Fatalf("expected (\"\", true) for matched CIDR, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_CIDRNoMatch(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"10.0.0.0/8"})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 8080})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "test" || !ok {
|
||||||
|
t.Fatalf("expected (\"test\", true) for non-matched CIDR, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_EmptyPatterns(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "test" || !ok {
|
||||||
|
t.Fatalf("expected (\"test\", true) for empty patterns, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_NoSrcAddr(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"192.168.1.100"})
|
||||||
|
id, ok := w.Authenticate(context.Background(), "user", "pass")
|
||||||
|
if id != "test" || !ok {
|
||||||
|
t.Fatalf("expected (\"test\", true) with no src addr in context, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_NilAuther(t *testing.T) {
|
||||||
|
// When IP matches, the whitelist returns OK regardless of nil auther.
|
||||||
|
w := WhitelistedAuthenticator(nil, []string{"192.168.1.100"})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "" || !ok {
|
||||||
|
t.Fatalf("expected (\"\", true) for matched whitelist even with nil auther, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_NilAutherNoMatch(t *testing.T) {
|
||||||
|
// When IP does not match and auther is nil, it falls through to ("", false).
|
||||||
|
w := WhitelistedAuthenticator(nil, []string{"192.168.1.100"})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("10.0.0.1"), Port: 54321})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "" || ok {
|
||||||
|
t.Fatalf("expected (\"\", false) for nil auther with non-matching IP, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_UnderlyingDenies(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: false}, []string{"10.0.0.0/8"})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.1"), Port: 8080})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "test" || ok {
|
||||||
|
t.Fatalf("expected (\"test\", false) when underlying auther denies, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWhitelistedAuthenticator_InvalidPatterns(t *testing.T) {
|
||||||
|
w := WhitelistedAuthenticator(&staticAuther{id: "test", ok: true}, []string{"not-an-ip", "also-not-cidr/", ""})
|
||||||
|
ctx := xctx.ContextWithSrcAddr(context.Background(), &net.TCPAddr{IP: net.ParseIP("192.168.1.100"), Port: 54321})
|
||||||
|
id, ok := w.Authenticate(ctx, "user", "pass")
|
||||||
|
if id != "test" || !ok {
|
||||||
|
t.Fatalf("expected (\"test\", true) for all-invalid patterns, got (%q, %v)", id, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/auth"
|
||||||
|
xctx "github.com/go-gost/x/ctx"
|
||||||
|
"github.com/go-gost/x/internal/matcher"
|
||||||
|
)
|
||||||
|
|
||||||
|
// whitelistedAuthenticator wraps an Authenticator to skip authentication
|
||||||
|
// for client IPs matching the configured whitelist patterns.
|
||||||
|
type whitelistedAuthenticator struct {
|
||||||
|
auther auth.Authenticator
|
||||||
|
ipMatcher matcher.Matcher
|
||||||
|
cidrMatcher matcher.Matcher
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhitelistedAuthenticator returns an Authenticator that skips the underlying
|
||||||
|
// auther when the client's IP address matches one of the given patterns.
|
||||||
|
// Patterns may be IP addresses (e.g. "192.168.1.1") or CIDR ranges
|
||||||
|
// (e.g. "10.0.0.0/8"). Invalid patterns are silently ignored.
|
||||||
|
func WhitelistedAuthenticator(auther auth.Authenticator, patterns []string) auth.Authenticator {
|
||||||
|
var ips []net.IP
|
||||||
|
var inets []*net.IPNet
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
pattern = strings.TrimSpace(pattern)
|
||||||
|
if pattern == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ip := net.ParseIP(pattern); ip != nil {
|
||||||
|
ips = append(ips, ip)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, inet, err := net.ParseCIDR(pattern); err == nil {
|
||||||
|
inets = append(inets, inet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &whitelistedAuthenticator{
|
||||||
|
auther: auther,
|
||||||
|
ipMatcher: matcher.IPMatcher(ips),
|
||||||
|
cidrMatcher: matcher.CIDRMatcher(inets),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *whitelistedAuthenticator) Authenticate(ctx context.Context, user, password string, opts ...auth.Option) (string, bool) {
|
||||||
|
if srcAddr := xctx.SrcAddrFromContext(ctx); srcAddr != nil {
|
||||||
|
host, _, err := net.SplitHostPort(srcAddr.String())
|
||||||
|
if err == nil && host != "" {
|
||||||
|
if w.ipMatcher.Match(host) || w.cidrMatcher.Match(host) {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if w.auther != nil {
|
||||||
|
return w.auther.Authenticate(ctx, user, password, opts...)
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
+4
-1
@@ -95,7 +95,9 @@ func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error)
|
|||||||
resolverCfg.Nameservers = append(
|
resolverCfg.Nameservers = append(
|
||||||
resolverCfg.Nameservers,
|
resolverCfg.Nameservers,
|
||||||
&config.NameserverConfig{
|
&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{
|
&config.NameserverConfig{
|
||||||
Addr: rs,
|
Addr: rs,
|
||||||
Prefer: mdutil.GetString(md, "prefer"),
|
Prefer: mdutil.GetString(md, "prefer"),
|
||||||
|
Only: mdutil.GetString(md, "only"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-19
@@ -211,13 +211,13 @@ func TestNorm(t *testing.T) {
|
|||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty string",
|
name: "empty string",
|
||||||
input: "",
|
input: "",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "whitespace only",
|
name: "whitespace only",
|
||||||
input: " ",
|
input: " ",
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -326,12 +326,12 @@ func TestCutHost(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseSelector(t *testing.T) {
|
func TestParseSelector(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input map[string]any
|
input map[string]any
|
||||||
wantNil bool
|
wantNil bool
|
||||||
wantStrat string
|
wantStrat string
|
||||||
wantFails int
|
wantFails int
|
||||||
wantDurGt bool // failTimeout > 0
|
wantDurGt bool // failTimeout > 0
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no params",
|
name: "no params",
|
||||||
@@ -359,9 +359,9 @@ func TestParseSelector(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "all params with failTimeout",
|
name: "all params with failTimeout",
|
||||||
input: map[string]any{
|
input: map[string]any{
|
||||||
"strategy": "hash",
|
"strategy": "hash",
|
||||||
"maxFails": 5,
|
"maxFails": 5,
|
||||||
"failTimeout": "10s",
|
"failTimeout": "10s",
|
||||||
},
|
},
|
||||||
wantStrat: "hash",
|
wantStrat: "hash",
|
||||||
wantFails: 5,
|
wantFails: 5,
|
||||||
@@ -455,8 +455,8 @@ func TestCopyConnectorConfig(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("non-nil deep copy", func(t *testing.T) {
|
t.Run("non-nil deep copy", func(t *testing.T) {
|
||||||
orig := &config.ConnectorConfig{
|
orig := &config.ConnectorConfig{
|
||||||
Type: "http",
|
Type: "http",
|
||||||
Auth: &config.AuthConfig{Username: "u", Password: "p"},
|
Auth: &config.AuthConfig{Username: "u", Password: "p"},
|
||||||
Metadata: map[string]any{"k": "v"},
|
Metadata: map[string]any{"k": "v"},
|
||||||
}
|
}
|
||||||
c := copyConnectorConfig(orig)
|
c := copyConnectorConfig(orig)
|
||||||
@@ -487,9 +487,9 @@ func TestCopyDialerConfig(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("non-nil deep copy", func(t *testing.T) {
|
t.Run("non-nil deep copy", func(t *testing.T) {
|
||||||
orig := &config.DialerConfig{
|
orig := &config.DialerConfig{
|
||||||
Type: "tls",
|
Type: "tls",
|
||||||
TLS: &config.TLSConfig{ServerName: "example.com"},
|
TLS: &config.TLSConfig{ServerName: "example.com"},
|
||||||
Auth: &config.AuthConfig{Username: "u"},
|
Auth: &config.AuthConfig{Username: "u"},
|
||||||
Metadata: map[string]any{"k": "v"},
|
Metadata: map[string]any{"k": "v"},
|
||||||
}
|
}
|
||||||
c := copyDialerConfig(orig)
|
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) {
|
func TestBuildConfigFromCmd_NodeWithHosts(t *testing.T) {
|
||||||
cfg, err := BuildConfigFromCmd(
|
cfg, err := BuildConfigFromCmd(
|
||||||
[]string{":8080"},
|
[]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) {
|
func TestBuildConfigFromCmd_ServiceWithHosts(t *testing.T) {
|
||||||
cfg, err := BuildConfigFromCmd(
|
cfg, err := BuildConfigFromCmd(
|
||||||
[]string{":8080?hosts=example.com:1.2.3.4"},
|
[]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")
|
t.Error("TLS.Secure should be true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-2
@@ -316,6 +316,11 @@ type RecorderObject struct {
|
|||||||
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
|
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 {
|
type LimiterConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Limits []string `yaml:",omitempty" json:"limits,omitempty"`
|
Limits []string `yaml:",omitempty" json:"limits,omitempty"`
|
||||||
@@ -397,6 +402,8 @@ type HTTPBodyRewriteConfig struct {
|
|||||||
Type string
|
Type string
|
||||||
Match string
|
Match string
|
||||||
Replacement string
|
Replacement string
|
||||||
|
// name of the rewriter plugin (via registry)
|
||||||
|
Rewriter string
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeFilterConfig struct {
|
type NodeFilterConfig struct {
|
||||||
@@ -405,6 +412,15 @@ type NodeFilterConfig struct {
|
|||||||
Path string `yaml:",omitempty" json:"path,omitempty"`
|
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 {
|
type NodeMatcherConfig struct {
|
||||||
Rule string `yaml:",omitempty" json:"rule,omitempty"`
|
Rule string `yaml:",omitempty" json:"rule,omitempty"`
|
||||||
Priority int `yaml:",omitempty" json:"priority,omitempty"`
|
Priority int `yaml:",omitempty" json:"priority,omitempty"`
|
||||||
@@ -478,8 +494,9 @@ type ServiceConfig struct {
|
|||||||
Logger string `yaml:",omitempty" json:"logger,omitempty"`
|
Logger string `yaml:",omitempty" json:"logger,omitempty"`
|
||||||
Loggers []string `yaml:",omitempty" json:"loggers,omitempty"`
|
Loggers []string `yaml:",omitempty" json:"loggers,omitempty"`
|
||||||
Observer string `yaml:",omitempty" json:"observer,omitempty"`
|
Observer string `yaml:",omitempty" json:"observer,omitempty"`
|
||||||
Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"`
|
Rewriter string `yaml:",omitempty" json:"rewriter,omitempty"`
|
||||||
Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"`
|
Recorders []*RecorderObject `yaml:",omitempty" json:"recorders,omitempty"`
|
||||||
|
Handler *HandlerConfig `yaml:",omitempty" json:"handler,omitempty"`
|
||||||
Listener *ListenerConfig `yaml:",omitempty" json:"listener,omitempty"`
|
Listener *ListenerConfig `yaml:",omitempty" json:"listener,omitempty"`
|
||||||
Forwarder *ForwarderConfig `yaml:",omitempty" json:"forwarder,omitempty"`
|
Forwarder *ForwarderConfig `yaml:",omitempty" json:"forwarder,omitempty"`
|
||||||
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
|
Metadata map[string]any `yaml:",omitempty" json:"metadata,omitempty"`
|
||||||
@@ -628,6 +645,7 @@ type Config struct {
|
|||||||
Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"`
|
Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"`
|
||||||
SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"`
|
SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"`
|
||||||
Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"`
|
Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"`
|
||||||
|
Rewriters []*RewriterConfig `yaml:",omitempty" json:"rewriters,omitempty"`
|
||||||
Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"`
|
Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"`
|
||||||
Quotas []*QuotaConfig `yaml:",omitempty" json:"quotas,omitempty"`
|
Quotas []*QuotaConfig `yaml:",omitempty" json:"quotas,omitempty"`
|
||||||
CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"`
|
CLimiters []*LimiterConfig `yaml:"climiters,omitempty" json:"climiters,omitempty"`
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
reg "github.com/go-gost/core/registry"
|
reg "github.com/go-gost/core/registry"
|
||||||
"github.com/go-gost/core/resolver"
|
"github.com/go-gost/core/resolver"
|
||||||
"github.com/go-gost/core/router"
|
"github.com/go-gost/core/router"
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
"github.com/go-gost/core/sd"
|
"github.com/go-gost/core/sd"
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/config/parsing"
|
"github.com/go-gost/x/config/parsing"
|
||||||
@@ -38,6 +39,7 @@ import (
|
|||||||
quota_parser "github.com/go-gost/x/config/parsing/quota"
|
quota_parser "github.com/go-gost/x/config/parsing/quota"
|
||||||
recorder_parser "github.com/go-gost/x/config/parsing/recorder"
|
recorder_parser "github.com/go-gost/x/config/parsing/recorder"
|
||||||
resolver_parser "github.com/go-gost/x/config/parsing/resolver"
|
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"
|
router_parser "github.com/go-gost/x/config/parsing/router"
|
||||||
sd_parser "github.com/go-gost/x/config/parsing/sd"
|
sd_parser "github.com/go-gost/x/config/parsing/sd"
|
||||||
service_parser "github.com/go-gost/x/config/parsing/service"
|
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]
|
var entries []named[traffic.TrafficLimiter]
|
||||||
for _, c := range cfg.Limiters {
|
for _, c := range cfg.Limiters {
|
||||||
|
|||||||
@@ -29,6 +29,29 @@ import (
|
|||||||
// metadata-driven options (so_mark, interface, netns, proxy protocol), sets up
|
// metadata-driven options (so_mark, interface, netns, proxy protocol), sets up
|
||||||
// bypass rules, node filters, HTTP settings, and TLS node settings. The hop
|
// bypass rules, node filters, HTTP settings, and TLS node settings. The hop
|
||||||
// parameter is used only for logging context.
|
// 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) {
|
func ParseNode(hop string, cfg *config.NodeConfig, log logger.Logger) (*chain.Node, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, 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 rule := strings.TrimSpace(cfg.Matcher.Rule); rule != "" {
|
||||||
if matcher, err := routing.NewMatcher(rule); err == nil {
|
if matcher, err := routing.NewMatcher(rule); err == nil {
|
||||||
log.Debugf("new matcher for node %s with rule %s", cfg.Name, cfg.Matcher.Rule)
|
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 {
|
if priority == 0 {
|
||||||
priority = len(cfg.Matcher.Rule)
|
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 {
|
settings.RewriteResponseBody = append(settings.RewriteResponseBody, parseBodyRewrites(cfg.HTTP.RewriteBody, log)...)
|
||||||
if pattern, _ := regexp.Compile(v.Match); pattern != nil {
|
settings.RewriteResponseBody = append(settings.RewriteResponseBody, parseBodyRewrites(cfg.HTTP.RewriteResponseBody, log)...)
|
||||||
settings.RewriteResponseBody = append(settings.RewriteResponseBody, chain.HTTPBodyRewriteSettings{
|
settings.RewriteRequestBody = append(settings.RewriteRequestBody, parseBodyRewrites(cfg.HTTP.RewriteRequestBody, log)...)
|
||||||
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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opts = append(opts, chain.HTTPNodeOption(settings))
|
opts = append(opts, chain.HTTPNodeOption(settings))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package rewriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
|
"github.com/go-gost/x/config"
|
||||||
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
rewriter_plugin "github.com/go-gost/x/rewriter/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseRewriter converts a RewriterConfig into a rewriter.Rewriter.
|
||||||
|
// It currently supports plugin backends only (HTTP or gRPC).
|
||||||
|
// Returns nil when cfg is nil or no backend is configured.
|
||||||
|
func ParseRewriter(cfg *config.RewriterConfig) rewriter.Rewriter {
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Plugin != nil {
|
||||||
|
var tlsCfg *tls.Config
|
||||||
|
if cfg.Plugin.TLS != nil {
|
||||||
|
tlsCfg = &tls.Config{
|
||||||
|
ServerName: cfg.Plugin.TLS.ServerName,
|
||||||
|
InsecureSkipVerify: !cfg.Plugin.TLS.Secure,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch strings.ToLower(cfg.Plugin.Type) {
|
||||||
|
case "http":
|
||||||
|
return rewriter_plugin.NewHTTPPlugin(
|
||||||
|
cfg.Name, cfg.Plugin.Addr,
|
||||||
|
plugin.TokenOption(cfg.Plugin.Token),
|
||||||
|
plugin.TLSConfigOption(tlsCfg),
|
||||||
|
plugin.TimeoutOption(cfg.Plugin.Timeout),
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
return rewriter_plugin.NewGRPCPlugin(
|
||||||
|
cfg.Name, cfg.Plugin.Addr,
|
||||||
|
plugin.TokenOption(cfg.Plugin.Token),
|
||||||
|
plugin.TLSConfigOption(tlsCfg),
|
||||||
|
plugin.TimeoutOption(cfg.Plugin.Timeout),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/go-gost/core/handler"
|
"github.com/go-gost/core/handler"
|
||||||
"github.com/go-gost/core/hop"
|
"github.com/go-gost/core/hop"
|
||||||
"github.com/go-gost/core/listener"
|
"github.com/go-gost/core/listener"
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
"github.com/go-gost/core/logger"
|
"github.com/go-gost/core/logger"
|
||||||
"github.com/go-gost/core/observer/stats"
|
"github.com/go-gost/core/observer/stats"
|
||||||
"github.com/go-gost/core/recorder"
|
"github.com/go-gost/core/recorder"
|
||||||
@@ -303,6 +304,32 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
|||||||
auther = xauth.AuthenticatorGroup(authers...)
|
auther = xauth.AuthenticatorGroup(authers...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse skipauth from handler metadata to whitelist client IPs that
|
||||||
|
// should bypass authentication. Accepts both YAML arrays
|
||||||
|
// (skipauth: ["10.0.0.0/8"]) and comma-separated URL query values
|
||||||
|
// (?skipauth=10.0.0.0/8,192.168.1.1).
|
||||||
|
if cfg.Handler.Metadata != nil {
|
||||||
|
hmd := metadata.NewMetadata(cfg.Handler.Metadata)
|
||||||
|
skipauth := mdutil.GetStrings(hmd, "skipauth")
|
||||||
|
if len(skipauth) == 0 {
|
||||||
|
if s := mdutil.GetString(hmd, "skipauth"); s != "" {
|
||||||
|
for _, p := range strings.Split(s, ",") {
|
||||||
|
if p = strings.TrimSpace(p); p != "" {
|
||||||
|
skipauth = append(skipauth, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(skipauth) > 0 {
|
||||||
|
if auther != nil {
|
||||||
|
auther = xauth.WhitelistedAuthenticator(auther, skipauth)
|
||||||
|
handlerLogger.Debugf("skipauth whitelist applied: %v", skipauth)
|
||||||
|
} else {
|
||||||
|
handlerLogger.Warnf("skipauth configured but no auther set — authentication is already disabled for all clients")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var recorders []recorder.RecorderObject
|
var recorders []recorder.RecorderObject
|
||||||
for _, r := range cfg.Recorders {
|
for _, r := range cfg.Recorders {
|
||||||
md := metadata.NewMetadata(r.Metadata)
|
md := metadata.NewMetadata(r.Metadata)
|
||||||
@@ -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{
|
routerOpts = []chain.RouterOption{
|
||||||
chain.RetriesRouterOption(cfg.Handler.Retries),
|
chain.RetriesRouterOption(cfg.Handler.Retries),
|
||||||
chain.TimeoutRouterOption(dialTimeout),
|
chain.TimeoutRouterOption(dialTimeout),
|
||||||
@@ -356,6 +391,7 @@ func ParseService(cfg *config.ServiceConfig) (service.Service, error) {
|
|||||||
handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)),
|
handler.TrafficLimiterOption(registry.TrafficLimiterRegistry().Get(cfg.Handler.Limiter)),
|
||||||
handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)),
|
handler.ObserverOption(registry.ObserverRegistry().Get(cfg.Handler.Observer)),
|
||||||
handler.RecordersOption(recorders...),
|
handler.RecordersOption(recorders...),
|
||||||
|
handler.RewriterOption(rew),
|
||||||
handler.LoggerOption(handlerLogger),
|
handler.LoggerOption(handlerLogger),
|
||||||
handler.ServiceOption(cfg.Name),
|
handler.ServiceOption(cfg.Name),
|
||||||
handler.NetnsOption(netnsIn),
|
handler.NetnsOption(netnsIn),
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package http
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"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)
|
log.Debugf("connect %s/%s", address, network)
|
||||||
|
|
||||||
|
connectHost := address
|
||||||
|
if c.md.host != "" {
|
||||||
|
// 伪装的地址
|
||||||
|
connectHost = c.md.host
|
||||||
|
}
|
||||||
|
|
||||||
req := &http.Request{
|
req := &http.Request{
|
||||||
Method: http.MethodConnect,
|
Method: http.MethodConnect,
|
||||||
URL: &url.URL{Host: address},
|
URL: &url.URL{Host: connectHost},
|
||||||
Host: address,
|
Host: connectHost,
|
||||||
ProtoMajor: 1,
|
ProtoMajor: 1,
|
||||||
ProtoMinor: 1,
|
ProtoMinor: 1,
|
||||||
Header: c.md.header,
|
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")
|
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 {
|
if user := c.options.Auth; user != nil {
|
||||||
u := user.Username()
|
u := user.Username()
|
||||||
p, _ := user.Password()
|
p, _ := user.Password()
|
||||||
@@ -129,3 +144,10 @@ func (c *httpConnector) Connect(ctx context.Context, conn net.Conn, network, add
|
|||||||
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func encodeServerName(name string) string {
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
binary.Write(buf, binary.BigEndian, crc32.ChecksumIEEE([]byte(name)))
|
||||||
|
buf.WriteString(base64.RawURLEncoding.EncodeToString([]byte(name)))
|
||||||
|
return base64.RawURLEncoding.EncodeToString(buf.Bytes())
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,15 +11,19 @@ import (
|
|||||||
type metadata struct {
|
type metadata struct {
|
||||||
connectTimeout time.Duration
|
connectTimeout time.Duration
|
||||||
header http.Header
|
header http.Header
|
||||||
|
//伪装混淆用host
|
||||||
|
host string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *httpConnector) parseMetadata(md mdata.Metadata) (err error) {
|
func (c *httpConnector) parseMetadata(md mdata.Metadata) (err error) {
|
||||||
const (
|
const (
|
||||||
connectTimeout = "timeout"
|
connectTimeout = "timeout"
|
||||||
header = "header"
|
header = "header"
|
||||||
|
host = "host"
|
||||||
)
|
)
|
||||||
|
|
||||||
c.md.connectTimeout = mdutil.GetDuration(md, connectTimeout)
|
c.md.connectTimeout = mdutil.GetDuration(md, connectTimeout)
|
||||||
|
c.md.host = mdutil.GetString(md, host)
|
||||||
|
|
||||||
if mm := mdutil.GetStringMapString(md, header); len(mm) > 0 {
|
if mm := mdutil.GetStringMapString(md, header); len(mm) > 0 {
|
||||||
hd := http.Header{}
|
hd := http.Header{}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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,7 +2,6 @@ package mtls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
"github.com/go-gost/x/internal/net/proxyproto"
|
"github.com/go-gost/x/internal/net/proxyproto"
|
||||||
"github.com/go-gost/x/internal/util/mux"
|
"github.com/go-gost/x/internal/util/mux"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
|
"github.com/go-gost/x/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
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) {
|
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 {
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-4
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/go-gost/x/internal/util/mux"
|
"github.com/go-gost/x/internal/util/mux"
|
||||||
ws_util "github.com/go-gost/x/internal/util/ws"
|
ws_util "github.com/go-gost/x/internal/util/ws"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
|
"github.com/go-gost/x/util"
|
||||||
"github.com/gorilla/websocket"
|
"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 {
|
if d.tlsEnabled {
|
||||||
url.Scheme = "wss"
|
urlObj.Scheme = "wss"
|
||||||
dialer.TLSClientConfig = d.options.TLSConfig
|
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 {
|
if d.md.handshakeTimeout > 0 {
|
||||||
conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout))
|
conn.SetReadDeadline(time.Now().Add(d.md.handshakeTimeout))
|
||||||
}
|
}
|
||||||
|
urlStr, errUnescape := url.QueryUnescape(urlObj.String())
|
||||||
c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
mdata "github.com/go-gost/core/metadata"
|
mdata "github.com/go-gost/core/metadata"
|
||||||
mdutil "github.com/go-gost/x/metadata/util"
|
|
||||||
"github.com/go-gost/x/internal/util/mux"
|
"github.com/go-gost/x/internal/util/mux"
|
||||||
|
mdutil "github.com/go-gost/x/metadata/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -18,6 +18,8 @@ type metadata struct {
|
|||||||
host string
|
host string
|
||||||
path string
|
path string
|
||||||
|
|
||||||
|
useH2 bool
|
||||||
|
|
||||||
handshakeTimeout time.Duration
|
handshakeTimeout time.Duration
|
||||||
readHeaderTimeout time.Duration
|
readHeaderTimeout time.Duration
|
||||||
readBufferSize int
|
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.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval")
|
||||||
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
|
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
|
||||||
|
|
||||||
|
d.md.useH2 = mdutil.GetBool(md, "h2")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package tls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,6 +12,7 @@ import (
|
|||||||
xnet "github.com/go-gost/x/internal/net"
|
xnet "github.com/go-gost/x/internal/net"
|
||||||
"github.com/go-gost/x/internal/net/proxyproto"
|
"github.com/go-gost/x/internal/net/proxyproto"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
|
"github.com/go-gost/x/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -77,7 +77,7 @@ func (d *tlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dia
|
|||||||
defer conn.SetDeadline(time.Time{})
|
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 {
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/go-gost/core/dialer"
|
"github.com/go-gost/core/dialer"
|
||||||
"github.com/go-gost/core/logger"
|
"github.com/go-gost/core/logger"
|
||||||
@@ -14,6 +13,7 @@ import (
|
|||||||
xnet "github.com/go-gost/x/internal/net"
|
xnet "github.com/go-gost/x/internal/net"
|
||||||
"github.com/go-gost/x/internal/net/proxyproto"
|
"github.com/go-gost/x/internal/net/proxyproto"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
|
"github.com/go-gost/x/util"
|
||||||
utls "github.com/refraction-networking/utls"
|
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)
|
clientHelloID, ok := GetClientHelloID(d.md.fingerprint)
|
||||||
if ok {
|
if ok {
|
||||||
d.log.Debugf("utls handshake with fingerprint: %s", d.md.fingerprint)
|
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)
|
uconn := utls.UClient(conn, utlsCfg, clientHelloID)
|
||||||
if err := uconn.HandshakeContext(ctx); err != nil {
|
if err := uconn.HandshakeContext(ctx); err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
|
|||||||
+18
-4
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/go-gost/x/internal/net/proxyproto"
|
"github.com/go-gost/x/internal/net/proxyproto"
|
||||||
ws_util "github.com/go-gost/x/internal/util/ws"
|
ws_util "github.com/go-gost/x/internal/util/ws"
|
||||||
"github.com/go-gost/x/registry"
|
"github.com/go-gost/x/registry"
|
||||||
|
"github.com/go-gost/x/util"
|
||||||
"github.com/gorilla/websocket"
|
"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 {
|
if d.tlsEnabled {
|
||||||
url.Scheme = "wss"
|
urlObj.Scheme = "wss"
|
||||||
dialer.TLSClientConfig = d.options.TLSConfig
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
urlStr, errUnescape := url.QueryUnescape(urlObj.String())
|
||||||
c, resp, err := dialer.DialContext(ctx, url.String(), d.md.header)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type metadata struct {
|
type metadata struct {
|
||||||
host string
|
host string
|
||||||
path string
|
path string
|
||||||
|
useH2 bool
|
||||||
|
|
||||||
handshakeTimeout time.Duration
|
handshakeTimeout time.Duration
|
||||||
readHeaderTimeout 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.tcpKeepaliveInterval = mdutil.GetDuration(md, "tcp.keepalive.interval")
|
||||||
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
|
d.md.tcpKeepaliveCount = mdutil.GetInt(md, "tcp.keepalive.count")
|
||||||
|
|
||||||
|
d.md.useH2 = mdutil.GetBool(md, "h2")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ require (
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||||
github.com/gin-contrib/cors v1.7.2
|
github.com/gin-contrib/cors v1.7.2
|
||||||
github.com/gin-gonic/gin v1.10.1
|
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/go-shadowsocks2 v0.1.3
|
||||||
github.com/go-gost/gosocks4 v0.1.0
|
github.com/go-gost/gosocks4 v0.1.0
|
||||||
github.com/go-gost/gosocks5 v0.5.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/relay v0.6.1
|
||||||
github.com/go-gost/tls-dissector v0.2.0
|
github.com/go-gost/tls-dissector v0.2.0
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
|
|||||||
@@ -51,16 +51,18 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
|
|||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-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 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
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.5.0 h1:3SkPojjqw2Av9l5OSyRVlBpRvWhaXgU5BWMXB9aR4SM=
|
||||||
github.com/go-gost/core v0.4.3/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
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 h1:6CUZLp+mTWXnKP2aK8/Z9ZP+ERMX9gSbywmPu4kGX/A=
|
||||||
github.com/go-gost/go-shadowsocks2 v0.1.3/go.mod h1:866zFNNI3He6Wef1M/IvAjTal74WhcfKfBgRpTlkKys=
|
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 h1:eAzev6qw4fzkFQKC9uCHLVNnnPdHyqCggbnfNN80Pmk=
|
||||||
github.com/go-gost/gosocks4 v0.1.0/go.mod h1:hzVjwijJuZR1pp3GqpTj+AKcSGrx68RlWTrQMFMYBP0=
|
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 h1:YE37l1MJwde8diIQdynStqogMotG5enoTdborhA5yic=
|
||||||
github.com/go-gost/gosocks5 v0.5.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
|
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.4.0 h1:M7MR5PL7QAFCrdWfcXVX+5iLNTVIZlhl8FfdV/K8dZY=
|
||||||
github.com/go-gost/plugin v0.3.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts=
|
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 h1:7SqnHFbY8x/DzvjpK03a5zcVH9+TbJAcnW/RT6s1ecc=
|
||||||
github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
|
github.com/go-gost/relay v0.6.1/go.mod h1:Dku0f5sfjOClrZFiDmQUrYYJ4uof7rnkCUBfsl0PSAI=
|
||||||
github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg=
|
github.com/go-gost/tls-dissector v0.2.0 h1:9tE6WOzzpurATTBWn60DU4R8gibpGNY8/qVcc1SicVg=
|
||||||
|
|||||||
+26
-15
@@ -98,16 +98,13 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
|||||||
}
|
}
|
||||||
|
|
||||||
br := bufio.NewReader(conn)
|
br := bufio.NewReader(conn)
|
||||||
proto, _ := sniffing.Sniff(ctx, br)
|
|
||||||
|
|
||||||
conn = xnet.NewReadWriteConn(br, conn, conn)
|
// Peek 1 byte first to detect small protocols (SOCKS4/SOCKS5)
|
||||||
|
// that send fewer bytes than the 5-byte TLS record header that
|
||||||
if proto == sniffing.ProtoTLS {
|
// Sniff() needs. If we called Sniff() first, its Peek(5) would
|
||||||
return h.handleTLS(ctx, conn, log)
|
// 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.
|
||||||
// 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.
|
|
||||||
b, err := br.Peek(1)
|
b, err := br.Peek(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
@@ -116,18 +113,32 @@ func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch b[0] {
|
switch b[0] {
|
||||||
case gosocks4.Ver4: // socks4
|
case gosocks4.Ver4:
|
||||||
|
conn = xnet.NewReadWriteConn(br, conn, conn)
|
||||||
if h.socks4Handler != nil {
|
if h.socks4Handler != nil {
|
||||||
return h.socks4Handler.Handle(ctx, conn)
|
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 {
|
if h.socks5Handler != nil {
|
||||||
return h.socks5Handler.Handle(ctx, conn)
|
return h.socks5Handler.Handle(ctx, conn)
|
||||||
}
|
}
|
||||||
default: // http
|
return nil
|
||||||
if h.httpHandler != nil {
|
}
|
||||||
return h.httpHandler.Handle(ctx, conn)
|
|
||||||
}
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,21 @@ func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, network st
|
|||||||
return err
|
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 := gosocks5.Addr{}
|
||||||
saddr.ParseFrom(cc.LocalAddr().String())
|
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
|
// domainResolvePacketConn wraps a net.PacketConn and resolves domain
|
||||||
// addresses to IPs in WriteTo calls. This guarantees that SOCKS5 UDP
|
// addresses to IPs in WriteTo calls. This guarantees that SOCKS5 UDP
|
||||||
// response datagrams never carry ATYP=Domain (0x03), which some clients
|
// response datagrams never carry ATYP=Domain (0x03), which some clients
|
||||||
|
|||||||
+7
-3
@@ -208,13 +208,17 @@ func (p *chainHop) Select(ctx context.Context, opts ...hop.SelectOption) *chain.
|
|||||||
})
|
})
|
||||||
|
|
||||||
if nodes[0].Options().Priority > 0 &&
|
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.
|
// Priority short-circuit: highest-priority non-backup node wins.
|
||||||
// Conditions: (1) top priority > 0 means a matcher indicated routing
|
// Conditions: (1) top priority > 0 means a matcher indicated routing
|
||||||
// specificity, so the top node is authoritative for this request;
|
// specificity, so the top node is authoritative for this request;
|
||||||
// (2) no backup node is present, otherwise BackupFilter would be
|
// (2) no backup node is present, otherwise BackupFilter would be
|
||||||
// silently bypassed. When both hold the selector (FailFilter,
|
// silently bypassed; (3) the top priority is strictly higher than
|
||||||
// BackupFilter, strategy) is skipped and the best node wins directly.
|
// 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)
|
p.logger.Debugf("priority shortcut: node %s selected", nodes[0].Name)
|
||||||
return nodes[0]
|
return nodes[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -522,6 +522,31 @@ func TestSelect_WithSelector(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSelect_EqualPriorityDoesNotShortcut(t *testing.T) {
|
||||||
|
// Two nodes with identical matchers get the same default priority.
|
||||||
|
// The priority shortcut must NOT trigger when multiple nodes share
|
||||||
|
// the highest priority — the selector should still apply.
|
||||||
|
n1 := chain.NewNode("n1", "127.0.0.1:8080",
|
||||||
|
chain.MatcherNodeOption(&testMatcher{match: true}),
|
||||||
|
chain.PriorityNodeOption(50),
|
||||||
|
)
|
||||||
|
n2 := chain.NewNode("n2", "127.0.0.1:9090",
|
||||||
|
chain.MatcherNodeOption(&testMatcher{match: true}),
|
||||||
|
chain.PriorityNodeOption(50),
|
||||||
|
)
|
||||||
|
sel := &testNodeSelector{selectedIdx: 1} // picks n2
|
||||||
|
h := newTestHop(NodeOption(n1, n2), SelectorOption(sel))
|
||||||
|
defer h.Close()
|
||||||
|
|
||||||
|
node := h.Select(context.Background())
|
||||||
|
if node == nil {
|
||||||
|
t.Fatal("expected node, got nil")
|
||||||
|
}
|
||||||
|
if node.Name != "n2" {
|
||||||
|
t.Errorf("expected 'n2' (selected by selector, not shortcut), got %q", node.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSelect_NilNodesSkipped(t *testing.T) {
|
func TestSelect_NilNodesSkipped(t *testing.T) {
|
||||||
h := newTestHop()
|
h := newTestHop()
|
||||||
defer h.Close()
|
defer h.Close()
|
||||||
|
|||||||
@@ -145,8 +145,12 @@ func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, i
|
|||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("dial: unsupported network %s", network)
|
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{
|
netd := net.Dialer{
|
||||||
Resolver: &net.Resolver{PreferGo: true},
|
Resolver: resolver,
|
||||||
LocalAddr: ifAddr,
|
LocalAddr: ifAddr,
|
||||||
Control: func(network, address string, c syscall.RawConn) error {
|
Control: func(network, address string, c syscall.RawConn) error {
|
||||||
return c.Control(func(fd uintptr) {
|
return c.Control(func(fd uintptr) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -356,6 +356,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
|||||||
if re.Pattern.MatchString(req.URL.Path) {
|
if re.Pattern.MatchString(req.URL.Path) {
|
||||||
if s := re.Pattern.ReplaceAllString(req.URL.Path, re.Replacement); s != "" {
|
if s := re.Pattern.ReplaceAllString(req.URL.Path, re.Replacement); s != "" {
|
||||||
req.URL.Path = s
|
req.URL.Path = s
|
||||||
|
ro.HTTP.URI = req.URL.RequestURI()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,7 +369,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
|||||||
|
|
||||||
// Rewrite request body before wrapping for recording,
|
// Rewrite request body before wrapping for recording,
|
||||||
// so the recorder sees the rewritten content.
|
// 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)
|
log.Errorf("rewrite request body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -445,7 +446,7 @@ func (h *Sniffer) httpRoundTrip(ctx context.Context, rw, cc io.ReadWriteCloser,
|
|||||||
resp.Header.Set("Connection", "close")
|
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)
|
log.Errorf("rewrite body: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func (h *Sniffer) handleUpgradeResponse(ctx context.Context, rw, cc io.ReadWrite
|
|||||||
return xnet.Pipe(ctx, rw, cc)
|
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 {
|
if resp == nil || len(rewrites) == 0 || resp.ContentLength <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,14 @@ func rewriteRespBody(resp *http.Response, rewrites ...chain.HTTPBodyRewriteSetti
|
|||||||
continue
|
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)
|
body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +97,7 @@ func drainBody(b io.ReadCloser) (body []byte, err error) {
|
|||||||
return buf.Bytes(), nil
|
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 {
|
if req == nil || len(rewrites) == 0 || req.Body == nil || req.ContentLength <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -114,7 +121,14 @@ func rewriteReqBody(req *http.Request, rewrites ...chain.HTTPBodyRewriteSettings
|
|||||||
continue
|
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)
|
body = rewrite.Pattern.ReplaceAll(body, rewrite.Replacement)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/go-gost/core/chain"
|
"github.com/go-gost/core/chain"
|
||||||
"github.com/go-gost/core/hop"
|
"github.com/go-gost/core/hop"
|
||||||
"github.com/go-gost/core/recorder"
|
"github.com/go-gost/core/recorder"
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
xlogger "github.com/go-gost/x/logger"
|
xlogger "github.com/go-gost/x/logger"
|
||||||
xrecorder "github.com/go-gost/x/recorder"
|
xrecorder "github.com/go-gost/x/recorder"
|
||||||
)
|
)
|
||||||
@@ -164,7 +165,7 @@ func TestRewriteReqBody(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if tt.req != nil {
|
if tt.req != nil {
|
||||||
_ = rewriteReqBody(tt.req, tt.rewrites...)
|
_ = rewriteReqBody(context.Background(), tt.req, tt.rewrites...)
|
||||||
if tt.req.Body != nil {
|
if tt.req.Body != nil {
|
||||||
body, _ := io.ReadAll(tt.req.Body)
|
body, _ := io.ReadAll(tt.req.Body)
|
||||||
tt.req.Body.Close()
|
tt.req.Body.Close()
|
||||||
@@ -178,7 +179,7 @@ func TestRewriteReqBody(t *testing.T) {
|
|||||||
t.Errorf("ContentLength = %d, want %d", tt.req.ContentLength, tt.wantCL)
|
t.Errorf("ContentLength = %d, want %d", tt.req.ContentLength, tt.wantCL)
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
func TestRewriteRespBody(t *testing.T) {
|
||||||
t.Run("nil response", func(t *testing.T) {
|
t.Run("nil response", func(t *testing.T) {
|
||||||
if err := rewriteRespBody(nil); err != nil {
|
if err := rewriteRespBody(context.Background(), nil); err != nil {
|
||||||
t.Errorf("rewriteRespBody(nil) = %v, want nil", err)
|
t.Errorf("rewriteRespBody(context.Background(), nil) = %v, want nil", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -354,7 +355,7 @@ func TestRewriteRespBody(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
Body: io.NopCloser(strings.NewReader("original")),
|
||||||
}
|
}
|
||||||
_ = rewriteRespBody(resp)
|
_ = rewriteRespBody(context.Background(), resp)
|
||||||
// Body unchanged
|
// Body unchanged
|
||||||
got, _ := io.ReadAll(resp.Body)
|
got, _ := io.ReadAll(resp.Body)
|
||||||
if string(got) != "original" {
|
if string(got) != "original" {
|
||||||
@@ -367,7 +368,7 @@ func TestRewriteRespBody(t *testing.T) {
|
|||||||
ContentLength: 0,
|
ContentLength: 0,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
Body: io.NopCloser(strings.NewReader("original")),
|
||||||
}
|
}
|
||||||
_ = rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||||
Pattern: regexp.MustCompile("."),
|
Pattern: regexp.MustCompile("."),
|
||||||
Type: "*",
|
Type: "*",
|
||||||
Replacement: []byte("replaced"),
|
Replacement: []byte("replaced"),
|
||||||
@@ -385,7 +386,7 @@ func TestRewriteRespBody(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
Body: io.NopCloser(strings.NewReader("original")),
|
||||||
}
|
}
|
||||||
_ = rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
_ = rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||||
Type: "*",
|
Type: "*",
|
||||||
Replacement: []byte("replaced"),
|
Replacement: []byte("replaced"),
|
||||||
})
|
})
|
||||||
@@ -413,7 +414,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
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)
|
got, _ := io.ReadAll(resp.Body)
|
||||||
if string(got) != "replaced" {
|
if string(got) != "replaced" {
|
||||||
t.Errorf("body = %q, want %q", string(got), "replaced")
|
t.Errorf("body = %q, want %q", string(got), "replaced")
|
||||||
@@ -426,7 +427,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("hello")),
|
Body: io.NopCloser(strings.NewReader("hello")),
|
||||||
}
|
}
|
||||||
_ = rewriteRespBody(resp, makeRewrite("*", "world"))
|
_ = rewriteRespBody(context.Background(), resp, makeRewrite("*", "world"))
|
||||||
got, _ := io.ReadAll(resp.Body)
|
got, _ := io.ReadAll(resp.Body)
|
||||||
if string(got) != "world" {
|
if string(got) != "world" {
|
||||||
t.Errorf("body = %q, want %q", string(got), "world")
|
t.Errorf("body = %q, want %q", string(got), "world")
|
||||||
@@ -439,7 +440,7 @@ func TestRewriteRespBodyContentTypeFilter(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
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)
|
got, _ := io.ReadAll(resp.Body)
|
||||||
if string(got) != "original" {
|
if string(got) != "original" {
|
||||||
t.Errorf("body = %q, want %q (unchanged)", 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.
|
// 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.
|
// 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)
|
got, _ := io.ReadAll(resp.Body)
|
||||||
if string(got) != "replaced" {
|
if string(got) != "replaced" {
|
||||||
t.Errorf("body = %q, want %q (should be rewritten)", 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,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("<title>Old Title</title>")),
|
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"),
|
Pattern: regexp.MustCompile("Old"),
|
||||||
Type: "text/html",
|
Type: "text/html",
|
||||||
Replacement: []byte("New"),
|
Replacement: []byte("New"),
|
||||||
@@ -1169,7 +1170,7 @@ func TestRewriteRespBody_MultipleRewrites(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("hello")),
|
Body: io.NopCloser(strings.NewReader("hello")),
|
||||||
}
|
}
|
||||||
err := rewriteRespBody(resp,
|
err := rewriteRespBody(context.Background(), resp,
|
||||||
chain.HTTPBodyRewriteSettings{
|
chain.HTTPBodyRewriteSettings{
|
||||||
Pattern: regexp.MustCompile("h.*o"),
|
Pattern: regexp.MustCompile("h.*o"),
|
||||||
Type: "text/html",
|
Type: "text/html",
|
||||||
@@ -1196,7 +1197,7 @@ func TestRewriteRespBody_NilPattern(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
Body: io.NopCloser(strings.NewReader("original")),
|
||||||
}
|
}
|
||||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||||
Pattern: nil,
|
Pattern: nil,
|
||||||
Type: "*",
|
Type: "*",
|
||||||
Replacement: []byte("replaced"),
|
Replacement: []byte("replaced"),
|
||||||
@@ -1215,7 +1216,7 @@ func TestRewriteRespBody_EmptyContentTypeUsesDefault(t *testing.T) {
|
|||||||
ContentLength: 100,
|
ContentLength: 100,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
Body: io.NopCloser(strings.NewReader("original")),
|
||||||
}
|
}
|
||||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||||
Pattern: regexp.MustCompile(".*"),
|
Pattern: regexp.MustCompile(".*"),
|
||||||
Type: "",
|
Type: "",
|
||||||
Replacement: []byte("rewritten"),
|
Replacement: []byte("rewritten"),
|
||||||
@@ -1235,7 +1236,7 @@ func TestRewriteRespBody_NegativeContentLength(t *testing.T) {
|
|||||||
ContentLength: -1,
|
ContentLength: -1,
|
||||||
Body: io.NopCloser(strings.NewReader("original")),
|
Body: io.NopCloser(strings.NewReader("original")),
|
||||||
}
|
}
|
||||||
err := rewriteRespBody(resp, chain.HTTPBodyRewriteSettings{
|
err := rewriteRespBody(context.Background(), resp, chain.HTTPBodyRewriteSettings{
|
||||||
Pattern: regexp.MustCompile(".*"),
|
Pattern: regexp.MustCompile(".*"),
|
||||||
Type: "*",
|
Type: "*",
|
||||||
Replacement: []byte("replaced"),
|
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) {
|
func TestCopyWebsocketFrame_EmptyPayload(t *testing.T) {
|
||||||
h := &Sniffer{
|
h := &Sniffer{
|
||||||
Recorder: &noopRecorder{},
|
Recorder: &noopRecorder{},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package socks
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"net"
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-gost/core/common/bufpool"
|
"github.com/go-gost/core/common/bufpool"
|
||||||
"github.com/go-gost/gosocks5"
|
"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.
|
// ReadFrom reads an UDP datagram.
|
||||||
// NOTE: for server side,
|
// NOTE: for server side,
|
||||||
// the returned addr is the target address the client want to relay to.
|
// 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])
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,14 @@ import (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registry.ListenerRegistry().Register("dns", NewListener)
|
registry.ListenerRegistry().Register("dns", NewListener)
|
||||||
|
registry.ListenerRegistry().Register("dns-tunnel", NewTunnelListener)
|
||||||
|
registry.ListenerRegistry().Register("dnst", NewTunnelListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
type dnsListener struct {
|
type dnsListener struct {
|
||||||
addr net.Addr
|
addr net.Addr
|
||||||
server Server
|
server Server
|
||||||
|
tunnel listener.Listener
|
||||||
cqueue chan net.Conn
|
cqueue chan net.Conn
|
||||||
errChan chan error
|
errChan chan error
|
||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
@@ -55,6 +58,29 @@ func (l *dnsListener) Init(md md.Metadata) (err error) {
|
|||||||
return
|
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) {
|
switch strings.ToLower(l.md.mode) {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
l.addr, err = net.ResolveTCPAddr("tcp", l.options.Addr)
|
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) {
|
func (l *dnsListener) Accept() (conn net.Conn, err error) {
|
||||||
|
if l.tunnel != nil {
|
||||||
|
return l.tunnel.Accept()
|
||||||
|
}
|
||||||
|
|
||||||
var ok bool
|
var ok bool
|
||||||
select {
|
select {
|
||||||
case conn = <-l.cqueue:
|
case conn = <-l.cqueue:
|
||||||
@@ -254,10 +284,16 @@ func (l *dnsListener) Accept() (conn net.Conn, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *dnsListener) Close() error {
|
func (l *dnsListener) Close() error {
|
||||||
|
if l.tunnel != nil {
|
||||||
|
return l.tunnel.Close()
|
||||||
|
}
|
||||||
return l.server.Shutdown()
|
return l.server.Shutdown()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *dnsListener) Addr() net.Addr {
|
func (l *dnsListener) Addr() net.Addr {
|
||||||
|
if l.tunnel != nil {
|
||||||
|
return l.tunnel.Addr()
|
||||||
|
}
|
||||||
return l.addr
|
return l.addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mdata "github.com/go-gost/core/metadata"
|
mdata "github.com/go-gost/core/metadata"
|
||||||
@@ -20,6 +21,15 @@ type metadata struct {
|
|||||||
mptcp bool
|
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) {
|
func (l *dnsListener) parseMetadata(md mdata.Metadata) (err error) {
|
||||||
const (
|
const (
|
||||||
backlog = "backlog"
|
backlog = "backlog"
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
reg "github.com/go-gost/core/registry"
|
reg "github.com/go-gost/core/registry"
|
||||||
"github.com/go-gost/core/resolver"
|
"github.com/go-gost/core/resolver"
|
||||||
"github.com/go-gost/core/router"
|
"github.com/go-gost/core/router"
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
"github.com/go-gost/core/sd"
|
"github.com/go-gost/core/sd"
|
||||||
"github.com/go-gost/core/service"
|
"github.com/go-gost/core/service"
|
||||||
"github.com/go-gost/x/limiter/quota"
|
"github.com/go-gost/x/limiter/quota"
|
||||||
@@ -46,6 +47,7 @@ var (
|
|||||||
resolverReg reg.Registry[resolver.Resolver] = new(resolverRegistry)
|
resolverReg reg.Registry[resolver.Resolver] = new(resolverRegistry)
|
||||||
hostsReg reg.Registry[hosts.HostMapper] = new(hostsRegistry)
|
hostsReg reg.Registry[hosts.HostMapper] = new(hostsRegistry)
|
||||||
recorderReg reg.Registry[recorder.Recorder] = new(recorderRegistry)
|
recorderReg reg.Registry[recorder.Recorder] = new(recorderRegistry)
|
||||||
|
rewriterReg reg.Registry[rewriter.Rewriter] = new(rewriterRegistry)
|
||||||
|
|
||||||
trafficLimiterReg reg.Registry[traffic.TrafficLimiter] = new(trafficLimiterRegistry)
|
trafficLimiterReg reg.Registry[traffic.TrafficLimiter] = new(trafficLimiterRegistry)
|
||||||
connLimiterReg reg.Registry[conn.ConnLimiter] = new(connLimiterRegistry)
|
connLimiterReg reg.Registry[conn.ConnLimiter] = new(connLimiterRegistry)
|
||||||
@@ -187,6 +189,11 @@ func RecorderRegistry() reg.Registry[recorder.Recorder] {
|
|||||||
return recorderReg
|
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.
|
// TrafficLimiterRegistry returns the global registry of traffic limiter instances.
|
||||||
func TrafficLimiterRegistry() reg.Registry[traffic.TrafficLimiter] {
|
func TrafficLimiterRegistry() reg.Registry[traffic.TrafficLimiter] {
|
||||||
return trafficLimiterReg
|
return trafficLimiterReg
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rewriterRegistry implements a hot-reload-safe registry for rewriter.Rewriter.
|
||||||
|
type rewriterRegistry struct {
|
||||||
|
registry[rewriter.Rewriter]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register stores a Rewriter under the given name.
|
||||||
|
func (r *rewriterRegistry) Register(name string, v rewriter.Rewriter) error {
|
||||||
|
return r.registry.Register(name, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a wrapper that delegates to the currently registered Rewriter.
|
||||||
|
// Returns nil if name is empty.
|
||||||
|
func (r *rewriterRegistry) Get(name string) rewriter.Rewriter {
|
||||||
|
if name != "" {
|
||||||
|
return &rewriterWrapper{name: name, r: r}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rewriterRegistry) get(name string) rewriter.Rewriter {
|
||||||
|
return r.registry.Get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type rewriterWrapper struct {
|
||||||
|
name string
|
||||||
|
r *rewriterRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *rewriterWrapper) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) {
|
||||||
|
v := w.r.get(w.name)
|
||||||
|
if v == nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
return v.Rewrite(ctx, b, opts...)
|
||||||
|
}
|
||||||
@@ -82,6 +82,11 @@ func NewExchanger(addr string, opts ...Option) (Exchanger, error) {
|
|||||||
opt(&options)
|
opt(&options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addr = strings.TrimSpace(addr)
|
||||||
|
if isSystemResolverAddr(addr) {
|
||||||
|
return newSystemExchanger(addr, options), nil
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.Contains(addr, "://") {
|
if !strings.Contains(addr, "://") {
|
||||||
addr = "udp://" + addr
|
addr = "udp://" + addr
|
||||||
}
|
}
|
||||||
@@ -143,6 +148,15 @@ func NewExchanger(addr string, opts ...Option) (Exchanger, error) {
|
|||||||
return ex, nil
|
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) {
|
func (ex *exchanger) Exchange(ctx context.Context, msg []byte) ([]byte, error) {
|
||||||
if ex.network == "https" {
|
if ex.network == "https" {
|
||||||
return ex.dohExchange(ctx, msg)
|
return ex.dohExchange(ctx, msg)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-gost/core/chain"
|
"github.com/go-gost/core/chain"
|
||||||
xlogger "github.com/go-gost/x/logger"
|
xlogger "github.com/go-gost/x/logger"
|
||||||
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewExchanger_UDP(t *testing.T) {
|
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) {
|
func TestExchange_DialFailure(t *testing.T) {
|
||||||
ex, err := NewExchanger("udp://127.0.0.1:1", TimeoutOption(100*time.Millisecond), LoggerOption(xlogger.Nop()))
|
ex, err := NewExchanger("udp://127.0.0.1:1", TimeoutOption(100*time.Millisecond), LoggerOption(xlogger.Nop()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package rewriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
|
"github.com/go-gost/plugin/rewriter/proto"
|
||||||
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type grpcPlugin struct {
|
||||||
|
conn grpc.ClientConnInterface
|
||||||
|
client proto.RewriterClient
|
||||||
|
log logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGRPCPlugin creates a Rewriter plugin based on gRPC.
|
||||||
|
func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) rewriter.Rewriter {
|
||||||
|
var options plugin.Options
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logger.Default().WithFields(map[string]any{
|
||||||
|
"kind": "rewriter",
|
||||||
|
"rewriter": name,
|
||||||
|
})
|
||||||
|
conn, err := plugin.NewGRPCConn(addr, &options)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
if conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &grpcPlugin{
|
||||||
|
conn: conn,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
p.client = proto.NewRewriterClient(conn)
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *grpcPlugin) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) {
|
||||||
|
if p.client == nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var options rewriter.RewriteOptions
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := json.Marshal(options.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := p.client.Rewrite(ctx,
|
||||||
|
&proto.RewriteRequest{
|
||||||
|
Data: b,
|
||||||
|
Metadata: md,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error(err)
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
if reply == nil || !reply.Ok {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
if reply.Data != nil {
|
||||||
|
return reply.Data, nil
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *grpcPlugin) Close() error {
|
||||||
|
if p.conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if closer, ok := p.conn.(io.Closer); ok {
|
||||||
|
return closer.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package rewriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-gost/core/logger"
|
||||||
|
"github.com/go-gost/core/rewriter"
|
||||||
|
"github.com/go-gost/x/internal/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpPluginRequest struct {
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
Metadata []byte `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpPluginResponse struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpPlugin struct {
|
||||||
|
url string
|
||||||
|
client *http.Client
|
||||||
|
header http.Header
|
||||||
|
log logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTTPPlugin creates a Rewriter plugin based on HTTP.
|
||||||
|
func NewHTTPPlugin(name string, url string, opts ...plugin.Option) rewriter.Rewriter {
|
||||||
|
var options plugin.Options
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &httpPlugin{
|
||||||
|
url: url,
|
||||||
|
client: plugin.NewHTTPClient(&options),
|
||||||
|
header: options.Header,
|
||||||
|
log: logger.Default().WithFields(map[string]any{
|
||||||
|
"kind": "rewriter",
|
||||||
|
"rewriter": name,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *httpPlugin) Rewrite(ctx context.Context, b []byte, opts ...rewriter.RewriteOption) ([]byte, error) {
|
||||||
|
if p.client == nil {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var options rewriter.RewriteOptions
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
md, err := json.Marshal(options.Metadata)
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rb := httpPluginRequest{
|
||||||
|
Data: b,
|
||||||
|
Metadata: md,
|
||||||
|
}
|
||||||
|
v, err := json.Marshal(&rb)
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.url, bytes.NewReader(v))
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.header != nil {
|
||||||
|
req.Header = p.header.Clone()
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := p.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
defer io.Copy(io.Discard, resp.Body)
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res := httpPluginResponse{}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !res.OK {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
if res.Data != nil {
|
||||||
|
return res.Data, nil
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdtls "crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
tls "github.com/refraction-networking/utls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewUTLSClient(conn net.Conn, config *stdtls.Config, clientHelloID tls.ClientHelloID) *tls.UConn {
|
||||||
|
return tls.UClient(conn, NewUTLSConfig(config), clientHelloID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUTLSChromeClient(conn net.Conn, config *stdtls.Config) *tls.UConn {
|
||||||
|
return NewUTLSClient(conn, config, tls.HelloChrome_Auto)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUTLSWebSocketClient(conn net.Conn, config *stdtls.Config, useH2 bool) (*tls.UConn, error) {
|
||||||
|
utlsConfig := NewLegacyUTLSConfig(config)
|
||||||
|
if useH2 {
|
||||||
|
return tls.UClient(conn, utlsConfig, tls.HelloChrome_Auto), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
client := tls.UClient(conn, utlsConfig, tls.HelloCustom)
|
||||||
|
spec, err := NewWebSocketClientHelloSpec()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := client.ApplyPreset(spec); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLegacyUTLSConfig(config *stdtls.Config) *tls.Config {
|
||||||
|
if config == nil {
|
||||||
|
return &tls.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &tls.Config{
|
||||||
|
InsecureSkipVerify: config.InsecureSkipVerify,
|
||||||
|
ServerName: config.ServerName,
|
||||||
|
ClientAuth: tls.ClientAuthType(config.ClientAuth),
|
||||||
|
ClientCAs: config.ClientCAs,
|
||||||
|
RootCAs: config.RootCAs,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Certificates = make([]tls.Certificate, 0, len(config.Certificates))
|
||||||
|
for _, cert := range config.Certificates {
|
||||||
|
cfg.Certificates = append(cfg.Certificates, tls.Certificate{
|
||||||
|
Certificate: cert.Certificate,
|
||||||
|
PrivateKey: cert.PrivateKey,
|
||||||
|
OCSPStaple: cert.OCSPStaple,
|
||||||
|
SignedCertificateTimestamps: cert.SignedCertificateTimestamps,
|
||||||
|
Leaf: cert.Leaf,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUTLSConfig(config *stdtls.Config) *tls.Config {
|
||||||
|
if config == nil {
|
||||||
|
return &tls.Config{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &tls.Config{
|
||||||
|
Rand: config.Rand,
|
||||||
|
Time: config.Time,
|
||||||
|
RootCAs: config.RootCAs,
|
||||||
|
NextProtos: append([]string(nil), config.NextProtos...),
|
||||||
|
ServerName: config.ServerName,
|
||||||
|
ClientAuth: tls.ClientAuthType(config.ClientAuth),
|
||||||
|
ClientCAs: config.ClientCAs,
|
||||||
|
InsecureSkipVerify: config.InsecureSkipVerify,
|
||||||
|
CipherSuites: append([]uint16(nil), config.CipherSuites...),
|
||||||
|
SessionTicketsDisabled: config.SessionTicketsDisabled,
|
||||||
|
MinVersion: config.MinVersion,
|
||||||
|
MaxVersion: config.MaxVersion,
|
||||||
|
DynamicRecordSizingDisabled: config.DynamicRecordSizingDisabled,
|
||||||
|
Renegotiation: tls.RenegotiationSupport(config.Renegotiation),
|
||||||
|
KeyLogWriter: config.KeyLogWriter,
|
||||||
|
VerifyPeerCertificate: config.VerifyPeerCertificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Certificates = make([]tls.Certificate, 0, len(config.Certificates))
|
||||||
|
for _, cert := range config.Certificates {
|
||||||
|
cfg.Certificates = append(cfg.Certificates, toUTLSCertificate(cert))
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.CurvePreferences = make([]tls.CurveID, 0, len(config.CurvePreferences))
|
||||||
|
for _, curve := range config.CurvePreferences {
|
||||||
|
cfg.CurvePreferences = append(cfg.CurvePreferences, tls.CurveID(curve))
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.VerifyConnection != nil {
|
||||||
|
cfg.VerifyConnection = func(state tls.ConnectionState) error {
|
||||||
|
return config.VerifyConnection(toStdTLSConnectionState(state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUTLSCertificate(cert stdtls.Certificate) tls.Certificate {
|
||||||
|
signatureAlgorithms := make([]tls.SignatureScheme, 0, len(cert.SupportedSignatureAlgorithms))
|
||||||
|
for _, sig := range cert.SupportedSignatureAlgorithms {
|
||||||
|
signatureAlgorithms = append(signatureAlgorithms, tls.SignatureScheme(sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tls.Certificate{
|
||||||
|
Certificate: cert.Certificate,
|
||||||
|
PrivateKey: cert.PrivateKey,
|
||||||
|
SupportedSignatureAlgorithms: signatureAlgorithms,
|
||||||
|
OCSPStaple: cert.OCSPStaple,
|
||||||
|
SignedCertificateTimestamps: cert.SignedCertificateTimestamps,
|
||||||
|
Leaf: cert.Leaf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toStdTLSConnectionState(state tls.ConnectionState) stdtls.ConnectionState {
|
||||||
|
return stdtls.ConnectionState{
|
||||||
|
Version: state.Version,
|
||||||
|
HandshakeComplete: state.HandshakeComplete,
|
||||||
|
DidResume: state.DidResume,
|
||||||
|
CipherSuite: state.CipherSuite,
|
||||||
|
NegotiatedProtocol: state.NegotiatedProtocol,
|
||||||
|
NegotiatedProtocolIsMutual: state.NegotiatedProtocolIsMutual,
|
||||||
|
ServerName: state.ServerName,
|
||||||
|
PeerCertificates: state.PeerCertificates,
|
||||||
|
VerifiedChains: state.VerifiedChains,
|
||||||
|
SignedCertificateTimestamps: state.SignedCertificateTimestamps,
|
||||||
|
OCSPResponse: state.OCSPResponse,
|
||||||
|
TLSUnique: state.TLSUnique,
|
||||||
|
ECHAccepted: state.ECHAccepted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebSocketClientHelloSpec() (*tls.ClientHelloSpec, error) {
|
||||||
|
fingerprinter := &tls.Fingerprinter{}
|
||||||
|
rawCapturedClientHelloBytes, _ := hex.DecodeString("16030107b8010007b403036252c641762f4a4f55d93dcbe1a01e62839ba1b48e9082896737ca23dc6bd02120c719a58f491c56be1f21fcd68bd097f2ee5a370c576a7c512aba46b7ed73841a0020eaea130113021303c02bc02fc02cc030cca9cca8c013c014009c009d002f00350100074baaaa0000000a000c000a5a5a11ec001d00170018000d0012001004030804040105030805050108060601ff0100010000170000002b0007065a5a03040303001b00030200020010000b000908687474702f312e31000b00020100002d00020101fe0d00ba0000010001be0020a5fb3e1a8aa228913bdf2dc9c52670860d2032a04d71140d6dc08f4cb41b2a1c0090420727529f82e9a8b097215adb52126184c638a98f0a6f8121b04edb939a7ff2a9564b44a917160ceb0a41f7a2dc12b89c622bc6687a21759c813858c81fe2e9024b02c6501c4b4c9e87ed2bd1538a420b6045a0785a66b044e53260e19b6f09042b903fa6758ce571dff16b91e95a19dbe5da0a46cab946036f48ad1394534866ee874f8dc75b3e4efa444be660262f003304ef04ed5a5a00010011ec04c098b01e12861f0d1738ebdb83b5fb876e7a1382f50149279a62479352b110f9791d1710c99cd988348175a20884650682e5e6a48bd2b27bb52360882bd84c6f97b6bc142c300ba7ab420bbec1872c13ba6970501e2b473f5c325da09a1dda800060d3c14454a95e6a67ddf28be60140cc3b07d393826b8babaf9a074b97bfa6fb8a2da784e98c5a332c62d7a60b7ac5b85e6606d128a4e9d89056680c6ff3ad5c3a3018218006db0fd81843e4ccb3f3e42ec858554be50aaa5b9afb613a1c07a631551706c4840bdb11a41564db56cdd2e58d1c619fc34567b74a2c54755f4174a0d589bf5938388a0976456c2a2aca2c08b2af11a2b550da9b22e4a39e2a7759c887e884b362b0b0e8394a149b44e82b6f5e680cd280cd2167953c94b111c20c4d028b2fc0926ac1a0454977fdf4a6876a555048a0c8b23531215f8543b5af5a2a69fc4151a870f481204e3a1535ec35321697d0ea0a470239e7b40b99e9b7ee01205bf567c42479c137c346b8af4dd825d4324c6d34875dd50b1841a78c42b79075af259a0f45252f311997f39902337088ee6817565b2e63858924a2354d86b1ba77bfa324339ec4a72663134db7227e652aea43700f751f7f519c5a66a036e0274a48c58716a3eb8a6de4730d19d534dd52ae83b9a7f07ccbc8d63b073a5127e3170dc58031524b81b611c6c4cf2aaba01fc55747b643fb417c85c0a21aa823709a037e418fc8e9c16c51a0c105c07d7c2f56dbcd4516a17a10036531768fb04300f0cdcc7c5135517b1643c3204b47575b97d0a513287985a0353523d93d511b694d6bb5ecc0698823855a606b83c77723808dd6737c9129a5c50457e71a063f927299214e902861e89a81423715b60717e8551b29126f857499810a0328d7182b538276d15517aa21b828ba7afcc13c1cc4cd1574a1c97bcfe4490f766d0e95a0c2911bc0d7a47161bcf5c344558314e7a1ab6c4227950b76c3319acaa53a7776a91e3b62ecd38f0732504674247947855a95892cc43b7f56a09f0c4281976e2b73820ff48180f054f482638078b08cf01d2b221a35214002b5cc8e394a414230a4f562af739e70ec540c260c651009a0616c5830658e0cbb2d2a2f94dab9f539a9ef19750f4925f7106a3a9c0a68e4a7d442202678beeef25b92c8709d93133c08728603ac900719db6615d294ad1aa965546a0c655a6891ab8e9fea31b7262dbba5af94372262934a4feac79ad72a3277aa7e97ab48f9a06f6857d26a2d48306b80a4bb600785f5b26f00202d2eb6af0226774e93615a484fa42a91bfe6034e890f6642c95a979d8df8a39ab10977846b1dc1998b51a8dde62975dc54bf92ada8dc0d7c0a020f429d4ff0b11ba95b6e076ec0443d01f0cb963441ddca9244631b776a029f874b799534c9c14808863c756b2d8a0309a0fab26bc37db05601cd5741acf9acc4c84a9e6b76b1e8282908a8d2268562e72109c3c8cb93507e1accf48723a056bd4acab4317771d4e432216a0a39377792469757a8c186d8085078aa2a8bce201628bad8c7ce98bb41dc56d6715a1dc3cb1cab48cb5a8f18d04a651b9efdd751bd199c266aac08726177bb432ecba334b06e516c12449846dcf90a4080248e2b15cb435e021ee8c4a8c17c095babf8011d67e0a5f16e25dc7357fc1c78e5884fe32598695589294e9c431c94ac2f5d2053757f9a905df36b001d002022b1fc0ef89cbfbd8f197c3e230879a2e71c155a1a6cb1bc33136cf93380c41d00000011000f00000c7373682e6576616e2e72756e0012000000230000000500050100000000baba0001000029010b00e600e0d1ae9857a5731dd0d5ca9d726bf57748ff9f57a7bb61ee7cc84e0f06358291fd2694914ab4ab3abc95fec3a1b393f458cca49eddd4dc24c896429eba5464d54ed59c562be84fe5660c8f74263db3c5eefbd36f2c667b9bd8219e20d84162a545978844bce25f2dc0f1c56f032bcee7f0bcd9fc762cfa115f368d0fe3b2e03fce32673bfdafd419eb8bc36bcf9c4a0d49c8d750ea7373e41c2ac13eb97f8fdb6c619742e98951c6447b76b263e0040ae874c7bd486421af99541b3b40e5b6eb3c0ae555dc1dfa7448aee19f05e54f70eb6655a91eb03eb93015a45eb29adc73d82437927e002120022bd91157ac53b55c75c03be16f0bc0040f38fa0085949f0157f8e7db794cc5")
|
||||||
|
spec, err := fingerprinter.FingerprintClientHello(rawCapturedClientHelloBytes)
|
||||||
|
return spec, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
stdtls "crypto/tls"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
tls "github.com/refraction-networking/utls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewUTLSConfigPreservesClientSettings(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
cfg := NewUTLSConfig(&stdtls.Config{
|
||||||
|
ServerName: "example.com",
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
|
MinVersion: stdtls.VersionTLS12,
|
||||||
|
MaxVersion: stdtls.VersionTLS13,
|
||||||
|
CipherSuites: []uint16{stdtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
|
||||||
|
CurvePreferences: []stdtls.CurveID{stdtls.X25519, stdtls.CurveP256},
|
||||||
|
DynamicRecordSizingDisabled: true,
|
||||||
|
Renegotiation: stdtls.RenegotiateOnceAsClient,
|
||||||
|
VerifyConnection: func(state stdtls.ConnectionState) error {
|
||||||
|
called = true
|
||||||
|
if state.ServerName != "example.com" {
|
||||||
|
t.Fatalf("ServerName = %q, want example.com", state.ServerName)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if cfg.ServerName != "example.com" {
|
||||||
|
t.Fatalf("ServerName = %q, want example.com", cfg.ServerName)
|
||||||
|
}
|
||||||
|
if !cfg.InsecureSkipVerify {
|
||||||
|
t.Fatal("InsecureSkipVerify was not preserved")
|
||||||
|
}
|
||||||
|
if len(cfg.NextProtos) != 2 || cfg.NextProtos[0] != "h2" || cfg.NextProtos[1] != "http/1.1" {
|
||||||
|
t.Fatalf("NextProtos = %v", cfg.NextProtos)
|
||||||
|
}
|
||||||
|
if cfg.MinVersion != tls.VersionTLS12 || cfg.MaxVersion != tls.VersionTLS13 {
|
||||||
|
t.Fatalf("version range = %x-%x", cfg.MinVersion, cfg.MaxVersion)
|
||||||
|
}
|
||||||
|
if len(cfg.CipherSuites) != 1 || cfg.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 {
|
||||||
|
t.Fatalf("CipherSuites = %v", cfg.CipherSuites)
|
||||||
|
}
|
||||||
|
if len(cfg.CurvePreferences) != 2 || cfg.CurvePreferences[0] != tls.X25519 || cfg.CurvePreferences[1] != tls.CurveP256 {
|
||||||
|
t.Fatalf("CurvePreferences = %v", cfg.CurvePreferences)
|
||||||
|
}
|
||||||
|
if !cfg.DynamicRecordSizingDisabled {
|
||||||
|
t.Fatal("DynamicRecordSizingDisabled was not preserved")
|
||||||
|
}
|
||||||
|
if cfg.Renegotiation != tls.RenegotiateOnceAsClient {
|
||||||
|
t.Fatalf("Renegotiation = %v", cfg.Renegotiation)
|
||||||
|
}
|
||||||
|
if cfg.VerifyConnection == nil {
|
||||||
|
t.Fatal("VerifyConnection was not preserved")
|
||||||
|
}
|
||||||
|
if err := cfg.VerifyConnection(tls.ConnectionState{ServerName: "example.com"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !called {
|
||||||
|
t.Fatal("VerifyConnection wrapper was not called")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewUTLSChromeClientBuildsChromeHello(t *testing.T) {
|
||||||
|
c1, c2 := net.Pipe()
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
client := NewUTLSChromeClient(c1, &stdtls.Config{ServerName: "example.com"})
|
||||||
|
if client.ClientHelloID != tls.HelloChrome_Auto {
|
||||||
|
t.Fatalf("ClientHelloID = %+v, want %+v", client.ClientHelloID, tls.HelloChrome_Auto)
|
||||||
|
}
|
||||||
|
if err := client.BuildHandshakeStateWithoutSession(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(client.Extensions) == 0 {
|
||||||
|
t.Fatal("Chrome ClientHello has no extensions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewUTLSWebSocketClientAppliesCustomSpec(t *testing.T) {
|
||||||
|
c1, c2 := net.Pipe()
|
||||||
|
defer c1.Close()
|
||||||
|
defer c2.Close()
|
||||||
|
|
||||||
|
client, err := NewUTLSWebSocketClient(c1, &stdtls.Config{
|
||||||
|
ServerName: "example.com",
|
||||||
|
NextProtos: []string{"h2"},
|
||||||
|
MinVersion: stdtls.VersionTLS13,
|
||||||
|
}, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if client.ClientHelloID != tls.HelloCustom {
|
||||||
|
t.Fatalf("ClientHelloID = %+v, want %+v", client.ClientHelloID, tls.HelloCustom)
|
||||||
|
}
|
||||||
|
if len(client.Extensions) == 0 {
|
||||||
|
t.Fatal("custom WebSocket ClientHello preset has no extensions")
|
||||||
|
}
|
||||||
|
if err := client.BuildHandshakeStateWithoutSession(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(client.HandshakeState.Hello.AlpnProtocols) != 1 || client.HandshakeState.Hello.AlpnProtocols[0] != "http/1.1" {
|
||||||
|
t.Fatalf("ALPN = %v, want [http/1.1]", client.HandshakeState.Hello.AlpnProtocols)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user