v2.1.2
tg-ws-proxy-rs v2.1.2
This release is about fitting on routers. No behaviour changes — every CLI flag and TG_* variable is unchanged, so is the library API, and the regular binaries are the same build as before. Upgrading is a drop-in swap.
New
UPX-packed release assets for every Linux musl target. Alongside the usual downloads there is now a tg-ws-proxy-<target>-upx.tar.gz, for the case where the binary has to live on 8 or 16 MB of flash (#98).
Measured, not estimated — these are the sizes the release pipeline actually produced:
| Target | Plain | Packed | |
|---|---|---|---|
mipsel-unknown-linux-musl |
4.56 MiB | 1.35 MiB | −70% |
mips-unknown-linux-musl |
4.55 MiB | 1.33 MiB | −71% |
armv7-unknown-linux-musleabihf |
3.44 MiB | 1.27 MiB | −63% |
aarch64-unknown-linux-musl |
3.59 MiB | 1.35 MiB | −62% |
x86_64-unknown-linux-musl |
4.29 MiB | 1.61 MiB | −62% |
Note that the download is barely smaller: .tar.gz already compresses the binary. What shrinks is the file you copy onto the device, which is the part that runs out.
These are extra assets, not replacements, because the saving is not free. A normal ELF maps its code straight from the file, so only the pages actually touched are resident and the kernel can evict them under pressure. The UPX stub instead decompresses the whole image into anonymous memory at startup — nothing is file-backed, and a router has no swap to page it out to, so it stays resident for the life of the process.
| Plain build | UPX -9 --lzma |
|
|---|---|---|
| Flash | ~4.5 MB | ~1.35 MB |
| RSS | working set only, evictable | roughly +4 MB on top, permanently resident |
| Startup | instant | one LZMA decompression, ~1 s on a slow MIPS CPU |
| Throughput | — | unchanged |
Good trade on a device with plenty of RAM and little flash, bad one on a 32 MB-RAM device. Pick per device; the plain assets are staying.
If the startup delay bothers you more than the last few hundred KB, upx -9 without --lzma decompresses several times faster for about 5–8% more size.
Windows and macOS are deliberately not offered. UPX-packed PE files trip antivirus heuristics, which is unacceptable for a tool people use to get around censorship, and packing a Mach-O invalidates its code signature — arm64 macOS then refuses to run it at all.
Why the release profile is still opt-level = 3
Worth spelling out, because people were compiling with opt-level = "z" themselves to fit the flash — and with these assets you no longer need to.
Optimising for size does shrink the binary by about a third, but it costs throughput on exactly the machines that are short on flash. Every relayed byte is decrypted with the client's key and re-encrypted with the DC's key, and MIPS and ARMv7 have no AES instructions, so that runs on the aes crate's software backend. Measured on that backend (--cfg aes_force_soft, the same code path a router executes):
| Profile | AES-256-CTR relay throughput | Binary size |
|---|---|---|
opt-level = 3 (shipped) |
~135 MiB/s | baseline |
opt-level = "z" |
~103 MiB/s | −34% |
A quarter of the throughput for a third of the size is a worse deal than UPX's 70% for no steady-state cost at all. panic = "abort" is left off for a different reason: it would save ~18%, but today a panic while parsing a malformed connection is caught by the tokio runtime and kills only that connection, whereas with abort it would take down the proxy for everyone connected.
Both measurements and the reasoning are in docs/Building.md so nobody has to re-derive them.
Changed
- The README is 725 → 198 lines. Reference material moved into focused guides — docs/Fallbacks.md (routing tiers, domain fronting, upstream proxies, inbound FakeTLS, outbound proxy), docs/Building.md (building, OpenWrt cross-compilation, UPX), docs/Deployment.md (Docker, router deployment, procd,
TG_*reference). Nothing was dropped except a "Project structure" block that had drifted out of sync with the code. - The Docker image is documented.
valnesfjord/tg-ws-proxy-rshas been published on every release but was never mentioned in the README. It now has a Quick Start entry, plus the two settings a container actually needs:TG_SECRET, so the secret survives a restart instead of being regenerated, and--link-ip, so the printedtg://link is not the container's unreachable bridge address. --checkis in the flag table. It was used in the examples but missing from the reference.tokio'sfullfeature is replaced by the features actually used. LTO already stripped the unused code either way (~16 KB), so this is about the dependency tree: 180 → 172 crates, droppingparking_lot,signal-hook-registryand six more. No functional change.
Known limitations
The MIPS and MIPS-LE binaries are not self-contained. Both the plain and the packed asset are dynamically linked and need two things present on the device: musl's loader at /lib/ld-musl-mips-sf.so.1 (or -mipsel-sf), and libgcc_s.so.1 for stack unwinding. OpenWrt ships both in its base, which is why this has gone unnoticed.
On firmware that does not — Padavan + Entware and some Keenetic builds have been reported — the binary will refuse to start, often with an error that reads like a corrupt file or a wrong libc rather than a missing library. If you have hit that, this is the likely reason, and a different libc build is not what would fix it: a properly static binary uses no system libc at all. See #66 and #78.
crt-static is not the default on these two targets and cannot simply be switched on — the cross toolchain used to build them ships no static CRT objects, so the link fails. Fixing it properly needs a different toolchain and is tracked separately. Every other target here is already statically linked.
Memory use is also still higher than it should be, around 25 MB RSS with two active clients. Tracked in #97; it does not affect stability.
Verified
187 tests, clean clippy, clean cargo fmt, and cargo build --release --locked on both linux/amd64 and linux/arm64.
The release pipeline now has a rehearsal of its own that runs on pull requests instead of only on a tag, which is where the numbers above come from: every packed binary was executed under qemu-user and made to print its usage, not merely checksummed. upx -t alone would have missed a stub that packs cleanly and then dies on the target ABI.