Skip to content

dns alg: an aged-out mapping blocks the flow instead of being re-resolved, and the block is invisible #252

Description

@ShiroiKuma0

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions