diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index f9c0c181..1715e408 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -510,8 +510,7 @@ type Config_HostMapping struct { Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,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 - // domain. Xray will use this domain for IP queries. This field is only - // effective if ip is empty. + // domain. Xray will use this domain for IP queries. ProxiedDomain string `protobuf:"bytes,4,opt,name=proxied_domain,json=proxiedDomain,proto3" json:"proxied_domain,omitempty"` } diff --git a/app/dns/config.proto b/app/dns/config.proto index 108a3429..1e395c43 100644 --- a/app/dns/config.proto +++ b/app/dns/config.proto @@ -73,8 +73,7 @@ message Config { repeated bytes ip = 3; // 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 - // effective if ip is empty. + // domain. Xray will use this domain for IP queries. string proxied_domain = 4; } diff --git a/app/dns/dns.go b/app/dns/dns.go index 6c555718..73d417c3 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -222,7 +222,7 @@ func (s *DNS) lookupIPInternal(domain string, option *dns.IPOption) ([]net.IP, e // Successfully found ip records in static host. // Skip hosts mapping result in FakeDNS query. 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) } } diff --git a/app/dns/hosts.go b/app/dns/hosts.go index 44b79c47..bfbe5d64 100644 --- a/app/dns/hosts.go +++ b/app/dns/hosts.go @@ -47,6 +47,9 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma id := g.Add(matcher) ips := make([]net.Address, 0, len(mapping.Ip)+1) switch { + case len(mapping.ProxiedDomain) > 0: + ips = append(ips, net.DomainAddress(mapping.ProxiedDomain)) + case len(mapping.Ip) > 0: for _, ip := range mapping.Ip { addr := net.IPAddress(ip) @@ -56,18 +59,10 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma ips = append(ips, addr) } - case len(mapping.ProxiedDomain) > 0: - ips = append(ips, net.DomainAddress(mapping.ProxiedDomain)) - default: 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 } @@ -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 return nil 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 { unwrapped := h.lookup(addrs[0].Domain(), option, maxDepth-1) if unwrapped != nil { diff --git a/app/dns/hosts_test.go b/app/dns/hosts_test.go index 786aeca3..3e1b5437 100644 --- a/app/dns/hosts_test.go +++ b/app/dns/hosts_test.go @@ -20,6 +20,20 @@ func TestStaticHosts(t *testing.T) { {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, Domain: "example.cn", @@ -32,6 +46,7 @@ func TestStaticHosts(t *testing.T) { Domain: "baidu.com", Ip: [][]byte{ {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{ IPv4Enable: true, diff --git a/infra/conf/dns.go b/infra/conf/dns.go index 8ebf7c8e..d667265c 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -123,26 +123,60 @@ var typeMap = map[router.Domain_Type]dns.DomainMatchingType{ // DNSConfig is a JSON serializable object for dns.Config. type DNSConfig struct { - Servers []*NameServerConfig `json:"servers"` - Hosts map[string]*Address `json:"hosts"` - ClientIP *Address `json:"clientIp"` - Tag string `json:"tag"` - QueryStrategy string `json:"queryStrategy"` - CacheStrategy string `json:"cacheStrategy"` - DisableCache bool `json:"disableCache"` - DisableFallback bool `json:"disableFallback"` + Servers []*NameServerConfig `json:"servers"` + Hosts map[string]*HostAddress `json:"hosts"` + ClientIP *Address `json:"clientIp"` + Tag string `json:"tag"` + QueryStrategy string `json:"queryStrategy"` + CacheStrategy string `json:"cacheStrategy"` + DisableCache bool `json:"disableCache"` + DisableFallback bool `json:"disableFallback"` } -func getHostMapping(addr *Address) *dns.Config_HostMapping { - if addr.Family().IsIP() { - return &dns.Config_HostMapping{ - Ip: [][]byte{[]byte(addr.IP())}, +type HostAddress struct { + addr *Address + addrs []*Address +} + +// 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{ - 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 diff --git a/infra/conf/dns_test.go b/infra/conf/dns_test.go index b6e9a67d..26e30158 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -74,9 +74,10 @@ func TestDNSConfigParsing(t *testing.T) { }], "hosts": { "example.com": "127.0.0.1", + "xtls.github.io": ["1.2.3.4", "5.6.7.8"], "domain:example.com": "google.com", - "geosite:test": "10.0.0.1", - "keyword:google": "8.8.8.8", + "geosite:test": ["127.0.0.1", "127.0.0.2"], + "keyword:google": ["8.8.8.8", "8.8.4.4"], "regexp:.*\\.com": "8.8.4.4" }, "clientIp": "10.0.0.1", @@ -127,18 +128,23 @@ func TestDNSConfigParsing(t *testing.T) { { Type: dns.DomainMatchingType_Full, Domain: "example.com", - Ip: [][]byte{{10, 0, 0, 1}}, + Ip: [][]byte{{127, 0, 0, 1}, {127, 0, 0, 2}}, }, { Type: dns.DomainMatchingType_Keyword, Domain: "google", - Ip: [][]byte{{8, 8, 8, 8}}, + Ip: [][]byte{{8, 8, 8, 8}, {8, 8, 4, 4}}, }, { Type: dns.DomainMatchingType_Regex, Domain: ".*\\.com", 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}, QueryStrategy: dns.QueryStrategy_USE_IP4,