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 6b059e99395157d9479ec79dc9ccf8e9390e0514
parent fed9ed1ef108e68d1f967d3b45750412f234b6dc
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Thu, 6 Aug 2026 13:44:59 +0900

Score dests by whether the traffic they carry looks plausible

The previous selection weighed latency and certificate size, which misses the
thing that actually gets a server dropped. An ISP sees the clients traffic
labelled with the dest name and judges its volume and shape against what that
name implies. Hours of sustained multi-gigabyte transfer addressed to a
corporate brochure page is not something a human does, so DPI drops it; the
same bytes pulled from a video service are what everyone does all evening.
Moving a real deployment from www.microsoft.com to www.nicovideo.jp is what
fixed exactly this, and the algorithm now reproduces that reasoning instead of
leaving it to the operator.

Candidates carry a curated class -- bulk (video, streaming, download and CDN
hosts), mixed (media-heavy portals), light (corporate pages, DNS endpoints) --
weighted in milliseconds so it compares directly against measured latency, and
lopsided on purpose: being dropped is fatal, a slower dest only costs setup
time. The old table was full of exactly the wrong thing; from Korea it used to
pick www.kakaocorp.com, a corporate homepage. It now picks tv.naver.com, and
with CLIENT_REGION=JP it picks www.nicovideo.jp.

The class cannot be probed for. accept-ranges was tested for the job and does
not discriminate: dl.google.com answers none on its root path while
addons.mozilla.org answers bytes. Everything else is measured from the server
in parallel, and both halves of the hard filter earn their keep -- abema.tv is
an otherwise ideal Japanese video dest that serves only TLS 1.2, and
www.hulu.com has a 4717-byte chain.

The region that matters is where clients connect from, not where the server
sits, since the ISP judging the traffic is theirs. It defaults to the servers
country and CLIENT_REGION overrides it.

Scoring is a pure function with offline tests, because the weighting is the
whole design: a distant video service must beat a nearby corporate page.

Diffstat:
M.env.example | 47++++++++++++++++++++++++++++++-----------------
MREADME.md | 65+++++++++++++++++++++++++++++++++++++++++++++--------------------
Mserver/transtation | 167++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
3 files changed, 206 insertion(+), 73 deletion(-)

diff --git a/.env.example b/.env.example
@@ -23,14 +23,23 @@
 
 # The real site your server impersonates.
 #
-# LEAVE THIS UNSET. On first boot transtation probes a set of candidates from
-# the server itself and picks one, the way a commercial VPN picks your nearest
-# exit. It looks at the server's country (from cloudflare.com/cdn-cgi/trace),
-# probes same-region candidates alongside always-available anycast ones, and
-# keeps whichever passes every technical test with the lowest TLS handshake
-# latency. A same-region candidate wins ties up to 120 ms, because matching the
-# dest to the server's own region is what makes the traffic look ordinary to a
-# DPI box that is already suspicious of the address.
+# LEAVE THIS UNSET. On first boot transtation picks one, the way a commercial
+# VPN picks your nearest exit rather than handing you a server list.
+#
+# What it optimises for, in order:
+#
+#  1. Can the name carry your traffic without looking wrong? Your clients' ISP
+#     sees their traffic labelled with this name and judges the VOLUME AND
+#     SHAPE of it against what the name implies. Hours of sustained multi-
+#     gigabyte download addressed to a corporate brochure page is not a
+#     plausible thing for a human to be doing, and DPI drops it. The same bytes
+#     pulled from a video service are what everyone does all evening. So
+#     candidates are classed bulk / mixed / light, and this dominates the score.
+#  2. Does REALITY actually work with it? TLS 1.3 and a certificate chain with
+#     headroom under the size where the handshake silently dies.
+#  3. How far away is it? Measured TLS handshake time, because your server
+#     re-dials this site on every client connection.
+#  4. Is it in the same region as the traffic? Worth 150ms of latency.
 #
 # The choice is STICKY. It is stored in the data volume and never re-picked on
 # its own, because every share link carries `sni=` and silently changing it
@@ -40,21 +49,25 @@
 #   transtation sni auto         probe again and re-pick   (invalidates links)
 #   transtation sni <domain>     set by hand               (invalidates links)
 #
