Skip to content

Commit 7cf30d5

Browse files
committed
Feat: DNS hosts support multiple addresses
1 parent 598e15a commit 7cf30d5

File tree

7 files changed

+106
-31
lines changed

7 files changed

+106
-31
lines changed

app/dns/config.pb.go

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/dns/config.proto

+1-2
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ message Config {
7373
repeated bytes ip = 3;
7474

7575
// ProxiedDomain indicates the mapped domain has the same IP address on this
76-
// domain. Xray will use this domain for IP queries. This field is only
77-
// effective if ip is empty.
76+
// domain. Xray will use this domain for IP queries.
7877
string proxied_domain = 4;
7978
}
8079

app/dns/dns.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ func (s *DNS) lookupIPInternal(domain string, option *dns.IPOption) ([]net.IP, e
222222
// Successfully found ip records in static host.
223223
// Skip hosts mapping result in FakeDNS query.
224224
if isIPQuery(option) {
225-
newError("returning ", len(addrs), " IPs for domain ", domain).WriteToLog()
225+
newError("returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs).WriteToLog()
226226
return toNetIP(addrs)
227227
}
228228
}

app/dns/hosts.go

+4-8
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
4747
id := g.Add(matcher)
4848
ips := make([]net.Address, 0, len(mapping.Ip)+1)
4949
switch {
50+
case len(mapping.ProxiedDomain) > 0:
51+
ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
52+
5053
case len(mapping.Ip) > 0:
5154
for _, ip := range mapping.Ip {
5255
addr := net.IPAddress(ip)
@@ -56,18 +59,10 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
5659
ips = append(ips, addr)
5760
}
5861

59-
case len(mapping.ProxiedDomain) > 0:
60-
ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
61-
6262
default:
6363
return nil, newError("neither IP address nor proxied domain specified for domain: ", mapping.Domain).AtWarning()
6464
}
6565

66-
// Special handling for localhost IPv6. This is a dirty workaround as JSON config supports only single IP mapping.
67-
if len(ips) == 1 && ips[0] == net.LocalHostIP {
68-
ips = append(ips, net.LocalHostIPv6)
69-
}
70-
7166
sh.ips[id] = ips
7267
}
7368

@@ -100,6 +95,7 @@ func (h *StaticHosts) lookup(domain string, option *dns.IPOption, maxDepth int)
10095
case len(addrs) == 0: // Not recorded in static hosts, return nil
10196
return nil
10297
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain
98+
newError("found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it").AtDebug().WriteToLog()
10399
if maxDepth > 0 {
104100
unwrapped := h.lookup(addrs[0].Domain(), option, maxDepth-1)
105101
if unwrapped != nil {

app/dns/hosts_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ func TestStaticHosts(t *testing.T) {
2020
{1, 1, 1, 1},
2121
},
2222
},
23+
{
24+
Type: DomainMatchingType_Full,
25+
Domain: "proxy.example.com",
26+
Ip: [][]byte{
27+
{1, 2, 3, 4},
28+
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
29+
},
30+
ProxiedDomain: "another-proxy.example.com",
31+
},
32+
{
33+
Type: DomainMatchingType_Full,
34+
Domain: "proxy2.example.com",
35+
ProxiedDomain: "proxy.example.com",
36+
},
2337
{
2438
Type: DomainMatchingType_Subdomain,
2539
Domain: "example.cn",
@@ -32,6 +46,7 @@ func TestStaticHosts(t *testing.T) {
3246
Domain: "baidu.com",
3347
Ip: [][]byte{
3448
{127, 0, 0, 1},
49+
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
3550
},
3651
},
3752
}
@@ -52,6 +67,32 @@ func TestStaticHosts(t *testing.T) {
5267
}
5368
}
5469

