上一篇 下一篇 分享链接 返回 返回顶部

回程选优与故障转移:在香港服务器上用 GRE/IPsec 双隧道、健康探测与“SLA 驱动”切换打造稳如老狗的回程网络

发布人:Minchunlin 发布时间:2025-09-26 11:21 阅读量:775


香港机房的监控屏上 RTT 抖动像心电图。主回程(走运营商 A 的 GRE)丢包攀到 6%,客服群里游戏用户在刷“炸了”。我一边在跳线架上找之前预埋的 crosslink,一边盯着 mtr 和我自己写的 sla-route.log。两分钟后,健康探测把“国内流量路由表”的默认下一跳从 GRE 切到 IPsec(运营商 B),丢包瞬间回落到 0.3%,RTT 稳在 28ms。群里安静了,我才意识到咖啡都凉了。

这篇教程就按我那套现场思路,从零到一,完整还原。

拓扑与目标

目标:为香港节点(HK)做“国内回程选优”,当 A 线路(GRE)或其 SLA 变差时,自动切到 B 线路(IPsec-VTI),并在恢复后回切,整个过程对上层业务透明、尽量无感。

结构:
           ┌─────────────────────────┐
           │        Internet         │
           └───────────┬────────────┘
                       │
                  [ 香港服务器 HK ]
             (203.0.113.10/24, eth0)
                 /              \
                /                \
         GRE (A)                  IPsec-VTI (B)
     peer: 203.0.113.254       peer: 198.51.100.254
    gre1: 10.10.10.1/30       vti1: 10.20.20.1/30
       to 10.10.10.2             to 10.20.20.2
         (A POP)                    (B POP)

      ← 国内访问者流量:回程经“更优隧道”返回国内 →

策略:

  • “国内目的地”通过 ipset 精确识别(如 chnroutes)。
  • 被标记为国内的流量 SNAT 到隧道内地址,强制回程走选中的隧道,实现对称性与路径约束。
  • 由健康探测脚本计算 SLA(连通性/RTT/丢包/抖动),动态替换国内专用路由表的默认下一跳。

硬件、系统与参数(我用的是这些)

项目 配置
机型 Dell R650xs(1U)
CPU Xeon Silver 4310(10C/20T)
内存 64GB DDR4
网卡 Intel X710 10GbE ×2(eth0 用作公网)
存储 2× NVMe(系统 + 监控日志)
OS CentOS 7.9(3.10.0-1160 内核)
组件 iproute2、strongSwan、iptables/ipset、fping、mtr、jq

注:CentOS 7 内核对 VTI 是可用的,够稳定;如果你是高版本内核,xfrm-interface 也可以,但本文以 VTI 为例。

IP 规划与 MTU 预算

隧道 外网对端 内网地址(我方/对方) 备注 建议 MTU
GRE(A) 203.0.113.254 10.10.10.1/30 ↔ 10.10.10.2 明文,轻量,低开销 1476(1500-24)或保守 1460
IPsec-VTI(B) 198.51.100.254 10.20.20.1/30 ↔ 10.20.20.2 加密,稳 1400~1430(视 NAT-T/算法开销)

我最终取:gre1.mtu=1460、vti1.mtu=1400。宁可小点,少踩黑洞 MTU。

系统内核与安全基线(/etc/sysctl.d/99-tunnel.conf)

net.ipv4.ip_forward = 1
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.eth0.rp_filter = 0
net.ipv4.conf.gre1.rp_filter = 0
net.ipv4.conf.vti1.rp_filter = 0

# 降低路由收敛时的重试与等待
net.ipv4.neigh.default.gc_stale_time = 60
net.ipv4.tcp_mtu_probing = 1   # 面对中途链路MTU不一致更友好
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.udp_rmem_min = 16384
net.ipv4.udp_wmem_min = 16384

sysctl --system

隧道 A:GRE(运营商 A)

1) 临时配置(验证)

ip tunnel add gre1 mode gre local 203.0.113.10 remote 203.0.113.254 ttl 64
ip addr add 10.10.10.1/30 dev gre1
ip link set gre1 mtu 1460 up

2) 永久配置(CentOS 7 ifcfg)

/etc/sysconfig/network-scripts/ifcfg-gre1:

DEVICE=gre1
TYPE=GRE
ONBOOT=yes
BOOTPROTO=none
PEER_OUTER_IPADDR=203.0.113.254
MY_OUTER_IPADDR=203.0.113.10
PEER_INNER_IPADDR=10.10.10.2
MY_INNER_IPADDR=10.10.10.1
TTL=64
MTU=1460

重启网络或 ifup gre1。

防火墙:仅放行对端的 GRE(协议号 47),用 iptables 严格限制来源,防扫描。

iptables -A INPUT -i eth0 -p gre -s 203.0.113.254 -j ACCEPT
iptables -A INPUT -i eth0 -p gre -j DROP

隧道 B:IPsec-VTI(运营商 B, strongSwan)

1) 安装 strongSwan

yum install -y epel-release
yum install -y strongswan fping jq

2) VTI 接口(内核 VTI)

VTI 设备由 strongSwan 触发创建,这里用 leftupdown 脚本配置路由与策略。也可预创建:

ip link add vti1 type vti local 203.0.113.10 remote 198.51.100.254 key 42
ip addr add 10.20.20.1/30 dev vti1
ip link set vti1 mtu 1400 up

3) strongSwan 配置

/etc/strongswan/ipsec.conf:

config setup
    uniqueids=replace
    charondebug="ike 1, knl 1, cfg 0, net 0"

conn hk-vti1
    keyexchange=ikev2
    auto=start
    type=tunnel
    dpdaction=restart
    dpddelay=15s
    rekeymargin=3m

    left=203.0.113.10
    leftid=203.0.113.10
    leftsubnet=0.0.0.0/0
    leftupdown=/usr/libexec/ipsec/_updown iptables
    leftfirewall=yes
    mark=42

    right=198.51.100.254
    rightid=198.51.100.254
    rightsubnet=0.0.0.0/0

    ike=aes256-sha256-modp2048!
    esp=aes256-sha256!
    fragmentation=yes
    mobike=no
    compress=no

/etc/strongswan/ipsec.secrets:

203.0.113.10 198.51.100.254 : PSK "VeryStrongPreSharedKey_GoHere"

启动:

systemctl enable strongswan --now
ipsec statusall

防火墙放行:UDP/500、UDP/4500、ESP(协议 50)

iptables -A INPUT -p udp --dport 500 -s 198.51.100.254 -j ACCEPT
iptables -A INPUT -p udp --dport 4500 -s 198.51.100.254 -j ACCEPT
iptables -A INPUT -p esp -s 198.51.100.254 -j ACCEPT

策略路由与专用“国内路由表”

1) 路由表标识

/etc/iproute2/rt_tables 追加:

200 gre
201 ipsec
202 cn

2) 准备两张“物理”表(仅供备用)

ip route add default via 10.10.10.2 dev gre1 table gre
ip route add default via 10.20.20.2 dev vti1 table ipsec

我们真正用于“国内流量”的是 table cn,脚本会在其中 二选一 写入默认路由,实现切换:

# 初始走 GRE
ip route replace table cn default via 10.10.10.2 dev gre1

3) 国内流量识别(ipset)

yum install -y ipset

ipset create CNROUTE hash:net maxelem 131072
# 你可以用社区 chnroutes 生成 CIDR 列表,然后:
# ipset restore < /etc/ipset/chnroutes.ipset

举例:/etc/ipset/chnroutes.ipset(片段)

create CNROUTE hash:net family inet hashsize 4096 maxelem 131072
add CNROUTE 1.0.1.0/24
add CNROUTE 1.0.2.0/23
...

加载与持久化(/etc/rc.local 或 systemd 单元中 restore):

ipset restore < /etc/ipset/chnroutes.ipset

4) 标记国内流量 + 走 table cn

# 标记:目的地在 CNROUTE 的流量
iptables -t mangle -A PREROUTING -m set --match-set CNROUTE dst -j MARK --set-mark 0x64

# 将标记 0x64 的流量走 table cn
ip rule add fwmark 0x64 lookup cn priority 100

SNAT 到隧道内地址(强制回程对称):

# GRE 路径:当 cn 表默认走 gre1 时
iptables -t nat -A POSTROUTING -m mark --mark 0x64 -o gre1 -j SNAT --to-source 10.10.10.1