-# Set this variable only to override that permanently. Two hard requirements if
-# you do: TLS 1.3, and a certificate chain under about 5200 bytes -- above that
-# the REALITY handshake does not complete and the failure is silent, with the
-# server reporting healthy while every client gets the real website instead.
-# `www.microsoft.com` (5879 bytes) is the notable trap. Always follow an
-# override with `transtation verify`.
-#
-# Apple and iCloud domains are rejected outright: Xray warns that those targets
-# get your IP blocked by the GFW.
+# Set this only to override permanently, and follow it with `transtation
+# verify`. Two hard requirements: TLS 1.3, and a chain under about 5200 bytes.
+# `www.microsoft.com` (5879) fails the second one silently -- healthy server,
+# and every client gets the real website instead. Apple and iCloud domains are
+# rejected outright: Xray warns they get your IP blocked by the GFW.
 #SNI=dl.google.com
 
 # The address the Reality handshake is actually forwarded to. Defaults to
 # $SNI:443 and you almost never need to set this separately.
 #DEST=dl.google.com:443
 
+# Where your clients connect FROM, as a two-letter country code. The DPI that
+# judges your traffic belongs to their ISP, not to your server's datacentre, so
+# this is the region the dest should look native to. Defaults to the server's
+# own country, which is right when your server is near or relevant to your
+# users. Set it when it is not -- clients in Korea using a server in Brazil,
+# say.
+#CLIENT_REGION=KR
+
 # ------------------------------------------------------------------- egress
 
 # Cloudflare WARP egress. On by default: your traffic leaves from a Cloudflare
diff --git a/README.md b/README.md
@@ -237,6 +237,7 @@ what you want to change; every value is read fresh on each boot.
 | `PORT` | `443` | public port |
 | `HOST` | auto-detected | address that goes into the share link |
 | `SNI` | *auto-selected* | the site your server impersonates — leave unset |
+| `CLIENT_REGION` | server's country | two-letter code for where your clients connect from |
 | `DEST` | `$SNI:443` | where the handshake is forwarded |
 | `WARP` | `1` | `0` to egress from the VPS IP instead |
 | `WARP_ENDPOINT` | `162.159.192.1:2408` | override if this anycast address does not handshake |
@@ -247,36 +248,60 @@ what you want to change; every value is read fresh on each boot.
 ### The dest picks itself
 
 The single most important setting is which real site your server impersonates,
-so transtation chooses it for you on first boot — the way a commercial VPN
-picks your nearest exit rather than making you read a server list.
+so transtation chooses it on first boot — the way a commercial VPN picks your
+nearest exit rather than making you read a server list.
 
-It reads the server's own country from `cloudflare.com/cdn-cgi/trace`, then
-probes same-region candidates alongside always-available anycast ones, in
-parallel, and scores what survives:
+**The thing that gets you blocked is not a slow dest, it is an implausible
+one.** Your clients' ISP sees their traffic labelled with this name and judges
+the volume and shape of it against what the name implies. Hours of sustained
+multi-gigabyte download addressed to a corporate brochure page is not something
+a human does, and a DPI box that has already taken an interest in your server's
+address starts dropping the connection. The same bytes pulled from a video
+service are what everyone does all evening.
+
+So candidates carry a curated class, and it dominates the score:
+
+| class | what it is | weight |
+|---|---|---|
+| `bulk` | video, streaming, real download and CDN hosts. Sustained multi-GB transfer and hours-long connections are unremarkable. | 0 |
+| `mixed` | media-heavy portals. Moderate sustained traffic is fine; a torrent of it is a bit odd. | +800 |
+| `light` | corporate sites, marketing pages, DNS endpoints. Bulk transfer here looks exactly as wrong as it is. | +2500 |
+
+The weights are in milliseconds so they compare directly against measured
+latency, and they are deliberately lopsided: being dropped is fatal, a slower
+dest merely adds to connection setup.
+
+This class is a judgement about the internet, not something a probe can
+measure — `accept-ranges` was tested for the job and does not discriminate
+(`dl.google.com` answers `none` on its root path; `addons.mozilla.org` answers
+`bytes`). Everything else *is* measured, from the server, in parallel:
 
 - **hard filter** — the handshake must succeed, it must be TLS 1.3, and the
