What We’re Testing
MagicDNS hostname resolution works at two levels: (1) the CLI ztna dns query command which calls the backend’s resolve action over HTTPS, and (2) the local DNS resolver (pkg/dns/resolver.go) which intercepts system DNS queries on 127.0.0.53:53 and resolves tailnet hostnames from the in-memory peer list.
Key facts from source code:
- Backend resolve action (
dns-management.tslines 92-138): Three-stage lookup — FQDN match (hostname.{slug}.zt.net), short hostname match (first label, case-insensitive againstmachines.name), then customdns_recordstable - Local resolver (
resolver.go): Maintains an in-memoryrecordsmap (hostname -> tailnet IP). Updated viaUpdateRecords()when the peer list changes. Resolution order: exact match, then strip search domain suffix and retry (resolver.golines 257-277) - Search domain: Default
"ztna". The resolver treats queries ending in.{searchDomain}as tailnet queries (isTailnetQuery, line 360). Bare hostnames that exist in the records map also match. - DNS record TTL: 60 seconds (constant
defaultTTLinresolver.goline 38) - AAAA queries: Return empty response with NOERROR (no IPv6 tailnet IPs yet,
resolver.goline 386) - NXDOMAIN: Returned when a tailnet hostname is not found in the local records
Your Test Setup
| Machine | Role |
|---|---|
| ⊞ Win-A | Peer machine — will be resolved by name |
| ⊞ Win-B | Peer machine — will be resolved by name |
| 🐧 Linux-C | Resolver machine — runs DNS queries |
All three machines must be connected (ztna up) and MagicDNS must be enabled (see Chapter 36).
ST1 — Resolve Peer by Short Hostname
What it verifies: ztna dns query resolves a bare machine name to its tailnet IP via the backend.
Steps:
- On 🐧 Linux-C , confirm all machines are online:
ztna peers
You should see ⊞ Win-A and ⊞ Win-B in the peer list with their tailnet IPs.
- Resolve ⊞ Win-A by short name:
ztna dns query Win-A
Expected output:
Win-A.yourorg.zt.net -> 100.64.0.1
The CLI sends action: "resolve" with hostname: "Win-A" to /api/dns-management. The backend performs a case-insensitive match against machines.name and returns the tailnet IP with source: "machine_auto".
- Resolve ⊞ Win-B :
ztna dns query Win-B
Expected: Similar output with Win-B’s tailnet IP.
Pass: Both peers resolve to their correct tailnet IPs.
Fail / Common issues:
(not found)— the machine name must match exactly (case-insensitive). Checkztna peersfor the actual registered names.- The CLI first tries the hostname as-is. If no result and the hostname has no dots, it retries with the search domain appended (
Win-A.ztna). Both attempts go to the backend.
ST2 — Resolve Peer by FQDN
What it verifies: Resolution works when using the fully qualified tailnet domain name.
Steps:
- First, find your org’s DNS domain:
TOKEN="YOUR_ACCESS_TOKEN"
curl -s -X POST "https://login.quickztna.com/api/dns-management" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action":"get_settings","org_id":"YOUR_ORG_ID"}' | python3 -c "
import sys, json
print(json.load(sys.stdin)['data']['domain'])
"
This returns something like yourorg.zt.net.
- On 🐧 Linux-C , resolve using the full domain:
ztna dns query Win-A.yourorg.zt.net
Expected output:
Win-A.yourorg.zt.net -> 100.64.0.1
The backend checks if the hostname ends with .{slug}.zt.net, strips the suffix to get the machine name, and performs a case-insensitive lookup in the machines table (dns-management.ts lines 100-110).
Pass: FQDN resolves to the correct tailnet IP with source: "machine_fqdn" in the API response.
Fail / Common issues:
(not found)with FQDN — verify the domain matches your org’s slug exactly. The domain is{slug}.zt.netwhere slug comes from theorganizations.slugcolumn.- If the org has no slug, the domain defaults to
ztna.zt.net.
ST3 — System-Level DNS Resolution (Local Resolver)
What it verifies: When the local DNS resolver is running, system tools like nslookup and ping can resolve tailnet hostnames.
Steps:
-
On 🐧 Linux-C , check if the local DNS resolver is active. When
ztna upstarts with MagicDNS enabled, the resolver binds to127.0.0.53:53(or127.0.0.53:15353if port 53 is unavailable). -
Try resolving via the local resolver directly:
nslookup Win-A 127.0.0.53
If the resolver is on port 15353 (port 53 was busy):
nslookup -port=15353 Win-A 127.0.0.53
Expected output:
Server: 127.0.0.53
Address: 127.0.0.53#53
Name: Win-A
Address: 100.64.0.1
- On a Linux system with systemd-resolved configured (the DNS manager uses
resolvectlto set per-interface DNS onztna0), you can also try:
resolvectl query Win-A.ztna
- On ⊞ Win-A (Windows), the DNS manager uses NRPT (Name Resolution Policy Table) rules via PowerShell. Check if the rule exists:
Get-DnsClientNrptRule | Where-Object { $_.Comment -eq "QuickZTNA MagicDNS" }
Expected: An NRPT rule routing .ztna (or your search domain) queries to 127.0.0.53.
Pass: System DNS tools resolve tailnet hostnames to the correct IPs. The platform-specific DNS manager (NRPT on Windows, systemd-resolved or resolv.conf on Linux) routes tailnet queries to the local resolver.
Fail / Common issues:
SERVFAILor timeout — the local resolver may not be running. Check ifztna upis active and MagicDNS is enabled in the client config.- Port 53 conflict on Linux — another DNS service (e.g., systemd-resolved’s stub resolver on
127.0.0.53:53) may occupy the port. The resolver falls back to port 15353, but system DNS may not know about the high port unless the DNS manager configured it. - NRPT rule missing on Windows — the PowerShell command requires admin privileges. If
ztna upran without elevation, the NRPT rule creation may have failed silently.
ST4 — Case-Insensitive Resolution
What it verifies: Hostname resolution is case-insensitive at both the backend and local resolver levels.
Steps:
- On 🐧 Linux-C , try different casings via the CLI:
ztna dns query win-a
ztna dns query WIN-A
ztna dns query Win-a
Expected: All three resolve to the same tailnet IP. The backend uses LOWER(name) = LOWER(?) in the SQL query (dns-management.ts lines 104, 115). The local resolver lowercases the hostname before map lookup (resolver.go line 261).
- If the local resolver is active, test via nslookup:
nslookup win-a 127.0.0.53
nslookup WIN-A 127.0.0.53
Pass: All case variations resolve to the same IP. DNS is case-insensitive per RFC 4343.
Fail / Common issues:
- Different results for different casings would indicate a bug in the lowercase normalization. This should not happen with the current implementation.
ST5 — Resolution of Offline vs Online Machines
What it verifies: MagicDNS records include both online and offline machines (but not pending or quarantined).
Steps:
- On ⊞ Win-B , disconnect from the VPN:
ztna down
-
Wait 30 seconds for the heartbeat to expire and the machine to transition to
offlinestatus. -
On 🐧 Linux-C , try to resolve ⊞ Win-B :
ztna dns query Win-B
Expected: Win-B still resolves to its tailnet IP. The backend query filters machines with status IN ('online', 'offline') (dns-management.ts line 29), so offline machines retain their DNS records.
- Verify via the API that the DNS records include both machines:
curl -s -X POST "https://login.quickztna.com/api/dns-management" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"action":"get_settings","org_id":"YOUR_ORG_ID"}' | python3 -c "
import sys, json
records = json.load(sys.stdin)['data']['dns_records']
for r in records:
print(f\"{r['name']} -> {r['value']}\")
"
Expected: Both Win-A and Win-B appear in the records. Win-B’s record persists even though it is offline.
- Note: the local resolver’s in-memory records (
resolver.go) are updated from the peer list, which may only contain currently reachable peers. The CLIztna dns queryalways goes to the backend, which includes offline machines. This is a key difference between the two resolution paths.
Pass: Offline machines are resolvable via ztna dns query (backend). Local resolver may or may not include offline peers depending on peer list freshness.
Cleanup: Reconnect ⊞ Win-B :
ztna up
Summary
| Sub-test | What it proves | Pass condition |
|---|---|---|
| ST1 | Short hostname resolution | ztna dns query Win-A returns correct tailnet IP |
| ST2 | FQDN resolution | Win-A.{slug}.zt.net resolves via the FQDN lookup path |
| ST3 | System-level DNS | nslookup / resolvectl resolves tailnet names via local resolver |
| ST4 | Case-insensitive matching | win-a, WIN-A, Win-a all resolve identically |
| ST5 | Offline machine resolution | Offline machines retain DNS records at the backend level |