70+
{
71+
domain := hosts.Lookup("proxy.example.com", &dns.IPOption{
72+
IPv4Enable: true,
73+
IPv6Enable: false,
74+
})
75+
if len(domain) != 1 {
76+
t.Error("expect 1 domain, but got ", len(domain))
77+
}
78+
if diff := cmp.Diff(domain[0].Domain(), "another-proxy.example.com"); diff != "" {
79+
t.Error(diff)
80+
}
81+
}
82+
83+
{
84+
domain := hosts.Lookup("proxy2.example.com", &dns.IPOption{
85+
IPv4Enable: true,
86+
IPv6Enable: false,
87+
})
88+
if len(domain) != 1 {
89+
t.Error("expect 1 domain, but got ", len(domain))
90+
}
91+
if diff := cmp.Diff(domain[0].Domain(), "another-proxy.example.com"); diff != "" {
92+
t.Error(diff)
93+
}
94+
}
95+
5596
{
5697
ips := hosts.Lookup("www.example.cn", &dns.IPOption{
5798
IPv4Enable: true,

infra/conf/dns.go

+48-14
Original file line numberDiff line numberDiff line change
@@ -123,25 +123,59 @@ var typeMap = map[router.Domain_Type]dns.DomainMatchingType{
123123

124124
// DNSConfig is a JSON serializable object for dns.Config.
125125
type DNSConfig struct {
126-
Servers []*NameServerConfig `json:"servers"`
127-
Hosts map[string]*Address `json:"hosts"`
128-
ClientIP *Address `json:"clientIp"`
129-
Tag string `json:"tag"`
130-
QueryStrategy string `json:"queryStrategy"`
131-
CacheStrategy string `json:"cacheStrategy"`
132-
DisableCache bool `json:"disableCache"`
133-
DisableFallback bool `json:"disableFallback"`
126+
Servers []*NameServerConfig `json:"servers"`
127+
Hosts map[string]*HostAddress `json:"hosts"`
128+
ClientIP *Address `json:"clientIp"`
129+
Tag string `json:"tag"`
130+
QueryStrategy string `json:"queryStrategy"`
131+
CacheStrategy string `json:"cacheStrategy"`
132+
DisableCache bool `json:"disableCache"`
133+
DisableFallback bool `json:"disableFallback"`
134134
}
135135

136-
func getHostMapping(addr *Address) *dns.Config_HostMapping {
137-
if addr.Family().IsIP() {
138-
return &dns.Config_HostMapping{
139-
Ip: [][]byte{[]byte(addr.IP())},
136+
type HostAddress struct {
137+
addr *Address
138+
addrs []*Address
139+
}
140+
141+
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
142+
func (h *HostAddress) UnmarshalJSON(data []byte) error {
143+
addr := new(Address)
144+
var addrs []*Address
145+
switch {
146+
case json.Unmarshal(data, &addr) == nil:
147+
h.addr = addr
148+
case json.Unmarshal(data, &addrs) == nil:
149+
h.addrs = addrs
150+
default:
151+
return newError("invalid address")
152+
}
153+
return nil
154+
}
155+
156+
func getHostMapping(ha *HostAddress) *dns.Config_HostMapping {
157+
if ha.addr != nil {
158+
if ha.addr.Family().IsDomain() {
159+
return &dns.Config_HostMapping{
160+
ProxiedDomain: ha.addr.Domain(),
161+
}
140162
}
141-
} else {
142163
return &dns.Config_HostMapping{
143-
ProxiedDomain: addr.Domain(),
164+
Ip: [][]byte{ha.addr.IP()},
165+
}
166+
}
167+
168+
ips := make([][]byte, 0, len(ha.addrs))
169+
for _, addr := range ha.addrs {
170+
if addr.Family().IsDomain() {
171+
return &dns.Config_HostMapping{
172+
ProxiedDomain: addr.Domain(),
173+
}
144174
}
175+
ips = append(ips, []byte(addr.IP()))
176+
}
177+
return &dns.Config_HostMapping{
178+
Ip: ips,
145179
}
146180
}
147181

infra/conf/dns_test.go

+10-4
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ func TestDNSConfigParsing(t *testing.T) {
7474
}],
7575
"hosts": {
7676
"example.com": "127.0.0.1",
77+
"xtls.github.io": ["1.2.3.4", "5.6.7.8"],
7778
"domain:example.com": "google.com",
78-
"geosite:test": "10.0.0.1",
79-
"keyword:google": "8.8.8.8",
79+
"geosite:test": ["127.0.0.1", "127.0.0.2"],
80+
"keyword:google": ["8.8.8.8", "8.8.4.4"],
8081
"regexp:.*\\.com": "8.8.4.4"
8182
},
8283
"clientIp": "10.0.0.1",
@@ -127,18 +128,23 @@ func TestDNSConfigParsing(t *testing.T) {
127128
{
128129
Type: dns.DomainMatchingType_Full,
129130
Domain: "example.com",
130-
Ip: [][]byte{{10, 0, 0, 1}},
131+
Ip: [][]byte{{127, 0, 0, 1}, {127, 0, 0, 2}},
131132
},
132133
{
133134
Type: dns.DomainMatchingType_Keyword,
134135
Domain: "google",
135-
Ip: [][]byte{{8, 8, 8, 8}},
136+
Ip: [][]byte{{8, 8, 8, 8}, {8, 8, 4, 4}},
136137
},
137138
{
138139
Type: dns.DomainMatchingType_Regex,
139140
Domain: ".*\\.com",
140141
Ip: [][]byte{{8, 8, 4, 4}},
141142
},
143+
{
144+
Type: dns.DomainMatchingType_Full,
145+
Domain: "xtls.github.io",
146+
Ip: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
147+
},
142148
},
143149
ClientIp: []byte{10, 0, 0, 1},
144150
QueryStrategy: dns.QueryStrategy_USE_IP4,

0 commit comments

Comments
 (0)