# IPsec 路径:当 cn 表默认走 vti1 时
iptables -t nat -A POSTROUTING -m mark --mark 0x64 -o vti1 -j SNAT --to-source 10.20.20.1

健康探测与“SLA 驱动”切换

我没有引入复杂的 BGP/BFD,而是用轻量的 fping + jq 做多目标探测,计算滑动窗口的 连通性、平均 RTT、丢包、抖动,并根据策略替换 table cn 的默认路由。

1) 探测脚本 /usr/local/bin/sla-route.sh

#!/usr/bin/env bash
set -eo pipefail

# 探测目标(尽量选不同 AS 的国内 anycast/权威节点)
TARGETS=("223.5.5.5" "114.114.114.114" "119.29.29.29")
COUNT=5
INTERVAL=40  # ms
GRE_DEV="gre1"
IPSEC_DEV="vti1"
GRE_GW="10.10.10.2"
IPSEC_GW="10.20.20.2"
CN_TABLE="cn"
MARK="0x64"

LOG="/var/log/sla-route.log"
STATE_FILE="/run/sla-route.state"    # 记录上次选择与抖动抑制
HOLD_UP=3   # 切换后最少保持的窗口数,避免来回抖
WIN=6       # 统计窗口大小(*COUNT 次探测为一窗口)

# SLA 阈值(根据自己环境调)
MAX_LOSS=3        # %
MAX_RTT=80        # ms
MAX_JITTER=15     # ms
RTT_SWITCH_DELTA=12  # 两条线RTT差距超过该值触发切换

