DNS: Support returning upstream TTL to clients (#4526)

Closes https://github.com/XTLS/Xray-core/issues/4527
This commit is contained in:
Meo597 2025-03-24 21:33:56 +08:00 committed by GitHub
parent 2d3210e4b8
commit 4afe2d0cff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 168 additions and 157 deletions

View File

@ -157,16 +157,16 @@ func (s *DNS) IsOwnLink(ctx context.Context) bool {
}
// LookupIP implements dns.Client.
func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, uint32, error) {
if domain == "" {
return nil, errors.New("empty domain name")
return nil, 0, errors.New("empty domain name")
}
option.IPv4Enable = option.IPv4Enable && s.ipOption.IPv4Enable
option.IPv6Enable = option.IPv6Enable && s.ipOption.IPv6Enable
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns.ErrEmptyResponse
return nil, 0, dns.ErrEmptyResponse
}
// Normalize the FQDN form query
@ -177,13 +177,14 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
case addrs == nil: // Domain not recorded in static host
break
case len(addrs) == 0: // Domain recorded, but no valid IP returned (e.g. IPv4 address with only IPv6 enabled)
return nil, dns.ErrEmptyResponse
return nil, 0, dns.ErrEmptyResponse
case len(addrs) == 1 && addrs[0].Family().IsDomain(): // Domain replacement
errors.LogInfo(s.ctx, "domain replaced: ", domain, " -> ", addrs[0].Domain())
domain = addrs[0].Domain()
default: // Successfully found ip records in static host
errors.LogInfo(s.ctx, "returning ", len(addrs), " IP(s) for domain ", domain, " -> ", addrs)
return toNetIP(addrs)
ips, err := toNetIP(addrs)
return ips, 10, err // Hosts ttl is 10
}
// Name servers lookup
@ -194,9 +195,9 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name())
continue
}
ips, err := client.QueryIP(ctx, domain, option, s.disableCache)
ips, ttl, err := client.QueryIP(ctx, domain, option, s.disableCache)
if len(ips) > 0 {
return ips, nil
return ips, ttl, nil
}
if err != nil {
errors.LogInfoInner(s.ctx, err, "failed to lookup ip for domain ", domain, " at server ", client.Name())
@ -204,11 +205,11 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
}
// 5 for RcodeRefused in miekg/dns, hardcode to reduce binary size
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch && err != dns.ErrEmptyResponse && dns.RCodeFromError(err) != 5 {
return nil, err
return nil, 0, err
}
}
return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
return nil, 0, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
}
// LookupHosts implements dns.HostsLookup.

View File

