Feat: DNS hosts support multiple addresses

This commit is contained in:
秋のかえで 2021-04-11 23:07:48 +08:00
parent 598e15aed2
commit 7cf30d5101
No known key found for this signature in database
GPG Key ID: B208CDDD55BEF92C
7 changed files with 106 additions and 31 deletions

View File

@ -510,8 +510,7 @@ type Config_HostMapping struct {
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"` Ip [][]byte `protobuf:"bytes,3,rep,name=ip,proto3" json:"ip,omitempty"`
// ProxiedDomain indicates the mapped domain has the same IP address on this // ProxiedDomain indicates the mapped domain has the same IP address on this
// domain. Xray will use this domain for IP queries. This field is only // domain. Xray will use this domain for IP queries.
// effective if ip is empty.
ProxiedDomain string `protobuf:"bytes,4,opt,name=proxied_domain,json=proxiedDomain,proto3" json:"proxied_domain,omitempty"` ProxiedDomain string `protobuf:"bytes,4,opt,name=proxied_domain,json=proxiedDomain,proto3" json:"proxied_domain,omitempty"`
} }

View File

@ -73,8 +73,7 @@ message Config {
repeated bytes ip = 3; repeated bytes ip = 3;
// ProxiedDomain indicates the mapped domain has the same IP address on this // ProxiedDomain indicates the mapped domain has the same IP address on this
// domain. Xray will use this domain for IP queries. This field is only // domain. Xray will use this domain for IP queries.
// effective if ip is empty.
string proxied_domain = 4; string proxied_domain = 4;
} }

View File

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

View File

