From cb8cd048c12f902b673a0e4ed6e4c51017a85439 Mon Sep 17 00:00:00 2001 From: j2rong4cn <36783515+j2rong4cn@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:25:47 +0800 Subject: [PATCH] DNS outbound: Replace "reject" with "return" (`rCode` is 0 by default) (#6214) https://github.com/XTLS/Xray-core/pull/6214#issuecomment-4587988752 Example: https://github.com/XTLS/Xray-core/pull/6214#issue-4553786283 --------- Co-authored-by: Meo597 <197331664+Meo597@users.noreply.github.com> --- infra/conf/common.go | 3 +- infra/conf/dns_proxy.go | 42 +++++++++--------- infra/conf/dns_proxy_test.go | 43 ++++++++++-------- proxy/dns/config.pb.go | 31 ++++++++----- proxy/dns/config.proto | 5 ++- proxy/dns/dns.go | 84 ++++++++++++++++++------------------ proxy/dns/dns_test.go | 7 +-- 7 files changed, 116 insertions(+), 99 deletions(-) diff --git a/infra/conf/common.go b/infra/conf/common.go index ab3cfba7..0de3568c 100644 --- a/infra/conf/common.go +++ b/infra/conf/common.go @@ -3,6 +3,7 @@ package conf import ( "encoding/json" "fmt" + "math" "strconv" "strings" @@ -199,7 +200,7 @@ func (v *PortRange) UnmarshalJSON(data []byte) error { if err == nil { v.From = uint32(from) v.To = uint32(to) - if v.From > v.To { + if v.From > v.To || v.To > math.MaxUint16 { return errors.New("invalid port range ", v.From, " -> ", v.To) } return nil diff --git a/infra/conf/dns_proxy.go b/infra/conf/dns_proxy.go index 734175e3..aabdea86 100644 --- a/infra/conf/dns_proxy.go +++ b/infra/conf/dns_proxy.go @@ -12,8 +12,9 @@ import ( type DNSOutboundRuleConfig struct { Action string `json:"action"` - QType *PortList `json:"qtype"` + QType *PortList `json:"qType"` Domain *StringList `json:"domain"` + RCode uint32 `json:"rCode"` } func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { @@ -24,8 +25,8 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { rule.Action = dns.RuleAction_Direct case "drop": rule.Action = dns.RuleAction_Drop - case "reject": - rule.Action = dns.RuleAction_Reject + case "return": + rule.Action = dns.RuleAction_Return case "hijack": rule.Action = dns.RuleAction_Hijack default: @@ -34,14 +35,8 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { if c.QType != nil { for _, r := range c.QType.Range { - if r.From > r.To { - return nil, errors.New("invalid qtype range: ", r.String()) - } - if r.To > 65535 { - return nil, errors.New("dns rule qtype out of range: ", r.String()) - } - for qtype := r.From; qtype <= r.To; qtype++ { - rule.Qtype = append(rule.Qtype, int32(qtype)) + for qType := r.From; qType <= r.To; qType++ { + rule.QType = append(rule.QType, int32(qType)) } } } @@ -54,6 +49,11 @@ func (c *DNSOutboundRuleConfig) Build() (*dns.DNSRuleConfig, error) { rule.Domain = rules } + if c.RCode > 65535 { + return nil, errors.New("rCode out of range: ", c.RCode) + } + rule.RCode = c.RCode + return rule, nil } @@ -133,28 +133,30 @@ func (c *DNSOutboundConfig) buildLegacyDNSPolicy() ([]*dns.DNSRuleConfig, error) if c.BlockTypes != nil && len(*c.BlockTypes) > 0 { rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Drop} if mode == "reject" { - rule.Action = dns.RuleAction_Reject + rule.Action = dns.RuleAction_Return + rule.RCode = 5 } - for _, qtype := range *c.BlockTypes { - if qtype < 0 || qtype > 65535 { - return nil, errors.New("legacy blockTypes qtype out of range: ", qtype) + for _, qType := range *c.BlockTypes { + if qType < 0 || qType > 65535 { + return nil, errors.New("legacy blockTypes qType out of range: ", qType) } - rule.Qtype = append(rule.Qtype, qtype) + rule.QType = append(rule.QType, qType) } rules = append(rules, rule) } { rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Hijack} - rule.Qtype = append(rule.Qtype, 1) - rule.Qtype = append(rule.Qtype, 28) + rule.QType = append(rule.QType, 1) + rule.QType = append(rule.QType, 28) rules = append(rules, rule) } { - rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Reject} + rule := &dns.DNSRuleConfig{Action: dns.RuleAction_Return} if mode == "reject" { - rule.Action = dns.RuleAction_Reject + rule.Action = dns.RuleAction_Return + rule.RCode = 5 } else if mode == "drop" { rule.Action = dns.RuleAction_Drop } else if mode == "skip" { diff --git a/infra/conf/dns_proxy_test.go b/infra/conf/dns_proxy_test.go index 1656e2d6..b88b0cfe 100644 --- a/infra/conf/dns_proxy_test.go +++ b/infra/conf/dns_proxy_test.go @@ -35,10 +35,10 @@ func TestDnsProxyConfig(t *testing.T) { Input: `{ "rules": [{ "action": "direct", - "qtype": "1,3,23-24" + "qType": "1,3,23-24" }, { "action": "drop", - "qtype": 28, + "qType": 28, "domain": ["domain:example.com", "full:example.com"] }] }`, @@ -48,11 +48,11 @@ func TestDnsProxyConfig(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Direct, - Qtype: []int32{1, 3, 23, 24}, + QType: []int32{1, 3, 23, 24}, }, { Action: dns.RuleAction_Drop, - Qtype: []int32{28}, + QType: []int32{28}, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -78,7 +78,8 @@ func TestDnsProxyConfig(t *testing.T) { { Input: `{ "rules": [{ - "action": "reject", + "action": "return", + "rCode": 5, "domain": "keyword:example" }] }`, @@ -87,7 +88,8 @@ func TestDnsProxyConfig(t *testing.T) { RewriteServer: &net.Endpoint{}, Rule: []*dns.DNSRuleConfig{ { - Action: dns.RuleAction_Reject, + Action: dns.RuleAction_Return, + RCode: 5, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -106,7 +108,7 @@ func TestDnsProxyConfig(t *testing.T) { Input: `{ "rules": [{ "action": "drop", - "qtype": 257 + "qType": 257 }] }`, Parser: loadJSON(creator), @@ -115,7 +117,7 @@ func TestDnsProxyConfig(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Drop, - Qtype: []int32{257}, + QType: []int32{257}, }, }, }, @@ -140,10 +142,11 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { - Action: dns.RuleAction_Reject, + Action: dns.RuleAction_Return, + RCode: 5, }, }, }, @@ -157,15 +160,17 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { RewriteServer: &net.Endpoint{}, Rule: []*dns.DNSRuleConfig{ { - Action: dns.RuleAction_Reject, - Qtype: []int32{1, 65}, + Action: dns.RuleAction_Return, + QType: []int32{1, 65}, + RCode: 5, }, { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { - Action: dns.RuleAction_Reject, + Action: dns.RuleAction_Return, + RCode: 5, }, }, }, @@ -181,11 +186,11 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Drop, - Qtype: []int32{1}, + QType: []int32{1}, }, { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { Action: dns.RuleAction_Drop, @@ -204,11 +209,11 @@ func TestDnsProxyConfigLegacyCompatibility(t *testing.T) { Rule: []*dns.DNSRuleConfig{ { Action: dns.RuleAction_Drop, - Qtype: []int32{65, 28}, + QType: []int32{65, 28}, }, { Action: dns.RuleAction_Hijack, - Qtype: []int32{1, 28}, + QType: []int32{1, 28}, }, { Action: dns.RuleAction_Direct, @@ -228,7 +233,7 @@ func TestDnsProxyConfigRejectsMixedLegacyAndNewFields(t *testing.T) { _, err := loadJSON(creator)(`{ "rules": [{ "action": "direct", - "qtype": 65 + "qType": 65 }], "blockTypes": [65] }`) diff --git a/proxy/dns/config.pb.go b/proxy/dns/config.pb.go index b8712719..249fd022 100644 --- a/proxy/dns/config.pb.go +++ b/proxy/dns/config.pb.go @@ -28,7 +28,7 @@ type RuleAction int32 const ( RuleAction_Direct RuleAction = 0 RuleAction_Drop RuleAction = 1 - RuleAction_Reject RuleAction = 2 + RuleAction_Return RuleAction = 2 RuleAction_Hijack RuleAction = 3 ) @@ -37,13 +37,13 @@ var ( RuleAction_name = map[int32]string{ 0: "Direct", 1: "Drop", - 2: "Reject", + 2: "Return", 3: "Hijack", } RuleAction_value = map[string]int32{ "Direct": 0, "Drop": 1, - "Reject": 2, + "Return": 2, "Hijack": 3, } ) @@ -78,8 +78,9 @@ func (RuleAction) EnumDescriptor() ([]byte, []int) { type DNSRuleConfig struct { state protoimpl.MessageState `protogen:"open.v1"` Action RuleAction `protobuf:"varint,1,opt,name=action,proto3,enum=xray.proxy.dns.RuleAction" json:"action,omitempty"` - Qtype []int32 `protobuf:"varint,2,rep,packed,name=qtype,proto3" json:"qtype,omitempty"` + QType []int32 `protobuf:"varint,2,rep,packed,name=q_type,json=qType,proto3" json:"q_type,omitempty"` Domain []*geodata.DomainRule `protobuf:"bytes,3,rep,name=domain,proto3" json:"domain,omitempty"` + RCode uint32 `protobuf:"varint,4,opt,name=r_code,json=rCode,proto3" json:"r_code,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -121,9 +122,9 @@ func (x *DNSRuleConfig) GetAction() RuleAction { return RuleAction_Direct } -func (x *DNSRuleConfig) GetQtype() []int32 { +func (x *DNSRuleConfig) GetQType() []int32 { if x != nil { - return x.Qtype + return x.QType } return nil } @@ -135,6 +136,13 @@ func (x *DNSRuleConfig) GetDomain() []*geodata.DomainRule { return nil } +func (x *DNSRuleConfig) GetRCode() uint32 { + if x != nil { + return x.RCode + } + return 0 +} + type Config struct { state protoimpl.MessageState `protogen:"open.v1"` UserLevel uint32 `protobuf:"varint,1,opt,name=user_level,json=userLevel,proto3" json:"user_level,omitempty"` @@ -199,11 +207,12 @@ var File_proxy_dns_config_proto protoreflect.FileDescriptor const file_proxy_dns_config_proto_rawDesc = "" + "\n" + - "\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\x92\x01\n" + + "\x16proxy/dns/config.proto\x12\x0exray.proxy.dns\x1a\x1ccommon/net/destination.proto\x1a\x1bcommon/geodata/geodat.proto\"\xaa\x01\n" + "\rDNSRuleConfig\x122\n" + - "\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x14\n" + - "\x05qtype\x18\x02 \x03(\x05R\x05qtype\x127\n" + - "\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\"\x9c\x01\n" + + "\x06action\x18\x01 \x01(\x0e2\x1a.xray.proxy.dns.RuleActionR\x06action\x12\x15\n" + + "\x06q_type\x18\x02 \x03(\x05R\x05qType\x127\n" + + "\x06domain\x18\x03 \x03(\v2\x1f.xray.common.geodata.DomainRuleR\x06domain\x12\x15\n" + + "\x06r_code\x18\x04 \x01(\rR\x05rCode\"\x9c\x01\n" + "\x06Config\x12\x1d\n" + "\n" + "user_level\x18\x01 \x01(\rR\tuserLevel\x121\n" + @@ -215,7 +224,7 @@ const file_proxy_dns_config_proto_rawDesc = "" + "\x06Direct\x10\x00\x12\b\n" + "\x04Drop\x10\x01\x12\n" + "\n" + - "\x06Reject\x10\x02\x12\n" + + "\x06Return\x10\x02\x12\n" + "\n" + "\x06Hijack\x10\x03BL\n" + "\x12com.xray.proxy.dnsP\x01Z#github.com/xtls/xray-core/proxy/dns\xaa\x02\x0eXray.Proxy.Dnsb\x06proto3" diff --git a/proxy/dns/config.proto b/proxy/dns/config.proto index 3be782d1..cfa3e28d 100644 --- a/proxy/dns/config.proto +++ b/proxy/dns/config.proto @@ -12,14 +12,15 @@ import "common/geodata/geodat.proto"; enum RuleAction { Direct = 0; Drop = 1; - Reject = 2; + Return = 2; Hijack = 3; } message DNSRuleConfig { RuleAction action = 1; - repeated int32 qtype = 2; + repeated int32 q_type = 2; repeated xray.common.geodata.DomainRule domain = 3; + uint32 r_code = 4; } message Config { diff --git a/proxy/dns/dns.go b/proxy/dns/dns.go index 2d678dd2..efeb56cd 100644 --- a/proxy/dns/dns.go +++ b/proxy/dns/dns.go @@ -45,6 +45,7 @@ type DNSRule struct { action RuleAction qTypes []uint16 domains geodata.DomainMatcher + rCode dnsmessage.RCode } func (r *DNSRule) matchQType(qType uint16) bool { @@ -95,9 +96,10 @@ func (h *Handler) Init(config *Config, dnsClient dns.Client, policyManager polic for _, r := range config.Rule { rule := &DNSRule{ action: r.Action, - qTypes: make([]uint16, 0, len(r.Qtype)), + qTypes: make([]uint16, 0, len(r.QType)), + rCode: dnsmessage.RCode(r.RCode), } - for _, t := range r.Qtype { + for _, t := range r.QType { rule.qTypes = append(rule.qTypes, uint16(t)) } if len(r.Domain) > 0 { @@ -136,17 +138,17 @@ func parseQuery(b []byte) (id uint16, qType dnsmessage.Type, domain string, ok b return } -func (h *Handler) applyRules(qType dnsmessage.Type, domain string) RuleAction { +func (h *Handler) applyRules(qType dnsmessage.Type, domain string) (RuleAction, dnsmessage.RCode) { qCode := uint16(qType) for _, r := range h.rules { if r.Apply(qCode, domain) { - return r.action + return r.action, r.rCode } } if qType == dnsmessage.TypeA || qType == dnsmessage.TypeAAAA { - return RuleAction_Hijack + return RuleAction_Hijack, dnsmessage.RCodeSuccess } - return RuleAction_Reject + return RuleAction_Return, dnsmessage.RCodeSuccess } // Process implements proxy.Outbound. @@ -213,7 +215,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. } if session.TimeoutOnlyFromContext(ctx) { - ctx, _ = context.WithCancel(context.Background()) + ctx = context.Background() } ctx, cancel := context.WithCancel(ctx) @@ -250,21 +252,22 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. continue } - switch h.applyRules(qType, domain) { + action, rCode := h.applyRules(qType, domain) + switch action { case RuleAction_Drop: b.Release() errors.LogInfo(ctx, "blocked type ", qType, " query for domain ", domain) - case RuleAction_Reject: + case RuleAction_Return: b.Release() errors.LogInfo(ctx, "rejected type ", qType, " query for domain ", domain) - if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil { + if err := h.rejectNonIPQuery(id, qType, domain, writer, rCode); err != nil { return err } case RuleAction_Hijack: b.Release() if qType != dnsmessage.TypeA && qType != dnsmessage.TypeAAAA { errors.LogError(ctx, "can only hijack A/AAAA records") - if err := h.rejectNonIPQuery(id, qType, domain, writer); err != nil { + if err := h.rejectNonIPQuery(id, qType, domain, writer, rCode); err != nil { return err } } else { @@ -309,48 +312,35 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, d internet. func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter, timer *signal.ActivityTimer) { var ips []net.IP + var ttl uint32 var err error - var ttl4 uint32 - var ttl6 uint32 - switch qType { case dnsmessage.TypeA: - ips, ttl4, err = h.client.LookupIP(domain, dns.IPOption{ + ips, ttl, err = h.client.LookupIP(domain, dns.IPOption{ IPv4Enable: true, IPv6Enable: false, FakeEnable: true, }) case dnsmessage.TypeAAAA: - ips, ttl6, err = h.client.LookupIP(domain, dns.IPOption{ + ips, ttl, err = h.client.LookupIP(domain, dns.IPOption{ IPv4Enable: false, IPv6Enable: true, FakeEnable: true, }) } - rcode := dns.RCodeFromError(err) - if rcode == 0 && len(ips) == 0 && !go_errors.Is(err, dns.ErrEmptyResponse) { + rCode := dns.RCodeFromError(err) + if rCode == 0 && len(ips) == 0 && !go_errors.Is(err, dns.ErrEmptyResponse) { errors.LogInfoInner(context.Background(), err, "ip query") return } - switch qType { - case dnsmessage.TypeA: - for i, ip := range ips { - ips[i] = ip.To4() - } - case dnsmessage.TypeAAAA: - for i, ip := range ips { - ips[i] = ip.To16() - } - } - b := buf.New() rawBytes := b.Extend(buf.Size) builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ ID: id, - RCode: dnsmessage.RCode(rcode), + RCode: dnsmessage.RCode(rCode), RecursionAvailable: true, RecursionDesired: true, Response: true, @@ -365,17 +355,25 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, })) common.Must(builder.StartAnswers()) - rHeader4 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl4} - rHeader6 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl6} - for _, ip := range ips { - if len(ip) == net.IPv4len { - var r dnsmessage.AResource - copy(r.A[:], ip) - common.Must(builder.AResource(rHeader4, r)) - } else { - var r dnsmessage.AAAAResource - copy(r.AAAA[:], ip) - common.Must(builder.AAAAResource(rHeader6, r)) + rHeader := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl} + switch qType { + case dnsmessage.TypeA: + for _, ip := range ips { + ip = ip.To4() + if len(ip) == net.IPv4len { + var r dnsmessage.AResource + copy(r.A[:], ip) + common.Must(builder.AResource(rHeader, r)) + } + } + case dnsmessage.TypeAAAA: + for _, ip := range ips { + ip = ip.To16() + if len(ip) == net.IPv6len { + var r dnsmessage.AAAAResource + copy(r.AAAA[:], ip) + common.Must(builder.AAAAResource(rHeader, r)) + } } } msgBytes, err := builder.Finish() @@ -392,7 +390,7 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, } } -func (h *Handler) rejectNonIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter) error { +func (h *Handler) rejectNonIPQuery(id uint16, qType dnsmessage.Type, domain string, writer dns_proto.MessageWriter, rCode dnsmessage.RCode) error { domainT := strings.TrimSuffix(domain, ".") if domainT == "" { return errors.New("empty domain name") @@ -401,7 +399,7 @@ func (h *Handler) rejectNonIPQuery(id uint16, qType dnsmessage.Type, domain stri rawBytes := b.Extend(buf.Size) builder := dnsmessage.NewBuilder(rawBytes[:0], dnsmessage.Header{ ID: id, - RCode: dnsmessage.RCodeRefused, + RCode: rCode, RecursionAvailable: true, RecursionDesired: true, Response: true, diff --git a/proxy/dns/dns_test.go b/proxy/dns/dns_test.go index 6e4b468e..f93ff8dd 100644 --- a/proxy/dns/dns_test.go +++ b/proxy/dns/dns_test.go @@ -424,7 +424,7 @@ func TestDNSRules(t *testing.T) { ProxySettings: serial.ToTypedMessage(&dns_proxy.Config{ Rule: []*dns_proxy.DNSRuleConfig{ { - Qtype: []int32{int32(dns.TypeA)}, + QType: []int32{int32(dns.TypeA)}, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -438,7 +438,7 @@ func TestDNSRules(t *testing.T) { Action: dns_proxy.RuleAction_Direct, }, { - Qtype: []int32{int32(dns.TypeA)}, + QType: []int32{int32(dns.TypeA)}, Domain: []*geodata.DomainRule{ { Value: &geodata.DomainRule_Custom{ @@ -449,7 +449,8 @@ func TestDNSRules(t *testing.T) { }, }, }, - Action: dns_proxy.RuleAction_Reject, + Action: dns_proxy.RuleAction_Return, + RCode: 5, }, }, }),