T3 Connect reports "does not resolve" for up to 30 minutes while the hostname resolves publicly, because the local DNS cache stores a not-found answer#27250

opened15d agobySsimonsteiner

What happened

On the Windows desktop app with the WSL-only backend, Settings > Sharing & access showed:

T3 Connect is on, but other devices cannot reach this environment. The T3 Connect address does not resolve from this machine. Fix: Check the DNS settings on this machine or its VPN.

Settings > Sharing & access showing the T3 Connect error

t3 connect status on the WSL backend gave the same error. About 30 minutes later the error went away on its own, with no change on the machine or the router: ✓ Other devices can reach this machine · Tunnel connected · 4 connections · QUIC.

The tunnel was fine the whole time. The LAN router was still answering with a cached "this name doesn't exist" reply for the tunnel hostname. The check reported that as a hard DNS failure on this machine, so it pointed at the wrong thing to fix.

Diagnosis

The hostname resolved publicly the whole time. Only the local resolver chain answered NXDOMAIN:

Resolverprod-<id>.t3coderelay.com
WSL (10.255.255.254, forwards to Windows)NXDOMAIN
Windows DNS client cacheNotExist entry
LAN router 192.168.1.1NXDOMAIN
Tailscale 100.100.100.100 (forwards to the router)NXDOMAIN
1.1.1.1, 8.8.8.8A records for Cloudflare's proxy

The router's NXDOMAIN came with the t3coderelay.com SOA record, and that SOA serial (2418799786) was older than the one Cloudflare was serving at the time (2418801057). So it was a cached negative answer. Its TTL counted down from the zone's 1800 s SOA minimum: 92 s left at 00:16:35, which means it was cached at 23:48:07. It expired at 00:18:07. The next lookup returned A records, and the reachability check passed.

So a lookup of the hostname made while the record did not exist yet, or not everywhere yet, got cached for 30 minutes. That fits a brand-new tunnel being looked up within seconds of setup. The relay creates the CNAME before it returns the hostname (infra/relay/src/environments/ManagedEndpointProvider.ts), but Cloudflare can take a few seconds to publish a new record. I could not prove what made the 23:48:07 lookup or when this environment's tunnel was set up, because server.trace.ndjson had already rotated past that time. The WSL backend is its own environment with its own tunnel, so switching to WSL only around then would fit, but that is unconfirmed.

The bigger problem is that classifyReachability in apps/server/src/cloud/ConnectReachability.ts maps every ENOTFOUND/EAI_AGAIN to the same dns_failed cause. It has no way to tell these cases apart:

  1. The record is missing on Cloudflare. This is a relay-side problem and does not fix itself, as in #25628.
  2. This network's resolver holds a stale negative answer or filters the name. This is local and usually clears within 30 minutes.

Both show "Check the DNS settings on this machine or its VPN". For case 2 that sends people to restart routers and change VPN settings for something that fixes itself. For case 1 it hides that the problem is on the relay side.

Suggested fix: when the local lookup fails, query a public resolver directly, for example DoH to 1.1.1.1. Report "resolves publicly, but this network's DNS still has an old answer, this usually clears within 30 minutes" separately from "the T3 Connect hostname is missing from public DNS (relay problem)". The 5-minute recheck already handles recovery.

A docs gap also showed up: docs/user/t3-connect-troubleshooting.md says to run t3 connect status on the host. With the WSL backend there is no t3 on the WSL PATH. The working binary is the WSL runtime's ~/.t3/wsl-runtime/sha256-<hash>/t3 connect status.

Steps to reproduce

Not reproduced on purpose. The likely recipe:

  1. Use a network whose resolver caches negative answers. Most home routers do, through dnsmasq or similar.
  2. Make a new T3 Connect tunnel for an environment, for example by enabling WSL only on Windows desktop, or by linking a new machine.
  3. If the new hostname is looked up before Cloudflare publishes the record, the check reports dns_failed with the "check your DNS/VPN" fix for up to 30 minutes, while dig <host> @1.1.1.1 resolves.

The misleading message can be reproduced on its own: get any ENOTFOUND on a hostname that resolves publicly, for example with a local /etc/hosts or resolver override. The check reports the same dns_failed text it shows when the record is missing everywhere.

Version

0.1.6-nightly.20261029.3892 (Windows desktop, WSL backend)

Environment

Windows 11 desktop app with the WSL-only backend (Ubuntu, WSL2 kernel 6.18.54.1, DNS tunneling to Windows), managed cloudflared 2026.10.4, home router DNS at 192.168.1.1, Tailscale on the host.

Evidence

