diff --git a/app/dns/config.pb.go b/app/dns/config.pb.go index b4427442..db44dce3 100644 --- a/app/dns/config.pb.go +++ b/app/dns/config.pb.go @@ -404,8 +404,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 22733005..59f18b56 100644 --- a/app/dns/config.proto +++ b/app/dns/config.proto @@ -62,8 +62,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 7bdb74d4..c638735d 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -23,7 +23,7 @@ import ( type DNS struct { sync.Mutex tag string - cs CacheStrategy + cacheStrategy CacheStrategy disableFallback bool ipOption *dns.IPOption hosts *StaticHosts @@ -139,7 +139,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) { ctx: ctx, domainMatcher: domainMatcher, matcherInfos: matcherInfos, - cs: config.CacheStrategy, + cacheStrategy: config.CacheStrategy, disableFallback: config.DisableFallback, }, nil } @@ -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) } } @@ -231,7 +231,7 @@ func (s *DNS) lookupIPInternal(domain string, option *dns.IPOption) ([]net.IP, e errs := []error{} ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag}) for _, client := range s.sortClients(domain, option) { - ips, err := client.QueryIP(ctx, domain, *option, s.cs) + ips, err := client.QueryIP(ctx, domain, *option, s.cacheStrategy) if len(ips) > 0 { return ips, nil } diff --git a/app/dns/hosts.go b/app/dns/hosts.go index 8f656199..1444bc39 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 12242a86..e44c3715 100644 --- a/app/dns/hosts_test.go +++ b/app/dns/hosts_test.go @@ -21,6 +21,20 @@ func TestStaticHosts(t *testing.T) { {1, 1, 1, 1}, }, }, + { + Type: domain.MatchingType_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: domain.MatchingType_Full, + Domain: "proxy2.example.com", + ProxiedDomain: "proxy.example.com", + }, { Type: domain.MatchingType_Subdomain, Domain: "example.cn", @@ -33,6 +47,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}, }, }, } @@ -53,6 +68,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 1b160b32..464c0758 100644 --- a/infra/conf/dns.go +++ b/infra/conf/dns.go @@ -105,26 +105,60 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) { // 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 ffde967d..ad4c5c0e 100644 --- a/infra/conf/dns_test.go +++ b/infra/conf/dns_test.go @@ -75,14 +75,15 @@ 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", "queryStrategy": "UseIPv4", - "disableCache": true, + "cacheStrategy": "disable", "disableFallback": true }`, Parser: parserCreator(), @@ -128,18 +129,23 @@ func TestDNSConfigParsing(t *testing.T) { { Type: domain.MatchingType_Full, Domain: "example.com", - Ip: [][]byte{{10, 0, 0, 1}}, + Ip: [][]byte{{127, 0, 0, 1}, {127, 0, 0, 2}}, }, { Type: domain.MatchingType_Keyword, Domain: "google", - Ip: [][]byte{{8, 8, 8, 8}}, + Ip: [][]byte{{8, 8, 8, 8}, {8, 8, 4, 4}}, }, { Type: domain.MatchingType_Regex, Domain: ".*\\.com", Ip: [][]byte{{8, 8, 4, 4}}, }, + { + Type: domain.MatchingType_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,