Conversation
write_atomically used .<name>.<pid>.tmp for every call, so two compiles into one directory in the same process truncated each other's temporary file and all but one failed to rename it. One could also write into a file the other had already renamed into place, which breaks the promise that a mapped engine.dat or domains.bin never changes. Temporary names now carry a process-wide counter and are opened with create_new.
… is gone A reset only dropped idle pooled connections, so a streaming response (server-sent events, a long poll, a download) on a connection whose source address had gone kept waiting on the dead path: forever over HTTP/1.1, until the ping timeout over HTTP/2. Upstream sockets are now wrapped so a watcher can cut them on a reset when their address is no longer assigned, which fails the requests on them at once.
…ailures An intercepted request whose upstream cannot be dialed (no such name, refused, reset or timed out) now gets no response: hyper closes the HTTP/1.1 connection or resets the HTTP/2 stream, so Safari shows its own error page and can fall back to http://. No free upstream connection gets 503; TLS failures keep 502. Unreachable hosts are logged at info level, once a minute per host.
Upstream certificates for another name, expired, not yet valid, revoked or for another purpose no longer teach a pin: the client would reject them too. When a third different host would be learned from upstream failures within 60 s, none is kept and upstream learning pauses for 10 minutes. A wake or network path change drops upstream pins from the last 5 minutes. Pins from client rejections are never taken back.
The stats reply now carries the tunnel process start time, and the heartbeat is re-read on every stats refresh and when the app becomes active, so a jetsam kill and on-demand restart while the app was suspended changes "Tunnel started". M2 E12 now also checks the tunnel log for a second startTunnel line.
setup now shares the device options with the device command, as its error messages already advised, and falls back to the pinned pymobiledevice3 when libimobiledevice is not installed.
A suspended app never answers the ping of a graceful HTTP/2 shutdown, so its idle connection kept its interception slot until the keep-alive gave up and new connections were passed through. A reclaimed connection with nothing in flight is now dropped after 50 ms, any other closing one after 5 s.
…her name The client accepted the proxy's leaf, so Safari cannot show its own warning and rendered the empty 502 as a blank page. The 502 now carries a short text that names the problem, and M3 E15 step 2 says what the owner will see.
…list Since the proxy checks the DNS blocklist on CONNECT, curl prints 000 for doubleclick.net. Step 3.3 now checks the CONNECT status and adds a URL-rule block on an allowed host; step 5.3 says how Firefox shows both.
ListUpdater.run now holds background time, like the tunnel restart, so a download in flight is not cut off by suspension. A download that still fails with a lost connection or a timeout is retried once before the list counts as failed.
A server that accepts TLS and then closes or resets the connection, or resets the HTTP/2 stream or sends GOAWAY, before any response now gets NoResponse instead of an empty 502, so the browser shows its own error page or falls back from https:// to http:// as it does without the proxy. TLS alerts, malformed responses and client certificate requirements keep their 502.
A failed first request on a new connection to a server that asked for a client certificate was always learned as a 30-day pin. Now it is learned only when the handshake was TLS 1.3 (over TLS 1.2 a completed handshake proves the certificate was optional) and a fatal alert from the server was read, which a stream wrapper records for HTTP/1.1 and HTTP/2 alike. Resets, closes and cut network paths are handled like any other failure.
Absolute-form requests (http:// through the system proxy) whose upstream cannot be reached got an empty 502, which Safari shows as a blank page. The client connection now closes without a response, as for intercepted requests, so Safari shows its own error page. No free upstream connection gets 503, and certificate problems get the same explanatory 502 as intercepted requests.
The AdGuard DNS filter writes some host blocks as name^ (the host and its subdomains) and some as ://name^ (that host only). Both were skipped, so hosts such as dlsdk.appsflyer.com and jhf.ru resolved. The unanchored form must start with a letter or digit, so -pia.example^ stays skipped instead of blocking pia.example.
The request whose upstream TLS failure made the host a learned pin got an empty 502, shown by Safari as a blank page that also kept it from falling back to http. It now gets no response: HTTP/1.1 closes the connection and HTTP/2 resets the stream with REFUSED_STREAM and sends GOAWAY, so the browser retries on a new CONNECT, now passed through, or shows its own error page. A failure the burst guard declines to learn gets a 502 text that names a captive portal or a network filter as a likely cause.
IIS resets a request with HTTP_1_1_REQUIRED when it needs HTTP/1.1 for Windows authentication or client certificate renegotiation. The proxy treated it as a hang-up, so every visit failed. Such a reset now makes the host a learned pin, the reason is passed on to the client stream and its connection closes, so the browser falls back to HTTP/1.1 through a passed-through CONNECT. Other stream resets are unchanged.
In a List, a row holding a default-style button acts as one button, so a tap anywhere on the Home Filter lists row downloaded and recompiled every list. The Update button, and the Add button next to the host pattern field, are now borderless, so only a tap on the button itself acts.
iOS 27 Connectivity Assist retries a failed Wi-Fi attempt over cellular, with the carrier's DNS and no proxy, so a CONNECT refused with 403 let the blocked host load over cellular. A DNS-blocked host now gets 200, a TLS handshake with a Tollgate leaf and a connection that fails every request without a response (HTTP/2 stream reset with INTERNAL_ERROR, HTTP/1.1 closed). Pages still see a network error; iOS sees a working connection. Blocked hosts that the policy passes through keep the 403, and so does any blocked host when memory is low or when max_blocked (64) blocked connections are open and none can be closed within 150 ms. Failed handshakes on blocked connections teach no pin. After a list reload, open blocked connections refuse the next request with REFUSED_STREAM and close, so the client reconnects and is classified again.
Blocks made only by DNS still fail the lookup, which iOS 27 can retry over cellular data, and Tollgate cannot read the setting. Home shows a dismissible notice on iOS 27 and later; Settings keeps the advice and can show the notice again.
Adds the alpha 2 checklist, a design-spec revision for the connection race, and the new devproxy expectations for blocked hosts.
The X iOS app pins its certificates and cancels every intercepted connection in its own certificate check without a TLS alert, so pin learning never sees the rejection and the app fails on every attempt. Confirmed in a device log (X 12.29, iOS 27.0.1). X's hosts (*.x.com, *.twitter.com, *.twimg.com) are now in the bundled passthrough list, in a new group for apps that refuse our certificate silently.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings everything since M0 into
main: 151 commits built on stacked branches (m1athroughm3-ios) that were never merged. Alpha 1 was built from this code, and PR #5 was reviewed on top of it. 188 files, about 31,000 lines of code and tests plus 25,700 lines of docs (mostly the milestone plans).Rust core (
core/)common: shared pieces: stats counters, the block event log, clocks, TLS client setup, the resolver interface.policy: which hosts are never intercepted: the bundled pass-through list (Apple, sensitive services, banks, X), the owner's Never filtered and Allowed sites, and certificate pin learning.filter: list compilation: the adblock-rust request engine (engine.dat) and the hashed, memory-mapped DNS blocklist (domains.bin).dns: the tunnel's DNS responder: blocklist answers, DNS over HTTPS to 1.1.1.1 and 9.9.9.9 with a cache, and local network names sent to the Wi-Fi's own resolver.mitm: the local HTTPS filtering proxy: CONNECT handling, TLS interception with leaves from the user-trusted root, per-request filtering, pass-through tunnels, blocked-host connections, the upstream pool, WebSockets, idle and memory limits.tollgate-ffi: the uniffi bridge the app and tunnel call: engine start and stop, list compiling, stats, events, pins and the certificate authority.tools/devproxy: runs the DNS responder and proxy on Linux for testing.iOS (
ios/)Tooling and docs
docs/experiments.Testing
cargo fmtandcargo clippy -D warningsare clean.mainis fully contained inm3-ios, so the merge has no conflicts.🤖 Generated with Claude Code