From bf8596660042ce19a07314eac0348d389c32e8f4 Mon Sep 17 00:00:00 2001 From: wenyifan Date: Thu, 2 Jul 2026 20:50:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=87=E6=8D=A2=E4=B8=BA=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E7=9A=84dns=E8=A7=A3=E6=9E=90;=E5=A2=9E=E5=8A=A0resolver=3Dsys?= =?UTF-8?q?tem=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/cmd/cmd.go | 5 +- config/cmd/cmd_test.go | 90 ++++++++++++++++----- internal/net/dialer/dialer.go | 7 +- resolver/exchanger/exchanger.go | 14 ++++ resolver/exchanger/exchanger_test.go | 112 +++++++++++++++++++++++++++ resolver/exchanger/system.go | 108 ++++++++++++++++++++++++++ 6 files changed, 312 insertions(+), 24 deletions(-) create mode 100644 resolver/exchanger/system.go diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index 34e15296..30ba1f98 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -95,7 +95,9 @@ func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error) resolverCfg.Nameservers = append( resolverCfg.Nameservers, &config.NameserverConfig{ - Addr: rs, + Addr: rs, + Prefer: mdutil.GetString(md, "prefer"), + Only: mdutil.GetString(md, "only"), }, ) } @@ -272,6 +274,7 @@ func BuildConfigFromCmd(serviceList, nodeList []string) (*config.Config, error) &config.NameserverConfig{ Addr: rs, Prefer: mdutil.GetString(md, "prefer"), + Only: mdutil.GetString(md, "only"), }, ) } diff --git a/config/cmd/cmd_test.go b/config/cmd/cmd_test.go index 1c5a2a50..9d8b3edf 100644 --- a/config/cmd/cmd_test.go +++ b/config/cmd/cmd_test.go @@ -211,13 +211,13 @@ func TestNorm(t *testing.T) { wantErr bool }{ { - name: "empty string", - input: "", + name: "empty string", + input: "", wantErr: true, }, { - name: "whitespace only", - input: " ", + name: "whitespace only", + input: " ", wantErr: true, }, { @@ -326,12 +326,12 @@ func TestCutHost(t *testing.T) { func TestParseSelector(t *testing.T) { tests := []struct { - name string - input map[string]any - wantNil bool - wantStrat string - wantFails int - wantDurGt bool // failTimeout > 0 + name string + input map[string]any + wantNil bool + wantStrat string + wantFails int + wantDurGt bool // failTimeout > 0 }{ { name: "no params", @@ -359,9 +359,9 @@ func TestParseSelector(t *testing.T) { { name: "all params with failTimeout", input: map[string]any{ - "strategy": "hash", - "maxFails": 5, - "failTimeout": "10s", + "strategy": "hash", + "maxFails": 5, + "failTimeout": "10s", }, wantStrat: "hash", wantFails: 5, @@ -455,8 +455,8 @@ func TestCopyConnectorConfig(t *testing.T) { t.Run("non-nil deep copy", func(t *testing.T) { orig := &config.ConnectorConfig{ - Type: "http", - Auth: &config.AuthConfig{Username: "u", Password: "p"}, + Type: "http", + Auth: &config.AuthConfig{Username: "u", Password: "p"}, Metadata: map[string]any{"k": "v"}, } c := copyConnectorConfig(orig) @@ -487,9 +487,9 @@ func TestCopyDialerConfig(t *testing.T) { t.Run("non-nil deep copy", func(t *testing.T) { orig := &config.DialerConfig{ - Type: "tls", - TLS: &config.TLSConfig{ServerName: "example.com"}, - Auth: &config.AuthConfig{Username: "u"}, + Type: "tls", + TLS: &config.TLSConfig{ServerName: "example.com"}, + Auth: &config.AuthConfig{Username: "u"}, Metadata: map[string]any{"k": "v"}, } c := copyDialerConfig(orig) @@ -1068,6 +1068,31 @@ func TestBuildConfigFromCmd_NodeWithResolver(t *testing.T) { } } +func TestBuildConfigFromCmd_NodeResolverFamilyOptions(t *testing.T) { + cfg, err := BuildConfigFromCmd( + []string{":8080"}, + []string{"http://proxy:3128?resolver=auto&prefer=ipv6&only=ipv4"}, + ) + if err != nil { + t.Fatalf("BuildConfigFromCmd: %v", err) + } + + rs := cfg.Resolvers[0] + if len(rs.Nameservers) != 1 { + t.Fatalf("len(Nameservers) = %d, want 1", len(rs.Nameservers)) + } + ns := rs.Nameservers[0] + if ns.Addr != "auto" { + t.Errorf("Addr = %q, want auto", ns.Addr) + } + if ns.Prefer != "ipv6" { + t.Errorf("Prefer = %q, want ipv6", ns.Prefer) + } + if ns.Only != "ipv4" { + t.Errorf("Only = %q, want ipv4", ns.Only) + } +} + func TestBuildConfigFromCmd_NodeWithHosts(t *testing.T) { cfg, err := BuildConfigFromCmd( []string{":8080"}, @@ -1171,6 +1196,34 @@ func TestBuildConfigFromCmd_ServiceWithResolver(t *testing.T) { } } +func TestBuildConfigFromCmd_ServiceSystemResolverOnlyIPv4(t *testing.T) { + cfg, err := BuildConfigFromCmd( + []string{"http://:8080?resolver=system&only=ipv4"}, + nil, + ) + if err != nil { + t.Fatalf("BuildConfigFromCmd: %v", err) + } + + if len(cfg.Resolvers) != 1 { + t.Fatalf("len(Resolvers) = %d, want 1", len(cfg.Resolvers)) + } + rs := cfg.Resolvers[0] + if len(rs.Nameservers) != 1 { + t.Fatalf("len(Nameservers) = %d, want 1", len(rs.Nameservers)) + } + ns := rs.Nameservers[0] + if ns.Addr != "system" { + t.Errorf("Addr = %q, want system", ns.Addr) + } + if ns.Only != "ipv4" { + t.Errorf("Only = %q, want ipv4", ns.Only) + } + if cfg.Services[0].Resolver != rs.Name { + t.Errorf("Service.Resolver = %q, want %q", cfg.Services[0].Resolver, rs.Name) + } +} + func TestBuildConfigFromCmd_ServiceWithHosts(t *testing.T) { cfg, err := BuildConfigFromCmd( []string{":8080?hosts=example.com:1.2.3.4"}, @@ -1896,4 +1949,3 @@ func TestBuildNodeConfig_TLSOnlySecure(t *testing.T) { t.Error("TLS.Secure should be true") } } - diff --git a/internal/net/dialer/dialer.go b/internal/net/dialer/dialer.go index 5816218d..4ceabe22 100644 --- a/internal/net/dialer/dialer.go +++ b/internal/net/dialer/dialer.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "net" - "runtime" "strings" "syscall" "time" @@ -147,9 +146,9 @@ func (d *Dialer) dialOnce(ctx context.Context, network, addr, ifceName string, i return nil, fmt.Errorf("dial: unsupported network %s", network) } var resolver *net.Resolver - if runtime.GOOS != "android" { - resolver = &net.Resolver{PreferGo: true} - } + //if runtime.GOOS != "android" { + // resolver = &net.Resolver{PreferGo: true} + //} netd := net.Dialer{ Resolver: resolver, LocalAddr: ifAddr, diff --git a/resolver/exchanger/exchanger.go b/resolver/exchanger/exchanger.go index 0baf9a1e..52b85235 100644 --- a/resolver/exchanger/exchanger.go +++ b/resolver/exchanger/exchanger.go @@ -82,6 +82,11 @@ func NewExchanger(addr string, opts ...Option) (Exchanger, error) { opt(&options) } + addr = strings.TrimSpace(addr) + if isSystemResolverAddr(addr) { + return newSystemExchanger(addr, options), nil + } + if !strings.Contains(addr, "://") { addr = "udp://" + addr } @@ -143,6 +148,15 @@ func NewExchanger(addr string, opts ...Option) (Exchanger, error) { return ex, nil } +func isSystemResolverAddr(addr string) bool { + switch strings.ToLower(strings.TrimSpace(addr)) { + case "system", "auto", "system://", "auto://": + return true + default: + return false + } +} + func (ex *exchanger) Exchange(ctx context.Context, msg []byte) ([]byte, error) { if ex.network == "https" { return ex.dohExchange(ctx, msg) diff --git a/resolver/exchanger/exchanger_test.go b/resolver/exchanger/exchanger_test.go index b27f0487..5725fcd8 100644 --- a/resolver/exchanger/exchanger_test.go +++ b/resolver/exchanger/exchanger_test.go @@ -10,6 +10,7 @@ import ( "github.com/go-gost/core/chain" xlogger "github.com/go-gost/x/logger" + "github.com/miekg/dns" ) func TestNewExchanger_UDP(t *testing.T) { @@ -175,6 +176,117 @@ func TestNewExchanger_String(t *testing.T) { } } +func TestNewExchanger_System(t *testing.T) { + ex, err := NewExchanger("system", LoggerOption(xlogger.Nop())) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := ex.(*systemExchanger); !ok { + t.Fatalf("NewExchanger(system) = %T, want *systemExchanger", ex) + } + if ex.String() != "system" { + t.Errorf("String() = %q, want system", ex.String()) + } +} + +func TestNewExchanger_AutoSystem(t *testing.T) { + ex, err := NewExchanger("auto", LoggerOption(xlogger.Nop())) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := ex.(*systemExchanger); !ok { + t.Fatalf("NewExchanger(auto) = %T, want *systemExchanger", ex) + } +} + +func TestSystemExchanger_ExchangeA(t *testing.T) { + ex := &systemExchanger{ + rawAddr: "system", + lookupIP: func(_ context.Context, network, host string) ([]net.IP, error) { + if network != "ip4" { + t.Errorf("network = %q, want ip4", network) + } + if host != "example.com" { + t.Errorf("host = %q, want example.com", host) + } + return []net.IP{ + net.ParseIP("192.0.2.1"), + net.ParseIP("2001:db8::1"), + }, nil + }, + } + + mq := new(dns.Msg) + mq.SetQuestion(dns.Fqdn("example.com"), dns.TypeA) + packed, err := mq.Pack() + if err != nil { + t.Fatal(err) + } + + reply, err := ex.Exchange(context.Background(), packed) + if err != nil { + t.Fatalf("Exchange: %v", err) + } + mr := new(dns.Msg) + if err := mr.Unpack(reply); err != nil { + t.Fatalf("Unpack: %v", err) + } + if len(mr.Answer) != 1 { + t.Fatalf("len(Answer) = %d, want 1", len(mr.Answer)) + } + a, ok := mr.Answer[0].(*dns.A) + if !ok { + t.Fatalf("Answer[0] = %T, want *dns.A", mr.Answer[0]) + } + if !a.A.Equal(net.ParseIP("192.0.2.1")) { + t.Errorf("A = %s, want 192.0.2.1", a.A) + } +} + +func TestSystemExchanger_ExchangeAAAA(t *testing.T) { + ex := &systemExchanger{ + rawAddr: "system", + lookupIP: func(_ context.Context, network, host string) ([]net.IP, error) { + if network != "ip6" { + t.Errorf("network = %q, want ip6", network) + } + if host != "example.com" { + t.Errorf("host = %q, want example.com", host) + } + return []net.IP{ + net.ParseIP("192.0.2.1"), + net.ParseIP("2001:db8::1"), + }, nil + }, + } + + mq := new(dns.Msg) + mq.SetQuestion(dns.Fqdn("example.com"), dns.TypeAAAA) + packed, err := mq.Pack() + if err != nil { + t.Fatal(err) + } + + reply, err := ex.Exchange(context.Background(), packed) + if err != nil { + t.Fatalf("Exchange: %v", err) + } + mr := new(dns.Msg) + if err := mr.Unpack(reply); err != nil { + t.Fatalf("Unpack: %v", err) + } + if len(mr.Answer) != 1 { + t.Fatalf("len(Answer) = %d, want 1", len(mr.Answer)) + } + aaaa, ok := mr.Answer[0].(*dns.AAAA) + if !ok { + t.Fatalf("Answer[0] = %T, want *dns.AAAA", mr.Answer[0]) + } + if !aaaa.AAAA.Equal(net.ParseIP("2001:db8::1")) { + t.Errorf("AAAA = %s, want 2001:db8::1", aaaa.AAAA) + } +} + func TestExchange_DialFailure(t *testing.T) { ex, err := NewExchanger("udp://127.0.0.1:1", TimeoutOption(100*time.Millisecond), LoggerOption(xlogger.Nop())) if err != nil { diff --git a/resolver/exchanger/system.go b/resolver/exchanger/system.go new file mode 100644 index 00000000..8f622bd9 --- /dev/null +++ b/resolver/exchanger/system.go @@ -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 +}