mirror of
https://github.com/v2fly/v2ray-core.git
synced 2025-01-02 15:36:41 -05:00
Feat: DNS hosts support multiple addresses (#884)
This commit is contained in:
parent
400dc15c00
commit
bada0e48b4
@ -214,7 +214,7 @@ func (s *DNS) lookupIPInternal(domain string, option dns.IPOption) ([]net.IP, er
|
||||
newError("domain replaced: ", domain, " -> ", addrs[0].Domain()).WriteToLog()
|
||||
domain = addrs[0].Domain()
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,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},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ 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"`
|
||||
Hosts *HostsWrapper `json:"hosts"`
|
||||
ClientIP *Address `json:"clientIp"`
|
||||
Tag string `json:"tag"`
|
||||
QueryStrategy string `json:"queryStrategy"`
|
||||
@ -133,15 +133,174 @@ type DNSConfig struct {
|
||||
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{
|
||||
Ip: [][]byte{[]byte(addr.IP())},
|
||||
Ip: [][]byte{ha.addr.IP()},
|
||||
}
|
||||
}
|
||||
return &dns.Config_HostMapping{
|
||||
ProxiedDomain: addr.Domain(),
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -177,113 +336,12 @@ func (c *DNSConfig) Build() (*dns.Config, error) {
|
||||
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
|
||||
|
@ -69,9 +69,10 @@ func TestDNSConfigParsing(t *testing.T) {
|
||||
}],
|
||||
"hosts": {
|
||||
"v2fly.org": "127.0.0.1",
|
||||
"www.v2fly.org": ["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",
|
||||
@ -117,12 +118,12 @@ func TestDNSConfigParsing(t *testing.T) {
|
||||
{
|
||||
Type: dns.DomainMatchingType_Full,
|
||||
Domain: "test.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,
|
||||
@ -134,6 +135,11 @@ func TestDNSConfigParsing(t *testing.T) {
|
||||
Domain: "v2fly.org",
|
||||
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},
|
||||
QueryStrategy: dns.QueryStrategy_USE_IP4,
|
||||
|
Loading…
Reference in New Issue
Block a user