Skip to content

v1.9.0

Latest

Choose a tag to compare

@github-actions github-actions released this 23 Oct 11:35
· 34 commits to main since this release

Two kinds of work since v1.8.0. A panel can now govern a user the way it sells them: a speed for each direction, which of their devices is active, and a kick for one device. And the ee door gets its speed back without giving up its disguise: an opt-in way to pace records that downloads at the speed of the link, an ad-tag leg rebuilt to stop costing a system call per 16 bytes, replay protection on by default, and a fake certificate sized like the real cover site's.

Do I need to do anything?

No. Nothing is renamed or removed, and release asset names are unchanged. Three defaults change, and each has a switch:

  • Replay protection is on unless [defense.anti-replay] enabled = false. Until now it was off unless the config said otherwise, so every panel-written config, and any config without that table, ran without it.
  • The proxy fetches https://<host>/ from each cover site its ee secrets name, once at startup and then every 6 hours, to size its fake certificate. To stop it, set [defense.doppelganger] scout-covers = false.
  • An idle ee connection holds about 9 KiB more memory (77 KiB, up from 68), because the record reader's buffer grew from 4 KiB to 16 KiB.

ee pacing does not change unless you ask: shaping defaults to "record", exactly as before.

For a panel: governing a user

  • Speed per direction. speed-up and speed-down in [secret-limits] (speed_up and speed_down on the API) are each their own bucket, on top of the combined speed when that is set too. They read back as speed_limit_up and speed_limit_down, and throttled_ms counts time held to any of the three.
  • Which device is active. connected_ips_last_seen sits beside connected_ips and gives the second of each address's latest byte.
  • Kick one device. POST /secrets/{name}/kick closes the user's streams from the addresses you name, matched at the /64 the address limit counts, and leaves their other devices connected. Naming a place the user is not at kicks nothing; it is not an error.
  • Feature strings on /stats: speed-directions, client-ip-activity and kick for the above, plus quota-ceiling (the v1.8.0 data-path quota) and secret-read (GET /secrets/{name}), which had none.
  • mtg-proxy run --check <config> parses and validates a file exactly as run does, refuses any key this binary does not know, and exits without binding a socket. It answers "will this build read every key I write?" before you swap a binary in.

ee at full speed: shaping = "burst", opt-in

Until now the ee door waited a delay drawn from a web page's timing before every 16 KiB TLS record. That capped a download near 20 Mbit/s per connection and added milliseconds to every exchange. [defense.doppelganger] shaping = "burst" waits once when a connection wakes from a second of quiet, then sends records back to back, the way a server streams a response. On a 2-vCPU Linux box over loopback:

record (default) burst
Download, one connection 2.1–2.3 MiB/s 462–506 MiB/s (dd: 388–476)
Round trip, p99 84–104 ms 0.17–0.26 ms

Each record's size is drawn the same way in both modes. burst sends more short records, because less has queued when nothing waits before each one. Timing and record sizes are things a censor can see, so burst stays opt-in until it has run on real networks without connections being blocked.

Both modes now bound what a slow client can pile up. At most 256 KiB waits for a client that stops reading; the buffer holding it peaks near 512 KiB, and is let go after a quiet second. Past that bound the proxy stops reading from Telegram instead of growing its memory.

Uploads on ee are 18–32% faster and allocate 72% less: the record reader now reads several records per call, straight into the caller's buffer.

Ad tags at full speed

The leg between the proxy and Telegram's middle proxies, which every ad-tagged secret uses, read and decrypted 16 bytes per system call and allocated once per AES block. Now:

  • One read takes about 16 KiB, about 900 times fewer system calls for downloads.
  • Each packet allocates nothing, or one small buffer, down from about 1,000 allocations.
  • Throughput is 4× to 42× higher: about 343 MiB/s for 16 KiB answers, up from 8.

The bytes on the wire are identical. They are pinned by golden files captured before the rewrite, and an ee client with an ad tag is tested end to end through a real middle-proxy exchange.

A fake certificate sized like the real one

The ee handshake's single application-data record stands in for the real server's certificate chain, so its size should be the cover site's. It was sized from a generic 2,500–4,700 bytes unless [defense.doppelganger] urls was set, and even then one size served every host. Now, with no urls, the proxy measures each cover host its ee secrets name. It dials the address domain fronting would use, so a host that resolves back to this server is not measured against itself. Each client's handshake is then sized like the site in that client's SNI. A host is measured as soon as a secret names it, and uses the generic size until then. Only the size is taken from the site; record timing keeps the built-in model.

Fixes

  • ee streams lost the end of an answer. When Telegram answered and hung up, the FIN overtook the records still waiting to be paced out. On Linux an ee client could receive as little as 3–13% of a 512 KiB answer. The rest is now delivered before the FIN, bounded by 5 s for a client that stopped reading. dd and plain were not affected.
  • The relay closed the wrong halves when a stream ended. Two arguments were swapped since the mtg-multi import, so the FIN reached the wrong peer first. On Linux it was masked; on Windows streams hung until the idle timeout. A stream now ends by delivering, then closing.
  • The doppelganger's timing fit. With urls configured, one measured gap of zero — records that arrive in one read — pinned the fitted model to waits of minutes, or to negative waits. Zero gaps are now left out, a degenerate fit is refused and the current model kept, and no wait exceeds one second.

Build

  • Go 1.26.8 everywhere: the image, the release tarballs and CI. 1.26.7 fixed net/http, which the management API and the WEB relay serve.
  • golang.org/x/crypto v0.56.0. Three advisories against v0.53.0 are fixed; none was reachable from this code.
  • golangci-lint 2.13.2, the version the lint config is written for.

Compatibility

Additive in shape:

  • No config key or API field is renamed or removed, and release asset names are unchanged.
  • The default accept-modes is still ["ee"], and the default shaping is still "record".
  • New keys: [defense.doppelganger] shaping and scout-covers, and speed-up and speed-down in [secret-limits].
  • New fields: speed_limit_up, speed_limit_down and connected_ips_last_seen, and five feature strings.

Different in behaviour:

  • Replay protection is on without configuration. About one genuine connection attempt in a thousand is taken for a replay and sent to the cover site, and the client retries.
  • The proxy fetches its cover sites' front pages every 6 hours.
  • Idle ee connections hold about 9 KiB more.

Full Changelog: v1.8.0...v1.9.0