@ -155,7 +155,7 @@ func TestUDPServerSubnet(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
ips, err := client.LookupIP("google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -216,7 +216,7 @@ func TestUDPServer(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{
ips, err := client.LookupIP("google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -231,7 +231,7 @@ func TestUDPServer(t *testing.T) {
}
{
ips, err := client.LookupIP("facebook.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("facebook.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -246,7 +246,7 @@ func TestUDPServer(t *testing.T) {
}
{
_, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{
_, _, err := client.LookupIP("notexist.google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -260,7 +260,7 @@ func TestUDPServer(t *testing.T) {
}
{
ips, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("ipv4only.google.com", feature_dns.IPOption{
IPv4Enable: false,
IPv6Enable: true,
FakeEnable: false,
@ -276,7 +276,7 @@ func TestUDPServer(t *testing.T) {
dnsServer.Shutdown()
{
ips, err := client.LookupIP("google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -357,7 +357,7 @@ func TestPrioritizedDomain(t *testing.T) {
startTime := time.Now()
{
ips, err := client.LookupIP("google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -423,7 +423,7 @@ func TestUDPServerIPv6(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{
ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
IPv4Enable: false,
IPv6Enable: true,
FakeEnable: false,
@ -492,7 +492,7 @@ func TestStaticHostDomain(t *testing.T) {
client := v.GetFeature(feature_dns.ClientType()).(feature_dns.Client)
{
ips, err := client.LookupIP("example.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("example.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -603,7 +603,7 @@ func TestIPMatch(t *testing.T) {
startTime := time.Now()
{
ips, err := client.LookupIP("google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -726,7 +726,7 @@ func TestLocalDomain(t *testing.T) {
startTime := time.Now()
{ // Will match dotless:
ips, err := client.LookupIP("hostname", feature_dns.IPOption{
ips, _, err := client.LookupIP("hostname", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -741,7 +741,7 @@ func TestLocalDomain(t *testing.T) {
}
{ // Will match domain:local
ips, err := client.LookupIP("hostname.local", feature_dns.IPOption{
ips, _, err := client.LookupIP("hostname.local", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -756,7 +756,7 @@ func TestLocalDomain(t *testing.T) {
}
{ // Will match static ip
ips, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{
ips, _, err := client.LookupIP("hostnamestatic", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -771,7 +771,7 @@ func TestLocalDomain(t *testing.T) {
}
{ // Will match domain replacing
ips, err := client.LookupIP("hostnamealias", feature_dns.IPOption{
ips, _, err := client.LookupIP("hostnamealias", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -786,7 +786,7 @@ 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", feature_dns.IPOption{
ips, _, err := client.LookupIP("localhost", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -801,7 +801,7 @@ 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", feature_dns.IPOption{
ips, _, err := client.LookupIP("localhost-a", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -816,7 +816,7 @@ 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", feature_dns.IPOption{
ips, _, err := client.LookupIP("localhost-b", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -831,7 +831,7 @@ func TestLocalDomain(t *testing.T) {
}
{ // Will match dotless:
ips, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{
ips, _, err := client.LookupIP("Mijia Cloud", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -997,7 +997,7 @@ 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", feature_dns.IPOption{
ips, _, err := client.LookupIP("google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -1012,7 +1012,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}
{ // Will match server 1,2 and server 1 returns unexpected ip, then server 2 returns expected one
ips, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("ipv6.google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: false,
FakeEnable: false,
@ -1027,7 +1027,7 @@ func TestMultiMatchPrioritizedDomain(t *testing.T) {
}
{ // Will match server 3,1,2 and server 3 returns expected one
ips, err := client.LookupIP("api.google.com", feature_dns.IPOption{
ips, _, err := client.LookupIP("api.google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
@ -1042,7 +1042,7 @@ 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", feature_dns.IPOption{
ips, _, err := client.LookupIP("v2.api.google.com", feature_dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,

View File

@ -38,14 +38,15 @@ type IPRecord struct {
RawHeader *dnsmessage.Header
}
func (r *IPRecord) getIPs() ([]net.Address, error) {
func (r *IPRecord) getIPs() ([]net.Address, uint32, error) {
if r == nil || r.Expire.Before(time.Now()) {
return nil, errRecordNotFound
return nil, 0, errRecordNotFound
}
if r.RCode != dnsmessage.RCodeSuccess {
return nil, dns_feature.RCodeError(r.RCode)
return nil, 0, dns_feature.RCodeError(r.RCode)
}
return r.IP, nil
ttl := uint32(time.Until(r.Expire) / time.Second)
return r.IP, ttl, nil
}
func isNewer(baseRec *IPRecord, newRec *IPRecord) bool {

View File

@ -21,7 +21,7 @@ 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 dns.IPOption, disableCache bool) ([]net.IP, error)
QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]net.IP, uint32, error)
}
// Client is the interface for DNS client.
@ -191,7 +191,7 @@ func (c *Client) Name() string {
}
// QueryIP sends DNS query to the name server with the client's IP.
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption, disableCache bool) ([]net.IP, error) {
func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption, disableCache bool) ([]net.IP, uint32, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeoutMs)
if len(c.tag) != 0 {
content := session.InboundFromContext(ctx)
@ -200,13 +200,14 @@ func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption
// do not direct set *content.Tag, it might be used by other clients
ctx = session.ContextWithInbound(ctx, &session.Inbound{Tag: c.tag})
}
ips, err := c.server.QueryIP(ctx, domain, c.clientIP, option, disableCache)
ips, ttl, err := c.server.QueryIP(ctx, domain, c.clientIP, option, disableCache)
cancel()
if err != nil {
return ips, err
return ips, ttl, err
}
return c.MatchExpectedIPs(domain, ips)
netips, err := c.MatchExpectedIPs(domain, ips)
return netips, ttl, err
}
// MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones.

View File

@ -301,64 +301,66 @@ func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte,
return io.ReadAll(resp.Body)
}
func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) {
s.RLock()
record, found := s.ips[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
return nil, 0, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
var ttl uint32
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
ips, ttl, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ip6, ttl, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
netips, err := toNetIP(ips)
return netips, ttl, err
}
if err4 != nil {
return nil, err4
return nil, 0, err4
}
if err6 != nil {
return nil, err6
return nil, 0, err6
}
if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) {
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
return nil, errRecordNotFound
return nil, 0, errRecordNotFound
}
// QueryIP implements Server.
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { // nolint: dupl
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, uint32, error) { // nolint: dupl
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, err
return ips, ttl, err
}
}
@ -392,15 +394,15 @@ func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net
start := time.Now()
for {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
return ips, ttl, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
return nil, 0, ctx.Err()
case <-done:
}
}

View File

@ -19,7 +19,7 @@ func TestDOHNameServer(t *testing.T) {
s := NewDoHNameServer(url, QueryStrategy_USE_IP, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -36,7 +36,7 @@ func TestDOHNameServerWithCache(t *testing.T) {
s := NewDoHNameServer(url, QueryStrategy_USE_IP, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -47,7 +47,7 @@ func TestDOHNameServerWithCache(t *testing.T) {
}
ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
ips2, _, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, true)
@ -64,7 +64,7 @@ func TestDOHNameServerWithIPv4Override(t *testing.T) {
s := NewDoHNameServer(url, QueryStrategy_USE_IP4, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -87,7 +87,7 @@ func TestDOHNameServerWithIPv6Override(t *testing.T) {
s := NewDoHNameServer(url, QueryStrategy_USE_IP6, nil, false)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)

View File

@ -20,9 +20,9 @@ func (FakeDNSServer) Name() string {
return "FakeDNS"
}
func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, opt dns.IPOption, _ bool) ([]net.IP, error) {
func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, opt dns.IPOption, _ bool) ([]net.IP, uint32, error) {
if f.fakeDNSEngine == nil {
return nil, errors.New("Unable to locate a fake DNS Engine").AtError()
return nil, 0, errors.New("Unable to locate a fake DNS Engine").AtError()
}
var ips []net.Address
@ -34,13 +34,13 @@ func (f *FakeDNSServer) QueryIP(ctx context.Context, domain string, _ net.IP, op
netIP, err := toNetIP(ips)
if err != nil {
return nil, errors.New("Unable to convert IP to net ip").Base(err).AtError()
return nil, 0, errors.New("Unable to convert IP to net ip").Base(err).AtError()
}
errors.LogInfo(ctx, f.Name(), " got answer: ", domain, " -> ", ips)
if len(netIP) > 0 {
return netIP, nil
return netIP, 1, nil // fakeIP ttl is 1
}
return nil, dns.ErrEmptyResponse
return nil, 0, dns.ErrEmptyResponse
}

View File

@ -21,14 +21,14 @@ type LocalNameServer struct {
const errEmptyResponse = "No address associated with hostname"
// QueryIP implements Server.
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, _ net.IP, option dns.IPOption, _ bool) (ips []net.IP, err error) {
func (s *LocalNameServer) QueryIP(ctx context.Context, domain string, _ net.IP, option dns.IPOption, _ bool) (ips []net.IP, ttl uint32, err error) {
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns.ErrEmptyResponse
return nil, 0, dns.ErrEmptyResponse
}
start := time.Now()
ips, err = s.client.LookupIP(domain, option)
ips, ttl, err = s.client.LookupIP(domain, option)
if err != nil && strings.HasSuffix(err.Error(), errEmptyResponse) {
err = dns.ErrEmptyResponse

View File

@ -14,7 +14,7 @@ import (
func TestLocalNameServer(t *testing.T) {
s := NewLocalNameServer(QueryStrategy_USE_IP)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, err := s.QueryIP(ctx, "google.com", net.IP{}, dns.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP{}, dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,

View File

@ -244,64 +244,66 @@ func (s *QUICNameServer) sendQuery(ctx context.Context, domain string, clientIP
}
}
func (s *QUICNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *QUICNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) {
s.RLock()
record, found := s.ips[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
return nil, 0, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
var ttl uint32
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
ips, ttl, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ip6, ttl, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
netips, err := toNetIP(ips)
return netips, ttl, err
}
if err4 != nil {
return nil, err4
return nil, 0, err4
}
if err6 != nil {
return nil, err6
return nil, 0, err6
}
if (option.IPv4Enable && record.A != nil) || (option.IPv6Enable && record.AAAA != nil) {
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
return nil, errRecordNotFound
return nil, 0, errRecordNotFound
}
// QueryIP is called from dns.Server->queryIPTimeout
func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, uint32, error) {
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, err
return ips, ttl, err
}
}
@ -335,15 +337,15 @@ func (s *QUICNameServer) QueryIP(ctx context.Context, domain string, clientIP ne
start := time.Now()
for {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
return ips, ttl, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
return nil, 0, ctx.Err()
case <-done:
}
}

View File

@ -19,7 +19,7 @@ func TestQUICNameServer(t *testing.T) {
s, err := NewQUICNameServer(url, QueryStrategy_USE_IP)
common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -30,7 +30,7 @@ func TestQUICNameServer(t *testing.T) {
}
ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns.IPOption{
ips2, _, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, true)
@ -47,7 +47,7 @@ func TestQUICNameServerWithIPv4Override(t *testing.T) {
s, err := NewQUICNameServer(url, QueryStrategy_USE_IP4)
common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -70,7 +70,7 @@ func TestQUICNameServerWithIPv6Override(t *testing.T) {
s, err := NewQUICNameServer(url, QueryStrategy_USE_IP6)
common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)

View File

@ -273,60 +273,62 @@ func (s *TCPNameServer) sendQuery(ctx context.Context, domain string, clientIP n
}
}
func (s *TCPNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *TCPNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) {
s.RLock()
record, found := s.ips[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
return nil, 0, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
var ttl uint32
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
ips, ttl, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ip6, ttl, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
netips, err := toNetIP(ips)
return netips, ttl, err
}
if err4 != nil {
return nil, err4
return nil, 0, err4
}
if err6 != nil {
return nil, err6
return nil, 0, err6
}
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
// QueryIP implements Server.
func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, uint32, error) {
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, err
return ips, ttl, err
}
}
@ -360,15 +362,15 @@ func (s *TCPNameServer) QueryIP(ctx context.Context, domain string, clientIP net
start := time.Now()
for {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
return ips, ttl, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
return nil, 0, ctx.Err()
case <-done:
}
}

View File

@ -19,7 +19,7 @@ func TestTCPLocalNameServer(t *testing.T) {
s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP)
common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -36,7 +36,7 @@ func TestTCPLocalNameServerWithCache(t *testing.T) {
s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP)
common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -47,7 +47,7 @@ func TestTCPLocalNameServerWithCache(t *testing.T) {
}
ctx2, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips2, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
ips2, _, err := s.QueryIP(ctx2, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, true)
@ -64,7 +64,7 @@ func TestTCPLocalNameServerWithIPv4Override(t *testing.T) {
s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP4)
common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)
@ -88,7 +88,7 @@ func TestTCPLocalNameServerWithIPv6Override(t *testing.T) {
s, err := NewTCPLocalNameServer(url, QueryStrategy_USE_IP6)
common.Must(err)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
ips, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
ips, _, err := s.QueryIP(ctx, "google.com", net.IP(nil), dns_feature.IPOption{
IPv4Enable: true,
IPv6Enable: true,
}, false)

View File

@ -230,60 +230,62 @@ func (s *ClassicNameServer) sendQuery(ctx context.Context, domain string, client
}
}
func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, error) {
func (s *ClassicNameServer) findIPsForDomain(domain string, option dns_feature.IPOption) ([]net.IP, uint32, error) {
s.RLock()
record, found := s.ips[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
return nil, 0, errRecordNotFound
}
var err4 error
var err6 error
var ips []net.Address
var ip6 []net.Address
var ttl uint32
if option.IPv4Enable {
ips, err4 = record.A.getIPs()
ips, ttl, err4 = record.A.getIPs()
}
if option.IPv6Enable {
ip6, err6 = record.AAAA.getIPs()
ip6, ttl, err6 = record.AAAA.getIPs()
ips = append(ips, ip6...)
}
if len(ips) > 0 {
return toNetIP(ips)
netips, err := toNetIP(ips)
return netips, ttl, err
}
if err4 != nil {
return nil, err4
return nil, 0, err4
}
if err6 != nil {
return nil, err6
return nil, 0, err6
}
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
// QueryIP implements Server.
func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) {
func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, uint32, error) {
fqdn := Fqdn(domain)
option = ResolveIpOptionOverride(s.queryStrategy, option)
if !option.IPv4Enable && !option.IPv6Enable {
return nil, dns_feature.ErrEmptyResponse
return nil, 0, dns_feature.ErrEmptyResponse
}
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying IP for ", domain, " at ", s.name)
} else {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", ips)
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSCacheHit, Elapsed: 0, Error: err})
return ips, err
return ips, ttl, err
}
}
@ -317,15 +319,15 @@ func (s *ClassicNameServer) QueryIP(ctx context.Context, domain string, clientIP
start := time.Now()
for {
ips, err := s.findIPsForDomain(fqdn, option)
ips, ttl, err := s.findIPsForDomain(fqdn, option)
if err != errRecordNotFound {
log.Record(&log.DNSLog{Server: s.name, Domain: domain, Result: ips, Status: log.DNSQueried, Elapsed: time.Since(start), Error: err})
return ips, err
return ips, ttl, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
return nil, 0, ctx.Err()
case <-done:
}
}

View File

@ -177,7 +177,7 @@ func TestIPOnDemand(t *testing.T) {
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
}).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
}).Return([]net.IP{{192, 168, 0, 1}}, uint32(600), nil).AnyTimes()
r := new(Router)
common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil))
@ -222,7 +222,7 @@ func TestIPIfNonMatchDomain(t *testing.T) {
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,
}).Return([]net.IP{{192, 168, 0, 1}}, nil).AnyTimes()
}).Return([]net.IP{{192, 168, 0, 1}}, uint32(600), nil).AnyTimes()
r := new(Router)
common.Must(r.Init(context.TODO(), config, mockDNS, nil, nil))

View File

@ -21,7 +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, option IPOption) ([]net.IP, error)
LookupIP(domain string, option IPOption) ([]net.IP, uint32, error)
}
type HostsLookup interface {

View File

@ -20,10 +20,10 @@ func (*Client) Start() error { return nil }
func (*Client) Close() error { return nil }
// LookupIP implements Client.
func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, error) {
func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, uint32, error) {
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
return nil, 0, err
}
parsedIPs := make([]net.IP, 0, len(ips))
ipv4 := make([]net.IP, 0, len(ips))
@ -40,21 +40,22 @@ func (*Client) LookupIP(host string, option dns.IPOption) ([]net.IP, error) {
ipv6 = append(ipv6, ip)
}
}
// Local DNS ttl is 600
switch {
case option.IPv4Enable && option.IPv6Enable:
if len(parsedIPs) > 0 {
return parsedIPs, nil
return parsedIPs, 600, nil
}
case option.IPv4Enable:
if len(ipv4) > 0 {
return ipv4, nil
return ipv4, 600, nil
}
case option.IPv6Enable:
if len(ipv6) > 0 {
return ipv6, nil
return ipv6, 600, nil
}
}
return nil, dns.ErrEmptyResponse
return nil, 0, dns.ErrEmptyResponse
}
// New create a new dns.Client that queries localhost for DNS.

View File

@ -23,7 +23,7 @@ func (ctx *ResolvableContext) GetTargetIPs() []net.IP {
}
if domain := ctx.GetTargetDomain(); len(domain) != 0 {
ips, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{
ips, _, err := ctx.dnsClient.LookupIP(domain, dns.IPOption{
IPv4Enable: true,
IPv6Enable: true,
FakeEnable: false,

View File

@ -236,17 +236,18 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
var ips []net.IP
var err error
var ttl uint32 = 600
var ttl4 uint32
var ttl6 uint32
switch qType {
case dnsmessage.TypeA:
ips, err = h.client.LookupIP(domain, dns.IPOption{
ips, ttl4, err = h.client.LookupIP(domain, dns.IPOption{
IPv4Enable: true,
IPv6Enable: false,
FakeEnable: true,
})
case dnsmessage.TypeAAAA:
ips, err = h.client.LookupIP(domain, dns.IPOption{
ips, ttl6, err = h.client.LookupIP(domain, dns.IPOption{
IPv4Enable: false,
IPv6Enable: true,
FakeEnable: true,
@ -259,10 +260,6 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
return
}
if fkr0, ok := h.fdns.(dns.FakeDNSEngineRev0); ok && len(ips) > 0 && fkr0.IsIPInIPPool(net.IPAddress(ips[0])) {
ttl = 1
}
switch qType {
case dnsmessage.TypeA:
for i, ip := range ips {
@ -293,16 +290,17 @@ func (h *Handler) handleIPQuery(id uint16, qType dnsmessage.Type, domain string,
}))
common.Must(builder.StartAnswers())
rHeader := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl}
rHeader4 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl4}
rHeader6 := dnsmessage.ResourceHeader{Name: dnsmessage.MustNewName(domain), Class: dnsmessage.ClassINET, TTL: ttl6}
for _, ip := range ips {
if len(ip) == net.IPv4len {
var r dnsmessage.AResource
copy(r.A[:], ip)
common.Must(builder.AResource(rHeader, r))
common.Must(builder.AResource(rHeader4, r))
} else {
var r dnsmessage.AAAAResource
copy(r.AAAA[:], ip)
common.Must(builder.AAAAResource(rHeader, r))
common.Must(builder.AAAAResource(rHeader6, r))
}
}
msgBytes, err := builder.Finish()

View File

@ -71,13 +71,13 @@ func (h *Handler) policy() policy.Session {
}
func (h *Handler) resolveIP(ctx context.Context, domain string, localAddr net.Address) net.Address {
ips, err := h.dns.LookupIP(domain, dns.IPOption{
ips, _, err := h.dns.LookupIP(domain, dns.IPOption{
IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && h.config.preferIP4(),
IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && h.config.preferIP6(),
})
{ // Resolve fallback
if (len(ips) == 0 || err != nil) && h.config.hasFallback() && localAddr == nil {
ips, err = h.dns.LookupIP(domain, dns.IPOption{
ips, _, err = h.dns.LookupIP(domain, dns.IPOption{
IPv4Enable: h.config.fallbackIP4(),
IPv6Enable: h.config.fallbackIP6(),
})

View File

@ -54,7 +54,7 @@ func (n *netBind) ParseEndpoint(s string) (conn.Endpoint, error) {
addr := xnet.ParseAddress(ipStr)
if addr.Family() == xnet.AddressFamilyDomain {
ips, err := n.dns.LookupIP(addr.Domain(), n.dnsOption)
ips, _, err := n.dns.LookupIP(addr.Domain(), n.dnsOption)
if err != nil {
return nil, err
} else if len(ips) == 0 {

View File

@ -150,13 +150,13 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
// resolve dns
addr := destination.Address
if addr.Family().IsDomain() {
ips, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{
ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.preferIP4(),
IPv6Enable: h.hasIPv6 && h.conf.preferIP6(),
})
{ // Resolve fallback
if (len(ips) == 0 || err != nil) && h.conf.hasFallback() {
ips, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.fallbackIP4(),
IPv6Enable: h.hasIPv6 && h.conf.fallbackIP6(),
})
@ -284,13 +284,13 @@ func (h *Handler) createIPCRequest() string {
addr = net.ParseAddress(dialerIp.String())
errors.LogInfo(h.bind.ctx, "createIPCRequest use dialer dest ip: ", addr)
} else {
ips, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{
ips, _, err := h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.preferIP4(),
IPv6Enable: h.hasIPv6 && h.conf.preferIP6(),
})
{ // Resolve fallback
if (len(ips) == 0 || err != nil) && h.conf.hasFallback() {
ips, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
ips, _, err = h.dns.LookupIP(addr.Domain(), dns.IPOption{
IPv4Enable: h.hasIPv4 && h.conf.fallbackIP4(),
IPv6Enable: h.hasIPv6 && h.conf.fallbackIP6(),
})

View File

@ -50,12 +50,13 @@ func (mr *DNSClientMockRecorder) Close() *gomock.Call {
}
// LookupIP mocks base method
func (m *DNSClient) LookupIP(arg0 string, arg1 dns.IPOption) ([]net.IP, error) {
func (m *DNSClient) LookupIP(arg0 string, arg1 dns.IPOption) ([]net.IP, uint32, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LookupIP", arg0, arg1)
ret0, _ := ret[0].([]net.IP)
ret1, _ := ret[1].(error)
return ret0, ret1
ret1, _ := ret[1].(uint32)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// LookupIP indicates an expected call of LookupIP

View File

@ -90,13 +90,13 @@ func lookupIP(domain string, strategy DomainStrategy, localAddr net.Address) ([]
return nil, nil
}
ips, err := dnsClient.LookupIP(domain, dns.IPOption{
ips, _, err := dnsClient.LookupIP(domain, dns.IPOption{
IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && strategy.preferIP4(),
IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && strategy.preferIP6(),
})
{ // Resolve fallback
if (len(ips) == 0 || err != nil) && strategy.hasFallback() && localAddr == nil {
ips, err = dnsClient.LookupIP(domain, dns.IPOption{
ips, _, err = dnsClient.LookupIP(domain, dns.IPOption{
IPv4Enable: strategy.fallbackIP4(),
IPv6Enable: strategy.fallbackIP6(),
})