shell
1# While failing (00:16:35): cached negative answer, old SOA serial, TTL counting down from 1800
2$ dig prod-<id>.t3coderelay.com @192.168.1.1
3;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN
4;; AUTHORITY SECTION:
5t3coderelay.com. 92 IN SOA rosemary.ns.cloudflare.com. dns.cloudflare.com. 2418799786 10000 2400 604800 1800
6
7# Same moment, public resolver
8$ dig prod-<id>.t3coderelay.com @1.1.1.1 +short
9104.26.3.228
10104.26.2.228
11172.67.73.71
12
13$ t3 connect status
14✕ T3 Connect is NOT reachable from other devices
15 The T3 Connect address does not resolve from this machine.
16 Fix: Check the DNS settings on this machine or its VPN.
17
18# After the cached entry expired (00:18:50), no other change
19$ dig prod-<id>.t3coderelay.com @192.168.1.1 +short
20104.26.3.228 ...
21$ t3 connect status
22✓ Other devices can reach this machine · Tunnel connected · 4 connections · QUIC

#25628 shows the same dns_failed message, but there the record is NXDOMAIN on 1.1.1.1 and 8.8.8.8 too, so it is relay-side and does not clear. Here the record existed publicly and only the local cache was stale. They are different root causes, but the fix above would have told both cases apart.

Fix applied or workaround

None needed. The router's cached negative answer expired after 30 minutes. If you can't wait, flush the stale entry: ipconfig /flushdns on Windows plus a router DNS restart, or point the machine at a public resolver.

Filed by

Claude Opus 5.5 (claude-opus-5-5) via Claude Code in T3 Code, following .github/triage/PLAYBOOK.md

Synced from GitHub

Same bug on Linux (Omarchy, systemd-resolved stub → Cloudflare DoT, Tailscale split DNS for *.ts.net only). Version 0.1.6-nightly.20261030.3904, managed cloudflared 2026.10.4.

Timeline from server.trace.ndjson (local time, +07):

TimeConnectReachability.check
12:24:04✓ Tunnel connected · 4 connections · QUIC
12:27:57✕ no_connector
12:32:58✕ dns_failed
12:39managed cloudflared restarted
12:39:34+passes

CloudManagedEndpointRuntime.applyConfig ran at 12:23:56 and 12:39:21, so the tunnel was re-provisioned in this window.

While failing:

Resolverprod-<id>.t3coderelay.com
systemd-resolved, cachedNXDOMAIN
resolvectl query --cache=noA records
DoH 1.1.1.1 / dns.googleA records

This shows that a router cache isn't required. Any caching stub resolver (systemd-resolved here) reproduces it, so the public-resolver cross-check suggested above would catch this case too. resolvectl flush-caches clears it immediately.

Synced from GitHub

Got the same issue just now. I tried to setup my first machine and it resolved a prod-<id>.t3coderelay.com host before it become available, so my router cached NX response.

Synced from GitHub

Same on macOS, and it also blocks the client, not just the host's own check. On Nov 2 I added a Raspberry Pi 5 (Debian 13 arm64, headless) from a Mac (macOS 27.2, a 0.1.7 nightly) right after linking the Pi to T3 Connect. The resolvers on both machines answered "hostname does not exist" for the new prod-<id>.t3coderelay.com name, while Cloudflare's resolver returned it. The Mac couldn't connect over T3 Connect, and I fell back to the Pi's Tailscale HTTPS address. So a negative answer cached during setup also breaks the first connection from another device, and a public-resolver check would be useful on the connecting side as well.

Synced from GitHub

One data point for the suggested public-resolver cross-check: on some networks it needs DoH. A plain UDP query to 1.1.1.1 is not enough.

My LAN gateway redirects all outbound UDP/53 to two AdGuard Home resolvers. A new host's prod-<id>.t3coderelay.com got a cached NXDOMAIN (old SOA serial 2419412029, Cloudflare was serving 2419412342). Because of the redirect, plain queries to 1.1.1.1, 9.9.9.9 and even rosemary.ns.cloudflare.com returned that same cached NXDOMAIN. Only DoH showed the real A records:

shell
1$ dig @198.51.100.1 t3coderelay.com SOA # TEST-NET, nothing listens there
2status: NOERROR # port 53 is intercepted
3$ dig @rosemary.ns.cloudflare.com prod-<id>.t3coderelay.com A
4status: NXDOMAIN # answered by the LAN resolver, not Cloudflare
5$ curl -s -H 'accept: application/dns-json' \
6 'https://cloudflare-dns.com/dns-query?name=prod-<id>.t3coderelay.com&type=A'
7"Status":0 ... "data":"104.26.2.228" ...

So a UDP cross-check would have reported "missing from public DNS", which is the #25628 case, and pointed at the relay. It also misled me: I first diagnosed this as #25628. The negative entry expired after the 1800 s SOA minimum, and t3 connect status passed with no change on the host.

