Aether v2.4.0
Aether is a user-space proxy client. It tunnels traffic out of a filtered network and exposes a local SOCKS5 proxy on 127.0.0.1:1819. Point a browser, terminal, or system proxy at that address and your traffic leaves through the tunnel.
These notes cover the changes since 2.3.0.
Changes that can break a command written for 2.3.0
--enroll-addressis now--api-address, andAETHER_ENROLL_ADDRESSis nowAETHER_API_ADDRESS. A command with--enroll-addressstops with "unknown option", andAETHER_ENROLL_ADDRESSis no longer read, so where it is still set the calls go by the API's own names.--no-fragmentis gone, since leaving--fragmentout already means off, and so is--api-fragment, which 2.3.0 took but never read:--fragmentnow splits the WARP API calls as well. A command with either stops with "unknown option".--register goolnow registers what--goolruns: the MASQUE identity and the WireGuard identity carried inside it (<masque config>-gool.toml). The two WireGuard hops of the classic gool, which it registered in 2.3.0, now take--register gool-classic.- MASQUE handshakes send
www.cloudflare.comas their server name by default, instead ofconsumer-masque.cloudflareclient.com, the name the WARP client sends.--masque-sni consumer-masque.cloudflareclient.comsends the old name again. - With
--psiphonand--psiphon-only,AETHER_PSIPHON_INTERFACEand aListenInterfacein--psiphon-configno longer set where Psiphon listens; give that address with--psiphon-bindor--bind. They still apply to--psiphon-reverse. - An
--upstream(orAETHER_UPSTREAM) that aether cannot dial through, such as one with no port, port 0 or a kind it does not speak, now stops it as it starts, with the option named. 2.3.0 logged that the setting was ignored and connected straight out, around the proxy you named. - The Android builds need Android 10 (API 29) or later; those of 2.3.0 ran on Android 7 (API 24) and later.
The WARP API
- The calls to the WARP API, which register devices, enroll MASQUE keys and enable WARP, go by api.devices.cloudflare.com, the name Cloudflare's own client has called since its version 2026.6.0. An attempt that gets no answer there, or a refusal from another server than the API, such as an error page, goes by api.cloudflareclient.com next, the name 2.3.0 used, and the name the API answers by goes first for the rest of the run.
- Behind those two names the calls have a third way: the first of them on a Cloudflare edge of its own, 104.24.4.6 and then 104.18.0.6. A network that blocks the API's own addresses, or answers in their place, but lets an edge through no longer keeps aether from getting a key, and once an edge has answered, the later calls of the run go there first.
--api-addresssets where the calls connect,--api-hosttheir HTTP host and--api-snithe server name of their ClientHello. The server name follows the host, and the address follows the server name unless given. With--api-addressthe calls go to that address alone, by both names; with--api-hostor--api-snithey go by that one name, so--api-host api.cloudflareclient.comsends every call by the old name. Cloudflare refuses a call whose server name and host differ, with a 403 that the error message explains.--fragmentsplits the ClientHello of these calls too, on every transport, with the same--fragment-sizeand--fragment-delay, for networks that filter the API's name. Without it, MASQUE over HTTP/2 and these calls, the edge among them, send their ClientHello whole. The help and the guides give the HTTP/2 fragment defaults the code uses: off, and pieces of 8-16 bytes.- Getting an identity means registering it and then enabling WARP on it, as the WARP app does, and it is saved only when both went through; if enabling fails, the start or the registration fails and nothing is saved. An identity file that is already there is used as it is. 2.3.0 enabled WARP on the MASQUE identity at every start, before the tunnel came up, which held each start for about 10 seconds where the WARP API is filtered and the tunnel is not, or about 110 on a silent drop.
- A Retry-After from the WARP API no longer delays the error after the last attempt.
- The Zero Trust sign-in by email sends each of its requests over a connection of its own, as the WARP API calls do.
MASQUE
--masque-sni <name>(AETHER_MASQUE_SNI) sets the server name in the ClientHello of every MASQUE handshake, over HTTP/3 and HTTP/2: gateway scans and checks, the tunnel, and both hops of MASQUE-in-MASQUE. Only the TLS name changes; the HTTP host stays cloudflareaccess.com. With--echit is the name inside the encrypted ClientHello. A value that is not a domain name stops aether as it starts.- Without
--team, the MASQUE scan over IPv4 tries 297 gateways and no other address: 75 of 162.159.198.0/24 and 222 of 162.159.199.0/24, those of their 256 that completed a TCP handshake on port 443. It tries each of them on every MASQUE port, 443 first, then 500, 1701, 4500, 4443, 8443 and 8095; before, the scan swept the ranges, and only eight known gateways went past port 443. WARP's own gateways, 162.159.198.1 and 162.159.198.2, are not among the 297;--peerstill names any gateway. - Without
--team, the MASQUE scan over IPv6 tries WARP's own gateway, 2606:4700:103::1, first and on every MASQUE port, and samples 2606:4700:103::/48 alone, as many addresses as it took from each range before. It no longer tries 2606:4700:d0::/48, 2606:4700:d1::/48 or 2606:4700:102::/48, nor the four known gateways in them;--peerstill names any gateway. - With
--team, both MASQUE scans still try their known gateways and then every range, 162.159.197.0/24 and 2606:4700:102::/48 first. They now also try 162.159.199.0/24 and 2606:4700:103::/48, and the gateways 162.159.199.2, 162.159.197.2 and 2606:4700:103::1.
Reconnects
- A MASQUE gateway whose tunnel keeps ending, as when
--exit-locrefuses its exit, is left after two tunnels in a row that each ended within five minutes, rested for five minutes and replaced by a scan, as WireGuard endpoints already were. This holds for gool's gateway too; a gateway named with--peerstays. - Only tunnels that end within five minutes count towards leaving a gateway, endpoint or hop, for MASQUE, WireGuard, the classic gool and MASQUE-in-MASQUE. A tunnel that carried traffic for hours and saw two network changes reconnects at once to the same gateway, endpoint or hops, where 2.3.0 rested the WireGuard endpoint, or dropped the hops of the classic gool and MASQUE-in-MASQUE, and scanned.
Gool
--register goolmakes gool's WireGuard key directly. Where the key was registered does not set the exit: that comes from where the WireGuard hop is dialled, inside WARP.--register gool-classic(alsowiwandwarp-in-warp) registers both WireGuard hops of the classic gool, and--register allregisters all five identities.--tor-reverseand--psiphon-reversecarry gool over MASQUE. They refuse only WireGuard and the classic gool, which need UDP to reach WARP.- When gool registers its WireGuard key from inside its tunnel, it no longer changes
AETHER_UPSTREAMfor the whole process, so nothing else goes into the tunnel meanwhile.
Ports held from the start
- Aether binds the ports you dial as it starts and keeps them until it ends: its SOCKS5 proxy (
--bind) and HTTP proxy (--http-proxy), and those of Tor and Psiphon, inside the tunnel and on their own (--tor-bind,--tor-http,--psiphon-bind,--psiphon-http). In 2.3.0 most were bound only once the tunnel, Tor or Psiphon was up, and aether's own were let go whenever the tunnel dropped, so while aether connected or reconnected another program could take one and receive what your browser or system proxy sent there. A client that connects before the tunnel is up now waits for it instead of being refused. - Psiphon listens on a private loopback port behind the port aether holds for it.
- When Tor or Psiphon inside the tunnel stops, or a held port breaks, aether ends with its error instead of running on without the proxy you dial. When the tunnel cannot be dialled through Tor or Psiphon around it, aether ends rather than go around them.
- A listener on port 0 is dialled on the port the system gave it, and one on every address (0.0.0.0 or ::) on the loopback address, which Windows needs, so Tor or Psiphon inside the tunnel, the tunnel through Tor or Psiphon around it, and the look at the exit location all reach it.
- An error about a port in use names the option that sets that port, such as
--psiphon-bind, instead of always--bind. - A proxy that runs short of sockets or buffers for a moment on Windows, or meets a connection that failed while it waited on Linux and Android, tries again instead of stopping.
- Where a program runs aether through its C library, Tor and Psiphon stop when aether does.
TLS fingerprint
- Aether is built on boring 5.3.0, Cloudflare's bindings to BoringSSL, with the BoringSSL of 2026-09-29 and Cloudflare's post-quantum patch. Without the options below every ClientHello is as it was, but for one change of the newer BoringSSL, which Chrome's has as well: a ClientHello of 256 to 511 bytes is no longer padded to 512.
--tls13-cipherssets the TLS 1.3 cipher suites, in order, by their standard names; without it they keep Chrome's order.--tls-key-sharessets the groups the ClientHello sends key shares for, in order, each one of--tls-groups; without it BoringSSL picks them as Chrome does.--tls-sigalgssets the signature algorithms, in order, by BoringSSL's names, and--enable-grease-sigalgsputs a GREASE value first among them, as Chrome does.--tls-cert-compressionoffers certificate compression (RFC 8879) with zlib, brotli and zstd, in the order given:brotlias Chrome offers it,zlib:brotli:zstdas Firefox does.--tls-alps newor--tls-alps oldoffers application settings (ALPS) for HTTP/2 and HTTP/3, under the codepoint Chrome sends now (0x44cd) or the one it sent before (0x4469).--tls-trust-anchorsasks for a certificate from the trust anchors it names, by their IDs in dotted decimal, and--tls-server-paddingasks the server for padding in its handshake, as Chrome does.--enable-ech-greasesends a GREASE ECH extension, which looks like ECH and hides nothing, when there is no ECH config to offer, as Chrome and Firefox do.--disable-ocsp-stapling,--disable-sctand--disable-session-ticketleave out the request for OCSP, the request for signed certificate timestamps and the session ticket, which the handshakes otherwise send as Chrome does.--disable-extension-permutationkeeps one order of the extensions on every handshake, as Firefox, Python and Go do.- All of them shape every TLS handshake the fingerprint shapes: MASQUE over HTTP/2 and over HTTP/3, the WARP API and the DoH lookup of the ECH key. A value BoringSSL does not take stops aether as it starts, with the option named.
Fixes
- With
--team, the TLS of every MASQUE handshake, over HTTP/3 and HTTP/2, is verified by default: the gateway's certificate has to match one of the pinned Cloudflare keys, one more of which joins them. A middlebox that answers with a certificate of its own can no longer stand in for the gateway and see the traffic inside the Zero Trust tunnel.AETHER_TLS_VERIFY=0turns the check off; without--teamit stays off unless--tls-verifyturns it on. - Psiphon dialled through a proxy, as
--psiphoninside the tunnel is, no longer takes tactics from its servers: they had it attach a BPF program to every TCP dial, which needs root, so without it each dial of the chain failed with "operation not permitted" and the chain took a long way around to come up.
Builds and docs
aether.exeno longer needs the Microsoft Visual C++ Redistributable: the C runtime is built into it, so it needs only the DLLs that come with Windows.- Every archive has a LICENSES folder next to aether, with the licenses of aether (AGPL-3.0), psiphon-tunnel-core and lyrebird, THIRD-PARTY-NOTICES.txt for the Rust crates and the Rust standard library linked into aether, and a notice each for the Go modules and the Go standard library linked into psiphon-tunnel-core and lyrebird.
- Every release archive and the Docker image build with the latest stable release of Rust, and the crate no longer declares a minimum Rust version (2.3.0 needed 1.98); the README asks for the latest stable release to build from source.
- The READMEs, docs and guides, in English and Persian, describe gool, WireGuard inside MASQUE, and the classic gool on
--gool-classic. Two new guides: Docs/OPENWRT.en.md, linked from the README, runs aether on an OpenWrt router as a service that starts on boot, and a section of Docs/DOCS.fa.md runs it in Termux inside tmux, so it outlives the terminal, and shares its proxy with other devices on the network through--bind 0.0.0.0:1819.
Every archive ships with a matching .sha256 file, and SHA256SUMS.txt lists them all.
See README.md and the Docs directory for transport selection, obfuscation profiles, and the full environment-variable reference.
What's Changed
- Add --masque-sni, fix gool and WARP registration, rotate failing gateways by @patterniha in #147
- Update DOCS.fa.md by @AlirezaM19999 in #124
- Docs: run Aether on OpenWrt with the PattNG HTTP/2 setup by @mfat in #152
- sync by @patterniha in #156
New Contributors
- @AlirezaM19999 made their first contribution in #124
- @mfat made their first contribution in #152
Full Changelog: v2.3.0...v2.4.0