Browse Source

Feat: DNS hosts support multiple addresses (#884)

Loyalsoldier 4 years ago
parent
commit
bada0e48b4
5 changed files with 181 additions and 121 deletions
  1. 1 1
      app/dns/dns.go
  2. 0 5
      app/dns/hosts.go
  3. 1 0
      app/dns/hosts_test.go
  4. 169 111
      infra/conf/dns.go
  5. 10 4
      infra/conf/dns_test.go

+ 1 - 1
app/dns/dns.go

@@ -214,7 +214,7 @@ func (s *DNS) lookupIPInternal(domain string, option dns.IPOption) ([]net.IP, er
 		newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog()
 		newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog()
 		domain = addrs[0].Domain()
 		domain = addrs[0].Domain()
 	default: // Successfully found ip records in static host
 	default: // Successfully found ip records in static host
-		newError("returning ", len(addrs), " IPs for domain ", domain).WriteToLog()
+		newError("returning ", len(addrs), " IPs for domain ", domain, " -> ", addrs).WriteToLog()
 		return toNetIP(addrs)
 		return toNetIP(addrs)
 	}
 	}
 
 

+ 0 - 5
app/dns/hosts.go

@@ -65,11 +65,6 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma
 			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
 	}
 	}
 
 

+ 1 - 0
app/dns/hosts_test.go

@@ -32,6 +32,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},
 			},
 			},
 		},
 		},
 	}
 	}

+ 169 - 111
infra/conf/dns.go

@@ -125,7 +125,7 @@ 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           *HostsWrapper       `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"`
@@ -133,15 +133,174 @@ type DNSConfig struct {
 	DisableFallback bool                `json:"disableFallback"`
 	DisableFallback bool                `json:"disableFallback"`
 }
 }
 
 