Server 0.1.7 (background service, Ubuntu 24.04, managed cloudflared 2026.10.4).


Investigated and written by Claude Code (Claude Opus 5.5) with the user, who reviewed this text before posting.

Synced from GitHub

Additional real client-side reproduction on stable 0.1.7 (6 November 2026): macOS desktop client connecting to a native Windows 11 T3 host after renewing its T3 Connect link to work around #15683.

The host reported linked: true, revoked: false, and reachability.status: reachable, with four QUIC tunnel connections. The macOS client reported:

This device can't reach [host]'s tunnel

Its diagnostic identified a transport failure fetching https://prod-<redacted>.t3coderelay.com/.well-known/t3/environment.

Contemporaneous checks:

  • macOS system getaddrinfo and ordinary HTTPS failed to resolve the hostname.
  • Tailscale's 100.100.100.100 resolver and the university recursive resolvers returned NXDOMAIN with a t3coderelay.com SOA negative TTL counting down from approximately 1800 seconds.
  • Direct queries to Cloudflare's 1.1.1.1 returned valid A and AAAA records.
  • HTTPS using curl's --resolve against a publicly resolved address returned the expected descriptor with the correct environment ID and new label, retaining normal TLS hostname/certificate verification.
  • Changing only Wi-Fi DNS and flushing macOS caches did not restore resolution: Tailscale's higher-priority resolver still supplied the negative answer.
  • A macOS /etc/resolver/t3coderelay.com override to 1.1.1.1 / 1.0.0.1, followed by flushing caches, restored ordinary HTTPS and a successful T3 environment API read.

We observed the negative cache after relinking, but did not instrument the exact first lookup or prove which request originally populated it. This is evidence for the existing stale-negative-DNS case, not proof that all tunnel failures are local DNS problems. No current-main or latest-nightly reproduction is claimed.

PR #27770 improves the diagnostic wording; this case also illustrates that the connecting client's resolver can be the failing boundary while the host reports its tunnel healthy.

Synced from GitHub

Cause

The relay creates the DNS record before it gives the host its tunnel token (ensureDnsRecord runs before getToken in infra/relay/src/environments/ManagedEndpointProvider.ts). But Cloudflare's nameservers do not answer for a new record for a few seconds. The host's first reachability check runs about 2 seconds after cloudflared starts, inside that gap. The local resolver gets NXDOMAIN and caches it for the zone's SOA minimum, 1800 seconds. More checks from the host cannot fix this.

dig prod-doesnotexist12345.t3coderelay.com returns NXDOMAIN with t3coderelay.com. 1800 IN SOA ... 1800.

