transtation.git

transtation

~ Your own VLESS + REALITY proxy with optional Cloudflare WARP egress. Server in a container or native; native client with a fail-closed killswitch.
git clone https://github.com/debuman/transtation.git

README.md (26938B)

      1 # transtation
      2 
      3 Your own VLESS + REALITY proxy with optional Cloudflare WARP egress, on any
      4 Linux box. The server runs in a container or natively, whichever you prefer;
      5 the client is a native install, because putting a machine behind a proxy means
      6 owning its routing table and a container is the wrong shape for that.
      7 
      8 ```
      9 curl -fsSLO https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.18/install.sh
     10 sha256sum install.sh      # compare against the hash in the v1.0.18 release notes
     11 less install.sh           # please actually read it
     12 sudo sh install.sh
     13 ```
     14 
     15 That is the whole server install. It generates its own Reality keypair and its
     16 first user, registers a WARP identity, and prints the connection details as a
     17 `vless://` link with a QR of the same link.
     18 
     19 ---
     20 
     21 ## This is not a new idea, and you should know the alternatives
     22 
     23 [`reality-ezpz`](https://github.com/aleskxyz/reality-ezpz) already does Reality
     24 + WARP + share link + QR over Docker Compose. [`3x-ui`](https://github.com/MHSanaei/3x-ui)
     25 owns the web-panel end of this space with 44k stars and does far more than this
     26 does. If you want a panel, multiple users with quotas, subscription links or a
     27 Telegram bot, **use 3x-ui and stop reading here.**
     28 
     29 transtation exists because of a narrower set of preferences:
     30 
     31 - **A published, digest-pinned image** if you want one, rather than a
     32   2,700-line bash generator that writes your config on the host. The native
     33   path is the same program with a systemd unit, not a second codebase.
     34 - **Zero capabilities.** The container holds no `NET_ADMIN`, mounts no
     35   `/dev/net/tun`, and does not use host networking. WARP runs in Xray's
     36   in-process userspace network stack.
     37 - **Tracking upstream.** Xray ships roughly monthly and its config schema moves.
     38   If this project stops keeping up within a year it is worse than what it
     39   replaced, and the right thing to do then is archive it and point at
     40   reality-ezpz. That is a promise, not a disclaimer.
     41 
     42 ---
     43 
     44 ## Server
     45 
     46 Two ways to run it. They are the same software with the same config; pick on
     47 how you feel about a container runtime on your VPS.
     48 
     49 ### In a container (default)
     50 
     51 ```
     52 curl -fsSLO https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.18/install.sh
     53 sha256sum install.sh      # compare against the hash in the v1.0.18 release notes
     54 less install.sh           # please actually read it
     55 sudo sh install.sh
     56 ```
     57 
     58 Installs Docker if it is missing, writes `/opt/transtation/{docker-compose.yml,.env}`,
     59 resolves the image tag to a digest and pins the compose file to it, then waits
     60 for the container to be healthy and proves the handshake works. Re-running it is
     61 how you upgrade.
     62 
     63 ### Natively
     64 
     65 ```
     66 sudo sh install.sh server --native
     67 ```
     68 
     69 No container runtime. It downloads Xray and wgcf, verifies both against
     70 upstream checksums, installs `/usr/local/bin/transtation`, keeps state in
     71 `/var/lib/transtation` owned by a dedicated `transtation` user, and runs under
     72 `transtation.service`.
     73 
     74 ### Which one
     75 
     76 Measured on the same host, same binary:
     77 
     78 | | container | native |
     79 |---|---|---|
     80 | capabilities held | **none** — `CapEff 0000000000000000` | `CAP_NET_BIND_SERVICE`, and only that |
     81 | root filesystem | read-only | `ProtectSystem=strict` |
     82 | runs as | uid 10001 | `transtation` user |
     83 | costs | Docker daemon, ~250 MB, `ip_forward=1`, published ports bypass `ufw` | needs systemd |
     84 | upgrades | pull a new digest | re-run the installer |
     85 
     86 The container needs no capability at all because dockerd performs the
     87 privileged bind on 443 and the container listens high; natively there is
     88 nothing in front, so it keeps exactly one capability and nothing else is even
     89 reachable. Both pin and verify the same Xray release.
     90 
     91 If you have no strong feeling, use the container: one artifact, dependencies
     92 verified and frozen inside it, and rollback is a digest. If you would rather not
     93 run a container runtime on a proxy server, the native path is not a lesser
     94 option — it is the same program with a systemd unit instead of a compose file.
     95 
     96 Either way `transtation` is the command:
     97 
     98 ### Day to day
     99 
    100 ```
    101 transtation show              # share link + QR for every user
    102 transtation status            # address, SNI, egress IP, WARP state, users
    103 transtation user add alice    # new UUID + shortId, applied immediately
    104 transtation user rm alice     # revokes both
    105 transtation backup            # tar.gz of everything irreplaceable, mode 0600
    106 transtation restore <file>    # put one back (server must be stopped)
    107 transtation verify            # prove a client can actually complete the handshake
    108 transtation logs -f
    109 ```
    110 
    111 `transtation user add` and `user rm` restart Xray to apply. Existing connections
    112 drop and clients reconnect on their own; Xray has no config reload signal.
    113 
    114 ### Open the port
    115 
    116 The healthcheck only proves **outbound** traffic works. Nothing on the server
    117 can prove inbound does. If clients time out and the server log shows nothing at
    118 all, open TCP 443 in your provider's firewall or security group. Oracle Cloud
    119 additionally needs the port opened in the instance's own iptables.
    120 
    121 Note the difference between the two install paths here: Docker's published
    122 ports **bypass `ufw`** in both directions, so on the container path a ufw rule
    123 is neither necessary nor sufficient. The native path has no such shortcut — a
    124 host firewall applies to it normally and must permit the port.
    125 
    126 ---
    127 
    128 ## Client
    129 
    130 You may well not need this project's client. The server prints a `vless://`
    131 link, and every platform already has a good client that takes one:
    132 
    133 | Platform | Use |
    134 |---|---|
    135 | Android | v2rayNG, Hiddify |
    136 | iOS | sing-box, Streisand, Shadowrocket |
    137 | Windows / macOS | v2rayN, Hiddify, sing-box |
    138 
    139 What those do not cover well is a Linux machine you want *entirely* behind the
    140 proxy, with a fail-closed killswitch, managed by the init system. That is what
    141 this client is for, and it installs natively — a verified static `sing-box`
    142 binary, a generated config and a systemd unit. **No Docker on the client.**
    143 
    144 Capturing a machine's traffic means owning its routing table. A container
    145 handed `--network host` and `CAP_NET_ADMIN` to do that isolates nothing; it is
    146 a packaging format with extra failure modes, and it excludes every phone and
    147 desktop anyway.
    148 
    149 ### Whole-host tunnel (default)
    150 
    151 ```
    152 sudo sh install.sh client
    153 ```
    154 
    155 It asks for the link, verifies and installs sing-box, writes
    156 `transtation-client.service`, starts it, and then checks that this machine's
    157 public address actually **changed** — a reply on its own only proves the network
    158 works, which it did before you installed anything. If the address did not move,
    159 the installer says so and exits non-zero.
    160 
    161 ```
    162 stop:    sudo systemctl stop transtation-client
    163 logs:    journalctl -u transtation-client -f
    164 panic:   sudo transtation-panic        # undo everything, needs no network
    165 ```
    166 
    167 Everything the machine sends goes through the tunnel with no per-program
    168 configuration — including traffic from containers running on it.
    169 
    170 `DIRECT_SUFFIXES` is a comma-separated list of domain suffixes that bypass the
    171 tunnel entirely — for sites that block proxy IPs, or a bank that objects to a
    172 foreign exit. Those domains also resolve locally rather than through the tunnel,
    173 so you get the geographically correct answer before connecting.
    174 
    175 ```
    176 DIRECT_SUFFIXES=example.com,example.net sudo sh install.sh client --killswitch
    177 ```
    178 
    179 Needs Linux, root and systemd. The tunnel interface is created directly on the
    180 host, so there is no rootless story to get wrong and nothing to be confused
    181 about on macOS or Windows — use one of the apps above there.
    182 
    183 ### Proxy mode — a local SOCKS5 + HTTP proxy
    184 
    185 ```
    186 sudo sh install.sh client --proxy
    187 ```
    188 
    189 **This does not put your machine behind the proxy.** It opens a proxy on
    190 `127.0.0.1:1080` and nothing else. Every program that should use it has to be
    191 pointed at it, one at a time. Your browser keeps using your own address until
    192 you configure it; so does everything started from your desktop, and every
    193 systemd service.
    194 
    195 ```
    196 export ALL_PROXY=socks5h://127.0.0.1:1080
    197 export HTTPS_PROXY=http://127.0.0.1:1080 HTTP_PROXY=http://127.0.0.1:1080
    198 ```
    199 
    200 Those two lines apply to **that shell and the programs it starts**, and nothing
    201 else. Measured on a host with the client running:
    202 
    203 ```
    204 curl https://api.ipify.org                 198.51.100.23   your own address
    205 curl https://api.ipify.org   (after the exports)
    206                                            192.0.2.7       through the proxy
    207 env -i curl https://api.ipify.org          198.51.100.23   your own address
    208    … and the same from any other terminal, and from anything your desktop starts
    209 ```
    210 
    211 The **`h`** in `socks5h` is load-bearing: plain `socks5://` makes curl resolve
    212 DNS locally and leaks every hostname you visit. In Firefox, set the SOCKS host
    213 in the connection settings and tick "Proxy DNS when using SOCKS v5" — the
    214 environment variables above will not reach it.
    215 
    216 Two more things this mode will not do even for a program you *have* pointed at
    217 it: QUIC (browsers do not send UDP over SOCKS, so it silently falls back to
    218 TCP), and anything that reads no proxy setting at all.
    219 
    220 Settings, all optional:
    221 
    222 | Variable | Default | What it does |
    223 |---|---|---|
    224 | `DIRECT_SUFFIXES` | *(empty)* | comma-separated domain suffixes that bypass the tunnel |
    225 | `SOCKS_PORT` | `1080` | proxy mode's listening port |
    226 | `TT_URI_FILE` | `/etc/transtation/uri` | where the share link is read from |
    227 
    228 ### Killswitch and recovery
    229 
    230 `--killswitch` installs a host-owned nftables table with a `policy drop` output
    231 chain. It permits loopback, established connections, the tun interface, the
    232 split-tunnel fwmark, RFC1918/CGNAT/link-local/multicast, DHCP, and your server's
    233 address and port. Everything else is dropped, so nothing leaks when the tunnel
    234 is down.
    235 
    236 It is **additive** — a separate `table inet transtation` — and removing it is
    237 one command. Nothing in this project ever runs `nft flush ruleset`, which on a
    238 modern host would also wipe the iptables-nft tables Docker installs and silently
    239 break every container on the box.
    240 
    241 ```
    242 sudo transtation-killswitch on [--persist]
    243 sudo transtation-killswitch off
    244 sudo transtation-killswitch status
    245 ```
    246 
    247 Persistence is opt-in. By default the killswitch dies at reboot, because reboot
    248 is the first thing everyone tries when the network stops working.
    249 
    250 **Two limits, stated plainly.** It is an output-hook chain: it covers this host's
    251 own processes, and traffic *forwarded* from your other containers does not
    252 traverse it. And if you test it by SIGKILLing the client, note that sing-box's
    253 own ip rules at priority 9000–9010 survive — they are not bound to the
    254 interface.
    255 
    256 If your network ever dies:
    257 
    258 ```
    259 sudo transtation-panic
    260 ```
    261 
    262 Small, and it needs neither a network nor Docker. The same commands are printed
    263 in the client's startup banner every boot, so they are in
    264 `journalctl -u transtation-client` and in your scrollback.
    265 
    266 ---
    267 
    268 ## Configuration
    269 
    270 Nothing is required. Copy `.env.example` to `/opt/transtation/.env` — or, on a
    271 native install, to `/etc/transtation/server.env` — and set only what you want to
    272 change; every value is read fresh on each boot.
    273 
    274 | Variable | Default | What it does |
    275 |---|---|---|
    276 | `PORT` | `443` | public port |
    277 | `HOST` | auto-detected | address that goes into the share link |
    278 | `SNI` | *auto-selected* | the site your server impersonates — leave unset |
    279 | `CLIENT_REGION` | server's country | two-letter code for where your clients connect from |
    280 | `DEST` | `$SNI:443` | where the handshake is forwarded |
    281 | `WARP` | `1` | `0` to egress from the VPS IP instead |
    282 | `WARP_ENDPOINT` | `162.159.192.1:2408` | override if this anycast address does not handshake |
    283 | `USER_NAME` | `main` | name of the user created on first boot |
    284 | `MIN_CLIENT_VER` | `0.0.0` | Reality client version floor |
    285 | `SKIP_DEST_CHECK` | `0` | skip the startup TLS probe of `$SNI` |
    286 
    287 ### The dest picks itself
    288 
    289 The single most important setting is which real site your server impersonates,
    290 so transtation chooses it on first boot — the way a commercial VPN picks your
    291 nearest exit rather than making you read a server list.
    292 
    293 **The thing that gets you blocked is not a slow dest, it is an implausible
    294 one.** Your clients' ISP sees their traffic labelled with this name and judges
    295 the volume and shape of it against what the name implies. Hours of sustained
    296 multi-gigabyte download addressed to a corporate brochure page is not something
    297 a human does, and a DPI box that has already taken an interest in your server's
    298 address starts dropping the connection. The same bytes pulled from a video
    299 service are what everyone does all evening.
    300 
    301 So candidates carry a curated class, and it dominates the score:
    302 
    303 | class | what it is | weight |
    304 |---|---|---|
    305 | `bulk` | video, streaming, real download and CDN hosts. Sustained multi-GB transfer and hours-long connections are unremarkable. | 0 |
    306 | `mixed` | media-heavy portals. Moderate sustained traffic is fine; a torrent of it is a bit odd. | +800 |
    307 | `light` | corporate sites, marketing pages, DNS endpoints. Bulk transfer here looks exactly as wrong as it is. | +2500 |
    308 
    309 The weights are in milliseconds so they compare directly against measured
    310 latency, and they are deliberately lopsided: being dropped is fatal, a slower
    311 dest merely adds to connection setup.
    312 
    313 This class is a judgement about the internet, not something a probe can
    314 measure — `accept-ranges` was tested for the job and does not discriminate
    315 (`dl.google.com` answers `none` on its root path; `addons.mozilla.org` answers
    316 `bytes`). Everything else *is* measured, from the server, in parallel:
    317 
    318 - **hard filter** — the handshake must succeed, it must be TLS 1.3, and the
    319   certificate chain must have headroom under the size where REALITY breaks.
    320   Both halves earn their keep: `abema.tv` is an otherwise ideal Japanese video
    321   dest that serves only TLS 1.2, and `www.hulu.com` has a 4717-byte chain.
    322 - **latency** — the measured TLS handshake time, because your server re-dials
    323   this site on every single client connection.
    324 - **region** — worth 150 ms. The region that matters is where your *clients*
    325   are, not where your server is: the ISP judging the traffic is theirs.
    326   Defaults to the server's country; set `CLIENT_REGION` when they differ.
    327 
    328 ```
    329 $ transtation sni
    330   sni     tv.naver.com
    331   why     sustained heavy traffic is unremarkable here; same region as your
    332           clients; 54ms TLS handshake, 3480-byte chain
    333   since   2026-08-06
    334 ```
    335 
    336 From a Korean VPS it picks a Korean streaming service; with `CLIENT_REGION=JP`
    337 it picks `www.nicovideo.jp`; with no region it can determine at all, a global
    338 streaming brand.
    339 
    340 **The choice is sticky.** It lives with your identity, is included in backups,
    341 and is never re-picked on its own — every share link carries `sni=`, so
    342 silently changing it would break every client you have already handed out.
    343 Re-picking is explicit, and says what it invalidates:
    344 
    345 ```
    346 transtation sni auto        # probe again and re-pick   (invalidates links)
    347 transtation sni <domain>    # set by hand               (invalidates links)
    348 ```
    349 
    350 Setting `SNI=` in `.env` overrides all of it, permanently.
    351 
    352 ### Why the size of a certificate matters
    353 
    354 **Above roughly 5200 bytes of certificate chain the REALITY handshake does not
    355 complete**, and the failure is silent in the worst way: the server reports
    356 itself healthy, the port listens, and every client is quietly handed the real
    357 website instead of your proxy. Measured end to end against Xray 26.3.27:
    358 
    359 | dest | chain | | dest | chain |
    360 |---|---|---|---|---|
    361 | `dl.google.com` | 895 ✓ | | `www.ibm.com` | 4331 ✓ |
    362 | `one.one.one.one` | 2307 ✓ | | `www.paypal.com` | 4883 ✓ |
    363 | `www.cloudflare.com` | 2493 ✓ | | `www.linkedin.com` | 5046 ✓ |
    364 | `addons.mozilla.org` | 2843 ✓ | | `www.microsoft.com` | 5879 ✗ |
    365 | `www.nicovideo.jp` | 3822 ✓ | | | |
    366 
    367 The auto-selection filters on this, so you only meet it if you override `SNI`
    368 yourself. When you do:
    369 
    370 ```
    371 transtation verify
    372 ```
    373 
    374 does a genuine REALITY handshake against the server's own inbound. It is the
    375 only check that proves clients can actually connect — the healthcheck goes
    376 through a loopback inbound and never touches REALITY at all. `install.sh` runs
    377 it for you and refuses to finish if it fails.
    378 
    379 Apple and iCloud domains are rejected outright — Xray warns that they get your
    380 IP blocked by the GFW. To inspect a candidate yourself:
    381 
    382 ```
    383 docker compose exec proxy xray tls ping example.com          # container
    384 /usr/local/lib/transtation/xray tls ping example.com         # native
    385 ```
    386 
    387 ---
    388 
    389 ## Capabilities
    390 
    391 The container holds **none at all**, verified on a running server:
    392 `CapEff`, `CapPrm` and `CapBnd` all `0000000000000000`, `NoNewPrivs 1`, root
    393 filesystem read-only. The native install holds `CAP_NET_BIND_SERVICE` and
    394 nothing else, because something has to bind 443 and there is no dockerd in
    395 front to do it.
    396 
    397 | | Needed? | |
    398 |---|---|---|
    399 | `cap_add: NET_ADMIN` | no | the generated config pins `"noKernelTun": true`, so WARP always runs in Xray's in-process userspace stack regardless of what the container holds. Adding the capability buys nothing and widens the container's privileges. |
    400 | `--device /dev/net/tun` | no | the userspace network stack needs no TUN device |
    401 | `CAP_NET_BIND_SERVICE` | container: no | it listens on 8443 inside; `-p 443:8443` does the privileged bind in dockerd, which also keeps this working under rootless Docker |
    402 | `--network host` | no, and must not be used | the healthcheck's SOCKS inbound is bound to `127.0.0.1`, which inside the container reaches nothing. On the host's loopback the same listener becomes an unauthenticated proxy into your tunnel for every local process and everyone with a shell on the box. |
    403 
    404 For reference, a hand-rolled native Xray unit typically holds `CAP_NET_ADMIN`
    405 *and* `CAP_NET_BIND_SERVICE` with a writable root filesystem. The `NET_ADMIN`
    406 half is only needed for kernel-TUN WARP, which this does not use.
    407 
    408 That `noKernelTun` pin is load-bearing rather than decorative. Left unset, Xray
    409 probes for `CAP_NET_ADMIN` and takes the kernel-TUN path when it finds it — and
    410 `createKernelTun` writes to `/proc/sys/.../rp_filter` *before* opening the
    411 device, which fails on Docker's read-only `/proc/sys` with no fallback. Pinning
    412 it makes behaviour independent of capabilities.
    413 
    414 To confirm:
    415 
    416 ```
    417 docker compose logs proxy | grep -o 'Using .* TUN'      # container
    418 journalctl -u transtation | grep -o 'Using .* TUN'     # native
    419 # both -> Using gVisor TUN
    420 ```
    421 
    422 ---
    423 
    424 ## What this does not protect you from
    425 
    426 - **Your account.** The VPS is rented in your name with your card. Reality hides
    427   *what* the traffic is, not *whose* server it is.
    428 - **Traffic analysis over time.** Reality is very good against a prober that
    429   connects and looks; it is not a defence against an adversary correlating flow
    430   timing and volume across a link they fully observe.
    431 - **The endpoint.** Sites still see a browser fingerprint, cookies, and a login.
    432 - **Publishing your link.** A `vless://` link is a credential *and* a permanent
    433   fingerprint of your server. Once it is public, revoke it (`transtation user rm`)
    434   — the shortId goes with it.
    435 - **BitTorrent.** The default routing blocks *unencrypted* BitTorrent
    436   handshakes. Encrypted BT (MSE/PE, the modern default) and DHT pass straight
    437   through. You have no DMCA protection here; your provider will forward the
    438   notice to you.
    439 - **Outbound abuse from your own box.** Port 25 is blocked by default so a
    440   WARP-less install is not an open SMTP relay, and the cloud metadata range
    441   `169.254.0.0/16` is blocked so a client of your proxy cannot read your
    442   instance's IAM credentials. Everything else your users do leaves from your IP.
    443 
    444 ## About WARP
    445 
    446 With `WARP=1` (the default) traffic exits from a Cloudflare address instead of
    447 your VPS's. Your server's IP never reaches the destination, and cloud-IP
    448 blocklists do not apply to you.
    449 
    450 The costs are real: a shared exit pool means more CAPTCHAs, the exit's geography
    451 is Cloudflare's choice and not your VPS's, the free tier is throttled under
    452 sustained load, and MTU drops to 1280. Registration also depends on an
    453 undocumented third-party API. If any of that bothers you, set `WARP=0` — the
    454 proxy works exactly the same, it just egresses from your own address.
    455 
    456 If registration fails, the server **starts anyway** with direct egress and says
    457 so on every boot and in `transtation status`. It never retries in a loop:
    458 hammering Cloudflare's registration endpoint is how a source IP, historically a
    459 whole region, gets blocked.
    460 
    461 ---
    462 
    463 ## Backup, upgrade, uninstall
    464 
    465 ```
    466 transtation backup                        # -> /root/transtation-backup.tgz, mode 0600
    467 ```
    468 
    469 That tarball holds your Reality private key, your users, and your WARP account.
    470 It is the only thing on the box you cannot regenerate. Restoring is one command
    471 into a fresh volume:
    472 
    473 ```
    474 cd /opt/transtation && docker compose down     # native: systemctl stop transtation
    475 transtation restore /root/transtation-backup.tgz
    476 docker compose up -d && transtation verify     # native: systemctl start transtation
    477 ```
    478 
    479 This started life as a documented `tar` one-liner and failed twice in testing,
    480 so it is a command now. Getting it right by hand means knowing that Docker
    481 seeds a fresh named volume with the ownership of the image directory it is
    482 first mounted into — so unpacking with a generic image leaves `/data` owned by
    483 root and the server refuses to start — *and* that the backup is mode 0600, so
    484 the unpacking container has to run as root to read it at all. The native path
    485 has the mirror-image version of the same trap: unpacked as root, the files come
    486 out owned by root and the service user can no longer read its own identity.
    487 `restore` also refuses to run while the server is up, and refuses a file that is
    488 not a transtation backup.
    489 
    490 Upgrades: re-run `install.sh` (with `server --native` if that is how you
    491 installed), or `docker compose pull && docker compose up -d`.
    492 Your identity lives in the volume and survives; the config is regenerated from
    493 the new image every boot, so schema changes come along for free.
    494 
    495 ```
    496 sudo sh install.sh --uninstall
    497 ```
    498 
    499 It removes whichever server is installed — containers and compose file, or the
    500 systemd unit and `/usr/local/lib/transtation` — and asks separately before
    501 deleting your data. Binaries it did not install are left alone.
    502 
    503 ---
    504 
    505 ## About `curl | sh`
    506 
    507 It is a real objection and this project does not wave it away:
    508 
    509 1. **You cannot review what you pipe.** So the documented form downloads the
    510    script, checks its hash against the release notes, and lets you read it. Do
    511    that.
    512 2. **A truncated download could execute half a script.** Everything here lives
    513    in functions and `main "$@"` is the last line, so a truncated file runs
    514    nothing.
    515 3. **It installs Docker via `get.docker.com`**, a ~780-line third-party script,
    516    as root. The installer tells you before it does, and skips the step entirely
    517    if Docker is already present.
    518 4. **The image tag is mutable.** The installer resolves the tag to the digest
    519    it points at during installation and writes *that* into the compose file, so
    520    from then on `docker compose up` can only start the exact image the install
    521    verified. Re-running the installer is how you move to a newer one.
    522 
    523 The Xray binary is verified inside the Dockerfile against the `.dgst` file from
    524 the same GitHub release. That defends against a bad mirror or a truncated
    525 transfer, **not** against a compromised upstream — Xray publishes no signatures.
    526 sing-box publishes no checksum file at all, so its hashes are pinned in
    527 `install.sh` and reviewed as part of this repo. The native server install
    528 verifies Xray the same way the image does, against the `.dgst` from its own
    529 release, and wgcf against upstream's `checksums.txt`.
    530 
    531 ## What has been tested
    532 
    533 Everything below was run, on a Korean client against an AWS Tokyo VPS with the
    534 host's own proxy stopped, so nothing was measured through a second proxy:
    535 
    536 - **Server on a real VPS.** `install.sh` on Ubuntu 24.04: installs Docker,
    537   pulls and digest-pins the image, and `verify` completes a REALITY handshake
    538   reporting `warp=on colo=NRT`.
    539 - **Client to server across the internet, no tunnels.** A client in Korea to a
    540   Tokyo VPS with the host's own proxy stopped, so nothing was measured through a
    541   second proxy. Steady-state request latency ≈0.20 s, throughput 6.5–8.3 MB/s
    542   for a 10 MB download, and 0.9 s from starting the client to the first byte
    543   through the tunnel.
    544 - **The camouflage, from outside.** `openssl s_client` against the server on
    545   :443 returns a genuine `C=JP, O=TVer INC., CN=*.tver.jp` certificate over
    546   TLS 1.3 — the dest the auto-selection chose for a Japanese server, unprompted.
    547 - **Users.** Added a second user, connected as them, revoked them; their client
    548   stopped working and the first one kept going.
    549 - **Whole-host tunnel.** It moved the host's egress from its ISP address to
    550   WARP, kept `DIRECT_SUFFIXES` domains on their real path, and `SIGKILL` of the
    551   client left `curl` timing out rather than leaking. `transtation-panic` restored
    552   direct egress with no rules, no interface and no table left behind, and without
    553   disturbing Tailscale's rules or other containers.
    554 - **The client, natively.** `install.sh client` on this laptop: verified
    555   sing-box, systemd unit, tunnel up, host egress moved off the ISP address,
    556   `DIRECT_SUFFIXES` domains still on their real path, other containers on the
    557   host carried too, and `transtation-panic` putting it all back. Pointed at a
    558   dead server it reports "still your own address" and exits non-zero.
    559 - **arm64.** The published arm64 server image was run under emulation with a
    560   client on the other architecture passing traffic through it.
    561 - **Timings, measured rather than asserted.** On that VPS: 7.3 s from
    562   `docker compose up` to healthy on a first boot that generates keys, picks a
    563   dest and registers with WARP; 14.4 s to a proven-working proxy. Most of the
    564   gap between the two is a fixed warm-up inside Xray before its first proxied
    565   connection, not work this project controls.
    566 
    567 ## Building and testing yourself
    568 
    569 ```
    570 git clone https://github.com/sqzer-x/transtation && cd transtation
    571 sh test.sh
    572 ```
    573 
    574 Builds the server image and runs its offline self-checks: the config renders and
    575 is accepted by the pinned Xray with WARP on *and* off, the `xray x25519` parser
    576 survives all three historical output formats, every invariant that protects you
    577 (no empty shortId, port 25 blocked, metadata range blocked, loopback-only health
    578 inbound) holds, both client configs are accepted by sing-box, and malformed
    579 share links are rejected. Everything runs with `--network none`; the test suite
    580 never registers a WARP identity.
    581 
    582 ## License
    583 
    584 MIT. See [LICENSE](LICENSE). The third-party binaries this project ships or
    585 fetches — Xray-core (MPL-2.0), wgcf (MIT), sing-box (GPL-3.0-or-later) — and
    586 their obligations are listed in [NOTICE](NOTICE).