Release 1.5.1 — the exit chain, and the routes the app was describing wrong - #20
Merged
Conversation
A failed attempt used to leave the system proxy pointing at a bridge whose tunnel had already gone. Nothing restored it until the supervisor either gave up or the user disconnected, so for the whole backoff and the whole next attempt every application on the machine was aimed at a listener that answered nothing. In "Whole machine" mode that reads as the app having broken the network rather than as one attempt not coming up. Ordinary retries now put the proxy back; the kill switch, whose entire purpose is to hold it, still does. The reason that took so long to find is the second half of this change. The core's output lived only in a 1000-line ring buffer inside the running process, so every diagnosis was inference: the evidence was gone by the time anyone went looking. Each line was also one IPC message, emitted from the thread that is the sole drainer of the core's stdout pipe — the interface could throttle the engine. Lines are now buffered and a single pump delivers them, both to the window in batches and to a rotating core.log in the app's log directory. With the log in place the connection itself turned out to be healthy: a cached gateway that had expired, then a full sweep of 3007 candidates inside its own 120s budget, then MASQUE H2 up in half a second. Verified end to end — connected on 162.159.198.98:443 at 33ms, exit in Singapore with warp=on, and no orphaned processes after disconnect. Also carries the chain work that never landed: the through-the-tunnel switch, the guard that stopped a working chain being torn down and rebuilt on every log line, and chain failures surfaced on screen instead of swallowed.
mihomo's control API is Go's net/http, which sets Content-Length only for a
reply small enough to buffer and frames anything larger in chunks. The client
here read neither: it took whatever bytes followed the headers and handed them
to the JSON parser.
/version is 35 bytes, so it arrived whole and the readiness check passed — the
chain reported itself up and started carrying traffic. The node list is the one
reply that is always large, so it always arrived chunked, and always reached the
parser with its chunk header still on the front:
8a0c\r\n{"proxies":{"COMPATIBLE":...
which is exactly the "expected value at line 1 column 1" on screen. It read like
the chain had crashed rather than like a reply we had not finished reading, so
the dashboard showed no nodes and an error for a second hop that was working.
The reply is now decoded before it is parsed. Verified end to end: the tunnel
alone exits 104.28.214.151 in Singapore with warp=on, and through the chain
64.110.98.173 in Japan with warp=off — the node's address, not Cloudflare's.
Four nodes list with their delays, and nothing is left running after disconnect.
Six things, all of them the app describing or trusting a route that was not the one carrying traffic. The exit card read the tunnel even with a second hop running, so it reported Cloudflare's address while every application left through the node. "This app only" named the tunnel's port for the same reason, which sent anything pointed at it straight past the hop — the address on that card was a literal, so it was also wrong for anyone who had changed the SOCKS port. Both now follow the address traffic is carried on, and the badge says "Through your node" rather than treating warp=off as a leak: through a hop it is off by definition. That card is also read a few times before it gives up, because the first read lands while a hop that has started still has no nodes loaded. The hop's own port was ephemeral — 62704, then 36701, then 7677 — so the one address a person has to type into a browser moved on every launch. It is now the tunnel's port plus one, falling back only if something holds it. The latency probe timed a SOCKS5 CONNECT, which stays fast on a tunnel whose data plane is congested. Measured on one machine, one minute apart: the pinned edge took 3 to 10 seconds to first byte and then timed out, while a freshly scanned one took 0.11s — and the chart read 33ms throughout. The probe now waits for a byte to come back, and a watcher re-scans when three checks in a row come back over two seconds. The engine only ever asked whether its cached gateway still answered, never whether it was still worth having, and every successful connect wrote it back — so a degraded edge stayed pinned for good. mihomo's output was piped and never read, which blocks it on its own logging once the pipe fills. It also threw away the account of what the chain was doing: it had marked every node dead behind the slow tunnel and said so. Drained into the app log now, at info. Last, launching the app while it was already running started a second process that knew nothing about the first: it showed "Not connected" over a live tunnel, and its startup proxy recovery reverted the system proxy the running copy had applied. A second launch now focuses the window already open.
DarkPoesidon
force-pushed
the
fix/connection-root-cause
branch
from
August 19, 2026 08:04
d5b2390 to
4f901c5
Compare
Twice in a row the Linux x86_64 build sat in "Install Linux build dependencies" for over half an hour, while every other architecture — including the arm runner on the same image — finished in ten minutes. apt retries without a deadline, so the job neither failed nor finished, and a release that was otherwise ready waited on it. Bound the step to ten minutes, cap apt's own retries and socket timeout, and answer needrestart's service prompt rather than leaving it on a terminal nobody is watching. A re-run of one job costs minutes; a hang costs the cycle. The runner image stays 22.04 on purpose: building against the older glibc is what keeps the .deb and .rpm installable on distributions people actually run.
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.
Everything since 1.5.0. Each of these was the app trusting or describing a route that was not the one carrying traffic.
The tunnel would not connect
A failed attempt left the system proxy pointing at a bridge whose tunnel had already gone. Nothing restored it until the supervisor gave up or the user disconnected — so for the whole backoff and the whole next attempt, every application was aimed at a listener that answered nothing. In Whole machine mode that does not read as "one attempt did not come up"; it reads as the app having taken the network down. Ordinary retries now restore it. The kill switch, whose purpose is to hold it, still does.
The chain's node list was never decoded
mihomo's control API is Go's
net/http, which setsContent-Lengthonly for a reply small enough to buffer and frames anything larger in chunks. The client read neither./versionis 35 bytes so it arrived whole and the readiness check passed — the chain reported itself up and carried traffic — while the node list, the one reply that is always large, reached the JSON parser as8a0c\r\n{"proxies":.... That is exactly theexpected value at line 1 column 1on screen, and it read like a crash rather than a reply we had not finished reading.The app named the wrong address
The exit card read the tunnel even with a hop running, so it reported Cloudflare's address while everything left through the node. "This app only" named the tunnel's port for the same reason, sending anything pointed at it straight past the hop — and that address was a literal, so it was also wrong for anyone who had changed the SOCKS port. The hop's own port was ephemeral (
62704, then36701, then7677), so the one address a person types into a browser moved every launch; it is now the tunnel's port plus one.A degraded gateway stayed pinned for good
The engine caches the gateway that last worked and asks only whether it still answers — never whether it is still worth having — and every successful connect writes it back. Measured on one machine, one minute apart:
162.159.198.128162.159.198.130Nothing noticed, because the latency probe timed a SOCKS5 CONNECT — which stays fast on a tunnel whose data plane is congested. The chart read 33 ms while no page would load. The probe now waits for a byte to come back, and a watcher re-scans after three checks in a row over two seconds. It leaves the transport alone: alternating H2/H3 answers a transport that cannot get through, and here the transport was fine.
Nobody could see any of it
The core's output lived only in a 1000-line ring buffer inside the running process, so every diagnosis was inference — and inference kept landing on the engine, which was never at fault. Each line was also its own IPC message, emitted from the thread that is the sole drainer of the core's stdout pipe. mihomo's output was piped and never read at all, which blocks it on its own logging once the pipe fills, and threw away the only account of what the chain was doing.
Lines are now buffered and one pump delivers them: in batches to the window, and to a rotating
core.logthat survives a crash. mihomo's output joins it.And one found by accident
Launching the app while it was already running started a second process that knew nothing about the first — it showed "Not connected" over a live tunnel, and its startup proxy recovery reverted the system proxy the running copy had applied. A second launch now focuses the window already open.
Verified
Nodes list with their delays, latency reads a true 79 ms,
This app onlynames127.0.0.1:1820, the chain log captures traffic through the selected node, and nothing is left running after disconnect. Built, installed and exercised on Windows 11.cargo test55 ·pnpm checkclean ·pnpm test33