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
commit 93db4d9b3585a57f9b070ff1d746b723d6f9b7ba
parent 15e8e4f89a3fb5c9615a50cacd3c933dd25bec7e
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Thu, 6 Aug 2026 15:05:34 +0900

panic left routing rules behind, found by running it on a real host

sing-box installs several ip rules that share a priority -- on this host two
sat at 9003 -- and `ip rule del priority N` removes exactly one. The sweep made
a single pass per priority, so after `transtation-panic` reported success three
rules were still there, including a detached one pointing at the deleted tun
interface. Every copy of the sweep is fixed: the host script, the panic verb in
the client image, the copy install.sh writes, the banner the client prints on
every boot, and the manual commands in TROUBLESHOOTING.

Verified live end to end afterwards, against the production Tokyo server:
egress moved 198.51.100.23 -> 192.0.2.7, naver stayed on its real Korean
address through the split tunnel, SIGKILLing the client left curl timing out
rather than leaking, and panic restored direct egress with zero rules, no tun
interface and no nftables table left -- while Tailscale rules at 52xx were
untouched and another container kept working.

Diffstat:
MREADME.md | 12++++++------
MTROUBLESHOOTING.md | 3++-
Mclient/transtation-client | 12+++++++++---
Mhost/transtation-panic | 10++++++++--
Minstall.sh | 8++++----
5 files changed, 29 insertion(+), 16 deletion(-)

diff --git a/README.md b/README.md
@@ -4,8 +4,8 @@ Your own VLESS + REALITY proxy, with optional Cloudflare WARP egress, in one
 container on any Linux box that has Docker.
 
 ```
-curl -fsSLO https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.1/install.sh
-sha256sum install.sh      # compare against the hash in the v1.0.1 release notes
+curl -fsSLO https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.2/install.sh
+sha256sum install.sh      # compare against the hash in the v1.0.2 release notes
 less install.sh           # please actually read it
 sudo sh install.sh
 ```
@@ -45,7 +45,7 @@ transtation exists because of a narrower set of preferences:
 Two-step form above is the recommended one. If you are going to pipe it anyway:
 
 ```
-curl -fsSL https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.1/install.sh | sudo sh
+curl -fsSL https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.2/install.sh | sudo sh
 ```
 
 The installer needs root, refuses anything other than x86_64/aarch64, installs
@@ -59,7 +59,7 @@ If you already run Docker and prefer to do it yourself:
 # /opt/transtation/docker-compose.yml
 services:
   proxy:
-    image: ghcr.io/sqzer-x/transtation:v1.0.1
+    image: ghcr.io/sqzer-x/transtation:v1.0.2
     container_name: transtation
     restart: unless-stopped
     ports: ["443:8443"]
@@ -128,7 +128,7 @@ install -Dm600 /dev/stdin /etc/transtation/uri   # paste the vless:// link, then
 docker run -d --name transtation-client --restart unless-stopped \
   -p 127.0.0.1:1080:1080 \
   -v /etc/transtation:/etc/transtation:ro \
-  ghcr.io/sqzer-x/transtation-client:v1.0.1
+  ghcr.io/sqzer-x/transtation-client:v1.0.2
 ```
 
 ```
@@ -162,7 +162,7 @@ docker run -d --name transtation-client --restart unless-stopped \
   -e MODE=tun \
   -e DIRECT_SUFFIXES=example.com,example.net \
   -v /etc/transtation:/etc/transtation:ro \
-  ghcr.io/sqzer-x/transtation-client:v1.0.1
+  ghcr.io/sqzer-x/transtation-client:v1.0.2
 ```
 
 Exactly three extra flags. `--privileged` is **not** needed and must not be
diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md
@@ -139,7 +139,8 @@ If `transtation-panic` is not installed, the same thing by hand:
 
 ```
 sudo nft delete table inet transtation
