Repository navigation
0.4.1 — 2026-09-04
Added
-
Split DNS, off by default. Captured queries can now be answered by a
resolver reached without the proxy, chosen two ways that compose. A name list
decides from the question, before anything leaves the host:--direct-domain
and--direct-domain-filename what the direct resolver answers,
--trusted-domainand--trusted-domain-filename what it must never be
asked. An address set decides from the answer:--dns-directnames the
resolver,--dns-direct-prefixand--dns-direct-prefix-filename the
addresses that make its answer credible, and both resolvers are asked with the
direct one believed only when it returns an address inside that set. With none
of these configured every captured query goes to--dns-upstreamthrough the
proxy exactly as before.This exists for two costs of routing every lookup through one tunnel. A
geographically aware name resolves from where the tunnel exits, so a nearby
service hands the host a distant address. And an upstream outage does not
degrade name resolution, it ends it — including for destinations already
excluded from capture, which stay reachable but unresolvable. A name the
address set covers now resolves at the speed of the near resolver and does not
consult the tunnel at all.An answer naming addresses outside the set is never served: a good answer for
a distant service and an injected one are the same message, so the trusted
resolver decides, and when it cannot the query fails with SERVFAIL rather than
being answered with something unverified. An answer naming no address is
served once the trusted resolver has had its chance and could not take it,
because there is nothing in it to misroute a connection to and refusing it
denies the name just as thoroughly — but while the tunnel is up a forged
NXDOMAIN still loses to the real answer. Adjudication reads addresses, so it
applies to A and AAAA only; HTTPS and SVCB go to the trusted resolver, since
they carry the encrypted-client-hello configuration and serving one from the
direct path would inflict the downgrade this project exists to prevent.Only answers from the trusted resolver are ever learned from for hostname
recovery, adjudicated exchanges included. An association learned from the
network's own path would let whoever supplied that answer choose the name a
later flow is proxied under. -
tunless --explain NAMEreports which resolver would answer a name and why,
and when both would be asked, runs the real exchanges and prints what each
returned, which addresses fell inside the credible set, and the verdict. -
The status endpoint reports the configured policy and a per-layer decision
count underdns_policy. A prefix set that matches nothing is otherwise
indistinguishable from a network with no interference on it. -
macOS carries the same policy, spelled the same way, through the launcher and
the control protocol. List files are read by the launcher rather than opened
by the system extension, so parse errors land where the operator is.
Changed
-
A settled adjudication no longer delivers the answer it rejected. Both
resolvers are asked, so the losing half is still in flight when the verdict is
reached. The arbiter deleted the exchange as soon as it settled, which made
thesettledbranches indeliverTrustedandcompleteDirectunreachable:
the late half was disowned, treated as an ordinary forwarded reply, restored
against a translation that no longer existed, and written to the application
as a second datagram carrying an unrewritten identifier — carrying,
specifically, the answer adjudication had just decided not to serve. The
exchange is now kept as a tombstone until the adjudication deadline, which is
what those branches were written for, and the datapath asks the arbiter rather
than the translation map whether an identifier is its own. The identifier
still goes back at the verdict, and cannot go back twice.This was reachable whenever the direct half won a race it usually wins by a
wide margin, so it showed up first as a flaky test rather than as a report. -
A degraded capture session now has a deadline.
probationSecondsbounded
capture that never worked; nothing bounded capture that worked and then
stopped. Once the watchdog paused a confirmed session, every later failure was
absorbed and the host stayed fail-closed for as long as the upstream stayed
down. New--degraded-timeout(default 300 seconds,TUNLESS_DEGRADED_TIMEOUT)
rolls capture back instead, using the same response the launcher already makes
to a failed post-start verification.--degraded-timeout 0keeps the previous
unbounded behaviour for hosts that must never bypass.The bound matters most where recovery is impossible rather than slow. An
upstream addressed by hostname has to resolve that name to open its datapath,
and while--dns-upstreamis set capture sends that lookup to the upstream
that is waiting on it, so the query is dropped and the name never resolves.
Every probe then fails exactly like a transient outage, and waiting cannot
end it. Sleep does not spend the deadline and a path change restarts it, so
the only thing that exhausts it is a host that has genuinely stopped
resolving names through capture. -
socks5.Client.LocalDomainsis replaced bysocks5.Client.DNSPolicy, which
carries the split-horizon suffixes along with the rest of the policy.
--local-domainis unchanged and still outranks every other layer.