Browse Source

Dotless domain support in built-in DNS

Vigilans 5 years ago
parent
commit
17f51f412c
4 changed files with 239 additions and 9 deletions
  1. 11 8
      app/dns/server.go
  2. 217 0
      app/dns/server_test.go
  3. 1 1
      app/dns/udpns.go
  4. 10 0
      infra/conf/router.go

+ 11 - 8
app/dns/server.go

@@ -86,10 +86,18 @@ func New(ctx context.Context, config *Config) (*Server, error) {
 	}
 	}
 	server.hosts = hosts
 	server.hosts = hosts
 
 
-	addNameServer := func(endpoint *net.Endpoint) int {
+	addNameServer := func(ns *NameServer) int {
+		endpoint := ns.Address
 		address := endpoint.Address.AsAddress()
 		address := endpoint.Address.AsAddress()
 		if address.Family().IsDomain() && address.Domain() == "localhost" {
 		if address.Family().IsDomain() && address.Domain() == "localhost" {
 			server.clients = append(server.clients, NewLocalNameServer())
 			server.clients = append(server.clients, NewLocalNameServer())
+			if len(ns.PrioritizedDomain) == 0 { // Priotize local domain with .local domain or without any dot to local DNS
+				ns.PrioritizedDomain = []*NameServer_PriorityDomain{
+					{Type: DomainMatchingType_Regex, Domain: "^[^.]*$"}, // This will only match domain without any dot
+					{Type: DomainMatchingType_Subdomain, Domain: "local"},
+					{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
+				}
+			}
 		} else if address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https+local://") {
 		} else if address.Family().IsDomain() && strings.HasPrefix(address.Domain(), "https+local://") {
 			// URI schemed string treated as domain
 			// URI schemed string treated as domain
 			// DOH Local mode
 			// DOH Local mode
@@ -137,7 +145,7 @@ func New(ctx context.Context, config *Config) (*Server, error) {
 	if len(config.NameServers) > 0 {
 	if len(config.NameServers) > 0 {
 		features.PrintDeprecatedFeatureWarning("simple DNS server")
 		features.PrintDeprecatedFeatureWarning("simple DNS server")
 		for _, destPB := range config.NameServers {
 		for _, destPB := range config.NameServers {
-			addNameServer(destPB)
+			addNameServer(&NameServer{Address: destPB})
 		}
 		}
 	}
 	}
 
 
@@ -148,7 +156,7 @@ func New(ctx context.Context, config *Config) (*Server, error) {
 		var geoIPMatcherContainer router.GeoIPMatcherContainer
 		var geoIPMatcherContainer router.GeoIPMatcherContainer
 
 
 		for _, ns := range config.NameServer {
 		for _, ns := range config.NameServer {
-			idx := addNameServer(ns.Address)
+			idx := addNameServer(ns)
 
 
 			for _, domain := range ns.PrioritizedDomain {
 			for _, domain := range ns.PrioritizedDomain {
 				matcher, err := toStrMatcher(domain.Type, domain.Domain)
 				matcher, err := toStrMatcher(domain.Type, domain.Domain)
@@ -307,11 +315,6 @@ func (s *Server) lookupIPInternal(domain string, option IPOption) ([]net.IP, err
 		domain = domain[:len(domain)-1]
 		domain = domain[:len(domain)-1]
 	}
 	}
 
 
-	// skip domain without any dot
-	if !strings.Contains(domain, ".") {
-		return nil, newError("invalid domain name").AtWarning()
-	}
-
 	ips := s.lookupStatic(domain, option, 0)
 	ips := s.lookupStatic(domain, option, 0)
 	if ips != nil && ips[0].Family().IsIP() {
 	if ips != nil && ips[0].Family().IsIP() {
 		newError("returning ", len(ips), " IPs for domain ", domain).WriteToLog()
 		newError("returning ", len(ips), " IPs for domain ", domain).WriteToLog()

+ 217 - 0
app/dns/server_test.go

@@ -63,6 +63,27 @@ func (*staticHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
 			ans.Answer = append(ans.Answer, rr)
 			ans.Answer = append(ans.Answer, rr)
 		} else if q.Name == "notexist.google.com." && q.Qtype == dns.TypeAAAA {
 		} else if q.Name == "notexist.google.com." && q.Qtype == dns.TypeAAAA {
 			ans.MsgHdr.Rcode = dns.RcodeNameError
 			ans.MsgHdr.Rcode = dns.RcodeNameError
+		} else if q.Name == "hostname." && q.Qtype == dns.TypeA {
+			rr, _ := dns.NewRR("hostname. IN A 127.0.0.1")
+			ans.Answer = append(ans.Answer, rr)
+		} else if q.Name == "hostname.local." && q.Qtype == dns.TypeA {
+			rr, _ := dns.NewRR("hostname.local. IN A 127.0.0.1")
+			ans.Answer = append(ans.Answer, rr)
+		} else if q.Name == "hostname.localdomain." && q.Qtype == dns.TypeA {
+			rr, _ := dns.NewRR("hostname.localdomain. IN A 127.0.0.1")
+			ans.Answer = append(ans.Answer, rr)
+		} else if q.Name == "localhost." && q.Qtype == dns.TypeA {
+			rr, _ := dns.NewRR("localhost. IN A 127.0.0.2")
+			ans.Answer = append(ans.Answer, rr)
+		} else if q.Name == "localhost-a." && q.Qtype == dns.TypeA {
+			rr, _ := dns.NewRR("localhost-a. IN A 127.0.0.3")
+			ans.Answer = append(ans.Answer, rr)
+		} else if q.Name == "localhost-b." && q.Qtype == dns.TypeA {
+			rr, _ := dns.NewRR("localhost-b. IN A 127.0.0.4")
+			ans.Answer = append(ans.Answer, rr)
+		} else if q.Name == "Mijia\\ Cloud." && q.Qtype == dns.TypeA {
+			rr, _ := dns.NewRR("Mijia\\ Cloud. IN A 127.0.0.1")
+			ans.Answer = append(ans.Answer, rr)
 		}
 		}
 	}
 	}
 	w.WriteMsg(ans)
 	w.WriteMsg(ans)
@@ -537,3 +558,199 @@ func TestIPMatch(t *testing.T) {
 		t.Error("DNS query doesn't finish in 2 seconds.")
 		t.Error("DNS query doesn't finish in 2 seconds.")
 	}
 	}
 }
 }
+
+func TestLocalDomain(t *testing.T) {
+	port := udp.PickPort()
+
+	dnsServer := dns.Server{
+		Addr:    "127.0.0.1:" + port.String(),
+		Net:     "udp",
+		Handler: &staticHandler{},
+		UDPSize: 1200,
+	}
+
+	go dnsServer.ListenAndServe()
+	time.Sleep(time.Second)
+
+	config := &core.Config{
+		App: []*serial.TypedMessage{
+			serial.ToTypedMessage(&Config{
+				NameServers: []*net.Endpoint{
+					{
+						Network: net.Network_UDP,
+						Address: &net.IPOrDomain{
+							Address: &net.IPOrDomain_Ip{
+								Ip: []byte{127, 0, 0, 1},
+							},
+						},
+						Port: 9999, /* unreachable */
+					},
+				},
+				NameServer: []*NameServer{
+					{
+						Address: &net.Endpoint{
+							Network: net.Network_UDP,
+							Address: &net.IPOrDomain{
+								Address: &net.IPOrDomain_Ip{
+									Ip: []byte{127, 0, 0, 1},
+								},
+							},
+							Port: uint32(port),
+						},
+						PrioritizedDomain: []*NameServer_PriorityDomain{
+							// Equivalent of dotless:localhost
+							{Type: DomainMatchingType_Regex, Domain: "^[^.]*localhost[^.]*$"},
+						},
+						Geoip: []*router.GeoIP{
+							{ // Will match localhost, localhost-a and localhost-b,
+								CountryCode: "local",
+								Cidr: []*router.CIDR{
+									{Ip: []byte{127, 0, 0, 2}, Prefix: 32},
+									{Ip: []byte{127, 0, 0, 3}, Prefix: 32},
+									{Ip: []byte{127, 0, 0, 4}, Prefix: 32},
+								},
+							},
+						},
+					},
+					{
+						Address: &net.Endpoint{
+							Network: net.Network_UDP,
+							Address: &net.IPOrDomain{
+								Address: &net.IPOrDomain_Ip{
+									Ip: []byte{127, 0, 0, 1},
+								},
+							},
+							Port: uint32(port),
+						},
+						PrioritizedDomain: []*NameServer_PriorityDomain{
+							// Equivalent of dotless: and domain:local
+							{Type: DomainMatchingType_Regex, Domain: "^[^.]*$"},
+							{Type: DomainMatchingType_Subdomain, Domain: "local"},
+							{Type: DomainMatchingType_Subdomain, Domain: "localdomain"},
+						},
+					},
+				},
+				StaticHosts: []*Config_HostMapping{
+					{
+						Type:   DomainMatchingType_Full,
+						Domain: "hostnamestatic",
+						Ip:     [][]byte{{127, 0, 0, 53}},
+					},
+					{
+						Type:          DomainMatchingType_Full,
+						Domain:        "hostnamealias",
+						ProxiedDomain: "hostname.localdomain",
+					},
+				},
+			}),
+			serial.ToTypedMessage(&dispatcher.Config{}),
+			serial.ToTypedMessage(&proxyman.OutboundConfig{}),
+			serial.ToTypedMessage(&policy.Config{}),
+		},
+		Outbound: []*core.OutboundHandlerConfig{
+			{
+				ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
+			},
+		},
+	}
+
+	v, err := core.New(config)
+	common.Must(err)
+
+	client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
+
+	startTime := time.Now()
+
+	{ // Will match dotless:
+		ips, err := client.LookupIP("hostname")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	{ // Will match domain:local
+		ips, err := client.LookupIP("hostname.local")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	{ // Will match static ip
+		ips, err := client.LookupIP("hostnamestatic")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 53}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	{ // Will match domain replacing
+		ips, err := client.LookupIP("hostnamealias")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	{ // Will match dotless:localhost, but not expectIPs: 127.0.0.2, 127.0.0.3, then matches at dotless:
+		ips, err := client.LookupIP("localhost")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 2}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
+		ips, err := client.LookupIP("localhost-a")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 3}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
+		ips, err := client.LookupIP("localhost-b")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 4}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	{ // Will match dotless:
+		ips, err := client.LookupIP("Mijia Cloud")
+		if err != nil {
+			t.Fatal("unexpected error: ", err)
+		}
+
+		if r := cmp.Diff(ips, []net.IP{{127, 0, 0, 1}}); r != "" {
+			t.Fatal(r)
+		}
+	}
+
+	endTime := time.Now()
+	if startTime.After(endTime.Add(time.Second * 2)) {
+		t.Error("DNS query doesn't finish in 2 seconds.")
+	}
+}

+ 1 - 1
app/dns/udpns.go

@@ -134,7 +134,7 @@ func (s *ClassicNameServer) HandleResponse(ctx context.Context, packet *udp_prot
 	}
 	}
 
 
 	elapsed := time.Since(req.start)
 	elapsed := time.Since(req.start)
-	newError(s.name, " got answere: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed).AtInfo().WriteToLog()
+	newError(s.name, " got answer: ", req.domain, " ", req.reqType, " -> ", ipRec.IP, " ", elapsed).AtInfo().WriteToLog()
 	if len(req.domain) > 0 && (rec.A != nil || rec.AAAA != nil) {
 	if len(req.domain) > 0 && (rec.A != nil || rec.AAAA != nil) {
 		s.updateIP(req.domain, rec)
 		s.updateIP(req.domain, rec)
 	}
 	}

+ 10 - 0
infra/conf/router.go

@@ -299,6 +299,16 @@ func parseDomainRule(domain string) ([]*router.Domain, error) {
 	case strings.HasPrefix(domain, "keyword:"):
 	case strings.HasPrefix(domain, "keyword:"):
 		domainRule.Type = router.Domain_Plain
 		domainRule.Type = router.Domain_Plain
 		domainRule.Value = domain[8:]
 		domainRule.Value = domain[8:]
+	case strings.HasPrefix(domain, "dotless:"):
+		domainRule.Type = router.Domain_Regex
+		switch substr := domain[8:]; {
+		case substr == "":
+			domainRule.Value = "^[^.]*$"
+		case !strings.Contains(substr, "."):
+			domainRule.Value = "^[^.]*" + substr + "[^.]*$"
+		default:
+			return nil, newError("Substr in dotless rule should not contain a dot: ", substr)
+		}
 	default:
 	default:
 		domainRule.Type = router.Domain_Plain
 		domainRule.Type = router.Domain_Plain
 		domainRule.Value = domain
 		domainRule.Value = domain