-  certificate chain must be comfortably under the size where REALITY breaks
+  certificate chain must have headroom under the size where REALITY breaks.
+  Both halves earn their keep: `abema.tv` is an otherwise ideal Japanese video
+  dest that serves only TLS 1.2, and `www.hulu.com` has a 4717-byte chain.
 - **latency** — the measured TLS handshake time, because your server re-dials
-  this site on every single client connection
-- **region bonus** — a same-region candidate wins ties up to 120 ms. This is not
-  cosmetic: matching the dest to the server's own region and ASN is what makes
-  the traffic look ordinary to a DPI box that has already taken an interest in
-  the address.
-
-From a Korean VPS it picks a Korean site at 61 ms over `dl.google.com` at
-164 ms; from a German one it will not.
+  this site on every single client connection.
+- **region** — worth 150 ms. The region that matters is where your *clients*
+  are, not where your server is: the ISP judging the traffic is theirs.
+  Defaults to the server's country; set `CLIENT_REGION` when they differ.
 
 ```
 $ transtation sni
-  sni     www.kakaocorp.com
-  why     best of KR-region and global candidates: 61ms TLS handshake, 884-byte certificate chain
+  sni     tv.naver.com
+  why     sustained heavy traffic is unremarkable here; same region as your
+          clients; 54ms TLS handshake, 3480-byte chain
   since   2026-08-06
 ```
 
