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 15e8e4f89a3fb5c9615a50cacd3c933dd25bec7e
parent 9bc8c1632ec758b29f1bb2d549af45690dc177f8
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Thu, 6 Aug 2026 14:46:34 +0900

The installer never ran on Debian or Ubuntu

Traced on a real Ubuntu VPS: the script printed nothing and exited 2, at

+ main
+ shift

`shift` with no positional parameters is a special-builtin error, and POSIX
says a shell exits on those. dash -- /bin/sh on Debian and Ubuntu, the primary
target -- does exactly that, so `sh install.sh` with no arguments died before
the first line of output. The `|| true` next to it never had a chance. bash is
forgiving here and bash is /bin/sh on the machine every earlier test ran on,
which is precisely why nothing caught it.

The regression test runs the real installer as a non-root user under dash in
debian:stable-slim and under busybox ash in alpine, and requires the "needs
root" message rather than merely a non-zero exit, so a silent death fails it.

Also refuses an IPv6 server address in the killswitch instead of splicing it
into an `ip daddr` rule, which nft rejects -- leaving the host with no
killswitch at all rather than a broken one. transtation is IPv4-only end to
end: the client resolves with getent ahostsv4 and the tun DNS strategy is
ipv4_only.

Diffstat:
MREADME.md | 12++++++------
Mhost/transtation-killswitch | 13+++++++++++--
Minstall.sh | 19+++++++++++++------
Mtest.sh | 14++++++++++++++
4 files changed, 44 insertion(+), 14 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.0/install.sh
-sha256sum install.sh      # compare against the hash in the v1.0.0 release notes
+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
 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.0/install.sh | sudo sh
+curl -fsSL https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.1/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.0
+    image: ghcr.io/sqzer-x/transtation:v1.0.1
     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.0
+  ghcr.io/sqzer-x/transtation-client:v1.0.1
 ```
 
 ```
@@ -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.0
+  ghcr.io/sqzer-x/transtation-client:v1.0.1
 ```
 
 Exactly three extra flags. `--privileged` is **not** needed and must not be
diff --git a/host/transtation-killswitch b/host/transtation-killswitch
@@ -43,9 +43,18 @@ server_endpoint() {
 		*) die "cannot parse a host:port out of $URI_FILE" ;;
 	esac
 	case "$_h" in
+		*:*)
+			# An IPv6 literal would be spliced into an `ip daddr` rule and nft
+			# would reject the whole ruleset, leaving the host with no killswitch
+			# at all rather than a broken one. transtation is IPv4-only end to end
+			# anyway: the client resolves the server with getent ahostsv4 and the
+			# tun DNS strategy is ipv4_only.
+			die "the server address in $URI_FILE is IPv6 ($_h).
+    transtation is IPv4-only for now, so the killswitch cannot be built for it.
+    Use an IPv4 address or a name with an A record, or set SERVER_IP yourself." ;;
 		*[!0-9.]*)
-			_h=$(getent ahostsv4 "$_h" | awk 'NR==1 {print $1}')
-			[ -n "$_h" ] || die "cannot resolve the server address in $URI_FILE"
+			_h=$(getent ahostsv4 "$_h" 2>/dev/null | awk 'NR==1 {print $1}')
+			[ -n "$_h" ] || die "cannot resolve the server address in $URI_FILE to an IPv4 address"
 			;;
 	esac
 	SERVER_IP=${SERVER_IP:-$_h}
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.0}
+VERSION=${TT_VERSION:-v1.0.1}
 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.0, and the hash-verified installer you read would vouch for nothing.
+# v1.0.1, and the hash-verified installer you read would vouch for nothing.
 SERVER_DIGEST=${TT_SERVER_DIGEST:-}
 CLIENT_DIGEST=${TT_CLIENT_DIGEST:-}
 
@@ -408,14 +408,21 @@ uninstall() {
 }
 
 main() {
-	case "${1:-server}" in
-		server) shift 2>/dev/null || true; install_server ;;
-		client) shift; install_client "$@" ;;
+	# `shift` with no positional parameters is a special-builtin error, and a
+	# POSIX shell exits on those -- `|| true` does not save you. dash is
+	# /bin/sh on Debian and Ubuntu, so `sh install.sh` with no arguments used
+	# to die here, silently, on the primary target. bash (Arch's /bin/sh) is
+	# forgiving, which is exactly why local testing never saw it.
+	_cmd=${1:-server}
+	if [ $# -gt 0 ]; then shift; fi
+	case "$_cmd" in
+		server) install_server ;;
+		client) install_client "$@" ;;
 		--uninstall | uninstall) uninstall ;;
 		-h | --help)
 			sed -n '2,10p' "$0" 2>/dev/null || say "see https://github.com/sqzer-x/transtation"
 			;;
-		*) die "unknown command: $1 (expected: server, client, --uninstall)" ;;
+		*) die "unknown command: $_cmd (expected: server, client, --uninstall)" ;;
 	esac
 }
 
diff --git a/test.sh b/test.sh
@@ -105,6 +105,20 @@ no_geodata() {
 check "server image default user is not root" image_user_nonroot
 check "server image ships no geoip/geosite data" no_geodata
 
+echo
+echo "== the installer runs under a real POSIX /bin/sh, not just bash =="
+# `shift` with no positional parameters is a special-builtin error and a POSIX
+# shell EXITS on it, `|| true` notwithstanding. dash is /bin/sh on Debian and
+# Ubuntu, so `sh install.sh` with no arguments died there silently while every
+# bash-based test passed. Run the real thing, as a non-root user, in both
+# shells, and require the root message rather than merely a non-zero exit.
+posix_sh_ok() {
+	_o=$(docker run --rm -v "$PWD":/w:ro -u 65534 "$1" sh -c "cd /w && $2 install.sh" 2>&1) || true
+	case "$_o" in *"needs root"*) return 0 ;; *) return 1 ;; esac
+}
+check "install.sh survives dash (Debian, Ubuntu) with no arguments" posix_sh_ok debian:stable-slim dash
+check "install.sh survives busybox ash (Alpine) with no arguments" posix_sh_ok alpine:3.23 sh
+
 echo
 echo "== client config renders and sing-box accepts it =="
 docker run --rm --network none \