From b05f58b469cfba767ff7a90825305de4ee54ef2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 29 Mar 2026 12:58:46 +0800 Subject: [PATCH] Use typed SVCB hint structs instead of string parsing --- adapter/inbound.go | 13 ++++--- adapter/inbound_test.go | 45 +++++++++++++++++++++++ dns/router_test.go | 79 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 4 deletions(-) create mode 100644 adapter/inbound_test.go diff --git a/adapter/inbound.go b/adapter/inbound.go index 048699f6d..d13adb5cc 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -4,13 +4,11 @@ import ( "context" "net" "net/netip" - "strings" "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" M "github.com/sagernet/sing/common/metadata" "github.com/miekg/dns" @@ -145,8 +143,15 @@ func DNSResponseAddresses(response *dns.Msg) []netip.Addr { addresses = append(addresses, M.AddrFromIP(record.AAAA)) case *dns.HTTPS: for _, value := range record.SVCB.Value { - if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT { - addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...) + switch hint := value.(type) { + case *dns.SVCBIPv4Hint: + for _, ip := range hint.Hint { + addresses = append(addresses, M.AddrFromIP(ip).Unmap()) + } + case *dns.SVCBIPv6Hint: + for _, ip := range hint.Hint { + addresses = append(addresses, M.AddrFromIP(ip)) + } } } } diff --git a/adapter/inbound_test.go b/adapter/inbound_test.go new file mode 100644 index 000000000..ec8c31289 --- /dev/null +++ b/adapter/inbound_test.go @@ -0,0 +1,45 @@ +package adapter + +import ( + "net" + "net/netip" + "testing" + + "github.com/miekg/dns" + "github.com/stretchr/testify/require" +) + +func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) { + t.Parallel() + + ipv4Hint := net.ParseIP("1.1.1.1") + require.NotNil(t, ipv4Hint) + + response := &dns.Msg{ + MsgHdr: dns.MsgHdr{ + Response: true, + Rcode: dns.RcodeSuccess, + }, + Answer: []dns.RR{ + &dns.HTTPS{ + SVCB: dns.SVCB{ + Hdr: dns.RR_Header{ + Name: dns.Fqdn("example.com"), + Rrtype: dns.TypeHTTPS, + Class: dns.ClassINET, + Ttl: 60, + }, + Priority: 1, + Target: ".", + Value: []dns.SVCBKeyValue{ + &dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}}, + }, + }, + }, + }, + } + + addresses := DNSResponseAddresses(response) + require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses) + require.True(t, addresses[0].Is4()) +} diff --git a/dns/router_test.go b/dns/router_test.go index 7c3c4b5fb..46ddcd028 100644 --- a/dns/router_test.go +++ b/dns/router_test.go @@ -298,6 +298,26 @@ func fixedHTTPSHintResponse(question mDNS.Question, addresses ...netip.Addr) *mD return response } +func fixedHTTPSHintResponseWithRawHints(question mDNS.Question, ipv4Hints []net.IP, ipv6Hints []net.IP) *mDNS.Msg { + response := fixedHTTPSHintResponse(question) + https := response.Answer[0].(*mDNS.HTTPS) + if len(ipv4Hints) > 0 { + hints := make([]net.IP, 0, len(ipv4Hints)) + for _, ip := range ipv4Hints { + hints = append(hints, net.IP(append([]byte(nil), ip...))) + } + https.SVCB.Value = append(https.SVCB.Value, &mDNS.SVCBIPv4Hint{Hint: hints}) + } + if len(ipv6Hints) > 0 { + hints := make([]net.IP, 0, len(ipv6Hints)) + for _, ip := range ipv6Hints { + hints = append(hints, net.IP(append([]byte(nil), ip...))) + } + https.SVCB.Value = append(https.SVCB.Value, &mDNS.SVCBIPv6Hint{Hint: hints}) + } + return response +} + func TestValidateLegacyDNSModeDisabledRules_RequireMatchResponseForDirectIPCIDR(t *testing.T) { t.Parallel() @@ -1010,6 +1030,65 @@ func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRouteWithHTTPSHints(t require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) } +func TestExchangeLegacyDNSModeDisabledEvaluateMatchResponseRouteWithMappedHTTPSIPv4Hints(t *testing.T) { + t.Parallel() + + transportManager := &fakeDNSTransportManager{ + defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + transports: map[string]adapter.DNSTransport{ + "upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP}, + "selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP}, + "default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}, + }, + } + client := &fakeDNSClient{ + exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) { + switch transport.Tag() { + case "upstream": + return fixedHTTPSHintResponseWithRawHints(message.Question[0], []net.IP{net.ParseIP("1.1.1.1")}, nil), nil + case "selected": + return fixedHTTPSHintResponse(message.Question[0], netip.MustParseAddr("8.8.8.8")), nil + default: + return nil, errors.New("unexpected transport") + } + }, + } + rules := []option.DNSRule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + Domain: badoption.Listable[string]{"example.com"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeEvaluate, + RouteOptions: option.DNSRouteActionOptions{Server: "upstream"}, + }, + }, + }, + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultDNSRule{ + RawDefaultDNSRule: option.RawDefaultDNSRule{ + MatchResponse: true, + IPCIDR: badoption.Listable[string]{"1.1.1.0/24"}, + }, + DNSRuleAction: option.DNSRuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.DNSRouteActionOptions{Server: "selected"}, + }, + }, + }, + } + router := newTestRouter(t, rules, transportManager, client) + + response, err := router.Exchange(context.Background(), &mDNS.Msg{ + Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeHTTPS)}, + }, adapter.DNSQueryOptions{}) + require.NoError(t, err) + require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response)) +} + func TestExchangeLegacyDNSModeDisabledEvaluateDoesNotLeakAddressesToNextQuery(t *testing.T) { t.Parallel()