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.13/install.sh
-sha256sum install.sh # compare against the hash in the v1.0.13 release notes
+curl -fsSLO https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.14/install.sh
+sha256sum install.sh # compare against the hash in the v1.0.14 release notes
less install.sh # please actually read it
sudo sh install.sh
```
@@ -46,8 +46,8 @@ how you feel about a container runtime on your VPS.
### In a container (default)
```
-curl -fsSLO https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.13/install.sh
-sha256sum install.sh # compare against the hash in the v1.0.13 release notes
+curl -fsSLO https://raw.githubusercontent.com/sqzer-x/transtation/v1.0.14/install.sh
+sha256sum install.sh # compare against the hash in the v1.0.14 release notes
less install.sh # please actually read it
sudo sh install.sh
```
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.13}
+VERSION=${TT_VERSION:-v1.0.14}
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.13, and the hash-verified installer you read would vouch for nothing.
+# v1.0.14, and the hash-verified installer you read would vouch for nothing.
SERVER_DIGEST=${TT_SERVER_DIGEST:-}
CLIENT_DIGEST=${TT_CLIENT_DIGEST:-}
@@ -33,6 +33,12 @@ SINGBOX_VERSION=${TT_SINGBOX_VERSION:-1.13.16}
SINGBOX_SHA_amd64=9ff0345fde4157a6bdab45a615668d41ccc93f6d0f361108042a48b8a49a9baa
SINGBOX_SHA_arm64=3ea951c68f2eea10fd3ee8f8cc7794c12ccc7405afa99279a79e0b41cb183adf
+# Our copies of third-party binaries live here, never in /usr/local/bin. That
+# is where the official Xray installer and hand-rolled setups put theirs, and
+# writing there means overwriting somebody's working server -- and then deleting
+# it on uninstall. Which is exactly what happened once.
+PRIVLIB=/usr/local/lib/transtation
+
DIR=${TT_DIR:-/opt/transtation}
URI_DIR=/etc/transtation
RAW=https://raw.githubusercontent.com/sqzer-x/transtation/$VERSION
@@ -125,6 +131,29 @@ wait_healthy() {
# --- native server -------------------------------------------------------
+# The two server installs both own /usr/local/bin/transtation and both want the
+# same port. Installing one over the other leaves the first running and
+# orphaned -- still holding a port, still serving its own identity, no longer
+# reachable by the command. Refuse instead.
+refuse_other_server() {
+ case "$1" in
+ container)
+ [ -e /etc/systemd/system/transtation.service ] &&
+ die "a native server is already installed here.
+ Remove it first: sh install.sh --uninstall
+ Or keep it -- it is the same software."
+ ;;
+ native)
+ { [ -e "$DIR/docker-compose.yml" ] || docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx transtation; } &&
+ die "a containerised server is already installed here.
+ Remove it first: sh install.sh --uninstall
+ Or keep it -- it is the same software."
+ ;;
+ esac
+ return 0
+}
+
+
fetch_verified_xray() {
case "$(uname -m)" in
x86_64 | amd64) _xz=Xray-linux-64.zip; _wa=amd64 ;;
@@ -143,7 +172,7 @@ fetch_verified_xray() {
{ rm -rf "$_t"; die "Xray checksum mismatch -- refusing to install it"; }
command -v unzip >/dev/null 2>&1 || install_pkg unzip
unzip -q -j "$_t/$_xz" xray -d "$_t"
- install -Dm0755 "$_t/xray" /usr/local/bin/xray
+ install -Dm0755 "$_t/xray" "$PRIVLIB/xray"
_w="https://github.com/ViRb3/wgcf/releases/download/v${WGCF_VERSION}"
curl -fsSL -o "$_t/wgcf" "$_w/wgcf_${WGCF_VERSION}_linux_${_wa}" ||
@@ -151,9 +180,9 @@ fetch_verified_xray() {
curl -fsSL "$_w/checksums.txt" | grep " wgcf_${WGCF_VERSION}_linux_${_wa}\$" |
awk -v f="$_t/wgcf" '{print $1" "f}' | sha256sum -c - >/dev/null 2>&1 ||
{ rm -rf "$_t"; die "wgcf checksum mismatch -- refusing to install it"; }
- install -Dm0755 "$_t/wgcf" /usr/local/bin/wgcf
+ install -Dm0755 "$_t/wgcf" "$PRIVLIB/wgcf"
rm -rf "$_t"
- step binaries "xray $(/usr/local/bin/xray version | head -1 | cut -d' ' -f2) and wgcf ${WGCF_VERSION}, both checksum-verified"
+ step binaries "xray $("$PRIVLIB/xray" version | head -1 | cut -d' ' -f2) and wgcf ${WGCF_VERSION}, both checksum-verified, in $PRIVLIB"
}
install_pkg() {
@@ -168,6 +197,7 @@ install_pkg() {
install_server_native() {
need_root
check_arch
+ refuse_other_server native
say ""
say " transtation $VERSION -- native install, no container runtime"
say " https://github.com/sqzer-x/transtation"
@@ -178,7 +208,7 @@ install_server_native() {
command -v curl >/dev/null 2>&1 || die "curl is required"
fetch_verified_xray
- install -Dm0755 "$(server_script)" /usr/local/lib/transtation/server
+ install -Dm0755 "$(server_script)" "$PRIVLIB/server"
id -u transtation >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin transtation 2>/dev/null ||
adduser -S -D -H -s /sbin/nologin transtation 2>/dev/null || true
@@ -194,13 +224,13 @@ install_server_native() {
#!/bin/sh
set -eu
if [ "$(id -un)" = transtation ]; then
- exec env TT_DATA=/var/lib/transtation /usr/local/lib/transtation/server "$@"
+ exec env TT_DATA=/var/lib/transtation PATH=/usr/local/lib/transtation:$PATH /usr/local/lib/transtation/server "$@"
fi
[ "$(id -u)" = 0 ] || { echo "transtation: run this as root or as the transtation user" >&2; exit 1; }
if command -v runuser >/dev/null 2>&1; then
- exec runuser -u transtation -- env TT_DATA=/var/lib/transtation /usr/local/lib/transtation/server "$@"
+ exec runuser -u transtation -- env TT_DATA=/var/lib/transtation PATH=/usr/local/lib/transtation:$PATH /usr/local/lib/transtation/server "$@"
fi
- exec su -s /bin/sh transtation -c 'exec env TT_DATA=/var/lib/transtation /usr/local/lib/transtation/server "$@"' -- sh "$@"
+ exec su -s /bin/sh transtation -c 'exec env TT_DATA=/var/lib/transtation PATH=/usr/local/lib/transtation:$PATH /usr/local/lib/transtation/server "$@"' -- sh "$@"
WRAP
step config "wrote /etc/transtation/server.env, data in /var/lib/transtation"
@@ -215,6 +245,7 @@ install_server_native() {
User=transtation
Group=transtation
Environment=TT_DATA=/var/lib/transtation
+ Environment=PATH=/usr/local/lib/transtation:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
EnvironmentFile=-/etc/transtation/server.env
ExecStart=/usr/local/lib/transtation/server run
Restart=on-failure
@@ -293,6 +324,7 @@ server_script() {
install_server() {
need_root
check_arch
+ refuse_other_server container
say ""
say " transtation $VERSION -- your own VLESS + REALITY proxy"
say " https://github.com/sqzer-x/transtation"
@@ -506,9 +538,9 @@ install_sing_box() {
echo "$_sum $_t/sb.tgz" | sha256sum -c - >/dev/null 2>&1 ||
{ rm -rf "$_t"; die "sing-box checksum mismatch -- refusing to install it"; }
tar xzf "$_t/sb.tgz" -C "$_t"
- install -Dm0755 "$_t/$_n/sing-box" /usr/local/bin/sing-box
+ install -Dm0755 "$_t/$_n/sing-box" "$PRIVLIB/sing-box"
rm -rf "$_t"
- SINGBOX=/usr/local/bin/sing-box
+ SINGBOX=$PRIVLIB/sing-box
step sing-box "$($SINGBOX version | head -1) (verified, statically linked)"
}
@@ -686,7 +718,7 @@ uninstall() {
systemctl disable --now transtation.service >/dev/null 2>&1 || true
rm -f /etc/systemd/system/transtation.service
systemctl daemon-reload 2>/dev/null || true
- rm -rf /usr/local/bin/transtation /usr/local/lib/transtation /usr/local/bin/xray /usr/local/bin/wgcf
+ rm -rf /usr/local/bin/transtation "$PRIVLIB"
say "native server removed. Its data is still in /var/lib/transtation."
ask 'also delete /var/lib/transtation? That destroys your Reality key and every issued link. [y/N] '
say ""
@@ -700,6 +732,8 @@ uninstall() {
rm -f /etc/systemd/system/transtation-client.service
systemctl daemon-reload 2>/dev/null || true
rm -f /usr/local/bin/transtation-client
+ rm -f "$PRIVLIB/sing-box"
+ rmdir "$PRIVLIB" 2>/dev/null || true
say "client removed."
fi
rm -f /usr/local/sbin/transtation-panic
diff --git a/server/transtation b/server/transtation
@@ -932,10 +932,12 @@ cmd_selftest() {
# Same injection class as the client's link parser, arriving by environment.
_env_rejects() {
- (SNI=www.cloudflare.com DEST="" PORT=443
+ (SNI=www.cloudflare.com DEST="" PORT=443 HOST=203.0.113.10
eval "$1=\$2"
validate_config_env) >/dev/null 2>&1 && return 1 || return 0
}
+ _assert "an IPv6 HOST is refused (the rest of the stack is IPv4-only)" \
+ _env_rejects HOST '2406:da14:157d:8900::1'
_assert "MIN_CLIENT_VER is not free text" _env_rejects MIN_CLIENT_VER '0.0.0", "show": true, "x": "'
_assert "WARP_ENDPOINT must be an IPv4 address and port" _env_rejects WARP_ENDPOINT 'evil.example:2408'
@@ -975,6 +977,12 @@ cmd_selftest() {
# meaningfully more trustworthy than a link someone sends you, and each check is
# one line. Kept as its own function so the selftest can attack it directly.
validate_config_env() {
+ case "${HOST:-}" in
+ *:*) die "HOST is an IPv6 address ($(q "$HOST")). transtation is IPv4-only for now:
+ the client resolves A records only and the killswitch refuses an IPv6
+ server, so a link naming this address would not work. Set HOST to an IPv4
+ address or a name with an A record." ;;
+ esac
printf '%s' "$SNI" | grep -Eq '^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$' ||
die "SNI must be a hostname, got: $(q "$SNI")"
case "$SNI" in
@@ -1041,11 +1049,18 @@ cmd_run() {
fi
# --- where are we? one request, used for both the address and the region ---
- _trace=$(curl -fsS --max-time 8 https://cloudflare.com/cdn-cgi/trace 2>/dev/null) || _trace=""
+ # -4 is not optional. On a dual-stack host curl prefers IPv6, so the address
+ # discovered here -- the one that goes into every share link -- came back as
+ # an IPv6 literal, which nothing else in this stack can use: the client
+ # resolves A records only, its DNS strategy is ipv4_only, and the killswitch
+ # refuses an IPv6 server outright. A container hid this, because Docker's
+ # default bridge is IPv4-only.
+ _trace=$(curl -4 -fsS --max-time 8 https://cloudflare.com/cdn-cgi/trace 2>/dev/null) || _trace=""
[ -n "${HOST:-}" ] || HOST=$(printf '%s' "$_trace" | sed -n 's/^ip=//p')
[ -n "${HOST:-}" ] || die \
- "could not auto-detect this host's public address.
- Set HOST=<your.ip.or.domain> in your .env and start again."
+ "could not auto-detect this host's public IPv4 address.
+ Set HOST=<your.ip.or.domain> in $(env_hint) and start again.
+ (transtation is IPv4-only for now; an IPv6-only host is not supported.)"
LOC=$(printf '%s' "$_trace" | sed -n 's/^loc=//p')
# --- SNI: env override, else the sticky auto-selection, else pick now ---
diff --git a/test.sh b/test.sh
@@ -56,6 +56,16 @@ check "the compose block caps the log size (xray logs to stdout)" present_in_com
check "the compose block uses a named volume, not a bind mount" present_in_compose 'transtation-data:/data'
check "no script ever runs 'nft flush ruleset'" uncommented_absent 'flush ruleset' host/transtation-killswitch
+# /usr/local/bin/xray is where the official Xray installer and most hand-rolled
+# setups put their binary. Installing ours there overwrote a working server, and
+# uninstalling then deleted it -- taking that server down. Ours live in a
+# private directory and the uninstall only ever removes that.
+no_clobber() { ! grep -qE "install -Dm?0?755 [^ ]+ /usr/local/bin/(xray|wgcf|sing-box)" install.sh; }
+no_foreign_rm() { ! grep -qE "rm -[rf]+ .*/usr/local/bin/(xray|wgcf|sing-box)" install.sh; }
+check "the installer never writes third-party binaries into /usr/local/bin" no_clobber
+check "the uninstaller never deletes third-party binaries it did not install" no_foreign_rm
+
+
# The version is repeated in the installer and in every documented command.
# It drifted five releases once, because bumping it was a hand-run sed, and the
# README ended up telling people to install a version without the security
|