@ -47,6 +47,9 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
id := g.Add(matcher) id := g.Add(matcher)
ips := make([]net.Address, 0, len(mapping.Ip)+1) ips := make([]net.Address, 0, len(mapping.Ip)+1)
switch { switch {
case len(mapping.ProxiedDomain) > 0:
ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
case len(mapping.Ip) > 0: case len(mapping.Ip) > 0:
for _, ip := range mapping.Ip { for _, ip := range mapping.Ip {
addr := net.IPAddress(ip) addr := net.IPAddress(ip)
@ -56,18 +59,10 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
ips = append(ips, addr) ips = append(ips, addr)
} }
case len(mapping.ProxiedDomain) > 0:
ips = append(ips, net.DomainAddress(mapping.ProxiedDomain))
default: default:
return nil, newError("neither IP address nor proxied domain specified for domain: ", mapping.Domain).AtWarning() return nil, newError("neither IP address nor proxied domain specified for domain: ", mapping.Domain).AtWarning()
} }
// Special handling for localhost IPv6. This is a dirty workaround as JSON config supports only single IP mapping.
if len(ips) == 1 && ips[0] == net.LocalHostIP {
ips = append(ips, net.LocalHostIPv6)
}
sh.ips[id] = ips sh.ips[id] = ips
} }
@ -100,6 +95,7 @@ func (h *StaticHosts) lookup(domain string, option *dns.IPOption, maxDepth int)
case len(addrs) == 0: // Not recorded in static hosts, return nil case len(addrs) == 0: // Not recorded in static hosts, return nil
return nil return nil
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Try to unwrap domain
newError("found replaced domain: ", domain, " -> ", addrs[0].Domain(), ". Try to unwrap it").AtDebug().WriteToLog()
if maxDepth > 0 { if maxDepth > 0 {
unwrapped := h.lookup(addrs[0].Domain(), option, maxDepth-1) unwrapped := h.lookup(addrs[0].Domain(), option, maxDepth-1)
if unwrapped != nil { if unwrapped != nil {

View File

@ -20,6 +20,20 @@ func TestStaticHosts(t *testing.T) {
{1, 1, 1, 1}, {1, 1, 1, 1},
}, },
}, },
{
Type: DomainMatchingType_Full,
Domain: "proxy.example.com",
Ip: [][]byte{
{1, 2, 3, 4},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
},
ProxiedDomain: "another-proxy.example.com",
},
{
Type: DomainMatchingType_Full,
Domain: "proxy2.example.com",
ProxiedDomain: "proxy.example.com",
},
{ {
Type: DomainMatchingType_Subdomain, Type: DomainMatchingType_Subdomain,
Domain: "example.cn", Domain: "example.cn",
@ -32,6 +46,7 @@ func TestStaticHosts(t *testing.T) {
Domain: "baidu.com", Domain: "baidu.com",
Ip: [][]byte{ Ip: [][]byte{
{127, 0, 0, 1}, {127, 0, 0, 1},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
}, },
}, },
} }
@ -52,6 +67,32 @@ func TestStaticHosts(t *testing.T) {
} }
} }
{
domain := hosts.Lookup("proxy.example.com", &dns.IPOption{
IPv4Enable: true,
IPv6Enable: false,
})
if len(domain) != 1 {
t.Error("expect 1 domain, but got ", len(domain))
}
if diff := cmp.Diff(domain[0].Domain(), "another-proxy.example.com"); diff != "" {
t.Error(diff)
}
}
{
domain := hosts.Lookup("proxy2.example.com", &dns.IPOption{
IPv4Enable: true,
IPv6Enable: false,
})
if len(domain) != 1 {
t.Error("expect 1 domain, but got ", len(domain))
}
if diff := cmp.Diff(domain[0].Domain(), "another-proxy.example.com"); diff != "" {
t.Error(diff)
}
}
{ {
ips := hosts.Lookup("www.example.cn", &dns.IPOption{ ips := hosts.Lookup("www.example.cn", &dns.IPOption{
IPv4Enable: true, IPv4Enable: true,

View File

@ -123,26 +123,60 @@ var typeMap = map[router.Domain_Type]dns.DomainMatchingType{
// DNSConfig is a JSON serializable object for dns.Config. // DNSConfig is a JSON serializable object for dns.Config.
type DNSConfig struct { type DNSConfig struct {
Servers []*NameServerConfig `json:"servers"` Servers []*NameServerConfig `json:"servers"`
Hosts map[string]*Address `json:"hosts"` Hosts map[string]*HostAddress `json:"hosts"`
ClientIP *Address `json:"clientIp"` ClientIP *Address `json:"clientIp"`
Tag string `json:"tag"` Tag string `json:"tag"`
QueryStrategy string `json:"queryStrategy"` QueryStrategy string `json:"queryStrategy"`
CacheStrategy string `json:"cacheStrategy"` CacheStrategy string `json:"cacheStrategy"`
DisableCache bool `json:"disableCache"` DisableCache bool `json:"disableCache"`
DisableFallback bool `json:"disableFallback"` DisableFallback bool `json:"disableFallback"`
} }
func getHostMapping(addr *Address) *dns.Config_HostMapping { type HostAddress struct {
if addr.Family().IsIP() { addr *Address
return &dns.Config_HostMapping{ addrs []*Address
Ip: [][]byte{[]byte(addr.IP())}, }
// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
func (h *HostAddress) UnmarshalJSON(data []byte) error {
addr := new(Address)
var addrs []*Address
switch {
case json.Unmarshal(data, &addr) == nil:
h.addr = addr
case json.Unmarshal(data, &addrs) == nil:
h.addrs = addrs
default:
return newError("invalid address")
}
return nil
}
func getHostMapping(ha *HostAddress) *dns.Config_HostMapping {
if ha.addr != nil {
if ha.addr.Family().IsDomain() {
return &dns.Config_HostMapping{
ProxiedDomain: ha.addr.Domain(),
}
} }
} else {
return &dns.Config_HostMapping{ return &dns.Config_HostMapping{
ProxiedDomain: addr.Domain(), Ip: [][]byte{ha.addr.IP()},
} }
} }
ips := make([][]byte, 0, len(ha.addrs))
for _, addr := range ha.addrs {
if addr.Family().IsDomain() {
return &dns.Config_HostMapping{
ProxiedDomain: addr.Domain(),
}
}
ips = append(ips, []byte(addr.IP()))
}
return &dns.Config_HostMapping{
Ip: ips,
}
} }
// Build implements Buildable // Build implements Buildable

View File

@ -74,9 +74,10 @@ func TestDNSConfigParsing(t *testing.T) {
}], }],
"hosts": { "hosts": {
"example.com": "127.0.0.1", "example.com": "127.0.0.1",
"xtls.github.io": ["1.2.3.4", "5.6.7.8"],
"domain:example.com": "google.com", "domain:example.com": "google.com",
"geosite:test": "10.0.0.1", "geosite:test": ["127.0.0.1", "127.0.0.2"],
"keyword:google": "8.8.8.8", "keyword:google": ["8.8.8.8", "8.8.4.4"],
"regexp:.*\\.com": "8.8.4.4" "regexp:.*\\.com": "8.8.4.4"
}, },
"clientIp": "10.0.0.1", "clientIp": "10.0.0.1",
@ -127,18 +128,23 @@ func TestDNSConfigParsing(t *testing.T) {
{ {
Type: dns.DomainMatchingType_Full, Type: dns.DomainMatchingType_Full,
Domain: "example.com", Domain: "example.com",
Ip: [][]byte{{10, 0, 0, 1}}, Ip: [][]byte{{127, 0, 0, 1}, {127, 0, 0, 2}},
}, },
{ {
Type: dns.DomainMatchingType_Keyword, Type: dns.DomainMatchingType_Keyword,
Domain: "google", Domain: "google",
Ip: [][]byte{{8, 8, 8, 8}}, Ip: [][]byte{{8, 8, 8, 8}, {8, 8, 4, 4}},
}, },
{ {
Type: dns.DomainMatchingType_Regex, Type: dns.DomainMatchingType_Regex,
Domain: ".*\\.com", Domain: ".*\\.com",
Ip: [][]byte{{8, 8, 4, 4}}, Ip: [][]byte{{8, 8, 4, 4}},
}, },
{
Type: dns.DomainMatchingType_Full,
Domain: "xtls.github.io",
Ip: [][]byte{{1, 2, 3, 4}, {5, 6, 7, 8}},
},
}, },
ClientIp: []byte{10, 0, 0, 1}, ClientIp: []byte{10, 0, 0, 1},
QueryStrategy: dns.QueryStrategy_USE_IP4, QueryStrategy: dns.QueryStrategy_USE_IP4,