diff --git a/app/dns/dns.go b/app/dns/dns.go index 4c4713764..7cce8d9da 100644 --- a/app/dns/dns.go +++ b/app/dns/dns.go @@ -8,6 +8,7 @@ package dns import ( "context" "fmt" + "strings" "sync" "github.com/v2fly/v2ray-core/v4/app/router" @@ -105,6 +106,7 @@ func New(ctx context.Context, config *Config) (*DNS, error) { clients = append(clients, client) } + // If there is no DNS client in config, add a `localhost` DNS client if len(clients) == 0 { clients = append(clients, NewLocalDNSClient()) } @@ -141,36 +143,13 @@ func (s *DNS) IsOwnLink(ctx context.Context) bool { } // LookupIP implements dns.Client. -func (s *DNS) LookupIP(domain string) ([]net.IP, error) { - return s.lookupIPInternal(domain, IPOption{ - IPv4Enable: true, - IPv6Enable: true, - }) -} - -// LookupIPv4 implements dns.IPv4Lookup. -func (s *DNS) LookupIPv4(domain string) ([]net.IP, error) { - return s.lookupIPInternal(domain, IPOption{ - IPv4Enable: true, - IPv6Enable: false, - }) -} - -// LookupIPv6 implements dns.IPv6Lookup. -func (s *DNS) LookupIPv6(domain string) ([]net.IP, error) { - return s.lookupIPInternal(domain, IPOption{ - IPv4Enable: false, - IPv6Enable: true, - }) -} - -func (s *DNS) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) { +func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) { if domain == "" { return nil, newError("empty domain name") } // Normalize the FQDN form query - if domain[len(domain)-1] == '.' { + if strings.HasSuffix(domain, ".") { domain = domain[:len(domain)-1] } @@ -192,6 +171,10 @@ func (s *DNS) lookupIPInternal(domain string, option IPOption) ([]net.IP, error) errs := []error{} ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag}) for _, client := range s.sortClients(domain) { + if !option.FakeEnable && strings.EqualFold(client.Name(), "FakeDNS") { + newError("skip DNS resolution for domain ", domain, " at server ", client.Name()).AtDebug().WriteToLog() + continue + } ips, err := client.QueryIP(ctx, domain, option) if len(ips) > 0 { return ips, nil diff --git a/app/dns/dns_test.go b/app/dns/dns_test.go index 996c96c38..6bfde1075 100644 --- a/app/dns/dns_test.go +++ b/app/dns/dns_test.go @@ -154,7 +154,11 @@ func TestUDPServerSubnet(t *testing.T) { client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) - ips, err := client.LookupIP("google.com") + ips, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -209,7 +213,11 @@ func TestUDPServer(t *testing.T) { client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) { - ips, err := client.LookupIP("google.com") + ips, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -220,7 +228,11 @@ func TestUDPServer(t *testing.T) { } { - ips, err := client.LookupIP("facebook.com") + ips, err := client.LookupIP("facebook.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -231,7 +243,11 @@ func TestUDPServer(t *testing.T) { } { - _, err := client.LookupIP("notexist.google.com") + _, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err == nil { t.Fatal("nil error") } @@ -241,8 +257,11 @@ func TestUDPServer(t *testing.T) { } { - clientv6 := client.(feature_dns.IPv6Lookup) - ips, err := clientv6.LookupIPv6("ipv4only.google.com") + ips, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + }) if err != feature_dns.ErrEmptyResponse { t.Fatal("error: ", err) } @@ -254,7 +273,11 @@ func TestUDPServer(t *testing.T) { dnsServer.Shutdown() { - ips, err := client.LookupIP("google.com") + ips, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -331,7 +354,11 @@ func TestPrioritizedDomain(t *testing.T) { startTime := time.Now() { - ips, err := client.LookupIP("google.com") + ips, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -390,10 +417,12 @@ func TestUDPServerIPv6(t *testing.T) { common.Must(err) client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) - client6 := client.(feature_dns.IPv6Lookup) - { - ips, err := client6.LookupIPv6("ipv6.google.com") + ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -456,7 +485,11 @@ func TestStaticHostDomain(t *testing.T) { client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client) { - ips, err := client.LookupIP("example.com") + ips, err := client.LookupIP("example.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -563,7 +596,11 @@ func TestIPMatch(t *testing.T) { startTime := time.Now() { - ips, err := client.LookupIP("google.com") + ips, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -682,7 +719,11 @@ func TestLocalDomain(t *testing.T) { startTime := time.Now() { // Will match dotless: - ips, err := client.LookupIP("hostname") + ips, err := client.LookupIP("hostname", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -693,7 +734,11 @@ func TestLocalDomain(t *testing.T) { } { // Will match domain:local - ips, err := client.LookupIP("hostname.local") + ips, err := client.LookupIP("hostname.local", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -704,7 +749,11 @@ func TestLocalDomain(t *testing.T) { } { // Will match static ip - ips, err := client.LookupIP("hostnamestatic") + ips, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -715,7 +764,11 @@ func TestLocalDomain(t *testing.T) { } { // Will match domain replacing - ips, err := client.LookupIP("hostnamealias") + ips, err := client.LookupIP("hostnamealias", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -726,7 +779,11 @@ func TestLocalDomain(t *testing.T) { } { // Will match dotless:localhost, but not expectIPs: 127.0.0.2, 127.0.0.3, then matches at dotless: - ips, err := client.LookupIP("localhost") + ips, err := client.LookupIP("localhost", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -737,7 +794,11 @@ func TestLocalDomain(t *testing.T) { } { // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3 - ips, err := client.LookupIP("localhost-a") + ips, err := client.LookupIP("localhost-a", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -748,7 +809,11 @@ func TestLocalDomain(t *testing.T) { } { // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3 - ips, err := client.LookupIP("localhost-b") + ips, err := client.LookupIP("localhost-b", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -759,7 +824,11 @@ func TestLocalDomain(t *testing.T) { } { // Will match dotless: - ips, err := client.LookupIP("Mijia Cloud") + ips, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -921,7 +990,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { startTime := time.Now() { // Will match server 1,2 and server 1 returns expected ip - ips, err := client.LookupIP("google.com") + ips, err := client.LookupIP("google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -932,8 +1005,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { } { // Will match server 1,2 and server 1 returns unexpected ip, then server 2 returns expected one - clientv4 := client.(feature_dns.IPv4Lookup) - ips, err := clientv4.LookupIPv4("ipv6.google.com") + ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -944,7 +1020,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { } { // Will match server 3,1,2 and server 3 returns expected one - ips, err := client.LookupIP("api.google.com") + ips, err := client.LookupIP("api.google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } @@ -955,7 +1035,11 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) { } { // Will match server 4,3,1,2 and server 4 returns expected one - ips, err := client.LookupIP("v2.api.google.com") + ips, err := client.LookupIP("v2.api.google.com", feature_dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err != nil { t.Fatal("unexpected error: ", err) } diff --git a/app/dns/dnscommon.go b/app/dns/dnscommon.go index 8ddfdffdf..4cd64e8d2 100644 --- a/app/dns/dnscommon.go +++ b/app/dns/dnscommon.go @@ -4,6 +4,7 @@ package dns import ( "encoding/binary" + "strings" "time" "golang.org/x/net/dns/dnsmessage" @@ -16,7 +17,7 @@ import ( // Fqdn normalize domain make sure it ends with '.' func Fqdn(domain string) string { - if len(domain) > 0 && domain[len(domain)-1] == '.' { + if len(domain) > 0 && strings.HasSuffix(domain, ".") { return domain } return domain + "." @@ -115,7 +116,7 @@ func genEDNS0Options(clientIP net.IP) *dnsmessage.Resource { return opt } -func buildReqMsgs(domain string, option IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest { +func buildReqMsgs(domain string, option dns_feature.IPOption, reqIDGen func() uint16, reqOpts *dnsmessage.Resource) []*dnsRequest { qA := dnsmessage.Question{ Name: dnsmessage.MustNewName(domain), Type: dnsmessage.TypeA, diff --git a/app/dns/dnscommon_test.go b/app/dns/dnscommon_test.go index 689f2934f..72ff54fba 100644 --- a/app/dns/dnscommon_test.go +++ b/app/dns/dnscommon_test.go @@ -13,6 +13,7 @@ import ( "github.com/v2fly/v2ray-core/v4/common" "github.com/v2fly/v2ray-core/v4/common/net" + dns_feature "github.com/v2fly/v2ray-core/v4/features/dns" ) func Test_parseResponse(t *testing.T) { @@ -95,7 +96,7 @@ func Test_buildReqMsgs(t *testing.T) { } type args struct { domain string - option IPOption + option dns_feature.IPOption reqOpts *dnsmessage.Resource } tests := []struct { @@ -103,10 +104,26 @@ func Test_buildReqMsgs(t *testing.T) { args args want int }{ - {"dual stack", args{"test.com", IPOption{true, true}, nil}, 2}, - {"ipv4 only", args{"test.com", IPOption{true, false}, nil}, 1}, - {"ipv6 only", args{"test.com", IPOption{false, true}, nil}, 1}, - {"none/error", args{"test.com", IPOption{false, false}, nil}, 0}, + {"dual stack", args{"test.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }, nil}, 2}, + {"ipv4 only", args{"test.com", dns_feature.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: false, + }, nil}, 1}, + {"ipv6 only", args{"test.com", dns_feature.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, + }, nil}, 1}, + {"none/error", args{"test.com", dns_feature.IPOption{ + IPv4Enable: false, + IPv6Enable: false, + FakeEnable: false, + }, nil}, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/app/dns/hosts.go b/app/dns/hosts.go index 1bae9485e..28a40f440 100644 --- a/app/dns/hosts.go +++ b/app/dns/hosts.go @@ -7,6 +7,7 @@ import ( "github.com/v2fly/v2ray-core/v4/common/net" "github.com/v2fly/v2ray-core/v4/common/strmatcher" "github.com/v2fly/v2ray-core/v4/features" + "github.com/v2fly/v2ray-core/v4/features/dns" ) // StaticHosts represents static domain-ip mapping in DNS server. @@ -75,7 +76,7 @@ func NewStaticHosts(hosts []*Config_HostMapping, legacy map[string]*net.IPOrDoma return sh, nil } -func filterIP(ips []net.Address, option IPOption) []net.Address { +func filterIP(ips []net.Address, option dns.IPOption) []net.Address { filtered := make([]net.Address, 0, len(ips)) for _, ip := range ips { if (ip.Family().IsIPv4() && option.IPv4Enable) || (ip.Family().IsIPv6() && option.IPv6Enable) { @@ -93,7 +94,7 @@ func (h *StaticHosts) lookupInternal(domain string) []net.Address { return ips } -func (h *StaticHosts) lookup(domain string, option IPOption, maxDepth int) []net.Address { +func (h *StaticHosts) lookup(domain string, option dns.IPOption, maxDepth int) []net.Address { switch addrs := h.lookupInternal(domain); { case len(addrs) == 0: // Not recorded in static hosts, return nil return nil @@ -111,6 +112,6 @@ func (h *StaticHosts) lookup(domain string, option IPOption, maxDepth int) []net } // Lookup returns IP addresses or proxied domain for the given domain, if exists in this StaticHosts. -func (h *StaticHosts) Lookup(domain string, option IPOption) []net.Address { +func (h *StaticHosts) Lookup(domain string, option dns.IPOption) []net.Address { return h.lookup(domain, option, 5) } diff --git a/app/dns/hosts_test.go b/app/dns/hosts_test.go index 5846f6213..293fda440 100644 --- a/app/dns/hosts_test.go +++ b/app/dns/hosts_test.go @@ -8,6 +8,7 @@ import ( . "github.com/v2fly/v2ray-core/v4/app/dns" "github.com/v2fly/v2ray-core/v4/common" "github.com/v2fly/v2ray-core/v4/common/net" + "github.com/v2fly/v2ray-core/v4/features/dns" ) func TestStaticHosts(t *testing.T) { @@ -39,7 +40,7 @@ func TestStaticHosts(t *testing.T) { common.Must(err) { - ips := hosts.Lookup("v2fly.org", IPOption{ + ips := hosts.Lookup("v2fly.org", dns.IPOption{ IPv4Enable: true, IPv6Enable: true, }) @@ -52,7 +53,7 @@ func TestStaticHosts(t *testing.T) { } { - ips := hosts.Lookup("www.v2ray.cn", IPOption{ + ips := hosts.Lookup("www.v2ray.cn", dns.IPOption{ IPv4Enable: true, IPv6Enable: true, }) @@ -65,7 +66,7 @@ func TestStaticHosts(t *testing.T) { } { - ips := hosts.Lookup("baidu.com", IPOption{ + ips := hosts.Lookup("baidu.com", dns.IPOption{ IPv4Enable: false, IPv6Enable: true, }) diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go index 3cca0f097..b373677b2 100644 --- a/app/dns/nameserver.go +++ b/app/dns/nameserver.go @@ -5,6 +5,7 @@ package dns import ( "context" "net/url" + "strings" "time" core "github.com/v2fly/v2ray-core/v4" @@ -12,21 +13,16 @@ import ( "github.com/v2fly/v2ray-core/v4/common/errors" "github.com/v2fly/v2ray-core/v4/common/net" "github.com/v2fly/v2ray-core/v4/common/strmatcher" + "github.com/v2fly/v2ray-core/v4/features/dns" "github.com/v2fly/v2ray-core/v4/features/routing" ) -// IPOption is an object for IP query options. -type IPOption struct { - IPv4Enable bool - IPv6Enable bool -} - // Server is the interface for Name Server. type Server interface { // Name of the Client. Name() string // QueryIP sends IP queries to its configured server. - QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) + QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption) ([]net.IP, error) } // Client is the interface for DNS client. @@ -47,15 +43,15 @@ func NewServer(dest net.Destination, dispatcher routing.Dispatcher) (Server, err return nil, err } switch { - case u.String() == "localhost": + case strings.EqualFold(u.String(), "localhost"): return NewLocalNameServer(), nil - case u.Scheme == "https": // DOH Remote mode + case strings.EqualFold(u.Scheme, "https"): // DOH Remote mode return NewDoHNameServer(u, dispatcher) - case u.Scheme == "https+local": // DOH Local mode + case strings.EqualFold(u.Scheme, "https+local"): // DOH Local mode return NewDoHLocalNameServer(u), nil - case u.Scheme == "quic+local": // DNS-over-QUIC Local mode + case strings.EqualFold(u.Scheme, "quic+local"): // DNS-over-QUIC Local mode return NewQUICNameServer(u) - case u.String() == "fakedns": + case strings.EqualFold(u.String(), "fakedns"): return NewFakeDNSServer(), nil } } @@ -173,7 +169,7 @@ func (c *Client) Name() string { } // QueryIP send DNS query to the name server with the client's IP. -func (c *Client) QueryIP(ctx context.Context, domain string, option IPOption) ([]net.IP, error) { +func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption) ([]net.IP, error) { ctx, cancel := context.WithTimeout(ctx, 4*time.Second) ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option) cancel() diff --git a/app/dns/nameserver_doh.go b/app/dns/nameserver_doh.go index 9507d247c..94fae4277 100644 --- a/app/dns/nameserver_doh.go +++ b/app/dns/nameserver_doh.go @@ -207,7 +207,7 @@ func (s *DoHNameServer) newReqID() uint16 { return uint16(atomic.AddUint32(&s.reqID, 1)) } -func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option IPOption) { +func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) { newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx)) reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP)) @@ -286,7 +286,7 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, return ioutil.ReadAll(resp.Body) } -func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) { +func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) { s.RLock() record, found := s.ips[domain] s.RUnlock() @@ -329,7 +329,7 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option IPOption) ([]net. } // QueryIP implements Server. -func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) { // nolint: dupl +func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) ([]net.IP, error) { // nolint: dupl fqdn := Fqdn(domain) ips, err := s.findIPsForDomain(fqdn, option) diff --git a/app/dns/nameserver_fakedns.go b/app/dns/nameserver_fakedns.go index d67671a1b..ca624f993 100644 --- a/app/dns/nameserver_fakedns.go +++ b/app/dns/nameserver_fakedns.go @@ -22,7 +22,7 @@ func (FakeDNSServer) Name() string { return "FakeDNS" } -func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, _ IPOption) ([]net.IP, error) { +func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, _ dns.IPOption) ([]net.IP, error) { if f.fakeDNSEngine == nil { if err := core.RequireFeatures(ctx, func(fd dns.FakeDNSEngine) { f.fakeDNSEngine = fd @@ -37,5 +37,7 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, _ return nil, newError("Unable to convert IP to net ip").Base(err).AtError() } + newError(f.Name(), " got answer: ", domain, " -> ", ips).AtInfo().WriteToLog() + return netIP, nil } diff --git a/app/dns/nameserver_local.go b/app/dns/nameserver_local.go index f8ce9a923..5b6767969 100644 --- a/app/dns/nameserver_local.go +++ b/app/dns/nameserver_local.go @@ -6,6 +6,7 @@ import ( "context" "github.com/v2fly/v2ray-core/v4/common/net" + "github.com/v2fly/v2ray-core/v4/features/dns" "github.com/v2fly/v2ray-core/v4/features/dns/localdns" ) @@ -15,17 +16,9 @@ type LocalNameServer struct { } // QueryIP implements Server. -func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) { - if option.IPv4Enable && option.IPv6Enable { - return s.client.LookupIP(domain) - } - - if option.IPv4Enable { - return s.client.LookupIPv4(domain) - } - - if option.IPv6Enable { - return s.client.LookupIPv6(domain) +func (s *LocalNameServer) QueryIP(_ context.Context, domain string, _ net.IP, option dns.IPOption) ([]net.IP, error) { + if option.IPv4Enable || option.IPv6Enable { + return s.client.LookupIP(domain, option) } return nil, newError("neither IPv4 nor IPv6 is enabled") diff --git a/app/dns/nameserver_local_test.go b/app/dns/nameserver_local_test.go index 4a15fdec8..2b5efefa9 100644 --- a/app/dns/nameserver_local_test.go +++ b/app/dns/nameserver_local_test.go @@ -8,12 +8,13 @@ import ( . "github.com/v2fly/v2ray-core/v4/app/dns" "github.com/v2fly/v2ray-core/v4/common" "github.com/v2fly/v2ray-core/v4/common/net" + "github.com/v2fly/v2ray-core/v4/features/dns" ) func TestLocalNameServer(t *testing.T) { s := NewLocalNameServer() ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - ips, err := s.QueryIP(ctx, "google.com", net.IP{}, IPOption{ + ips, err := s.QueryIP(ctx, "google.com", net.IP{}, dns.IPOption{ IPv4Enable: true, IPv6Enable: true, }) diff --git a/app/dns/nameserver_quic.go b/app/dns/nameserver_quic.go index 9664a6728..4614bb68a 100644 --- a/app/dns/nameserver_quic.go +++ b/app/dns/nameserver_quic.go @@ -153,7 +153,7 @@ func (s *QUICNameServer) newReqID() uint16 { return uint16(atomic.AddUint32(&s.reqID, 1)) } -func (s *QUICNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option IPOption) { +func (s *QUICNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) { newError(s.name, " querying: ", domain).AtInfo().WriteToLog(session.ExportIDToError(ctx)) reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP)) @@ -223,7 +223,7 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, domain string, clientIP } } -func (s *QUICNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) { +func (s *QUICNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) { s.RLock() record, found := s.ips[domain] s.RUnlock() @@ -266,7 +266,7 @@ func (s *QUICNameServer) findIPsForDomain(domain string, option IPOption) ([]net } // QueryIP is called from dns.Server->queryIPTimeout -func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) { +func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) ([]net.IP, error) { fqdn := Fqdn(domain) ips, err := s.findIPsForDomain(fqdn, option) diff --git a/app/dns/nameserver_quic_test.go b/app/dns/nameserver_quic_test.go index 58777c2d3..2856bd3ef 100644 --- a/app/dns/nameserver_quic_test.go +++ b/app/dns/nameserver_quic_test.go @@ -9,6 +9,7 @@ import ( . "github.com/v2fly/v2ray-core/v4/app/dns" "github.com/v2fly/v2ray-core/v4/common" "github.com/v2fly/v2ray-core/v4/common/net" + dns_feature "github.com/v2fly/v2ray-core/v4/features/dns" ) func TestQUICNameServer(t *testing.T) { @@ -17,7 +18,7 @@ func TestQUICNameServer(t *testing.T) { s, err := NewQUICNameServer(url) common.Must(err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) - ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), IPOption{ + ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{ IPv4Enable: true, IPv6Enable: true, }) diff --git a/app/dns/nameserver_udp.go b/app/dns/nameserver_udp.go index bffa13902..c03ac49a4 100644 --- a/app/dns/nameserver_udp.go +++ b/app/dns/nameserver_udp.go @@ -55,7 +55,7 @@ func NewClassicNameServer(address net.Destination, dispatcher routing.Dispatcher Execute: s.Cleanup, } s.udpServer = udp.NewDispatcher(dispatcher, s.HandleResponse) - newError("DNS: created UDP client inited for ", address.NetAddr()).AtInfo().WriteToLog() + newError("DNS: created UDP client initialized for ", address.NetAddr()).AtInfo().WriteToLog() return s } @@ -184,7 +184,7 @@ func (s *ClassicNameServer) addPendingRequest(req *dnsRequest) { s.requests[id] = *req } -func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option IPOption) { +func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) { newError(s.name, " querying DNS for: ", domain).AtDebug().WriteToLog(session.ExportIDToError(ctx)) reqs := buildReqMsgs(domain, option, s.newReqID, genEDNS0Options(clientIP)) @@ -203,7 +203,7 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, client } } -func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([]net.IP, error) { +func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) { s.RLock() record, found := s.ips[domain] s.RUnlock() @@ -242,7 +242,7 @@ func (s *ClassicNameServer) findIPsForDomain(domain string, option IPOption) ([] } // QueryIP implements Server. -func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option IPOption) ([]net.IP, error) { +func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption) ([]net.IP, error) { fqdn := Fqdn(domain) ips, err := s.findIPsForDomain(fqdn, option) diff --git a/app/router/router_test.go b/app/router/router_test.go index e78f94b99..2276c8b65 100644 --- a/app/router/router_test.go +++ b/app/router/router_test.go @@ -10,6 +10,7 @@ import ( "github.com/v2fly/v2ray-core/v4/common" "github.com/v2fly/v2ray-core/v4/common/net" "github.com/v2fly/v2ray-core/v4/common/session" + "github.com/v2fly/v2ray-core/v4/features/dns" "github.com/v2fly/v2ray-core/v4/features/outbound" routing_session "github.com/v2fly/v2ray-core/v4/features/routing/session" "github.com/v2fly/v2ray-core/v4/testing/mocks" @@ -116,7 +117,11 @@ func TestIPOnDemand(t *testing.T) { defer mockCtl.Finish() mockDNS := mocks.NewDNSClient(mockCtl) - mockDNS.EXPECT().LookupIP(gomock.Eq("v2fly.org")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes() + mockDNS.EXPECT().LookupIP(gomock.Eq("v2fly.org"), dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes() r := new(Router) common.Must(r.Init(config, mockDNS, nil)) @@ -151,7 +156,11 @@ func TestIPIfNonMatchDomain(t *testing.T) { defer mockCtl.Finish() mockDNS := mocks.NewDNSClient(mockCtl) - mockDNS.EXPECT().LookupIP(gomock.Eq("v2fly.org")).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes() + mockDNS.EXPECT().LookupIP(gomock.Eq("v2fly.org"), dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes() r := new(Router) common.Must(r.Init(config, mockDNS, nil)) diff --git a/features/dns/client.go b/features/dns/client.go index 0eed4205e..67b314a0a 100644 --- a/features/dns/client.go +++ b/features/dns/client.go @@ -7,6 +7,13 @@ import ( "github.com/v2fly/v2ray-core/v4/features" ) +// IPOption is an object for IP query options. +type IPOption struct { + IPv4Enable bool + IPv6Enable bool + FakeEnable bool +} + // Client is a V2Ray feature for querying DNS information. // // v2ray:api:stable @@ -14,21 +21,7 @@ type Client interface { features.Feature // LookupIP returns IP address for the given domain. IPs may contain IPv4 and/or IPv6 addresses. - LookupIP(domain string) ([]net.IP, error) -} - -// IPv4Lookup is an optional feature for querying IPv4 addresses only. -// -// v2ray:api:beta -type IPv4Lookup interface { - LookupIPv4(domain string) ([]net.IP, error) -} - -// IPv6Lookup is an optional feature for querying IPv6 addresses only. -// -// v2ray:api:beta -type IPv6Lookup interface { - LookupIPv6(domain string) ([]net.IP, error) + LookupIP(domain string, option IPOption) ([]net.IP, error) } // ClientType returns the type of Client interface. Can be used for implementing common.HasType. diff --git a/features/dns/localdns/client.go b/features/dns/localdns/client.go index 36ab66a65..963d1104d 100644 --- a/features/dns/localdns/client.go +++ b/features/dns/localdns/client.go @@ -20,58 +20,41 @@ func (*Client) Start() error { return nil } func (*Client) Close() error { return nil } // LookupIP implements Client. -func (*Client) LookupIP(host string) ([]net.IP, error) { +func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, error) { ips, err := net.LookupIP(host) if err != nil { return nil, err } parsedIPs := make([]net.IP, 0, len(ips)) + ipv4 := make([]net.IP, 0, len(ips)) + ipv6 := make([]net.IP, 0, len(ips)) for _, ip := range ips { parsed := net.IPAddress(ip) if parsed != nil { parsedIPs = append(parsedIPs, parsed.IP()) } - } - if len(parsedIPs) == 0 { - return nil, dns.ErrEmptyResponse - } - return parsedIPs, nil -} - -// LookupIPv4 implements IPv4Lookup. -func (c *Client) LookupIPv4(host string) ([]net.IP, error) { - ips, err := c.LookupIP(host) - if err != nil { - return nil, err - } - ipv4 := make([]net.IP, 0, len(ips)) - for _, ip := range ips { if len(ip) == net.IPv4len { ipv4 = append(ipv4, ip) } - } - if len(ipv4) == 0 { - return nil, dns.ErrEmptyResponse - } - return ipv4, nil -} - -// LookupIPv6 implements IPv6Lookup. -func (c *Client) LookupIPv6(host string) ([]net.IP, error) { - ips, err := c.LookupIP(host) - if err != nil { - return nil, err - } - ipv6 := make([]net.IP, 0, len(ips)) - for _, ip := range ips { if len(ip) == net.IPv6len { ipv6 = append(ipv6, ip) } } - if len(ipv6) == 0 { - return nil, dns.ErrEmptyResponse + switch { + case option.IPv4Enable && option.IPv6Enable: + if len(parsedIPs) > 0 { + return parsedIPs, nil + } + case option.IPv4Enable: + if len(ipv4) > 0 { + return ipv4, nil + } + case option.IPv6Enable: + if len(ipv6) > 0 { + return ipv6, nil + } } - return ipv6, nil + return nil, dns.ErrEmptyResponse } // New create a new dns.Client that queries localhost for DNS. diff --git a/features/routing/dns/context.go b/features/routing/dns/context.go index 88a20eb50..13f9faa06 100644 --- a/features/routing/dns/context.go +++ b/features/routing/dns/context.go @@ -26,7 +26,11 @@ func (ctx *ResolvableContext) GetTargetIPs() []net.IP { } if domain := ctx.GetTargetDomain(); len(domain) != 0 { - ips, err := ctx.dnsClient.LookupIP(domain) + ips, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + }) if err == nil { ctx.resolvedIPs = ips return ips diff --git a/proxy/dns/dns.go b/proxy/dns/dns.go index d510fd5d4..3412e1322 100644 --- a/proxy/dns/dns.go +++ b/proxy/dns/dns.go @@ -39,26 +39,12 @@ type ownLinkVerifier interface { type Handler struct { client dns.Client - ipv4Lookup dns.IPv4Lookup - ipv6Lookup dns.IPv6Lookup ownLinkVerifier ownLinkVerifier server net.Destination } func (h *Handler) Init(config *Config, dnsClient dns.Client) error { h.client = dnsClient - ipv4lookup, ok := dnsClient.(dns.IPv4Lookup) - if !ok { - return newError("dns.Client doesn't implement IPv4Lookup") - } - h.ipv4Lookup = ipv4lookup - - ipv6lookup, ok := dnsClient.(dns.IPv6Lookup) - if !ok { - return newError("dns.Client doesn't implement IPv6Lookup") - } - h.ipv6Lookup = ipv6lookup - if v, ok := dnsClient.(ownLinkVerifier); ok { h.ownLinkVerifier = v } @@ -217,9 +203,17 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string, switch qType { case dnsmessage.TypeA: - ips, err = h.ipv4Lookup.LookupIPv4(domain) + ips, err = h.client.LookupIP(domain, dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: true, + }) case dnsmessage.TypeAAAA: - ips, err = h.ipv6Lookup.LookupIPv6(domain) + ips, err = h.client.LookupIP(domain, dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: true, + }) } rcode := dns.RCodeFromError(err) diff --git a/proxy/freedom/freedom.go b/proxy/freedom/freedom.go index ee0adfeca..ca0d8ad48 100644 --- a/proxy/freedom/freedom.go +++ b/proxy/freedom/freedom.go @@ -60,19 +60,26 @@ func (h *Handler) policy() policy.Session { } func (h *Handler) resolveIP(ctx context.Context, domain string, localAddr net.Address) net.Address { - var lookupFunc func(string) ([]net.IP, error) = h.dns.LookupIP - + var option dns.IPOption = dns.IPOption{ + IPv4Enable: true, + IPv6Enable: true, + FakeEnable: false, + } if h.config.DomainStrategy == Config_USE_IP4 || (localAddr != nil && localAddr.Family().IsIPv4()) { - if lookupIPv4, ok := h.dns.(dns.IPv4Lookup); ok { - lookupFunc = lookupIPv4.LookupIPv4 + option = dns.IPOption{ + IPv4Enable: true, + IPv6Enable: false, + FakeEnable: false, } } else if h.config.DomainStrategy == Config_USE_IP6 || (localAddr != nil && localAddr.Family().IsIPv6()) { - if lookupIPv6, ok := h.dns.(dns.IPv6Lookup); ok { - lookupFunc = lookupIPv6.LookupIPv6 + option = dns.IPOption{ + IPv4Enable: false, + IPv6Enable: true, + FakeEnable: false, } } - ips, err := lookupFunc(domain) + ips, err := h.dns.LookupIP(domain, option) if err != nil { newError("failed to get IP address for domain ", domain).Base(err).WriteToLog(session.ExportIDToError(ctx)) } @@ -123,7 +130,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte Address: ip, Port: dialDest.Port, } - newError("dialing to to ", dialDest).WriteToLog(session.ExportIDToError(ctx)) + newError("dialing to ", dialDest).WriteToLog(session.ExportIDToError(ctx)) } } diff --git a/testing/mocks/dns.go b/testing/mocks/dns.go index 0e05f1125..1961578c4 100644 --- a/testing/mocks/dns.go +++ b/testing/mocks/dns.go @@ -9,6 +9,7 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + dns "github.com/v2fly/v2ray-core/v4/features/dns" ) // DNSClient is a mock of Client interface. @@ -49,18 +50,18 @@ func (mr *DNSClientMockRecorder) Close() *gomock.Call { } // LookupIP mocks base method. -func (m *DNSClient) LookupIP(arg0 string) ([]net.IP, error) { +func (m *DNSClient) LookupIP(arg0 string, arg1 dns.IPOption) ([]net.IP, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LookupIP", arg0) + ret := m.ctrl.Call(m, "LookupIP", arg0, arg1) ret0, _ := ret[0].([]net.IP) ret1, _ := ret[1].(error) return ret0, ret1 } // LookupIP indicates an expected call of LookupIP. -func (mr *DNSClientMockRecorder) LookupIP(arg0 interface{}) *gomock.Call { +func (mr *DNSClientMockRecorder) LookupIP(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupIP", reflect.TypeOf((*DNSClient)(nil).LookupIP), arg0, arg1) } // Start mocks base method.