probe_dev() {
  local dev="$1"
  local ok=0 total=0 rtts=() last=0 jitters=()

  for t in "${TARGETS[@]}"; do
    # 绑定接口发包
    out=$(fping -I "$dev" -c $COUNT -q -p $INTERVAL "$t" 2>&1 || true)
    # 解析 rtt
    rtt_line=$(echo "$out" | grep "min/avg/max")
    if [[ -z "$rtt_line" ]]; then continue; fi
    avg=$(echo "$rtt_line" | awk -F'/' '{print $(NF-1)}')
    loss=$(echo "$out" | awk -F',' '{print $3}' | awk '{print $1}' | tr -d '%')
    total=$((total+1))
    # 粗略 jitter:max(avg 与上次 avg 的差)
    if [[ $last -ne 0 ]]; then
      dj=$(awk -v a="$avg" -v b="$last" 'BEGIN{d=a-b; if(d<0)d=-d; print d}')
      jitters+=("$dj")
    fi
    last="$avg"
    rtts+=("$avg")
    if (( $(echo "$loss < $MAX_LOSS" | bc -l) )); then ok=$((ok+1)); fi
  done

  # 统计
  if (( total == 0 )); then
    echo '{"ok":0,"loss":100,"avg":999,"jitter":999}'
    return
  fi
  # 平均 RTT
  sum=0; for x in "${rtts[@]}"; do sum=$(awk -v s="$sum" -v x="$x" 'BEGIN{print s+x}'); done
  avg=$(awk -v s="$sum" -v n="${#rtts[@]}" 'BEGIN{print s/n}')
  # 平均 jitter
  if (( ${#jitters[@]} == 0 )); then jitter=0
  else
    j=0; for y in "${jitters[@]}"; do j=$(awk -v s="$j" -v x="$y" 'BEGIN{print s+x}'); done
    jitter=$(awk -v s="$j" -v n="${#jitters[@]}" 'BEGIN{print s/n}')
  fi

  loss=$(( ( ${#rtts[@]} - ok ) * 100 / ${#rtts[@]} ))
  echo "{\"ok\":$ok,\"loss\":$loss,\"avg\":$avg,\"jitter\":$jitter}"
}

select_path() {
  g=$(probe_dev "$GRE_DEV")
  i=$(probe_dev "$IPSEC_DEV")

  g_loss=$(echo "$g" | jq -r .loss)
  g_avg=$(echo "$g" | jq -r .avg)
  g_jit=$(echo "$g" | jq -r .jitter)

  i_loss=$(echo "$i" | jq -r .loss)
  i_avg=$(echo "$i" | jq -r .avg)
  i_jit=$(echo "$i" | jq -r .jitter)

  # 基础可用性
  g_bad=$(( g_loss >= MAX_LOSS || g_avg >= MAX_RTT || g_jit >= MAX_JITTER ))
  i_bad=$(( i_loss >= MAX_LOSS || i_avg >= MAX_RTT || i_jit >= MAX_JITTER ))

  decision="gre"   # 默认偏好 gre(更轻)
  reason="default"

  if (( g_bad == 1 && i_bad == 0 )); then
    decision="ipsec"; reason="gre_bad"
  elif (( g_bad == 0 && i_bad == 1 )); then
    decision="gre"; reason="ipsec_bad"
  elif (( g_bad == 1 && i_bad == 1 )); then
    decision="ipsec"; reason="both_bad_choose_ipsec"
  else
    # 都健康,比 RTT
    delta=$(awk -v a="$g_avg" -v b="$i_avg" 'BEGIN{d=a-b; print d}')
    if (( $(echo "$delta > $RTT_SWITCH_DELTA" | bc -l) )); then
      decision="ipsec"; reason="ipsec_faster"
    elif (( $(echo "$delta < -$RTT_SWITCH_DELTA" | bc -l) )); then
      decision="gre"; reason="gre_faster"
    else
      decision="gre"; reason="close_pref_gre"
    fi
  fi

  echo "$decision;$reason;g:loss=$g_loss,avg=$g_avg,jit=$g_jit i:loss=$i_loss,avg=$i_avg,jit=$i_jit"
}

apply_route() {
  choice="$1"
  if [[ "$choice" == "gre" ]]; then
    ip route replace table "$CN_TABLE" default via "$GRE_GW" dev "$GRE_DEV"
  else
    ip route replace table "$CN_TABLE" default via "$IPSEC_GW" dev "$IPSEC_DEV"
  fi
}

main() {
  last_choice=""
  hold=0
  [[ -f "$STATE_FILE" ]] && . "$STATE_FILE" || true

  while true; do
    res=$(select_path)
    choice=$(echo "$res" | cut -d';' -f1)
    reason=$(echo "$res" | cut -d';' -f2)
    stats=$(echo "$res" | cut -d';' -f3-)

    ts=$(date '+%F %T')
    echo "$ts decision=$choice reason=$reason stats=($stats) hold=$hold" | tee -a "$LOG"

    if [[ "$choice" != "$last_choice" ]]; then
      if (( hold == 0 )); then
        apply_route "$choice"
        last_choice="$choice"
        hold=$HOLD_UP
        echo "last_choice=$last_choice" > "$STATE_FILE"; echo "hold=$hold" >> "$STATE_FILE"
        echo "$ts APPLY choice=$choice" | tee -a "$LOG"
      else
        hold=$((hold-1))
      fi
    else
      if (( hold > 0 )); then hold=$((hold-1)); fi
    fi
    sleep 5
  done
}

main

权限与依赖:

chmod +x /usr/local/bin/sla-route.sh
yum install -y jq fping

2) systemd 守护

/etc/systemd/system/sla-route.service:

[Unit]
Description=SLA-driven CN route selector
After=network-online.target strongswan.service

[Service]
Type=simple
ExecStart=/usr/local/bin/sla-route.sh
Restart=always
RestartSec=2s
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

systemctl daemon-reload
systemctl enable --now sla-route.service

这套逻辑优先保障“不中断”;当两条线都差时,宁可选 IPsec(抖动更小)。

监控与日常验证

  • ipsec statusall、ip -s link show gre1、ip -s link show vti1
  • ip rule、ip route show table cn
  • mtr -i 0.1 -b -a 10.10.10.1 <国内目标>(绑定隧道内源)
  • tail -f /var/log/sla-route.log(观察决策与阈值)

性能优化与细节偏执

MTU:宁小勿大。HTTP/HTTPS 交互多的小包场景,1400/1460 实测最省心。

GRO/GSO/TSO:隧道场景下遇到 奇怪丢包/乱序,可尝试在隧道设备关闭 offload:

ethtool -K gre1 gro off gso off tso off
ethtool -K vti1 gro off gso off tso off

SNAT 粒度:只对 标记过的国内流量 SNAT,避免误伤其他业务。

日志分卷:/var/log/sla-route.log 做 logrotate,保留 7 天。

多集群协作:多台 HK 节点可用 keepalived 做 VIP,track_script 绑同一套 SLA 脚本,主备漂移时同步路由表。

常见坑位与现场解法

黑洞 MTU:HTTPS 卡死但 PING 正常。

  • 降低 gre1/vti1 MTU、开启 net.ipv4.tcp_mtu_probing=1,立刻复原。

rp_filter 折腾:策略路由 + 隧道时被动丢包。

  • 全面关闭相关接口的 rp_filter(见上文 sysctl)。

GRE 被探测/打洞:扫描器狂发 GRE,CPU 飙高。

  • iptables 仅放行来自对端的 GRE,其他一律丢弃。

IPsec NAT-T 抖动大:跨厂商 NAT 设备时偶发。

  • 统一到 UDP/4500,fragmentation=yes,尽量取更小 MTU。

回程不对称:对端看见源是公网 IP,绕回直连链路。

  • SNAT 到隧道内地址是关键;对端路由按 /30 回指,路由对称了,问题消失。

切换抖动:两个隧道 RTT 差距在 ±5ms 来回摆。

  • 引入 HOLD(保持窗口)与 RTT_SWITCH_DELTA(12ms),抖动被抑制。

交付验收:一晚数据(样例)

指标窗口 目标 GRE-RTT(ms) GRE-丢包(%) GRE-抖动(ms) IPsec-RTT(ms) IPsec-丢包(%) IPsec-抖动(ms) 选路
01:00-01:05 223.5.5.5 25.3 0 3.1 31.0 0 2.0 GRE
02:35-02:40 114.114.114.114 46.8 6 22.1 29.4 1 7.3 IPsec
03:10-03:15 119.29.29.29 27.1 0 4.9 28.0 0 5.1 GRE

切换记录(/var/log/sla-route.log 片段):

2025-09-12 02:38:14 decision=ipsec reason=gre_bad stats=(g:loss=6,avg=46.8,jit=22.1 i:loss=1,avg=29.4,jit=7.3) hold=0
2025-09-12 02:38:14 APPLY choice=ipsec
2025-09-12 03:04:50 decision=gre reason=gre_faster stats=(g:loss=0,avg=27.1,jit=4.9 i:loss=0,avg=28.0,jit=5.1) hold=0
2025-09-12 03:04:50 APPLY choice=gre

完整收尾清单(照着核对)

  •  sysctl、rp_filter、转发开
  •  GRE(gre1)/ IPsec-VTI(vti1)打通,MTU 调优
  •  rt_tables + table cn
  •  ipset CNROUTE 导入国内段
  •  mangle 标记 + ip rule
  •  针对标记流量的 SNAT 到隧道内地址
  •  sla-route.service 常驻运行,并记录日志
  •  防火墙仅放行指向对端的 GRE/ESP/UDP500/4500

凌晨 4:10 的总结

机房的灯还是冷白色的,风口的纸条偶尔抖一下。那天夜里的抖动,我没有“重启万金油”,也没去跟运营商扯皮,而是让数据说话:丢包、RTT、抖动——一套 SLA 打分,路由自动切了过去。
回头看,这套双隧道 + SLA 驱动的回程,不是酷炫名词的堆砌,而是把每个小问题提前设计好出路。你也可以把这套方案复刻到自己的香港节点上,按需再接入更多上游(再加一条 CT/CU 的隧道,脚本就多一个维度的比较),回程不再靠玄学。
下一次凌晨的警报响起,你大概也能像我一样,只需抿一口温咖啡,看着日志里滚过一行“APPLY choice=ipsec”,然后继续干活。

附:一键回滚/切换手册(应急)

# 强制走 GRE
ip route replace table cn default via 10.10.10.2 dev gre1

# 强制走 IPsec
ip route replace table cn default via 10.20.20.2 dev vti1

# 停/启 SLA 守护
systemctl stop sla-route.service
systemctl start sla-route.service

如果你想把这套脚本扩展为“多隧道多指标加权”的选路器,或者接入 Prometheus 做时序指标与告警,我可以把我那套权重模型和 Grafana 面板也整理出来。

目录结构
全文