Skip to content

Querying single-label names over tailscale dns by appending the tailnet search domain #4006

@contrun

Description

@contrun

I want to query a single-label hostname (simple hostnames without any . in it, in the following example, I will use abc) over tailscale dns of singbox. But tailscale dns of sing-box will only resolve hostnames with the full tailnet-suffix.ts.net postfix. I.e. ping abc will fail, but ping abc.tailnet-suffix.ts.net will succeed.

I have been connecting to hosts only by the hostname over vanilla tailscale for a while. On both windows and linux (specifically, systemd-resolved described the mechinism to turn single label names to fully qualified domain names in systemd-resolved and VPNs), the default dns resolution mechinism will append the search domain while resolving single label names. Below is the output of resolvectl query --interface tailscale0 abc on a machine running tailscale daemon.

abc: 100.106.51.119                            -- link: tailscale0
     (abc.tailnet-suffix.ts.net)

-- Information acquired via protocol DNS in 3.7ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network

This also works for on windows machine. Resolve-DnsName -Name def -Server 100.100.100.100 outputs

Name                           Type   TTL   Section    NameHost
----                           ----   ---   -------    --------
def                            CNAME  60480 Answer     def.tailnet-suffix.ts.net.
                                      0

Name       : def.tailnet-suffix.ts.net.
QueryType  : A
TTL        : 604800
Section    : Answer
IP4Address : 100.104.216.55

We can see from resolvectl status tailscale0 that tailscale set the search domain to tailnet-suffix.ts.net, thus ping abc just works.

Link 5 (tailscale0)
    Current Scopes: DNS
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 100.100.100.100
       DNS Servers: 100.100.100.100
        DNS Domain: tailnet-suffix.ts.net ~0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa
                    ~100.100.in-addr.arpa ~101.100.in-addr.arpa ~102.100.in-addr.arpa
                    ~103.100.in-addr.arpa ~104.100.in-addr.arpa ~105.100.in-addr.arpa
                    ~106.100.in-addr.arpa ~107.100.in-addr.arpa ~108.100.in-addr.arpa
                    ~109.100.in-addr.arpa ~110.100.in-addr.arpa ~111.100.in-addr.arpa
                    ~112.100.in-addr.arpa ~113.100.in-addr.arpa ~114.100.in-addr.arpa
                    ~115.100.in-addr.arpa ~116.100.in-addr.arpa ~117.100.in-addr.arpa
                    ~118.100.in-addr.arpa ~119.100.in-addr.arpa ~120.100.in-addr.arpa
                    ~121.100.in-addr.arpa ~122.100.in-addr.arpa ~123.100.in-addr.arpa
                    ~124.100.in-addr.arpa ~125.100.in-addr.arpa ~126.100.in-addr.arpa
                    ~127.100.in-addr.arpa ~64.100.in-addr.arpa ~65.100.in-addr.arpa
                    ~66.100.in-addr.arpa ~67.100.in-addr.arpa ~68.100.in-addr.arpa
                    ~69.100.in-addr.arpa ~70.100.in-addr.arpa ~71.100.in-addr.arpa
                    ~72.100.in-addr.arpa ~73.100.in-addr.arpa ~74.100.in-addr.arpa
                    ~75.100.in-addr.arpa ~76.100.in-addr.arpa ~77.100.in-addr.arpa
                    ~78.100.in-addr.arpa ~79.100.in-addr.arpa ~80.100.in-addr.arpa
                    ~81.100.in-addr.arpa ~82.100.in-addr.arpa ~83.100.in-addr.arpa
                    ~84.100.in-addr.arpa ~85.100.in-addr.arpa ~86.100.in-addr.arpa
                    ~87.100.in-addr.arpa ~88.100.in-addr.arpa ~89.100.in-addr.arpa
                    ~90.100.in-addr.arpa ~91.100.in-addr.arpa ~92.100.in-addr.arpa
                    ~93.100.in-addr.arpa ~94.100.in-addr.arpa ~95.100.in-addr.arpa
                    ~96.100.in-addr.arpa ~97.100.in-addr.arpa ~98.100.in-addr.arpa
                    ~99.100.in-addr.arpa ~ts.net
     Default Route: no

But this is not the case for sing-box tailscale dns. I have abc: 'abc' not found while running resolvectl query --interface singtun abc. If I append the domain suffix to the hostname resolvectl query --interface singtun abc.tailnet-suffix.ts.net, then everything works.

abc.tailnet-suffix.ts.net: 100.106.51.119       -- link: singtun

-- Information acquired via protocol DNS in 2.8ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network

I searched the code base and found out the relevant code to handle dns query over tailscale here

for domainSuffix, transports := range t.routes {
if strings.HasSuffix(question.Name, domainSuffix) {
if len(transports) == 0 {
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeNameError,
Response: true,
},
Question: []mDNS.Question{question},
}, nil
}
var lastErr error
for _, dnsTransport := range transports {
response, err := dnsTransport.Exchange(ctx, message)
if err != nil {
lastErr = err
continue
}
return response, nil
}
return nil, lastErr
}
.

Why do you think appending the search domain obtained from tailscale control plane while resolving single label names in the code above? This would match the behavior of tailscale daemon's current behavior and make tailscale dns much more usable.

Here is my sing-box configuration.

