start-transparent.sh 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. usage() {
  4. cat <<'EOH'
  5. Usage: start-transparent.sh [-v|--verbose] [--enable-udp] [--capture-uid UID] [--kernel auto|20|24] [config_path]
  6. Options:
  7. -v, --verbose 启动后实时输出 mynetspeeder 日志
  8. --capture-uid UID 指定时只接管该 UID;不指定时接管所有用户流量
  9. --enable-udp 额外启用 UDP 透明接管(实验性,默认关闭)
  10. --kernel MODE 指定内核优化模式:auto|20|24,默认 auto
  11. -h, --help 显示帮助
  12. EOH
  13. }
  14. VERBOSE=0
  15. ENABLE_UDP=0
  16. KERNEL_MODE="${MYNETSPEEDER_KERNEL_MODE:-auto}"
  17. CONFIG_PATH="/home/mynetspeeder/config.json"
  18. CAPTURE_UID="${MYNETSPEEDER_CAPTURE_UID:-}"
  19. UDP_CAPTURE_EFFECTIVE=0
  20. while [[ $# -gt 0 ]]; do
  21. case "$1" in
  22. -v|--verbose)
  23. VERBOSE=1; shift ;;
  24. --enable-udp)
  25. ENABLE_UDP=1; shift ;;
  26. --capture-uid)
  27. CAPTURE_UID="${2:-}"
  28. [[ -n "$CAPTURE_UID" ]] || { echo "missing value for --capture-uid"; exit 1; }
  29. shift 2 ;;
  30. --kernel)
  31. KERNEL_MODE="${2:-}"
  32. [[ -n "$KERNEL_MODE" ]] || { echo "missing value for --kernel"; exit 1; }
  33. shift 2 ;;
  34. -h|--help)
  35. usage; exit 0 ;;
  36. *)
  37. CONFIG_PATH="$1"; shift ;;
  38. esac
  39. done
  40. LISTEN_HOST="${MYNETSPEEDER_LISTEN_HOST:-127.0.0.1}"
  41. LISTEN_PORT="${MYNETSPEEDER_LISTEN_PORT:-19080}"
  42. RUNTIME_USER="${MYNETSPEEDER_USER:-mynetspeeder}"
  43. PID_FILE="/var/run/mynetspeeder-edge.pid"
  44. SOCKS_PID_FILE="/var/run/mynetspeeder-socks.pid"
  45. IPTABLES_WATCHDOG_PID_FILE="/var/run/mynetspeeder-iptables-watchdog.pid"
  46. LOG_FILE="/var/log/mynetspeeder-edge.log"
  47. SOCKS_LOG_FILE="/var/log/mynetspeeder-socks.log"
  48. LOG_MAX_MB="${MYNETSPEEDER_LOG_MAX_MB:-50}"
  49. LOG_BACKUPS="${MYNETSPEEDER_LOG_BACKUPS:-3}"
  50. CHAIN4="MYNETSPEEDER"
  51. CHAIN6="MYNETSPEEDER6"
  52. SSH_PORTS="${MYNETSPEEDER_SSH_PORTS:-}"
  53. SELF_EXCLUDE_V4="127.0.0.0/8 169.254.0.0/16"
  54. SELF_EXCLUDE_V6="::1/128 fe80::/10"
  55. if [[ $EUID -ne 0 ]]; then echo "need root"; exit 1; fi
  56. if [[ ! -f "$CONFIG_PATH" ]]; then echo "config not found: $CONFIG_PATH"; exit 1; fi
  57. if [[ -n "$CAPTURE_UID" ]] && ! [[ "$CAPTURE_UID" =~ ^[0-9]+$ ]]; then echo "capture uid must be numeric"; exit 1; fi
  58. case "$KERNEL_MODE" in
  59. auto|20|24) ;;
  60. *) echo "invalid kernel mode: $KERNEL_MODE"; exit 1 ;;
  61. esac
  62. if [[ "$KERNEL_MODE" == "auto" ]]; then
  63. if [[ -f /etc/os-release ]] && grep -q '^VERSION_ID="24' /etc/os-release; then
  64. KERNEL_MODE="24"
  65. else
  66. KERNEL_MODE="20"
  67. fi
  68. fi
  69. if [[ -z "$SSH_PORTS" && -n "${SSH_CONNECTION:-}" ]]; then
  70. SSH_PORTS="${SSH_CONNECTION##* }"
  71. fi
  72. SSH_PORT_ARRAY=()
  73. if [[ -n "$SSH_PORTS" ]]; then
  74. IFS=',' read -r -a SSH_PORT_ARRAY <<< "$SSH_PORTS"
  75. for ssh_port in "${SSH_PORT_ARRAY[@]}"; do
  76. [[ "$ssh_port" =~ ^[0-9]+$ ]] || { echo "ssh ports must be numeric, got: $ssh_port"; exit 1; }
  77. done
  78. fi
  79. id -u "$RUNTIME_USER" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$RUNTIME_USER"
  80. mkdir -p /var/log
  81. touch "$LOG_FILE"
  82. chown "$RUNTIME_USER":"$RUNTIME_USER" "$LOG_FILE"
  83. touch "$SOCKS_LOG_FILE"
  84. chown "$RUNTIME_USER":"$RUNTIME_USER" "$SOCKS_LOG_FILE"
  85. if ! [[ "$LOG_MAX_MB" =~ ^[0-9]+$ ]] || ! [[ "$LOG_BACKUPS" =~ ^[0-9]+$ ]]; then echo "log limits must be numeric"; exit 1; fi
  86. LOG_MAX_BYTES=$((LOG_MAX_MB * 1024 * 1024))
  87. IPTABLES_BACKEND="unknown"
  88. if iptables --version 2>/dev/null | grep -qi 'nf_tables'; then
  89. IPTABLES_BACKEND="nf_tables"
  90. else
  91. IPTABLES_BACKEND="legacy"
  92. fi
  93. ensure_rule() {
  94. local cmd="$1"
  95. local table="$2"
  96. local chain="$3"
  97. shift 3
  98. if ! "$cmd" -t "$table" -C "$chain" "$@" >/dev/null 2>&1; then
  99. "$cmd" -t "$table" -A "$chain" "$@"
  100. fi
  101. }
  102. ensure_first_rule() {
  103. local cmd="$1"
  104. local table="$2"
  105. local chain="$3"
  106. shift 3
  107. if ! "$cmd" -t "$table" -C "$chain" "$@" >/dev/null 2>&1; then
  108. "$cmd" -t "$table" -I "$chain" 1 "$@"
  109. fi
  110. }
  111. start_iptables_watchdog() {
  112. (
  113. while true; do
  114. sleep 5
  115. ensure_first_rule iptables nat OUTPUT -p tcp -j "$CHAIN4" || true
  116. if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
  117. ensure_first_rule iptables nat OUTPUT -p udp -j "$CHAIN4" || true
  118. fi
  119. if [[ "${IP6_ENABLED:-0}" == "1" && "${IP6_NAT_SUPPORTED:-0}" == "1" ]]; then
  120. ensure_first_rule ip6tables nat OUTPUT -p tcp -j "$CHAIN6" || true
  121. if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
  122. ensure_first_rule ip6tables nat OUTPUT -p udp -j "$CHAIN6" || true
  123. fi
  124. fi
  125. done
  126. ) >/dev/null 2>&1 &
  127. echo $! > "$IPTABLES_WATCHDOG_PID_FILE"
  128. }
  129. add_exclusions_v4() {
  130. iptables -t nat -A "$CHAIN4" -m addrtype --dst-type LOCAL -j RETURN
  131. for cidr in $SELF_EXCLUDE_V4; do
  132. iptables -t nat -A "$CHAIN4" -d "$cidr" -j RETURN
  133. done
  134. iptables -t nat -A "$CHAIN4" -m owner --uid-owner "$RUNTIME_USER" -j RETURN
  135. for ssh_port in "${SSH_PORT_ARRAY[@]}"; do
  136. iptables -t nat -A "$CHAIN4" -p tcp --sport "$ssh_port" -j RETURN
  137. done
  138. mapfile -t RELAY_HOSTS_V4 < <(python3 - <<'PY' "$CONFIG_PATH"
  139. import json, sys
  140. cfg = json.load(open(sys.argv[1]))
  141. for relay in cfg.get('relays', []):
  142. print(relay['host'])
  143. PY
  144. )
  145. for host in "${RELAY_HOSTS_V4[@]}"; do
  146. [[ -n "$host" && "$host" != *:* ]] && iptables -t nat -A "$CHAIN4" -d "$host" -j RETURN
  147. done
  148. }
  149. add_exclusions_v6() {
  150. ip6tables -t nat -A "$CHAIN6" -m addrtype --dst-type LOCAL -j RETURN
  151. for cidr in $SELF_EXCLUDE_V6; do
  152. ip6tables -t nat -A "$CHAIN6" -d "$cidr" -j RETURN
  153. done
  154. ip6tables -t nat -A "$CHAIN6" -m owner --uid-owner "$RUNTIME_USER" -j RETURN
  155. for ssh_port in "${SSH_PORT_ARRAY[@]}"; do
  156. ip6tables -t nat -A "$CHAIN6" -p tcp --sport "$ssh_port" -j RETURN
  157. done
  158. mapfile -t RELAY_HOSTS_V6 < <(python3 - <<'PY' "$CONFIG_PATH"
  159. import json, sys
  160. cfg = json.load(open(sys.argv[1]))
  161. for relay in cfg.get('relays', []):
  162. print(relay['host'])
  163. PY
  164. )
  165. for host in "${RELAY_HOSTS_V6[@]}"; do
  166. [[ -n "$host" && "$host" == *:* ]] && ip6tables -t nat -A "$CHAIN6" -d "$host" -j RETURN
  167. done
  168. if [[ -n "$CAPTURE_UID" ]]; then
  169. ip6tables -t nat -A "$CHAIN6" -p tcp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
  170. else
  171. ip6tables -t nat -A "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT"
  172. fi
  173. }
  174. add_udp_exclusions_v4() {
  175. iptables -t nat -A "$CHAIN4" -p udp -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
  176. }
  177. add_udp_exclusions_v6() {
  178. ip6tables -t nat -A "$CHAIN6" -p udp -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
  179. }
  180. pkill -f 'python3 -m mynetspeeder edge' || true
  181. pkill -f 'python3 -m mynetspeeder socks' || true
  182. SOCKS_HOST=$(python3 - <<'PY' "$CONFIG_PATH"
  183. import json, sys
  184. cfg = json.load(open(sys.argv[1]))
  185. print(cfg.get('socks_host', '127.0.0.1'))
  186. PY
  187. )
  188. SOCKS_PORT=$(python3 - <<'PY' "$CONFIG_PATH"
  189. import json, sys
  190. cfg = json.load(open(sys.argv[1]))
  191. print(int(cfg.get('socks_port', 0) or 0))
  192. PY
  193. )
  194. if [[ "$SOCKS_PORT" -gt 0 ]]; then
  195. runuser -u "$RUNTIME_USER" -- bash -lc "export PYTHONUNBUFFERED=1; export PYTHONPATH=/home; cd /home && exec nohup python3 -m mynetspeeder socks --listen-host ${SOCKS_HOST} --listen-port ${SOCKS_PORT} --config ${CONFIG_PATH} 2>&1 | python3 /home/mynetspeeder/scripts/rotate-log.py ${SOCKS_LOG_FILE} ${LOG_MAX_BYTES} ${LOG_BACKUPS}" &
  196. SOCKS_PID=$!
  197. echo "$SOCKS_PID" > "$SOCKS_PID_FILE"
  198. sleep 1
  199. ss -lntp | grep -qE "${SOCKS_HOST//./\\.}:${SOCKS_PORT}( |$)" || { echo "socks failed to listen"; tail -n 50 "$SOCKS_LOG_FILE" || true; exit 1; }
  200. fi
  201. if [[ "$ENABLE_UDP" == "1" && "$SOCKS_PORT" -gt 0 ]]; then
  202. echo "udp transparent capture disabled: socks5 is enabled, UDP will use socks only"
  203. else
  204. UDP_CAPTURE_EFFECTIVE="$ENABLE_UDP"
  205. fi
  206. EDGE_UDP_FLAG=""
  207. if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
  208. EDGE_UDP_FLAG="--enable-udp"
  209. fi
  210. runuser -u "$RUNTIME_USER" -- bash -lc "export PYTHONUNBUFFERED=1; export PYTHONPATH=/home; cd /home && exec nohup python3 -m mynetspeeder edge --listen-host ${LISTEN_HOST} --listen-port ${LISTEN_PORT} --kernel ${KERNEL_MODE} --config ${CONFIG_PATH} ${EDGE_UDP_FLAG} 2>&1 | python3 /home/mynetspeeder/scripts/rotate-log.py ${LOG_FILE} ${LOG_MAX_BYTES} ${LOG_BACKUPS}" &
  211. EDGE_PID=$!
  212. echo "$EDGE_PID" > "$PID_FILE"
  213. sleep 1
  214. ss -ln | grep -qE "[:.]${LISTEN_PORT}( |$)" || { echo "edge failed to listen"; tail -n 50 "$LOG_FILE" || true; exit 1; }
  215. iptables -t nat -N "$CHAIN4" 2>/dev/null || true
  216. iptables -t nat -F "$CHAIN4"
  217. add_exclusions_v4
  218. if [[ -n "$CAPTURE_UID" ]]; then
  219. iptables -t nat -A "$CHAIN4" -p tcp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
  220. else
  221. iptables -t nat -A "$CHAIN4" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT"
  222. fi
  223. ensure_first_rule iptables nat OUTPUT -p tcp -j "$CHAIN4"
  224. if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
  225. add_udp_exclusions_v4
  226. if [[ -n "$CAPTURE_UID" ]]; then
  227. iptables -t nat -A "$CHAIN4" -p udp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
  228. else
  229. iptables -t nat -A "$CHAIN4" -p udp -j REDIRECT --to-ports "$LISTEN_PORT"
  230. fi
  231. ensure_first_rule iptables nat OUTPUT -p udp -j "$CHAIN4"
  232. fi
  233. IP6_ENABLED=0
  234. IP6_NAT_SUPPORTED=0
  235. if command -v ip6tables >/dev/null 2>&1; then
  236. if ip6tables -t nat -S >/dev/null 2>&1; then
  237. IP6_ENABLED=1
  238. IP6_NAT_SUPPORTED=1
  239. ip6tables -t nat -N "$CHAIN6" 2>/dev/null || true
  240. ip6tables -t nat -F "$CHAIN6"
  241. add_exclusions_v6
  242. if [[ -n "$CAPTURE_UID" ]]; then
  243. ip6tables -t nat -A "$CHAIN6" -p tcp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
  244. else
  245. ip6tables -t nat -A "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT"
  246. fi
  247. ensure_first_rule ip6tables nat OUTPUT -p tcp -j "$CHAIN6"
  248. if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
  249. add_udp_exclusions_v6
  250. if [[ -n "$CAPTURE_UID" ]]; then
  251. ip6tables -t nat -A "$CHAIN6" -p udp -m owner --uid-owner "$CAPTURE_UID" -j REDIRECT --to-ports "$LISTEN_PORT"
  252. else
  253. ip6tables -t nat -A "$CHAIN6" -p udp -j REDIRECT --to-ports "$LISTEN_PORT"
  254. fi
  255. ensure_first_rule ip6tables nat OUTPUT -p udp -j "$CHAIN6"
  256. fi
  257. else
  258. echo "ipv6 nat unavailable: ip6tables nat table not supported, skip ipv6 transparent rules"
  259. fi
  260. fi
  261. RULES_V4=$(iptables -t nat -S "$CHAIN4" 2>/dev/null | wc -l)
  262. RULES_V6=0
  263. if [[ "$IP6_ENABLED" == "1" && "$IP6_NAT_SUPPORTED" == "1" ]]; then
  264. RULES_V6=$(ip6tables -t nat -S "$CHAIN6" 2>/dev/null | wc -l)
  265. fi
  266. iptables -t nat -C OUTPUT -p tcp -j "$CHAIN4" >/dev/null 2>&1 || { echo "self-check failed: ipv4 tcp output hook missing"; exit 1; }
  267. if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
  268. iptables -t nat -C OUTPUT -p udp -j "$CHAIN4" >/dev/null 2>&1 || { echo "self-check failed: ipv4 udp output hook missing"; exit 1; }
  269. fi
  270. if [[ "$IP6_ENABLED" == "1" && "$IP6_NAT_SUPPORTED" == "1" ]]; then
  271. ip6tables -t nat -C OUTPUT -p tcp -j "$CHAIN6" >/dev/null 2>&1 || { echo "self-check failed: ipv6 tcp output hook missing"; exit 1; }
  272. ip6tables -t nat -C "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT" >/dev/null 2>&1 || { echo "self-check failed: ipv6 tcp redirect missing"; exit 1; }
  273. if [[ "$UDP_CAPTURE_EFFECTIVE" == "1" ]]; then
  274. ip6tables -t nat -C OUTPUT -p udp -j "$CHAIN6" >/dev/null 2>&1 || { echo "self-check failed: ipv6 udp output hook missing"; exit 1; }
  275. ip6tables -t nat -C "$CHAIN6" -p udp -j REDIRECT --to-ports "$LISTEN_PORT" >/dev/null 2>&1 || { echo "self-check failed: ipv6 udp redirect missing"; exit 1; }
  276. fi
  277. fi
  278. start_iptables_watchdog
  279. echo "mynetspeeder transparent mode started on ${LISTEN_HOST}:${LISTEN_PORT}"
  280. echo "kernel mode: $KERNEL_MODE"
  281. echo "iptables backend: $IPTABLES_BACKEND"
  282. if [[ -n "$CAPTURE_UID" ]]; then
  283. echo "capture uid: $CAPTURE_UID"
  284. else
  285. echo "capture uid: all users"
  286. fi
  287. if [[ ${#SSH_PORT_ARRAY[@]} -gt 0 ]]; then
  288. echo "ssh exempt ports: ${SSH_PORT_ARRAY[*]}"
  289. else
  290. echo "ssh exempt ports: none"
  291. fi
  292. echo "udp capture requested: $ENABLE_UDP"
  293. echo "udp capture effective: $UDP_CAPTURE_EFFECTIVE"
  294. if [[ "$SOCKS_PORT" -gt 0 ]]; then
  295. echo "socks5: ${SOCKS_HOST}:${SOCKS_PORT}"
  296. echo "socks log: $SOCKS_LOG_FILE"
  297. else
  298. echo "socks5: disabled"
  299. fi
  300. echo "log file: $LOG_FILE"
  301. echo "log max: ${LOG_MAX_MB}MB x ${LOG_BACKUPS}"
  302. echo "ipv4 chain rules: $RULES_V4"
  303. echo "ipv6 chain rules: $RULES_V6"
  304. echo "self-check: ok"
  305. if [[ "$VERBOSE" == "1" ]]; then
  306. echo "verbose mode: press Ctrl+C to stop viewing logs, service keeps running"
  307. exec tail -n 80 -f "$LOG_FILE"
  308. fi