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)" \
|