-func getHostMapping(addr *Address) *dns.Config_HostMapping {
-	if addr.Family().IsIP() {
+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
+}
+
+type HostsWrapper struct {
+	Hosts map[string]*HostAddress
+}
+
+func getHostMapping(ha *HostAddress) *dns.Config_HostMapping {
+	if ha.addr != nil {
+		if ha.addr.Family().IsDomain() {
+			return &dns.Config_HostMapping{
+				ProxiedDomain: ha.addr.Domain(),
+			}
+		}
 		return &dns.Config_HostMapping{
 		return &dns.Config_HostMapping{
-			Ip: [][]byte{[]byte(addr.IP())},
+			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{
 	return &dns.Config_HostMapping{
-		ProxiedDomain: addr.Domain(),
+		Ip: ips,
+	}
+}
+
+// UnmarshalJSON implements encoding/json.Unmarshaler.UnmarshalJSON
+func (m *HostsWrapper) UnmarshalJSON(data []byte) error {
+	hosts := make(map[string]*HostAddress)
+	err := json.Unmarshal(data, &hosts)
+	if err == nil {
+		m.Hosts = hosts
+		return nil
 	}
 	}
+	return newError("invalid DNS hosts").Base(err)
+}
+
+// Build implements Buildable
+func (m *HostsWrapper) Build() ([]*dns.Config_HostMapping, error) {
+	mappings := make([]*dns.Config_HostMapping, 0, 20)
+
+	domains := make([]string, 0, len(m.Hosts))
+	for domain := range m.Hosts {
+		domains = append(domains, domain)
+	}
+	sort.Strings(domains)
+
+	for _, domain := range domains {
+		switch {
+		case strings.HasPrefix(domain, "domain:"):
+			domainName := domain[7:]
+			if len(domainName) == 0 {
+				return nil, newError("empty domain type of rule: ", domain)
+			}
+			mapping := getHostMapping(m.Hosts[domain])
+			mapping.Type = dns.DomainMatchingType_Subdomain
+			mapping.Domain = domainName
+			mappings = append(mappings, mapping)
+
+		case strings.HasPrefix(domain, "geosite:"):
+			listName := domain[8:]
+			if len(listName) == 0 {
+				return nil, newError("empty geosite rule: ", domain)
+			}
+			geositeList, err := loadGeosite(listName)
+			if err != nil {
+				return nil, newError("failed to load geosite: ", listName).Base(err)
+			}
+			for _, d := range geositeList {
+				mapping := getHostMapping(m.Hosts[domain])
+				mapping.Type = typeMap[d.Type]
+				mapping.Domain = d.Value
+				mappings = append(mappings, mapping)
+			}
+
+		case strings.HasPrefix(domain, "regexp:"):
+			regexpVal := domain[7:]
+			if len(regexpVal) == 0 {
+				return nil, newError("empty regexp type of rule: ", domain)
+			}
+			mapping := getHostMapping(m.Hosts[domain])
+			mapping.Type = dns.DomainMatchingType_Regex
+			mapping.Domain = regexpVal
+			mappings = append(mappings, mapping)
+
+		case strings.HasPrefix(domain, "keyword:"):
+			keywordVal := domain[8:]
+			if len(keywordVal) == 0 {
+				return nil, newError("empty keyword type of rule: ", domain)
+			}
+			mapping := getHostMapping(m.Hosts[domain])
+			mapping.Type = dns.DomainMatchingType_Keyword
+			mapping.Domain = keywordVal
+			mappings = append(mappings, mapping)
+
+		case strings.HasPrefix(domain, "full:"):
+			fullVal := domain[5:]
+			if len(fullVal) == 0 {
+				return nil, newError("empty full domain type of rule: ", domain)
+			}
+			mapping := getHostMapping(m.Hosts[domain])
+			mapping.Type = dns.DomainMatchingType_Full
+			mapping.Domain = fullVal
+			mappings = append(mappings, mapping)
+
+		case strings.HasPrefix(domain, "dotless:"):
+			mapping := getHostMapping(m.Hosts[domain])
+			mapping.Type = dns.DomainMatchingType_Regex
+			switch substr := domain[8:]; {
+			case substr == "":
+				mapping.Domain = "^[^.]*$"
+			case !strings.Contains(substr, "."):
+				mapping.Domain = "^[^.]*" + substr + "[^.]*$"
+			default:
+				return nil, newError("substr in dotless rule should not contain a dot: ", substr)
+			}
+			mappings = append(mappings, mapping)
+
+		case strings.HasPrefix(domain, "ext:"):
+			kv := strings.Split(domain[4:], ":")
+			if len(kv) != 2 {
+				return nil, newError("invalid external resource: ", domain)
+			}
+			filename := kv[0]
+			list := kv[1]
+			geositeList, err := loadGeositeWithAttr(filename, list)
+			if err != nil {
+				return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err)
+			}
+			for _, d := range geositeList {
+				mapping := getHostMapping(m.Hosts[domain])
+				mapping.Type = typeMap[d.Type]
+				mapping.Domain = d.Value
+				mappings = append(mappings, mapping)
+			}
+
+		default:
+			mapping := getHostMapping(m.Hosts[domain])
+			mapping.Type = dns.DomainMatchingType_Full
+			mapping.Domain = domain
+			mappings = append(mappings, mapping)
+		}
+	}
+	return mappings, nil
 }
 }
 
 
 // Build implements Buildable
 // Build implements Buildable
@@ -177,113 +336,12 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
 		config.NameServer = append(config.NameServer, ns)
 		config.NameServer = append(config.NameServer, ns)
 	}
 	}
 
 
