Skip to content

Release 1.5.0 — the exit chain - #19

Merged
DarkPoesidon merged 2 commits into
mainfrom
feature/chain-proxy
Aug 18, 2026
Merged

Release 1.5.0 — the exit chain#19
DarkPoesidon merged 2 commits into
mainfrom
feature/chain-proxy

Conversation

@DarkPoesidon

Copy link
Copy Markdown
Collaborator

Why

Cloudflare WARP is explicit that it does not change your country. It egresses near you and geolocates the exit address to your region, so a user in Iran connects successfully and still looks like they are in Iran. No setting in Aether can alter that, because the decision is Cloudflare's.

This adds the hop that can.

Verified before a line was written

Everything below was proven by running mihomo against a real subscription, not read from documentation:

without the chain:  loc=TH  colo=BKK
with the chain:     loc=JP  colo=KIX

the tunnel saw:     <node server>:8443   ← the node dialled through it
node delays:        548ms · 485ms · 601ms · one unreachable

Four things that documentation would not have told us, each of which cost a wrong turn:

  • provider nodes are not listed in /proxies; they answer only under their own provider
  • /proxies/{name}/delay returns 404 for them — the path is /providers/proxies/{p}/{n}/healthcheck
  • mihomo converts a base64 URI subscription itself, so there is no parser here
  • a wireguard:// entry in the subscription is skipped rather than failing the whole provider

The order is the design

apps → mihomo → [node] dialled through → Aether → MASQUE → Cloudflare → node → internet

Reversed, traffic leaves via Cloudflare again and nothing changes. Getting it right has two consequences worth having: the node is dialled from inside the tunnel, so local filtering sees an ordinary Cloudflare connection and never the node's address or SNI; and a node blocked from this network is still reachable, because it is reached from Cloudflare's network instead.

dialer-proxy is set per provider, so a subscription of any size arrives without rewriting a single entry.

What the dashboard shows

Every measurement travels the tunnel, so one number answers both questions the feature exists for: does this config work from behind the tunnel, and how fast is it. A failure against a row is the answer, not an error.

Subscription links are shown host-only — a link is a credential, and this screen appears in screenshots and support threads.

Three things the config must not get wrong

Each has a test, because each is silent when broken:

every source dials through the tunnel a provider without dialer-proxy reaches nodes directly and changes nothing
nothing takes a direct route a DIRECT rule puts that traffic on the local network
DNS resolves inside the chain a query that escapes names the destination even when the traffic does not

The control API is loopback-only behind a secret regenerated every run. Without both, any web page the user opened could reroute their traffic.

Ordering that matters

The chain starts before the system proxy is pointed anywhere, because which listener to point at is the question the chain answers. The other order aims the machine at the tunnel while mihomo loads a subscription — traffic leaving with the wrong exit address at the moment the user believes the opposite.

It stops whenever the proxy is restored: it exists only to carry a live tunnel's traffic.

Cost

The installer grows 6.2 → 18.8 MB (measured, not estimated). mihomo is pinned to v1.19.30 and staged as a sidecar for all five architectures.

GPL-3.0, shipped as a separate process — the licence text and a source pointer need to reach the Licences page before this is released publicly.

Verification

50 backend tests, 33 frontend, tsc --noEmit clean, Windows bundle built and the sidecar confirmed beside the binary. The dashboard was driven end to end in the browser: search reaches it, the toggle explains itself, and a pasted subscription is stored with its token hidden.

Not in this one

The chain is off unless configured, so nothing changes for existing users. Selecting a node from the Simple screen, and showing the node's country on the exit card, are follow-ups.

Cloudflare WARP is explicit that it does not change your country: it
egresses near you and geolocates the exit address to your region. A user in
Iran connects successfully and still looks like they are in Iran, and no
setting in Aether can alter that, because the decision is Cloudflare's.

So add a hop after the tunnel. mihomo's dialer-proxy dials a node *through*
another proxy, which puts every node behind MASQUE and makes the exit
address the node's rather than Cloudflare's. Verified end to end before any
of this was written: a real subscription moved the visible country from TH
to JP, and the tunnel logged the node being dialled through it.

The order is the whole design. Reversed, traffic would leave via Cloudflare
again and nothing would change. Two consequences fall out of getting it
right: the node is dialled from inside the tunnel, so local filtering sees
an ordinary Cloudflare connection and never the node's address or SNI; and
a node blocked from this network is still reachable, because it is reached
from Cloudflare's network instead.

dialer-proxy is set per provider rather than per node, so a subscription of
any size arrives without us parsing or rewriting a single entry -- and
mihomo converts pasted vless, vmess, trojan, ss and hysteria2 URIs itself,
so there is no parser here at all.

The chain is a dependent of the connected state rather than a peer process:
it starts once the tunnel is up and stops whenever the proxy is restored.
It also starts *before* the system proxy is pointed anywhere, because which
listener to point at is the question the chain answers -- the other order
would aim the machine at the tunnel while mihomo loaded a subscription,
sending traffic out with the wrong exit address at the moment the user
believes the opposite.

Three things the config must not get wrong, each covered by a test: every
source dials through the tunnel, nothing may take a direct route, and DNS
resolves inside the chain -- a query that escapes names the destination
even when the traffic does not.

The control API is loopback-only behind a secret regenerated every run.
Without both, any web page the user opened could reroute their traffic.

No interface yet, and the chain is off unless configured. The installer
grows from 6.2 to 18.8 MB.
The nodes were reachable from the backend but there was no way to see or
choose one. This is that screen: subscriptions, pasted configs, and every
node with the delay it last recorded.

The measurement is the point. mihomo sends each probe down that node's own
dialer-proxy, so a figure means the node works from behind the tunnel and
a failure means it does not -- "can this config be chained" and "how fast
is it" are one question with one answer, which is what the dashboard shows
against each row.

Subscription links are shown host-only. A link is a credential, anyone
holding it can use those nodes, and this screen turns up in screenshots
and support threads.

Settings search reaches the section, and ties in it now break on the order
the entries are written in rather than alphabetically. Searching a section
name matches everything in that section equally, and the useful answer is
the control the section exists for -- listed first -- rather than whichever
label happened to start with the earliest letter. Searching "chain"
returned "Configs pasted by hand" before this.
@DarkPoesidon
DarkPoesidon merged commit ff07d74 into main Aug 18, 2026
7 checks passed
@DarkPoesidon
DarkPoesidon deleted the feature/chain-proxy branch August 18, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant