#!/bin/bash # 网络调优:修路径 MTU 问题 + 给 QUIC 隧道留出安全包长。 # # 这个脚本是 **MSS / 接口 MTU / 路径 MTU 探测** 的唯一出处。 # deploy-tune.sh 会调它;不要在别处再设一遍 —— # "到底哪个值生效" 一旦有两处来源就会变得很难查。 # # ════════════════════════════════════════════════════════════════ # 证据链 # ════════════════════════════════════════════════════════════════ # 现象:新建 TLS 连接约 30% 失败,但连接一旦建立就接近 100% 成功 # -> 典型的大包被丢、小包能过 # # ping 实测(到 1.1.1.1,-M do 禁止分片): # 包 1448 通 包 1488 丢 包 1500 全丢 # 接口 MTU 写着 1500,但实际路径扛不住。 # TLS 握手要发大证书包 -> 被丢 -> 握手失败。 # # ════════════════════════════════════════════════════════════════ # 实测效果(同一台设备,各 24 次) # ════════════════════════════════════════════════════════════════ # 不夹 MSS(1500) 87% # MSS 1300 100% # MSS 1360 100% <- 采用(和 MTU 1400 配套) # MSS 1400 100% # MSS 1440 100% # # ════════════════════════════════════════════════════════════════ # 为什么接口 MTU 也要压到 1400(早期结论是错的,这里更正) # ════════════════════════════════════════════════════════════════ # 早先这里写的是「不要改接口 MTU,实测降到 1400 反而掉到 50%」。 # 那个结论只考虑了 TCP,**漏掉了 QUIC**: # # * MSS 钳制走的是 iptables 的 TCPMSS 目标,**只对 TCP 生效**; # * 隧道用 QUIC 时跑在 UDP 上,整套 TCPMSS 规则对它毫无作用。 # # QUIC 的包一旦超过路径 MTU 就会被中间设备丢掉,而 UDP 丢包 # 不会回 ICMP,客户端只能干等到握手超时 —— cloudflared 日志里就是 # "failed to dial to edge with quic: timeout: no recent network activity" # # 把 wlan0 MTU 从 1500 降到 1400 之后(实测): # 建齐 6 条 QUIC 连接 28 秒 -> 13 秒 # 建齐 10 条 QUIC 连接 28 秒 -> 12 秒 # 建连期间 dial 失败 2 次 -> 1 次 # # 【重要】MTU 和 MSS 必须配套: # MTU 1400 => TCP 段最大 1360(1400 - 20 IP头 - 20 TCP头) # 如果 MTU 压到 1400 却还把 MSS 设成 1400, # TCP 段(1440) 就超过接口 MTU(1400),包在本地就被分片,等于白改。 # 两个值在本脚本里一起设,改一个必须改另一个。 set -u MSS_VALUE=${MSS_VALUE:-1360} MTU_VALUE=${MTU_VALUE:-1400} # ──────────────────────────────────────────────────────────────── # 找对外的那张网卡 # ──────────────────────────────────────────────────────────────── # 以前这里写死 wlan0。写死的问题是换机器(或者用 usb0 上网)就错了, # 而 MTU 设错网卡是没有任何提示的 —— 改了等于没改。 IFACE=${IFACE:-} if [ -z "$IFACE" ] && command -v ip >/dev/null 2>&1; then IFACE=$(ip -4 route show default 2>/dev/null \ | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' \ | head -1) fi if [ -z "$IFACE" ]; then for cand in /sys/class/net/*; do name=$(basename "$cand") case "$name" in lo|pan0|docker*|veth*|br-*) continue ;; esac [ -r "$cand/operstate" ] || continue [ "$(cat "$cand/operstate" 2>/dev/null)" = "up" ] || continue IFACE="$name" break done fi [ -z "$IFACE" ] && IFACE=wlan0 echo " 1) 打开路径 MTU 探测" if [ -w /proc/sys/net/ipv4/tcp_mtu_probing ]; then sysctl -w net.ipv4.tcp_mtu_probing=1 >/dev/null echo " tcp_mtu_probing = $(cat /proc/sys/net/ipv4/tcp_mtu_probing)" else echo " [跳过] 读不到 /proc(不是 Linux 环境)" fi echo " 2) 夹 MSS 到 $MSS_VALUE(只限制 TCP 段,不改链路层)" if command -v iptables >/dev/null 2>&1; then for chain in OUTPUT POSTROUTING; do # 先清掉所有旧值,避免重启后越积越多。 # ⚠ 这个 while 必须有次数上限: # 正常情况下 -D 删掉之后 -C 就返回非 0,循环自然结束。 # 但如果哪天 -D 报成功而规则还在(被包了一层、或 iptables 出异常), # 原写法就是死循环,把整个部署卡死。 # 实测踩过:桩命令一律返回 0,脚本真的转不出来了。 for old in 1300 1360 1400 1440 1460; do guard=0 while iptables -t mangle -C "$chain" -p tcp --tcp-flags SYN,RST SYN \ -j TCPMSS --set-mss "$old" 2>/dev/null; do guard=$((guard + 1)) if [ "$guard" -gt 10 ]; then echo " ⚠ 清理旧 MSS 规则($old) 超过 10 次仍报存在,停手" break fi iptables -t mangle -D "$chain" -p tcp --tcp-flags SYN,RST SYN \ -j TCPMSS --set-mss "$old" 2>/dev/null || break done done iptables -t mangle -A "$chain" -p tcp --tcp-flags SYN,RST SYN \ -j TCPMSS --set-mss "$MSS_VALUE" 2>/dev/null done iptables -t mangle -L -n 2>/dev/null | grep -i mss | sed 's/^/ /' else echo " [跳过] 没有 iptables" fi echo " 3) 接口 MTU -> $MTU_VALUE(这一条是给 QUIC 用的,见文件头说明)" if command -v ip >/dev/null 2>&1; then CUR=$(cat "/sys/class/net/$IFACE/mtu" 2>/dev/null || echo "?") if [ "$CUR" = "$MTU_VALUE" ]; then echo " $IFACE 已经是 $MTU_VALUE,不用改" elif ip link set dev "$IFACE" mtu "$MTU_VALUE" 2>/dev/null; then echo " $IFACE 的 MTU: $CUR -> $(cat "/sys/class/net/$IFACE/mtu" 2>/dev/null)" else echo " ⚠ 改 $IFACE 的 MTU 失败(权限或网卡不支持),继续" fi else echo " [跳过] 没有 ip 命令" fi # 持久化:wlan0 归 NetworkManager 管,光用 ip 改重启就丢。 # 写进 NM 自己的连接配置,比塞 rc.local 干净。 if command -v nmcli >/dev/null 2>&1; then CONN=$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null \ | awk -F: -v dev="$IFACE" '$2==dev {print $1; exit}') if [ -n "$CONN" ]; then nmcli connection modify "$CONN" 802-11-wireless.mtu "$MTU_VALUE" \ 2>/dev/null \ || nmcli connection modify "$CONN" ethernet.mtu "$MTU_VALUE" \ 2>/dev/null || true echo " 已写进 NetworkManager 连接「$CONN」(重启后仍然生效)" fi fi # ════════════════════════════════════════════════════════════════ # 4) 关键:让已经在跑的隧道重建连接 # ════════════════════════════════════════════════════════════════ # MSS 和 MTU 只对**新建连接**生效(MSS 改的是 SYN 包里的选项, # MTU 影响的是新建的 socket)。隧道如果建在规则生效之前, # 它那条连接还带着旧参数 —— # 实测:隧道 17:01 建、规则 17:22 生效,可用率一直卡在 76%; # 重启隧道之后立刻回到 100%。 # # 开机时不会走到这一步(nettune 服务 Before=lanremote-tunnel.service, # 规则先于隧道生效);但**手动重跑本脚本**时必须重建, # 否则改了等于没改。 # ════════════════════════════════════════════════════════════════════ # 4) 让已经建立的隧道用上新参数 —— **只在手动跑的时候做** # ════════════════════════════════════════════════════════════════════ # # ⚠ 这一步绝对不能由 lanremote-nettune.service 执行。 # # 那个 unit 里写着 Before=lanremote-tunnel.service # (意思是"我必须先于隧道启动完")。如果脚本在自己还没结束的时候 # 去 `systemctl restart lanremote-tunnel`,systemd 会认为存在 # 顺序冲突 —— restart 永久阻塞、nettune 一直卡在 activating、 # 隧道被 Before= 挡着永远起不来。 # # 实测卡了 9 分半,表现就是"网站又挂了",而日志里什么都看不出来 # (只有 nettune 停在 activating)。手动跑本脚本却完全正常 # (因为那时没有 Before= 约束)—— 所以这个坑特别难发现。 # # 开机时本来也不需要这一步:顺序就是 nettune 先、隧道后, # 那时候隧道还没起来。 if [ "${NETTUNE_SERVICE_CALL:-0}" = "1" ]; then echo " 4) 由 nettune 服务调用,**跳过重启隧道**" echo " (unit 里有 Before=lanremote-tunnel.service," echo " 在这里 restart 会和 systemd 的顺序约束死锁)" echo " (开机顺序本来就是本脚本先、隧道后,不需要这一步)" elif [ "${SKIP_TUNNEL_RESTART:-0}" = "1" ]; then echo " 4) SKIP_TUNNEL_RESTART=1,跳过重启隧道" else echo " 4) 让已经建立的隧道用上新参数" RESTARTED=0 for svc in lanremote-tunnel lanremote-tunnel-h2; do if command -v systemctl >/dev/null 2>&1 \ && systemctl is-active --quiet "$svc" 2>/dev/null; then echo " $svc 正在跑,重启它让新参数生效" systemctl restart "$svc" >/dev/null 2>&1 && RESTARTED=1 fi done [ "$RESTARTED" = "0" ] && echo " 没有正在跑的隧道,跳过" fi echo " 网络调优已应用(MSS=$MSS_VALUE, MTU=$MTU_VALUE, 网卡=$IFACE)"