-**The choice is sticky.** It lives in the data volume and is never re-picked on
-its own, because every share link carries `sni=` — silently changing it would
-break every client you have already handed out. Re-picking is explicit, and
-says so:
+From a Korean VPS it picks a Korean streaming service; with `CLIENT_REGION=JP`
+it picks `www.nicovideo.jp`; with no region it can determine at all, a global
+streaming brand.
+
+**The choice is sticky.** It lives in the data volume, is included in backups,
+and is never re-picked on its own — every share link carries `sni=`, so
+silently changing it would break every client you have already handed out.
+Re-picking is explicit, and says what it invalidates:
 
 ```
 transtation sni auto        # probe again and re-pick   (invalidates links)
diff --git a/server/transtation b/server/transtation
@@ -420,36 +420,92 @@ check_dest() {
 # Candidates are filtered well below CHAIN_LIMIT so that a site adding one
 # intermediate certificate later does not silently break the server.
 CHAIN_SAFE=4500
-# How much slower a same-region candidate may be and still beat the best global
-# one. Region matching is not cosmetic: matching the dest to the server's own
-# region and ASN is what makes the traffic look ordinary to a DPI box that is
-# already suspicious of the address.
-REGION_BONUS_MS=120
-
-# Chain sizes measured 2026-08-06; every entry has real headroom under
-# CHAIN_SAFE. Anycast globals are always probed as well, so a region with no
-# entry here still gets a good answer.
+
+# --- what the dest has to survive -----------------------------------------
+#
+# The dest is not just a TLS handshake donor. Your clients' ISP sees their
+# traffic labelled with this name, and it will judge the VOLUME AND SHAPE of
+# that traffic against what the name implies. Hours of sustained multi-gigabyte
+# download addressed to a corporate brochure page is not a plausible thing for
+# a human to be doing, and a DPI box that has already taken an interest in the
+# address will start dropping the connection. Pulling the same bytes from a
+# video service is what everyone does all evening.
+#
+# That is a judgement about the internet, not something a TLS probe can
+# measure -- `accept-ranges` was tested for this and does not discriminate
+# (dl.google.com answers "none" on its root path; addons.mozilla.org answers
+# "bytes"). So the class below is curated, deliberately, and it dominates the
+# score:
+#
+#   bulk   video, streaming, and real download/CDN hosts. Sustained multi-GB
+#          transfer and hours-long connections are unremarkable here.
+#   mixed  media-heavy portals. Moderate sustained traffic is fine; a torrent
+#          of it is a bit odd.
+#   light  corporate sites, marketing pages, DNS endpoints. Bulk transfer to
+#          these looks exactly as wrong as it is. Last resort only.
+#
+# Weights are in milliseconds so they can be compared with the measured
+# handshake latency directly. They are deliberately lopsided: being dropped by
+# a DPI box is fatal, whereas a slower dest only adds to connection setup. Real
+# candidate latencies span roughly 50-800ms, so a bulk dest wins essentially
+# every realistic comparison, and a light one wins only when nothing else
+# answered at all.
+CLASS_BULK=0
+CLASS_MIXED=800
+CLASS_LIGHT=2500
+# How much slower a same-region candidate may be and still win. Matching the
+# dest to where the traffic is observed is what makes it unremarkable.
+REGION_BONUS_MS=150
+
+# Chain sizes and TLS versions measured 2026-08-06 from a live probe; every
+# entry passed. Entries that failed and are therefore NOT here: abema.tv
+# (TLS 1.2 only), www.hulu.com (4717-byte chain).
 sni_regional() {
 	case "$1" in
-		JP) echo "www.pixiv.net www.nicovideo.jp" ;;
-		KR) echo "www.kakaocorp.com www.daum.net" ;;
-		US | CA) echo "www.reddit.com www.bing.com" ;;
-		GB | IE | DE | FR | NL | SE | NO | DK | FI | PL | IT | ES | PT | CH | AT | BE | CZ | RO | HU | GR) echo "www.spotify.com www.booking.com" ;;
-		SG | HK | TW | MY | TH | VN | ID | PH) echo "www.grab.com www.lazada.sg" ;;
-		IN | PK | BD | LK) echo "www.hotstar.com www.flipkart.com" ;;
-		BR | AR | CL | MX | CO | PE | UY) echo "www.uol.com.br www.mercadolibre.com" ;;
+		JP) echo "bulk:www.nicovideo.jp bulk:tver.jp mixed:www.pixiv.net" ;;
+		KR) echo "bulk:www.tving.com bulk:tv.naver.com bulk:www.wavve.com" ;;
+		US | CA) echo "bulk:www.twitch.tv bulk:vimeo.com mixed:www.reddit.com" ;;
+		GB | IE | DE | FR | NL | SE | NO | DK | FI | PL | IT | ES | PT | CH | AT | BE | CZ | RO | HU | GR) echo "bulk:www.dailymotion.com bulk:www.viaplay.com bulk:www.rtl.de" ;;
+		SG | HK | TW | MY | TH | VN | ID | PH) echo "bulk:www.vidio.com bulk:www.viu.com" ;;
+		IN | PK | BD | LK) echo "bulk:www.hotstar.com bulk:www.zee5.com bulk:www.sonyliv.com" ;;
+		BR | AR | CL | MX | CO | PE | UY) echo "bulk:globoplay.globo.com bulk:vimeo.com" ;;
+		AU | NZ) echo "bulk:iview.abc.net.au bulk:www.stan.com.au" ;;
 		*) echo "" ;;
 	esac
 }
-SNI_GLOBAL="dl.google.com www.cloudflare.com addons.mozilla.org one.one.one.one"
+# Always probed, whatever the region. Global streaming brands and real download
+# hosts are plausible almost anywhere; the light entry exists only so that a
+# server on a restricted network still gets an answer.
+SNI_GLOBAL="bulk:dl.google.com bulk:www.netflix.com bulk:cdn.discordapp.com bulk:www.twitch.tv mixed:addons.mozilla.org light:www.cloudflare.com"
 SNI_FALLBACK=dl.google.com
 
+# Pure function, so the weighting can be tested offline.
+sni_score() {
+	# class rtt_ms chain_bytes region_match(0|1) -> score, lower is better
+	case "$1" in
+		bulk) _cp=$CLASS_BULK ;;
+		mixed) _cp=$CLASS_MIXED ;;
+		*) _cp=$CLASS_LIGHT ;;
+	esac
+	_rb=0
+	[ "$4" = 1 ] && _rb=$REGION_BONUS_MS
+	# The chain term is a tiebreak, not a filter: a smaller chain is headroom
+	# against the dest adding an intermediate certificate later.
+	_sc=$(($2 + _cp + $3 / 100 - _rb))
+	[ "$_sc" -lt 0 ] && _sc=0
+	printf '%s' "$_sc"
+}
+
 # One candidate: technical filter, then latency. Writes "score domain chain rtt"
 # to its own file so parallel probes cannot interleave.
 sni_probe() {
-	_c=$1 _bonus=$2 _dir=$3
+	# "class:domain" region_match dir
+	_cls=${1%%:*} _c=${1#*:} _rm=$2 _dir=$3
 	_p=$(timeout 12 xray tls ping "$_c" 2>&1) || return 0
 	case "$_p" in *"Handshake succeeded"*) ;; *) return 0 ;; esac
+	# TLS 1.2 is a hard reject, not a penalty -- REALITY needs 1.3. This is not
+	# theoretical: abema.tv looked like an ideal Japanese video dest and serves
+	# only TLS 1.2.
 	case "$_p" in *"TLS Version:"*"1.3"*) ;; *) return 0 ;; esac
 	_chain=$(printf '%s' "$_p" | sed -n "s/^Certificate chain's total length: *\([0-9][0-9]*\).*/\1/p" | head -1)
 	[ -n "$_chain" ] && [ "$_chain" -le "$CHAIN_SAFE" ] || return 0
@@ -457,43 +513,50 @@ sni_probe() {
 	_t=$(curl -sS -o /dev/null --max-time 8 -w '%{time_appconnect}' "https://$_c/" 2>/dev/null) || return 0
 	_ms=$(printf '%s' "$_t" | awk '{ printf "%d", $1 * 1000 }')
 	[ -n "$_ms" ] && [ "$_ms" -gt 0 ] || return 0
-	_score=$((_ms - _bonus))
-	[ "$_score" -lt 0 ] && _score=0
-	printf '%s %s %s %s\n' "$_score" "$_c" "$_chain" "$_ms" >"$_dir/$_c"
+	printf '%s %s %s %s %s %s\n' "$(sni_score "$_cls" "$_ms" "$_chain" "$_rm")" \
+		"$_c" "$_chain" "$_ms" "$_cls" "$_rm" >"$_dir/$_c"
 }
 
 # Picks the dest this server should impersonate, then STICKS to it. The choice
 # is persisted because every share link carries `sni=`: silently picking a
 # different dest on a later boot would break every client already configured.
 sni_auto() {
-	_loc=${1:-}
+	# The region that matters is where your clients connect FROM -- the DPI that
+	# judges the traffic is theirs, not your server's. We default to the
+	# server's own country because most people put the server near or relevant
+	# to their users; CLIENT_REGION overrides it.
+	_loc=${CLIENT_REGION:-${1:-}}
 	_dir=$(mktemp -d)
 	printf 'selecting the best dest for this server'
-	[ -n "$_loc" ] && printf ' (region %s)' "$_loc"
+	[ -n "$_loc" ] && printf ' (traffic observed from %s)' "$_loc"
 	printf ' ...\n'
 
-	for _c in $(sni_regional "$_loc"); do sni_probe "$_c" "$REGION_BONUS_MS" "$_dir" & done
-	for _c in $SNI_GLOBAL; do sni_probe "$_c" 0 "$_dir" & done
+	_regional=$(sni_regional "$_loc")
+	for _e in $_regional; do sni_probe "$_e" 1 "$_dir" & done
+	for _e in $SNI_GLOBAL; do sni_probe "$_e" 0 "$_dir" & done
 	wait
 
 	_best=$(cat "$_dir"/* 2>/dev/null | sort -n | head -1)
 	rm -rf "$_dir"
 
 	if [ -n "$_best" ]; then
+		# shellcheck disable=SC2086
 		set -- $_best
-		_sni=$2 _chain=$3 _rtt=$4
-		case " $(sni_regional "$_loc") " in
-			*" $_sni "*) _why="best of ${_loc:-?}-region and global candidates" ;;
-			*) _why="best global candidate${_loc:+ (no region list for $_loc)}" ;;
+		_sni=$2 _chain=$3 _rtt=$4 _cls=$5 _rm=$6
+		case "$_cls" in
+			bulk) _what="sustained heavy traffic is unremarkable here" ;;
+			mixed) _what="media-heavy site, moderate traffic is unremarkable" ;;
+			*) _what="LAST RESORT -- bulk traffic to this name looks odd; re-run 'transtation sni auto' from a less restricted network" ;;
 		esac
-		_reason="$_why: ${_rtt}ms TLS handshake, ${_chain}-byte certificate chain"
+		[ "$_rm" = 1 ] && _where="same region as your clients" || _where="global"
+		_reason="$_what; $_where; ${_rtt}ms TLS handshake, ${_chain}-byte chain"
 	else
 		_sni=$SNI_FALLBACK
-		_reason="FALLBACK -- no candidate could be probed (no network yet?). Re-run: transtation sni auto"
+		_reason="FALLBACK -- nothing could be probed (no network yet?). Re-run: transtation sni auto"
 	fi
 
 	sni_write "$_sni" "$_reason"
-	printf '  chose %s -- %s\n' "$_sni" "$_reason"
+	printf '  chose %s\n    %s\n' "$_sni" "$_reason"
 }
 
 sni_write() {
@@ -744,13 +807,45 @@ cmd_selftest() {
 	_assert "two users produce two clients and two shortIds" \
 		_jq '(.inbounds[0].settings.clients | length) == 2 and (.inbounds[0].streamSettings.realitySettings.shortIds | length) == 2'
 
-	# 6. SNI auto-selection: table sanity and the write-side trust boundary.
-	_has() { case " $(sni_regional "$1") " in *" $2 "*) return 0 ;; *) return 1 ;; esac; }
+	# 6. SNI auto-selection: the weighting, the table, and the write boundary.
 	_empty_region() { [ -z "$(sni_regional "$1")" ]; }
-	_assert "regional table resolves a known country" _has KR www.kakaocorp.com
+	_region_has_class() { case " $(sni_regional "$1") " in *" $2:"*) return 0 ;; *) return 1 ;; esac; }
+	_assert "regional table lists a bulk-traffic dest for a known country" _region_has_class KR bulk
 	_assert "regional table is empty for an unlisted country" _empty_region ZZ
+
+	# Every entry must parse as class:domain with a class the scorer knows,
+	# or it silently scores as "light" and never gets picked.
+	_table_well_formed() {
+		for _e in $(sni_regional JP) $(sni_regional KR) $(sni_regional US) $(sni_regional GB) \
+			$(sni_regional SG) $(sni_regional IN) $(sni_regional BR) $(sni_regional AU) $SNI_GLOBAL; do
+			case "$_e" in
+				bulk:?*.?* | mixed:?*.?* | light:?*.?*) ;;
+				*) return 1 ;;
+			esac
+		done
+	}
+	_assert "every candidate is class:domain with a known class" _table_well_formed
 	_assert "the fallback dest is itself a probed global candidate" \
-		sh -c "case ' $SNI_GLOBAL ' in *' $SNI_FALLBACK '*) exit 0 ;; *) exit 1 ;; esac"
+		sh -c "case ' $SNI_GLOBAL ' in *':$SNI_FALLBACK '*) exit 0 ;; *) exit 1 ;; esac"
+
+	# The weighting is the whole point of this design, so it gets tested rather
+	# than trusted. A far-away video service must beat a nearby corporate page:
+	# that is exactly the swap that stopped the traffic being dropped.
+	_beats() { [ "$(sni_score "$1" "$2" "$3" "$4")" -lt "$(sni_score "$5" "$6" "$7" "$8")" ]; }
+	_assert "a distant bulk dest beats a fast light one" \
+		_beats bulk 600 4000 0   light 30 900 0
+	_assert "a distant bulk dest beats a fast mixed one" \
+		_beats bulk 450 4000 0   mixed 40 900 0
+	_assert "between two bulk dests, the nearer one wins" \
+		_beats bulk 80 2000 0   bulk 300 2000 0
+	_assert "a same-region dest wins a close race" \
+		_beats bulk 200 2000 1   bulk 100 2000 0
+	_assert "but the region bonus does not override a big latency gap" \
+		_beats bulk 100 2000 0   bulk 400 2000 1
+	_assert "a smaller certificate chain breaks a tie" \
+		_beats bulk 100 900 0   bulk 100 4400 0
+	_assert "scores never go negative" test "$(sni_score bulk 10 100 1)" = 0
+
 	_sni_rejects() { (sni_write "$1" test) >/dev/null 2>&1 && return 1 || return 0; }
 	_assert "sni_write rejects a non-hostname" _sni_rejects 'not a host; rm -rf /'
 	_assert "sni_write rejects apple targets (Xray warns they get the IP blocked)" \