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

host/transtation-killswitch (6666B)

      1 #!/bin/sh
      2 # transtation-killswitch -- fail-closed egress filter for MODE=tun.
      3 #
      4 # Separate from the client service on purpose. Firewall rules are kernel state
      5 # that outlives whatever created them, and SIGKILL or OOM bypasses any cleanup
      6 # trap the client might install -- so a service that both closes the network and
      7 # can die without warning is a service that can strand you. Worse, under
      8 # `Restart=always` one that applies the killswitch and then fails to start would
      9 # sit in a loop re-closing your network faster than you can open it. Turning it
     10 # on and off stays an explicit, separate act.
     11 #
     12 #   transtation-killswitch on [--persist]
     13 #   transtation-killswitch off
     14 #   transtation-killswitch status
     15 set -eu
     16 
     17 URI_FILE=${TT_URI_FILE:-/etc/transtation/uri}
     18 TUN_IFACE=${TUN_IFACE:-tt0}
     19 UNIT=/etc/systemd/system/transtation-killswitch.service
     20 
     21 die() { printf 'transtation-killswitch: %s\n' "$*" >&2; exit 1; }
     22 
     23 [ "$(id -u)" = 0 ] || die "must run as root"
     24 command -v nft >/dev/null 2>&1 || die "nftables is not installed on this host"
     25 
     26 server_endpoint() {
     27 	[ -r "$URI_FILE" ] || die "cannot read $URI_FILE -- set TT_URI_FILE, or SERVER_IP and SERVER_PORT"
     28 	_u=$(cat "$URI_FILE")
     29 	_r=${_u#vless://}
     30 	_r=${_r%%#*}
     31 	_r=${_r%%\?*}
     32 	_hp=${_r#*@}
     33 	case "$_hp" in
     34 		\[*\]:*)
     35 			_h=${_hp%%]*}
     36 			_h=${_h#[}
     37 			_p=${_hp##*]:}
     38 			;;
     39 		*:*)
     40 			_h=${_hp%%:*}
     41 			_p=${_hp##*:}
     42 			;;
     43 		*) die "cannot parse a host:port out of $URI_FILE" ;;
     44 	esac
     45 	case "$_h" in
     46 		*:*)
     47 			# An IPv6 literal would be spliced into an `ip daddr` rule and nft
     48 			# would reject the whole ruleset, leaving the host with no killswitch
     49 			# at all rather than a broken one. transtation is IPv4-only end to end
     50 			# anyway: the client resolves the server with getent ahostsv4 and the
     51 			# tun DNS strategy is ipv4_only.
     52 			die "the server address in $URI_FILE is IPv6 ($_h).
     53     transtation is IPv4-only for now, so the killswitch cannot be built for it.
     54     Use an IPv4 address or a name with an A record, or set SERVER_IP yourself." ;;
     55 		*[!0-9.]*)
     56 			_h=$(getent ahostsv4 "$_h" 2>/dev/null | awk 'NR==1 {print $1}')
     57 			[ -n "$_h" ] || die "cannot resolve the server address in $URI_FILE to an IPv4 address"
     58 			;;
     59 	esac
     60 	SERVER_IP=${SERVER_IP:-$_h}
     61 	SERVER_PORT=${SERVER_PORT:-$_p}
     62 }
     63 
     64 ks_on() {
     65 	server_endpoint
     66 
     67 	_admin=""
     68 	if [ -n "${ADMIN_CIDR:-}" ]; then
     69 		_admin="ip daddr $ADMIN_CIDR accept"
     70 	fi
     71 
     72 	_allow=""
     73 	if [ -n "${KILLSWITCH_ALLOW:-}" ]; then
     74 		# Raw nft expressions, ';'-separated. No mini-DSL: whoever runs this
     75 		# already holds NET_ADMIN over the host's network namespace, so there is
     76 		# no privilege boundary here to defend.
     77 		_allow=$(printf '%s' "$KILLSWITCH_ALLOW" | tr ';' '\n' | sed 's/^[[:space:]]*/    /')
     78 	fi
     79 
     80 	# Additive and scoped to our own table. NEVER `nft flush ruleset`: on a
     81 	# modern host that also wipes the iptables-nft tables Docker installs and
     82 	# silently breaks every container's networking. A DROP in any one table
     83 	# still drops, so scoping costs nothing in correctness.
     84 	nft delete table inet transtation 2>/dev/null || true
     85 	nft delete table ip6 transtation 2>/dev/null || true
     86 
     87 	nft -f - <<-EOF
     88 		table inet transtation {
     89 		  chain out {
     90 		    type filter hook output priority filter; policy drop;
     91 		    oif lo accept
     92 		    ct state established,related accept
     93 		    ct state invalid drop
     94 		    oifname "$TUN_IFACE" accept
     95 		    meta mark 0x1 accept
     96 		    ip daddr { 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, 100.64.0.0/10, 224.0.0.0/4 } accept
     97 		    udp dport { 67, 68 } accept
     98 		    ip daddr $SERVER_IP tcp dport $SERVER_PORT accept
     99 		    ip daddr $SERVER_IP udp dport $SERVER_PORT accept
    100 		    $_admin
    101 		$_allow
    102 		    limit rate 5/minute log prefix "transtation-drop: "
    103 		  }
    104 		}
    105 		table ip6 transtation {
    106 		  chain out {
    107 		    type filter hook output priority filter; policy drop;
    108 		    oif lo accept
    109 		    ct state established,related accept
    110 		  }
    111 		}
    112 	EOF
    113 
    114 	cat <<-EOF
    115 		killswitch ON  (table inet transtation, output hook, policy drop)
    116 		  allowed out: loopback, established, $TUN_IFACE, fwmark 0x1 (split tunnel),
    117 		               RFC1918 + CGNAT + link-local + multicast, DHCP,
    118 		               $SERVER_IP:$SERVER_PORT${ADMIN_CIDR:+, admin $ADMIN_CIDR}
    119 		  IPv6 is dropped entirely except loopback and established.
    120 		  Your existing nftables rules were NOT modified; this is a separate table.
    121 
    122 		  LIMIT: this is an output-hook chain. It covers this host's own processes.
    123 		         Traffic FORWARDED from your other containers does not traverse the
    124 		         output hook and is NOT covered by it.
    125 
    126 		  off:   transtation-killswitch off
    127 		  panic: transtation-panic
    128 	EOF
    129 
    130 	if [ -n "${SSH_CONNECTION:-}" ]; then
    131 		cat <<-EOF
    132 
    133 			  NOTE: you are over SSH. Your session survives on the established rule, and
    134 			  new inbound SSH connections keep working too -- their reply packets are
    135 			  already ESTABLISHED. What stops is this host's own OUTBOUND traffic when
    136 			  the tunnel is down: DNS, package managers, monitoring agents. Set
    137 			  ADMIN_CIDR=<your.network/mask> if you want an explicit carve-out.
    138 		EOF
    139 	fi
    140 
    141 	if [ "${1:-}" = --persist ]; then
    142 		[ -d /run/systemd/system ] || {
    143 			cat <<-EOF
    144 
    145 				  --persist needs systemd, which is not running here. To make this survive a
    146 				  reboot on your own init, run this at boot:
    147 				      transtation-killswitch on
    148 			EOF
    149 			return 0
    150 		}
    151 		cat >"$UNIT" <<-EOF
    152 			[Unit]
    153 			Description=transtation killswitch
    154 			Before=network-pre.target
    155 			Wants=network-pre.target
    156 
    157 			[Service]
    158 			Type=oneshot
    159 			RemainAfterExit=yes
    160 			ExecStart=/usr/local/sbin/transtation-killswitch on
    161 			ExecStop=/usr/local/sbin/transtation-killswitch off
    162 
    163 			[Install]
    164 			WantedBy=multi-user.target
    165 		EOF
    166 		systemctl daemon-reload
    167 		systemctl enable transtation-killswitch.service >/dev/null
    168 		echo "  persisted: enabled transtation-killswitch.service"
    169 		echo "  (a broken fail-closed state now survives a reboot -- transtation-panic still clears it)"
    170 	fi
    171 }
    172 
    173 ks_off() {
    174 	nft delete table inet transtation 2>/dev/null || true
    175 	nft delete table ip6 transtation 2>/dev/null || true
    176 	if [ -f "$UNIT" ]; then
    177 		systemctl disable transtation-killswitch.service >/dev/null 2>&1 || true
    178 		rm -f "$UNIT"
    179 		systemctl daemon-reload 2>/dev/null || true
    180 	fi
    181 	echo "killswitch OFF (table inet transtation deleted; nothing else touched)"
    182 }
    183 
    184 ks_status() {
    185 	if nft list table inet transtation >/dev/null 2>&1; then
    186 		echo "killswitch is ON"
    187 		nft list table inet transtation
    188 	else
    189 		echo "killswitch is OFF (no table inet transtation)"
    190 	fi
    191 }
    192 
    193 case "${1:-}" in
    194 	on) shift; ks_on "${1:-}" ;;
    195 	off) ks_off ;;
    196 	status) ks_status ;;
    197 	*)
    198 		echo "usage: transtation-killswitch on [--persist] | off | status" >&2
    199 		exit 2
    200 		;;
    201 esac