-for p in $(seq 9000 9010); do sudo ip rule del priority $p 2>/dev/null; done
+# several rules can share one priority, so delete until each one is empty
+for p in $(seq 9000 9010); do while sudo ip rule del priority $p 2>/dev/null; do :; done; done
 sudo ip link del tt0
 ```
 
diff --git a/client/transtation-client b/client/transtation-client
@@ -285,7 +285,8 @@ panic_banner() {
 
 		  ┌─ IF YOUR NETWORK EVER DIES, RUN THIS ─────────────────────────────┐
 		  │   sudo nft delete table inet transtation 2>/dev/null              │
-		  │   for p in \$(seq 9000 9010); do sudo ip rule del priority \$p; done │
+		  │   for p in \$(seq 9000 9010); do while sudo ip rule del priority \$p;  │
+		  │     do :; done; done                                             │
 		  │   sudo ip link del $TUN_IFACE                                              │
 		  └───────────────────────────────────────────────────────────────────┘
 		  (or:  sudo transtation-panic  --  or the 'panic' verb of this image)
@@ -300,10 +301,15 @@ cmd_panic() {
 	nft delete table ip6 transtation 2>/dev/null || true
 	# sing-box's own ip rules live at priority 9000-9010 and are NOT bound to
 	# the interface, so they outlive a SIGKILLed container.
+	# Several rules can share one priority and `ip rule del priority N` removes
+	# exactly one, so one pass per priority leaves the rest behind. Verified on
+	# a live host, where two rules sat at 9003.
 	_p=9000
 	while [ "$_p" -le 9010 ]; do
-		ip -4 rule del priority "$_p" 2>/dev/null || true
-		ip -6 rule del priority "$_p" 2>/dev/null || true
+		_n=0
+		while [ "$_n" -lt 20 ] && ip -4 rule del priority "$_p" 2>/dev/null; do _n=$((_n + 1)); done
+		_n=0
+		while [ "$_n" -lt 20 ] && ip -6 rule del priority "$_p" 2>/dev/null; do _n=$((_n + 1)); done
 		_p=$((_p + 1))
 	done
 	ip route flush table 2022 2>/dev/null || true
diff --git a/host/transtation-panic b/host/transtation-panic
@@ -13,8 +13,14 @@ TUN_IFACE=${TUN_IFACE:-tt0}
 nft delete table inet transtation 2>/dev/null
 nft delete table ip6 transtation 2>/dev/null
 for p in $(seq 9000 9010); do
-	ip -4 rule del priority "$p" 2>/dev/null
-	ip -6 rule del priority "$p" 2>/dev/null
+	# Several rules can share one priority and `ip rule del priority N` removes
+	# exactly one, so a single pass leaves the rest behind -- verified on a live
+	# host, where two rules sat at 9003. Bounded so a misbehaving iproute2
+	# cannot spin here.
+	n=0
+	while [ "$n" -lt 20 ] && ip -4 rule del priority "$p" 2>/dev/null; do n=$((n + 1)); done
+	n=0
+	while [ "$n" -lt 20 ] && ip -6 rule del priority "$p" 2>/dev/null; do n=$((n + 1)); done
 done
 ip route flush table 2022 2>/dev/null
 ip -6 route flush table 2022 2>/dev/null
diff --git a/install.sh b/install.sh
@@ -15,12 +15,12 @@
 # last line. If the download is truncated, nothing runs.
 set -eu
 
-VERSION=${TT_VERSION:-v1.0.1}
+VERSION=${TT_VERSION:-v1.0.2}
 IMAGE_SERVER=${TT_IMAGE_SERVER:-ghcr.io/sqzer-x/transtation}
 IMAGE_CLIENT=${TT_IMAGE_CLIENT:-ghcr.io/sqzer-x/transtation-client}
 # Filled in by the release workflow. A tag is mutable -- the same stolen-token
 # compromise we already concede for Xray's .dgst would let someone retag
-# v1.0.1, and the hash-verified installer you read would vouch for nothing.
+# v1.0.2, and the hash-verified installer you read would vouch for nothing.
 SERVER_DIGEST=${TT_SERVER_DIGEST:-}
 CLIENT_DIGEST=${TT_CLIENT_DIGEST:-}
 
@@ -316,8 +316,8 @@ install_client() {
 			nft delete table inet transtation 2>/dev/null
 			nft delete table ip6 transtation 2>/dev/null
 			for p in $(seq 9000 9010); do
-			  ip -4 rule del priority "$p" 2>/dev/null
-			  ip -6 rule del priority "$p" 2>/dev/null
+			  n=0; while [ "$n" -lt 20 ] && ip -4 rule del priority "$p" 2>/dev/null; do n=$((n+1)); done
+			  n=0; while [ "$n" -lt 20 ] && ip -6 rule del priority "$p" 2>/dev/null; do n=$((n+1)); done
 			done
 			ip route flush table 2022 2>/dev/null
 			ip -6 route flush table 2022 2>/dev/null