|
|
@@ -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()},
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ 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{
|
|
|
- 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
|
|
|
@@ -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
|