Possible fixes

  1. Lower the zone's SOA Minimum TTL to 60 seconds. No code change. A cached NXDOMAIN then expires after about 1 minute. Cloudflare allows this only on Enterprise.
  2. Add a proxied wildcard record, *.t3coderelay.com. All plans support it. Before the new record is live, a lookup returns Cloudflare edge addresses instead of NXDOMAIN, so no negative answer is cached on the host, on clients, or at public resolvers. In DNS, an exact record always wins over the wildcard.
    • Point the wildcard at a small origin that answers fast with a marker, for example 404 plus T3-Endpoint: not-provisioned. A Worker route on *.t3coderelay.com/* would run in front of live tunnels, and Worker custom domains cannot be wildcards. A parking tunnel with no connector returns 530/1033, which looks the same as a dead connector.
    • ConnectReachability and client diagnostics need a new "endpoint not ready yet" cause for that marker.
    • Preview stages share the zone and use single-label hostnames, so the wildcard covers them. The preview sweep and record lookups match exact names, so they never touch *.
    • An unlinked hostname would answer over HTTP instead of failing DNS. The reaper uses the Cloudflare API, so it is not affected.
    • Not verified: whether the edge can start routing a new record later than DNS publishes it.

The relay could wait until the authoritative nameservers answer for the new record before it returns the token. But Workers cannot open TCP sockets to Cloudflare IP ranges, and some networks intercept port 53, so this is probably not reliable.

Claude Opus 5.5 in CloudCode did this investigation with the user, with independent checks by Claude and Gemini reviewers. The user reviewed this text before posting.

Synced from GitHub

Same bug on the latest nightly, 0.1.8-nightly.20261108.4380 (Linux x64, Omarchy, background service, managed cloudflared 2026.10.4). I ran t3 connect on a machine that had no tunnel. Sign-in worked, then the CLI printed "T3 Connect is NOT reachable from other devices. The T3 Connect address does not resolve from this machine."

Timeline from relay-client.ndjson and server.trace.ndjson (UTC):

TimeEvent
23:07:37.998Relay client process starts
23:07:39.336First tunnel connection registers (4 total, QUIC)
23:07:41.264First reachability probe, getaddrinfo ENOTFOUND
23:07:44 onwardProbes repeat about every 5 s while the CLI waits, all ENOTFOUND

Resolver answers at 23:10 UTC:

Resolverprod-<id>.t3coderelay.com A
LAN router 192.168.18.1NXDOMAIN, no SOA in the authority section
systemd-resolved stub, --cache=noNXDOMAIN (forwards to the router)
1.1.1.1, 8.8.8.8 over UDP3 Cloudflare A records
Cloudflare and Google DoH3 Cloudflare A records

The router serves cached negative answers without the SOA record. A first lookup of a made-up doesnotexist-….t3coderelay.com returned NXDOMAIN with the 1800 s SOA, and repeating it returned NXDOMAIN with no SOA. The relay hostname answer had the no-SOA form, so it came from the router's negative cache, populated by the probe at 23:07:41. This means checking for the SOA serial, as earlier comments did, won't work on every router. Some resolvers drop the SOA from cached negatives.

Workaround that fixed it in place without waiting 30 minutes or touching the router:

shell
1resolvectl dns <uplink-interface> 1.1.1.1 8.8.8.8
2resolvectl flush-caches
3t3 connect status # ✓ Other devices can reach this machine · Tunnel connected · 4 connections · QUIC
4# after the router's cache expires:
5resolvectl dns <uplink-interface> <router-ip> <previous-secondary>

This only fixes the host. Other devices that use the same router still get NXDOMAIN until the router's entry expires, which matches the client-side reports above.

Diagnosed with t3 triage by Claude Code (Claude Opus 5.5).

Synced from GitHub

Same bug on stable 0.1.7 (Linux arm64, Rock 5C, background service, managed cloudflared 2026.10.4, first-time t3 connect link). This adds two things: the stale NXDOMAIN was cached in Google Public DNS itself, not only the LAN router, and on this machine the failure is reported as a timeout instead of a DNS failure.

Timeline (UTC, from relay-client.ndjson / server.trace.ndjson)

TimeEvent
09:19:43.476Relay client starts
09:19:44.005First tunnel connection registers (4 total, QUIC, tpe01/khh01)
09:19:44.032First ConnectReachability.check probe, ~27 ms later, fails

Resolver state

The LAN router (192.168.1.1, forwarding to Google DNS) returned NXDOMAIN with the t3coderelay.com SOA. Its negative TTL counted down normally: 1729 s at 09:28:03, then 1559 s at 09:30:53.

12 direct queries to 8.8.8.8 / 8.8.4.4 at 09:30:53 returned:

  • 9× A records
  • 3× NXDOMAIN, SOA TTL ≈1130 s remaining

1800 − 1130 = 670 s before 09:30:53 puts the stale entry at ≈09:19:43, the same second as the first probe. So the host's own early probe can store the NXDOMAIN in some of Google's shared cache pools. Every device using Google DNS, or a router forwarding to it, can then fail to resolve the new hostname for up to 30 min. This isn't limited to the host's local cache. It fits the client-side failures reported above.

1.1.1.1 (UDP and DoH) returned A records. With DNS bypassed (curl --resolve <host>:443:<cloudflare-ip>), /.well-known/t3/environment returned this server (serverVersion: 0.1.7), so the tunnel and relay were fine the whole time.

Misleading diagnosis on 0.1.7

t3 connect status and boot-service.log reported:

1✕ T3 Connect is NOT reachable from other devices
2 The T3 Connect address did not answer from this machine.
3 Fix: Check the network, VPN, or proxy on this machine.
4 cause: timeout

The underlying error in the trace is a DNS lookup failure, not a timeout. Each probe fails in 8–25 ms:

1HttpClientError: Transport error (GET https://prod-<id>.t3coderelay.com/.well-known/t3/environment)
2 at ConnectReachability.check
3 [cause]: TypeError: fetch failed
4 [cause]: Error: getaddrinfo EBUSY prod-<id>.t3coderelay.com

Unlike ENOTFOUND in the earlier reports, glibc/systemd-resolved surfaced the NXDOMAIN here as EBUSY. The classifier then maps it to timeout instead of dns_failed, so the message points at network/VPN/proxy rather than DNS. Classifying any getaddrinfo error code as dns_failed would avoid this.

Workarounds: wait for the 1800 s negative TTL to expire, or temporarily point the host at a resolver that hasn't cached the answer (resolvectl dns <iface> 1.1.1.1).


Investigated with t3 triage by Claude Code (Claude Opus 5.5).