{
  "dns": {
    "client_subnet": "223.5.5.0/24",
    "final": "foreign",
    "independent_cache": true,
    "reverse_mapping": true,
    "rules": [
      {
        "domain_regex": "(.*ts.net$|^[^.]+$)",
        "ip_cidr": "100.64.0.0/10",
        "server": "ts-dns"
      },
      {
        "clash_mode": "direct",
        "server": "domestic"
      },
      {
        "clash_mode": "global",
        "server": "fakeip"
      },
      {
        "rule_set": "geosite-cn",
        "server": "domestic"
      },
      {
        "action": "reject",
        "query_type": "HTTPS"
      },
      {
        "query_type": [
          "A",
          "AAAA"
        ],
        "rewrite_ttl": 1,
        "server": "fakeip"
      }
    ],
    "servers": [
      {
        "accept_default_resolvers": false,
        "endpoint": "ts-endpoint",
        "tag": "ts-dns",
        "type": "tailscale"
      },
      {
        "server": "223.6.6.6",
        "tag": "domestic",
        "type": "https"
      },
      {
        "detour": "auto",
        "server": "8.8.8.8",
        "tag": "foreign",
        "type": "https"
      },
      {
        "inet4_range": "198.18.0.0/15",
        "inet6_range": "fc00::/18",
        "tag": "fakeip",
        "type": "fakeip"
      }
    ],
    "strategy": "ipv4_only"
  },
  "endpoints": [
    {
      "accept_routes": true,
      "ephemeral": false,
      "exit_node_allow_lan_access": true,
      "hostname": "test",
      "tag": "ts-endpoint",
      "type": "tailscale"
    }
  ],
  "experimental": {
    "clash_api": {
      "external_controller": "127.0.0.1:9090",
      "external_ui": "ui",
      "external_ui_download_detour": "auto"
    }
  },
  "inbounds": [
    {
      "listen": "::",
      "listen_port": 7890,
      "set_system_proxy": false,
      "type": "mixed"
    },
    {
      "address": [
        "172.19.0.1/30",
        "fdfe:dcba:9876::1/126"
      ],
      "auto_redirect": true,
      "auto_route": true,
      "exclude_interface": [],
      "interface_name": "singtun",
      "mtu": 9000,
      "platform": {
        "http_proxy": {
          "enabled": true,
          "server": "127.0.0.1",
          "server_port": 7890
        }
      },
      "route_address": [
        "0.0.0.0/1",
        "128.0.0.0/1",
        "::/1",
        "8000::/1"
      ],
      "route_exclude_address": [],
      "stack": "mixed",
      "tag": "tun-in",
      "type": "tun"
    }
  ],
  "log": {
    "level": "debug"
  },
  "outbounds": [
    {
      "tag": "direct",
      "type": "direct"
    },
    {
      "method": "chacha20-ietf-poly1305",
      "password": "password",
      "server": "ssserver.example.com",
      "server_port": 443,
      "tag": "shadowsocks_ssserver",
      "type": "shadowsocks"
    },
    {
      "outbounds": [
        "shadowsocks_ssserver"
      ],
      "tag": "auto",
      "type": "urltest"
    },
    {
      "idle_timeout": "2d",
      "interval": "1d",
      "outbounds": [
        "direct",
        "shadowsocks_ssserver"
      ],
      "tag": "bootstrap-auto",
      "type": "urltest",
      "url": "https://gh-proxy.com/github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo-lite/geoip/apple.srs"
    },
    {
      "default": "shadowsocks_ssserver",
      "outbounds": [
        "shadowsocks_ssserver",
        "auto"
      ],
      "tag": "proxy",
      "type": "selector"
    },
    {
      "default": "proxy",
      "outbounds": [
        "proxy",
        "auto",
        "direct",
        "shadowsocks_ssserver"
      ],
      "tag": "final",
      "type": "selector"
    }
  ],
  "route": {
    "auto_detect_interface": true,
    "default_domain_resolver": {
      "server": "domestic"
    },
    "final": "final",
    "rule_set": [
      {
        "download_detour": "bootstrap-auto",
        "format": "binary",
        "tag": "geosite-ads",
        "type": "remote",
        "url": "https://gh-proxy.com/github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/category-ads.srs"
      },
      {
        "download_detour": "bootstrap-auto",
        "format": "binary",
        "tag": "geoip-cn",
        "type": "remote",
        "url": "https://gh-proxy.com/github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geoip/cn.srs"
      },
      {
        "download_detour": "bootstrap-auto",
        "format": "binary",
        "tag": "geosite-cn",
        "type": "remote",
        "url": "https://gh-proxy.com/github.com/MetaCubeX/meta-rules-dat/raw/refs/heads/sing/geo/geosite/cn.srs"
      }
    ],
    "rules": [
      {
        "action": "sniff",
        "sniffer": [
          "http",
          "tls",
          "quic",
          "dns"
        ]
      },
      {
        "action": "hijack-dns",
        "protocol": "dns"
      },
      {
        "action": "hijack-dns",
        "port": 53
      },
      {
        "ip_cidr": "100.64.0.0/10",
        "outbound": "ts-endpoint"
      },
      {
        "ip_is_private": true,
        "outbound": "direct"
      },
      {
        "outbound": "direct",
        "rule_set": "geoip-cn"
      },
      {
        "outbound": "direct",
        "rule_set": "geosite-cn"
      },
      {
        "action": "reject",
        "rule_set": "geosite-ads"
      },
      {
        "action": "resolve"
      }
    ]
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions