#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: tcp_only_start.sh [config_path] Options via env: MYNETSPEEDER_LISTEN_HOST edge 监听地址,默认读取配置或 127.0.0.1 MYNETSPEEDER_LISTEN_PORT edge 监听端口,默认读取配置或 19080 MYNETSPEEDER_USER 运行用户,默认 mynetspeeder MYNETSPEEDER_LOG_MAX_MB 单个日志最大大小,默认 50 MYNETSPEEDER_LOG_BACKUPS 日志轮转保留份数,默认 3 EOF } SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(dirname "$SCRIPT_DIR")" PACKAGE_PARENT="$(dirname "$ROOT_DIR")" PACKAGE_NAME="$(basename "$ROOT_DIR")" CONFIG_PATH="${1:-$ROOT_DIR/config.json}" RUNTIME_USER="${MYNETSPEEDER_USER:-mynetspeeder}" PID_FILE="/var/run/mynetspeeder-edge.pid" LOG_FILE="/var/log/mynetspeeder-edge.log" LOG_MAX_MB="${MYNETSPEEDER_LOG_MAX_MB:-50}" LOG_BACKUPS="${MYNETSPEEDER_LOG_BACKUPS:-3}" CHAIN4="MYNETSPEEDER" CHAIN6="MYNETSPEEDER6" SSH_PORTS="${MYNETSPEEDER_SSH_PORTS:-}" SELF_EXCLUDE_V4="127.0.0.0/8 169.254.0.0/16" SELF_EXCLUDE_V6="::1/128 fe80::/10" if [[ $EUID -ne 0 ]]; then echo "need root" exit 1 fi if [[ ! -f "$CONFIG_PATH" ]]; then echo "config not found: $CONFIG_PATH" exit 1 fi LISTEN_HOST_FROM_CONFIG=$(python3 - <<'PY' "$CONFIG_PATH" import json, sys cfg = json.load(open(sys.argv[1])) print(cfg.get("listen_host", cfg.get("socks_host", "127.0.0.1"))) PY ) LISTEN_PORT_FROM_CONFIG=$(python3 - <<'PY' "$CONFIG_PATH" import json, sys cfg = json.load(open(sys.argv[1])) port = int(cfg.get("listen_port", cfg.get("socks_port", 19080)) or 19080) print(port if port > 0 else 19080) PY ) LISTEN_HOST="${MYNETSPEEDER_LISTEN_HOST:-$LISTEN_HOST_FROM_CONFIG}" LISTEN_PORT="${MYNETSPEEDER_LISTEN_PORT:-$LISTEN_PORT_FROM_CONFIG}" if ! [[ "$LISTEN_PORT" =~ ^[0-9]+$ ]]; then echo "listen port must be numeric" exit 1 fi if [[ -z "$SSH_PORTS" && -n "${SSH_CONNECTION:-}" ]]; then SSH_PORTS="${SSH_CONNECTION##* }" fi SSH_PORT_ARRAY=() if [[ -n "$SSH_PORTS" ]]; then IFS=',' read -r -a SSH_PORT_ARRAY <<< "$SSH_PORTS" for ssh_port in "${SSH_PORT_ARRAY[@]}"; do [[ "$ssh_port" =~ ^[0-9]+$ ]] || { echo "ssh ports must be numeric, got: $ssh_port"; exit 1; } done fi id -u "$RUNTIME_USER" >/dev/null 2>&1 || useradd --system --no-create-home --shell /usr/sbin/nologin "$RUNTIME_USER" mkdir -p /var/log touch "$LOG_FILE" chown "$RUNTIME_USER":"$RUNTIME_USER" "$LOG_FILE" if ! [[ "$LOG_MAX_MB" =~ ^[0-9]+$ ]] || ! [[ "$LOG_BACKUPS" =~ ^[0-9]+$ ]]; then echo "log limits must be numeric" exit 1 fi LOG_MAX_BYTES=$((LOG_MAX_MB * 1024 * 1024)) WAIT_TIMEOUT="${MYNETSPEEDER_LISTEN_TIMEOUT:-15}" ensure_rule() { local cmd="$1" local table="$2" local chain="$3" shift 3 if ! "$cmd" -t "$table" -C "$chain" "$@" >/dev/null 2>&1; then "$cmd" -t "$table" -A "$chain" "$@" fi } wait_for_listen() { local host="$1" local port="$2" local log_file="$3" local log_pattern="$4" local label="$5" local timeout_sec="${6:-10}" local deadline=$((SECONDS + timeout_sec)) local pattern pattern="${host//./\\.}:${port}( |$)" while (( SECONDS < deadline )); do if ss -H -lntp 2>/dev/null | grep -qE "$pattern"; then return 0 fi if [[ -f "$log_file" ]] && grep -qE "$log_pattern" "$log_file" 2>/dev/null; then return 0 fi sleep 0.2 done echo "$label failed to listen" return 1 } add_exclusions_v4() { iptables -t nat -A "$CHAIN4" -m addrtype --dst-type LOCAL -j RETURN for cidr in $SELF_EXCLUDE_V4; do iptables -t nat -A "$CHAIN4" -d "$cidr" -j RETURN done iptables -t nat -A "$CHAIN4" -m owner --uid-owner "$RUNTIME_USER" -j RETURN for ssh_port in "${SSH_PORT_ARRAY[@]}"; do iptables -t nat -A "$CHAIN4" -p tcp --sport "$ssh_port" -j RETURN done } add_exclusions_v6() { ip6tables -t nat -A "$CHAIN6" -m addrtype --dst-type LOCAL -j RETURN for cidr in $SELF_EXCLUDE_V6; do ip6tables -t nat -A "$CHAIN6" -d "$cidr" -j RETURN done ip6tables -t nat -A "$CHAIN6" -m owner --uid-owner "$RUNTIME_USER" -j RETURN for ssh_port in "${SSH_PORT_ARRAY[@]}"; do ip6tables -t nat -A "$CHAIN6" -p tcp --sport "$ssh_port" -j RETURN done } pkill -f 'python3 -m mynetspeeder edge' || true INLINE_CONFIG_JSON="$(python3 - <<'PY' "$CONFIG_PATH" import json, sys from pathlib import Path cfg = json.loads(Path(sys.argv[1]).read_text()) cfg["relays"] = [] cfg["socks_port"] = 0 cfg["direct_redundancy"] = max(int(cfg.get("direct_redundancy", 3) or 3), 3) cfg["direct_redundancy_v4"] = max(int(cfg.get("direct_redundancy_v4", cfg["direct_redundancy"]) or cfg["direct_redundancy"]), 3) cfg["direct_redundancy_v6"] = max(int(cfg.get("direct_redundancy_v6", 2) or 2), 2) cfg["direct_max_redundancy"] = max(int(cfg.get("direct_max_redundancy", 4) or 4), 4) cfg["direct_open_timeout"] = min(float(cfg.get("direct_open_timeout", 2.5) or 2.5), 2.5) cfg["tcp_connect_happy_eyeballs_delay"] = min(float(cfg.get("tcp_connect_happy_eyeballs_delay", 0.1) or 0.1), 0.1) cfg["tcp_warmup_bytes"] = max(int(cfg.get("tcp_warmup_bytes", 524288) or 524288), 524288) cfg["tcp_loser_grace_ms"] = min(int(cfg.get("tcp_loser_grace_ms", 80) or 80), 80) print(json.dumps(cfg, ensure_ascii=False)) PY )" echo "tcp-only config prepared: relays cleared, socks disabled" runuser -u "$RUNTIME_USER" -- env MYNETSPEEDER_CONFIG_JSON="$INLINE_CONFIG_JSON" bash -lc "export PYTHONUNBUFFERED=1; export PYTHONPATH=${PACKAGE_PARENT}; cd ${PACKAGE_PARENT} && exec nohup python3 -m ${PACKAGE_NAME} edge --listen-host ${LISTEN_HOST} --listen-port ${LISTEN_PORT} --config ${CONFIG_PATH} 2>&1 | python3 ${ROOT_DIR}/scripts/rotate-log.py ${LOG_FILE} ${LOG_MAX_BYTES} ${LOG_BACKUPS}" & EDGE_PID=$! echo "$EDGE_PID" > "$PID_FILE" if ! wait_for_listen "$LISTEN_HOST" "$LISTEN_PORT" "$LOG_FILE" "transparent tcp listening on" "edge" "$WAIT_TIMEOUT"; then tail -n 50 "$LOG_FILE" || true exit 1 fi iptables -t nat -N "$CHAIN4" 2>/dev/null || true iptables -t nat -F "$CHAIN4" add_exclusions_v4 iptables -t nat -A "$CHAIN4" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT" ensure_rule iptables nat OUTPUT -p tcp -j "$CHAIN4" IP6_ENABLED=0 IP6_NAT_SUPPORTED=0 if command -v ip6tables >/dev/null 2>&1; then if ip6tables -t nat -S >/dev/null 2>&1; then IP6_ENABLED=1 IP6_NAT_SUPPORTED=1 ip6tables -t nat -N "$CHAIN6" 2>/dev/null || true ip6tables -t nat -F "$CHAIN6" add_exclusions_v6 ip6tables -t nat -A "$CHAIN6" -p tcp -j REDIRECT --to-ports "$LISTEN_PORT" ensure_rule ip6tables nat OUTPUT -p tcp -j "$CHAIN6" fi fi iptables -t nat -C OUTPUT -p tcp -j "$CHAIN4" >/dev/null 2>&1 || { echo "self-check failed: ipv4 tcp output hook missing"; exit 1; } if [[ "$IP6_ENABLED" == "1" && "$IP6_NAT_SUPPORTED" == "1" ]]; then ip6tables -t nat -C OUTPUT -p tcp -j "$CHAIN6" >/dev/null 2>&1 || { echo "self-check failed: ipv6 tcp output hook missing"; exit 1; } fi echo "mynetspeeder tcp-only mode started on ${LISTEN_HOST}:${LISTEN_PORT}" echo "tcp: enabled" echo "udp: disabled" echo "relays: cleared from config" echo "pid file: $PID_FILE" echo "log file: $LOG_FILE" echo "log max: ${LOG_MAX_MB}MB x ${LOG_BACKUPS}"