大带宽服务器做SSH反向隧道内网穿透,10G口只跑出500Mbps——加密算法、TCP窗口缩放与内核splice的完整排查
故障现场:10G口服务器,内网穿透下载只有50MB/s
客户环境:轻云互联的大带宽服务器(10G口,公网IP 203.0.113.10),内网有一台NAS(192.168.1.100),需要通过公网访问NAS上的文件服务。最初用最朴素的SSH反向隧道:
# 在内网NAS上执行
ssh -R 0.0.0.0:18080:localhost:80 -N -f root@203.0.113.10
然后外网用户访问 http://203.0.113.10:18080 就能打到NAS的80端口。功能没问题,但客户抱怨:从公网下载NAS上的大文件,速度只有 50MB/s 左右(约400Mbps),而服务器是10G口,NAS到服务器也是万兆内网。这完全不合理。
第一反应:是不是服务器出口带宽被限了?先做基准测试。
# 服务器到公网测速,用iperf3
iperf3 -c speedtest.example.com -P 8 -t 10
# 结果:9.4 Gbits/sec —— 带宽没问题
再测NAS到服务器的裸TCP速度(不经过SSH隧道):
# NAS上
iperf3 -c 203.0.113.10 -P 8 -t 10
# 结果:9.2 Gbits/sec —— 内网也没问题
问题锁定在SSH隧道本身。
第一层排查:加密算法与MAC的代价
SSH隧道是用户态转发,每个数据包都要在内核和sshd进程之间拷贝。先看当前SSH连接用的什么加密算法:
# 在服务器上查看已建立的SSH连接
ss -tnp | grep 203.0.113.10:22
# 或者用ssh -v看协商过程
ssh -v -R 0.0.0.0:18080:localhost:80 -N root@203.0.113.10 2>&1 | grep -E "cipher|MAC"
输出显示:aes128-ctr + hmac-sha1。这是OpenSSH的默认老算法。在高带宽下,CTR模式虽然快,但HMAC-SHA1是逐个包计算,且没有ETM(Encrypt-then-MAC)优化。
替换为现代算法:
ssh -R 0.0.0.0:18080:localhost:80 -N \
-o Ciphers=aes128-gcm@openssh.com \
-o MACs=hmac-sha2-256-etm@openssh.com \
-o Compression=no \
root@203.0.113.10
重新测速:80MB/s(约640Mbps)。有提升,但离10G还差得远。
第二层排查:TCP窗口缩放与内核缓冲区
SSH隧道底层是一个TCP连接。带宽延迟积(BDP)决定了要跑满10G需要多大的窗口。假设RTT 0.2ms,10Gbps需要 10e9 * 0.0002 / 8 = 250KB 窗口。如果窗口缩放没开或缓冲区太小,就会卡在几十MB/s。
检查当前SSH连接的TCP参数:
ss -ti dst 203.0.113.10
关键输出:
rcv_space:29200 snd_cwnd:10 rcv_ssthresh:29200
rcv_wnd:29200 snd_wnd:29200
wscale:0,0
wscale:0,0 —— 窗口缩放因子为0!意味着最大窗口只有65535字节。这直接锁死了吞吐量。
为什么窗口缩放没生效?检查内核参数:
sysctl net.ipv4.tcp_window_scaling
# net.ipv4.tcp_window_scaling = 1 —— 是开的
但为什么wscale=0?因为sshd进程在创建socket时可能没有启用窗口缩放?实际上,窗口缩放是在TCP握手时协商的,如果双方都支持就会启用。但这里显示0,可能是sshd设置了SO_RCVBUF,导致内核禁用了窗口缩放?查一下:
# 查看sshd的socket缓冲区
cat /proc/$(pgrep -f "sshd: root@pts")/limits | grep -i "max memory"
更直接的办法:调整系统级TCP缓冲区,并强制SSH使用更大的缓冲区。OpenSSH没有直接参数,但可以修改内核默认值:
sysctl -w net.core.rmem_max=134217728
sysctl -w net.core.wmem_max=134217728
sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"
sysctl -w net.ipv4.tcp_congestion_control=bbr
重新建立SSH隧道后再看:
ss -ti dst 203.0.113.10 | grep wscale
# wscale:7,7
窗口缩放开了,测速:150MB/s(约1.2Gbps)。还是不够。
第三层排查:用户态转发的CPU瓶颈
此时用 perf top 看热点:
perf top -p $(pgrep -f "sshd: root@notty")
输出:
45.2% sshd [.] 0x000000000004a2c1
30.1% [kernel] [k] copy_user_enhanced_fast_string
12.3% [kernel] [k] tcp_sendmsg
5.6% [kernel] [k] tcp_recvmsg
copy_user_enhanced_fast_string 占30%——这是内核态和用户态之间的数据拷贝。SSH隧道每转发一个字节,都要经历:内核socket缓冲区 → sshd用户态缓冲区 → 另一个内核socket缓冲区。单核跑满也只能到1.2Gbps左右,因为每次拷贝都要消耗CPU周期。
尝试用 socat 替代?socat也是用户态,同样瓶颈。用 strace -c 看系统调用:
strace -c -p $(pgrep -f "sshd: root@notty")
结果:read 和 write 各几十万次,每次几KB。用户态转发无法避免上下文切换。
有没有内核态的转发方案?有,但SSH不支持。要么打HPN-SSH补丁,要么换工具。
换用WireGuard:内核态转发跑满9Gbps
既然SSH隧道用户态瓶颈无法根除,果断切换到WireGuard。WireGuard在内核态处理加解密和转发,性能天差地别。
配置步骤
服务器端(轻云互联大带宽服务器):
# 安装wireguard
apt install wireguard -y
# 生成密钥
wg genkey | tee privatekey | wg pubkey > publickey
# 配置wg0
cat > /etc/wireguard/wg0.conf <
内网NAS端:
cat > /etc/wireguard/wg0.conf <
然后在大带宽服务器上做DNAT,把公网18080映射到NAS的80:
iptables -t nat -A PREROUTING -p tcp --dport 18080 -j DNAT --to-destination 10.10.0.2:80
iptables -A FORWARD -p tcp -d 10.10.0.2 --dport 80 -j ACCEPT
测速:9.1 Gbps。搞定。
新问题:WireGuard的UDP流量导致单核softirq打满
跑满9Gbps后,观察CPU:
mpstat -P ALL 1
发现 %soft 在CPU 0上高达95%,其他核空闲。这是典型的网卡多队列RSS哈希问题:WireGuard的UDP流量(源端口51820)被网卡哈希到固定队列,所有软中断都落在CPU 0。
解决:启用RPS(Receive Packet Steering),把软中断分散到多核:
# 查看网卡队列数
ethtool -l eth0
# 开启RPS,将rx-0的软中断分配到CPU 0-7
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
# 同时调整RFS
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 32768 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
再调整网卡中断亲和性,把不同队列的中断绑到不同CPU:
# 查看中断号
grep eth0 /proc/interrupts
# 将队列0的中断绑到CPU0-3,队列1绑到CPU4-7
echo 0f > /proc/irq/25/smp_affinity
echo f0 > /proc/irq/26/smp_affinity
调整后,mpstat 显示各核softirq均匀分布,WireGuard吞吐稳定在9.4Gbps,不再波动。
总结与避坑清单
- SSH反向隧道只适合临时、低带宽的内网穿透。超过500Mbps就会撞到用户态拷贝墙,换加密算法和调TCP参数只能缓解,不能根治。
- 大带宽服务器做内网穿透中转,优先选内核态方案:WireGuard、IPsec、或者基于eBPF的转发(如Cilium)。轻云互联的10G口服务器默认内核已经开了BBR和合理的TCP缓冲区,但WireGuard的UDP多队列仍需手动RPS。
- 永远检查
ss -ti中的wscale。窗口缩放为0时,吞吐量天花板是几十MB/s。 - MTU别忘设。WireGuard默认1420,如果内网有PPPoE可能还要降到1400以下,否则分片会严重拖累性能。
- 用
perf top和strace -c定位用户态瓶颈,比盲目调参快得多。