#!/bin/bash
# anytls-fastpath — 把一组长连接逐步收敛到低延迟的那条 ECMP 路径上
#
# 背景:上游做 per-flow ECMP,同一目的地有多条延迟不同的路,每条新连接按 5 元组
# 哈希随机落在其中一条。若本机在运营商 NAT 后面(源端口出网会被重写),就无法靠
# 绑定源端口来选路。
#
# anytls / vless+mux 这类协议**不是**只开一条连接 —— 它们维护一个连接池,低负载
# 时可能只有 1 条,压力上来会扩到若干条,于是池子里快慢两条路的连接混在一起。
# 所以本脚本不是"整池重连",而是:逐条读 RTT,只踢掉落在慢路的那几条,让上层重新
# 建连再抽。反复几轮,池子就收敛到全部走快路。
#
# 用 ss -ti 读内核测的 RTT(不注入任何探测流量),用 ss -K 按 5 元组精准杀单条
# socket(需内核 CONFIG_INET_DIAG_DESTROY)。
#
# 用法:
#   anytls-fastpath.sh <目标IP> <目标端口>            收敛一次
#   anytls-fastpath.sh <目标IP> <目标端口> --status   只列出各连接的 RTT,不动手
#
# 可调环境变量:
#   THRESH=36      超过这个 ms 数算慢路。内核 smoothed rtt 含排队/应用延迟会飘,
#                  阈值要卡在两簇中间,别贴着快路上沿
#   MAX_ROUNDS=4   最多收敛几轮,防止一直抽不中时反复踹连接
#   SETTLE=3       踢掉后等上层重连的秒数
#
# 先用 ecmp-probe.py 摸清这条链路有几条路、各自多快,再定 THRESH:
#   python3 ecmp-probe.py scan <目标IP> <目标端口> -n 80
#
# 挂 cron 示例:
#   */5 * * * * /root/anytls-fastpath.sh 1.2.3.4 8444 >>/root/anytls-fastpath.log 2>&1

set -uo pipefail

usage() {
    sed -n '2,33p' "$0" | sed 's/^# \?//'
    exit "${1:-1}"
}

case "${1:-}" in
    -h|--help|"") usage 0 ;;
esac

DST="$1"
PORT="${2:-}"
MODE="${3:-run}"

if [ -z "$PORT" ]; then
    echo "错误: 缺少目标端口" >&2
    echo "用法: $0 <目标IP> <目标端口> [--status]" >&2
    exit 2
fi
case "$PORT" in
    ''|*[!0-9]*) echo "错误: 端口必须是数字,收到 '$PORT'" >&2; exit 2 ;;
esac

THRESH="${THRESH:-36}"
MAX_ROUNDS="${MAX_ROUNDS:-4}"
SETTLE="${SETTLE:-3}"

ts() { date '+%F %T'; }

# 列出到 DST:PORT 的每条 ESTAB 连接,输出 "<本地端口> <rtt>"。
# ss -tin 每条连接占两行:第一行含地址,第二行含 rtt,这里按行配对。
list_conns() {
    ss -tin "dst ${DST}:${PORT}" 2>/dev/null | awk '
        /ESTAB/ {
            # 本地地址是含冒号的那个字段,取最后一段做端口
            for (i = 1; i <= NF; i++) {
                if ($i ~ /:[0-9]+$/) { split($i, a, ":"); lport = a[length(a)]; break }
            }
            next
        }
        /rtt:/ {
            if (lport == "") next
            match($0, /rtt:[0-9.]+/)
            r = substr($0, RSTART + 4, RLENGTH - 4)
            print lport, r
            lport = ""
        }'
}

if [ "$MODE" = "--status" ]; then
    out=$(list_conns)
    if [ -z "$out" ]; then
        echo "$(ts)  ${DST}:${PORT} 连接数=0 — 当前空闲,没有连接可测"
        exit 0
    fi
    n=$(echo "$out" | wc -l)
    fast=$(echo "$out" | awk -v t="$THRESH" '$2<=t' | wc -l)
    echo "$(ts)  ${DST}:${PORT} 连接数=${n}  快路=${fast}  慢路=$((n - fast))  (阈值 ${THRESH}ms)"
    echo "$out" | awk -v t="$THRESH" '{printf "    sport=%-6s rtt=%-8s %s\n", $1, $2"ms", ($2<=t ? "快" : "慢")}'
    exit 0
fi

for round in $(seq 1 "$MAX_ROUNDS"); do
    out=$(list_conns)
    if [ -z "$out" ]; then
        echo "$(ts)  ${DST}:${PORT} 无活动连接,跳过(下次建连会自己抽一次)"
        exit 0
    fi
    n=$(echo "$out" | wc -l)
    slow=$(echo "$out" | awk -v t="$THRESH" '$2>t')
    if [ -z "$slow" ]; then
        echo "$(ts)  ${DST}:${PORT} ${n} 条连接全在快路 ✓ (第${round}轮)"
        exit 0
    fi
    ns=$(echo "$slow" | wc -l)
    echo "$(ts)  第${round}轮: ${n} 条连接,其中 ${ns} 条在慢路 — 逐条踢掉"
    echo "$slow" | while read -r lport rtt; do
        echo "    踢 sport=${lport} (rtt=${rtt}ms)"
        ss -K "dst ${DST}:${PORT} sport = :${lport}" >/dev/null 2>&1
    done
    sleep "$SETTLE"
done

out=$(list_conns)
n=$(echo "$out" | grep -c . || true)
fast=$(echo "$out" | awk -v t="$THRESH" '$2<=t' | grep -c . || true)
echo "$(ts)  跑满 ${MAX_ROUNDS} 轮,当前 ${n} 条连接中 ${fast} 条在快路 — 剩下的等下个周期"
exit 0
