Skip to content

v2.2.1

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Aug 14:29
632720d

tg-ws-proxy-rs v2.2.1

A release about getting onto routers — running on the ones where it previously
could not, and fitting on the ones short of flash.

No configuration changes. Every CLI flag and TG_* variable is unchanged,
so is the library API. For most users this is a drop-in swap; on MIPS the binary
itself is different, see below.

Fixed

The MIPS and MIPS-LE binaries are now actually static. They never were. Both
needed two files present on the device to start at all: 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 went unnoticed for
so long.

On firmware that does not — Padavan + Entware and some Keenetic builds — the
binary refused to start, usually with an error that reads like a corrupt file or
a wrong libc rather than a missing library. If you hit that and concluded you
needed a uClibc or glibc build (#66,
#78): neither would
have helped, and neither is needed now. A static binary uses no system libc at
all.
Please try this release and say whether it works on your device.

Every Linux target this release ships is now verified to carry no dynamic
dependencies whatsoever, and the build fails rather than publishing one that
does. Cost: 41 KB, or +0.9%.

Why it took a specific fix rather than one flag

crt-static is not the default on these two targets the way it is for every
other musl target, and switching it on alone fails. rustc then names
crt1.o/crti.o/crtn.o from the self-contained directory that ships with
rust-std — which tier-3 targets do not have, and which -Z build-std does not
produce, so the link dies on files that do not exist.

-C link-self-contained=no hands that job back to gcc, which does have them,
and leaves exactly one problem: rustc asks the linker for -lunwind, the LLVM
unwinder, while these toolchains carry GCC's libgcc_eh.a. Same ABI, different
name. Cross.toml aliases one to the other, locating it with
-print-file-name rather than a hardcoded path.

New

UPX-packed release assets for every Linux musl target (#98),
alongside the usual downloads as tg-ws-proxy-<target>-upx.tar.gz. Measured
from the release pipeline, not estimated:

Target Plain Packed
mipsel-unknown-linux-musl 4.60 MiB 1.38 MiB −70%
mips-unknown-linux-musl 4.58 MiB 1.36 MiB −70%
armv7-unknown-linux-musleabihf 3.45 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 −63%

Note 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.6 MB ~1.38 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
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.

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-rs
    has 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 printed tg:// link is not the container's
    unreachable bridge address.
  • --check is in the flag table. It was used in the examples but missing
    from the reference.
  • Two crypto backends were being compiled, not one. rustls' default
    features pulled in aws-lc-rs alongside ring through tokio-tungstenite.
    To be clear about the effect: the binary is byte-for-byte identical, so it was
    never actually linked in — but a large C library was being built on every
    compile. Together with dropping tokio's full feature, the dependency tree
    goes 180 → 163 crates. No functional change.

Known limitations

Memory use is 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 that runs on pull requests instead of
only on a tag, which is where the numbers above come from. Every binary is
checked for dynamic dependencies and then executed under qemu-user — both
before and after packing — and made to print its usage. upx -t alone proves
only that the payload round-trips, not that the result still runs on the target
ABI.