With the DNS ALG on, an app that dials a synthetic address after its mapping has aged out has the
flow dropped in onFlow — before listener.Flow — so the connection dies with no conn-id, no log
row, no summary and no rule. The app retries the same cached address and fails identically until
something makes it re-resolve. In the field this reads as "apps intermittently cannot connect, and
turning the VPN off fixes it instantly", with nothing in the app's own logs to explain it.
Observed on the engine pinned by rethink-app v0.5.6 (61894b7f); the code paths below are
unchanged on n2 HEAD.
What it looks like
alg: ptr: in nat? (natdoms? false / doms? false) for 100.74.49.249[NoTransport@10262] => in ptr? (useptr? false / gotalive? false)? ([])
alg: dns64: for [][10262] (didnotAlg? false / fresh? false / undidAlg? true / undidPtr? false / staleok? false)
maybealgip(100.74.49.249) => realips([]) => unnated([]); until: -16m36.558796184s
com: tcp: onFlow: alg, preflow? true, ips? false for ; pre: &{10262 false}; block!
Note for ; — the domain list is empty, which is what disarms the recovery path.
Why the state is reachable at all
registerLocked gives the nat entry and its contents different lifetimes:
ttl = max(ttl8s, ttl)
algttl := max(ttl2m, ttl)
ansttl := now.Add(algttl) // baseans.ttl — alg ip -> answer/domain
xipsttl := now.Add(ttl) // xips/xdomains — alg ip -> real ips
xipsttl <= ansttl always, and nothing ever deletes from t.nat (it grows for the tunnel's
lifetime), so undidAlg — which xLocked documents as "really 'hasAnyAlgEntry'" — stays true
long after the ips and domains inside the entry are gone. 02d568d8 ("min alg ttl at 2m") is what
opened this window: with the previous min(ttl2m, ttl) the nat entry expired first, undidAlg came
back false, and the flow was simply not treated as ALG.
Why the recovery does not fire
xLocked states the contract explicitly:
// when realips is empty but one of undidAlg / undidPtr is not false,
// it means the client code may retry re-resolving the corresponding
// domain to freshen up alg mapping
onFlow does try — but only over doms, which expired together with the ips. Two other sources are
present and unused:
pdoms is computed and thrown away. undoAlg fills probableDomains from forced PTR and
from dialers.Ptr(maybeAlg), and the re-resolve loop never looks at it.
- The 24h stale reserve is unreachable in ALG mode.
47e8d300 deliberately retains expired ips
and domains for staleXipsThres / staleDomainThres, but X gates them behind
usestale := !t.mod.Load() — i.e. only while translation is off — and ptrLocked asks
xalive on a t.nat hit, while its forced variant reads t.ptr, which is keyed by real ips and
so can never answer for an alg ip. A mapping two minutes past its ttl is therefore as good as
deleted, in precisely the mode the reserve was built for.
Why it is invisible
The branch returns at return fm, undidAlg, "", "", which is before h.listener.Flow(...). The
listener never sees the flow, so nothing is logged and no rule applies; Mark.CID is empty, so
processSummaries drops the summary too. TCP gets a RST after the 0–30 s stall, UDP is dropped
silently. From the user's side the app fails and the app that filters it shows no trace.
Suggested fix
The smallest correct change is to use the domains already in hand before giving up:
-for d := range strings.SplitSeq(doms, ",") {
- ...
-}
+for _, domcsv := range []string{doms, pdoms} {
+ for d := range strings.SplitSeq(domcsv, ",") {
+ ...
+ if hasNewIPs {
+ ips = dnsx.Netip2Csv(newips)
+ doms = d // Flow binds domain rules to doms; pdoms it only logs
+ break
+ }
+ }
+}
Two things worth doing alongside it: exposing the past maps to this path (a stale-tolerant X /
PTR) so the 24h reserve is usable while the ALG is on, and not returning before listener.Flow
when nothing can be recovered — a flow the listener has seen is at least attributed, logged and
governed by the user's rules, whereas the early return is unattributed filtering.
Whatever is done here must keep unspecified ips (0.0.0.0 / ::) flowing through as a usable
answer: that is how an rdns verdict reaches the listener, so filtering them out of a re-resolution
would turn a blocklisted domain into an allowed connection.
Field reports carrying the same signature, undiagnosed
Verified fix
We run a patched engine along these lines. Against a mapping seven minutes dead, a dial that
previously failed returns 200, while a domain carrying a block rule is still refused through the
same stale address and an unblocked one alongside it still connects. Happy to open a PR if useful.
With the DNS ALG on, an app that dials a synthetic address after its mapping has aged out has the
flow dropped in
onFlow— beforelistener.Flow— so the connection dies with no conn-id, no logrow, no summary and no rule. The app retries the same cached address and fails identically until
something makes it re-resolve. In the field this reads as "apps intermittently cannot connect, and
turning the VPN off fixes it instantly", with nothing in the app's own logs to explain it.
Observed on the engine pinned by rethink-app
v0.5.6(61894b7f); the code paths below areunchanged on
n2HEAD.What it looks like
Note
for ;— the domain list is empty, which is what disarms the recovery path.Why the state is reachable at all
registerLockedgives the nat entry and its contents different lifetimes:xipsttl <= ansttlalways, and nothing ever deletes fromt.nat(it grows for the tunnel'slifetime), so
undidAlg— whichxLockeddocuments as "really 'hasAnyAlgEntry'" — stays truelong after the ips and domains inside the entry are gone.
02d568d8("min alg ttl at 2m") is whatopened this window: with the previous
min(ttl2m, ttl)the nat entry expired first,undidAlgcameback false, and the flow was simply not treated as ALG.
Why the recovery does not fire
xLockedstates the contract explicitly:onFlowdoes try — but only overdoms, which expired together with the ips. Two other sources arepresent and unused:
pdomsis computed and thrown away.undoAlgfillsprobableDomainsfrom forcedPTRandfrom
dialers.Ptr(maybeAlg), and the re-resolve loop never looks at it.47e8d300deliberately retains expired ipsand domains for
staleXipsThres/staleDomainThres, butXgates them behindusestale := !t.mod.Load()— i.e. only while translation is off — andptrLockedasksxaliveon at.nathit, while its forced variant readst.ptr, which is keyed by real ips andso can never answer for an alg ip. A mapping two minutes past its ttl is therefore as good as
deleted, in precisely the mode the reserve was built for.
Why it is invisible
The branch returns at
return fm, undidAlg, "", "", which is beforeh.listener.Flow(...). Thelistener never sees the flow, so nothing is logged and no rule applies;
Mark.CIDis empty, soprocessSummariesdrops the summary too. TCP gets a RST after the 0–30 s stall, UDP is droppedsilently. From the user's side the app fails and the app that filters it shows no trace.
Suggested fix
The smallest correct change is to use the domains already in hand before giving up:
Two things worth doing alongside it: exposing the
pastmaps to this path (a stale-tolerantX/PTR) so the 24h reserve is usable while the ALG is on, and not returning beforelistener.Flowwhen nothing can be recovered — a flow the listener has seen is at least attributed, logged and
governed by the user's rules, whereas the early return is unattributed filtering.
Whatever is done here must keep unspecified ips (
0.0.0.0/::) flowing through as a usableanswer: that is how an rdns verdict reaches the listener, so filtering them out of a re-resolution
would turn a blocklisted domain into an allowed connection.
Field reports carrying the same signature, undiagnosed
alg: dns64: ... realips([]) => unnated([]); until: -6h26m14.4s, filed as a WireGuard bug.alg: ptr: in nat? (natdoms? false / doms? false),alg.go@ptrLocked, legit traffic blocked.Verified fix
We run a patched engine along these lines. Against a mapping seven minutes dead, a dial that
previously failed returns
200, while a domain carrying a block rule is still refused through thesame stale address and an unblocked one alongside it still connects. Happy to open a PR if useful.