-	if c.Hosts != nil && len(c.Hosts) > 0 {
-		domains := make([]string, 0, len(c.Hosts))
-		for domain := range c.Hosts {
-			domains = append(domains, domain)
-		}
-		sort.Strings(domains)
-
-		for _, domain := range domains {
-			addr := c.Hosts[domain]
-			var mappings []*dns.Config_HostMapping
-			switch {
-			case strings.HasPrefix(domain, "domain:"):
-				domainName := domain[7:]
-				if len(domainName) == 0 {
-					return nil, newError("empty domain type of rule: ", domain)
-				}
-				mapping := getHostMapping(addr)
-				mapping.Type = dns.DomainMatchingType_Subdomain
-				mapping.Domain = domainName
-				mappings = append(mappings, mapping)
-
-			case strings.HasPrefix(domain, "geosite:"):
-				listName := domain[8:]
-				if len(listName) == 0 {
-					return nil, newError("empty geosite rule: ", domain)
-				}
-				domains, err := loadGeosite(listName)
-				if err != nil {
-					return nil, newError("failed to load geosite: ", listName).Base(err)
-				}
-				for _, d := range domains {
-					mapping := getHostMapping(addr)
-					mapping.Type = typeMap[d.Type]
-					mapping.Domain = d.Value
-					mappings = append(mappings, mapping)
-				}
-
-			case strings.HasPrefix(domain, "regexp:"):
-				regexpVal := domain[7:]
-				if len(regexpVal) == 0 {
-					return nil, newError("empty regexp type of rule: ", domain)
-				}
-				mapping := getHostMapping(addr)
-				mapping.Type = dns.DomainMatchingType_Regex
-				mapping.Domain = regexpVal
-				mappings = append(mappings, mapping)
-
-			case strings.HasPrefix(domain, "keyword:"):
-				keywordVal := domain[8:]
-				if len(keywordVal) == 0 {
-					return nil, newError("empty keyword type of rule: ", domain)
-				}
-				mapping := getHostMapping(addr)
-				mapping.Type = dns.DomainMatchingType_Keyword
-				mapping.Domain = keywordVal
-				mappings = append(mappings, mapping)
-
-			case strings.HasPrefix(domain, "full:"):
-				fullVal := domain[5:]
-				if len(fullVal) == 0 {
-					return nil, newError("empty full domain type of rule: ", domain)
-				}
-				mapping := getHostMapping(addr)
-				mapping.Type = dns.DomainMatchingType_Full
-				mapping.Domain = fullVal
-				mappings = append(mappings, mapping)
-
-			case strings.HasPrefix(domain, "dotless:"):
-				mapping := getHostMapping(addr)
-				mapping.Type = dns.DomainMatchingType_Regex
-				switch substr := domain[8:]; {
-				case substr == "":
-					mapping.Domain = "^[^.]*$"
-				case !strings.Contains(substr, "."):
-					mapping.Domain = "^[^.]*" + substr + "[^.]*$"
-				default:
-					return nil, newError("substr in dotless rule should not contain a dot: ", substr)
-				}
-				mappings = append(mappings, mapping)
-
-			case strings.HasPrefix(domain, "ext:"):
-				kv := strings.Split(domain[4:], ":")
-				if len(kv) != 2 {
-					return nil, newError("invalid external resource: ", domain)
-				}
-				filename := kv[0]
-				list := kv[1]
-				domains, err := loadGeositeWithAttr(filename, list)
-				if err != nil {
-					return nil, newError("failed to load domain list: ", list, " from ", filename).Base(err)
-				}
-				for _, d := range domains {
-					mapping := getHostMapping(addr)
-					mapping.Type = typeMap[d.Type]
-					mapping.Domain = d.Value
-					mappings = append(mappings, mapping)
-				}
-
-			default:
-				mapping := getHostMapping(addr)
-				mapping.Type = dns.DomainMatchingType_Full
-				mapping.Domain = domain
-				mappings = append(mappings, mapping)
-			}
-
-			config.StaticHosts = append(config.StaticHosts, mappings...)
+	if c.Hosts != nil {
+		staticHosts, err := c.Hosts.Build()
+		if err != nil {
+			return nil, newError("failed to build hosts").Base(err)
 		}
 		}
+		config.StaticHosts = append(config.StaticHosts, staticHosts...)
 	}
 	}
 
 
 	return config, nil
 	return config, nil

+ 10 - 4
infra/conf/dns_test.go

@@ -69,9 +69,10 @@ func TestDNSConfigParsing(t *testing.T) {
 				}],
 				}],
 				"hosts": {
 				"hosts": {
 					"v2fly.org": "127.0.0.1",
 					"v2fly.org": "127.0.0.1",
+					"www.v2fly.org": ["1.2.3.4", "5.6.7.8"],
 					"domain:example.com": "google.com",
 					"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"
 					"regexp:.*\\.com": "8.8.4.4"
 				},
 				},
 				"clientIp": "10.0.0.1",
 				"clientIp": "10.0.0.1",
@@ -117,12 +118,12 @@ func TestDNSConfigParsing(t *testing.T) {
 					{
 					{
 						Type:   dns.DomainMatchingType_Full,
 						Type:   dns.DomainMatchingType_Full,
 						Domain: "test.example.com",
 						Domain: "test.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,
@@ -134,6 +135,11 @@ func TestDNSConfigParsing(t *testing.T) {
 						Domain: "v2fly.org",
 						Domain: "v2fly.org",
 						Ip:     [][]byte{{127, 0, 0, 1}},
 						Ip:     [][]byte{{127, 0, 0, 1}},
 					},
 					},
+					{
+						Type:   dns.DomainMatchingType_Full,
+						Domain: "www.v2fly.org",
+						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,