背景

手上有一条按月付费的 Panabit iWAN 隧道服务,跨境访问是没问题的,但出口 IP是服务商统一分配的默认出口,没法控制。想要的效果很简单:跨境这段继续走
已经付费的 iWAN 隧道,但最终出网的 IP 换成我自己买的那台 VPS
。

看起来是个很直觉的需求,但实际折腾下来,光是搞清楚”该在哪一端加配置”就走了不少弯路,中间也踩了几个挺隐蔽的坑,记录一下。

第一个误区:以为要在服务商的网络里”加一个自定义出口”

最开始的思路是:既然 iWAN 是 Panabit 的私有协议,服务端(网关)软件应该是能自己装的——查了一圈发现确实如此,Panabit 提供免费的网关软件,甚至有单网卡云服务器也能跑起来的教程。于是最初设想是在自己的 VPS 上装一套独立的Panabit 网关,完全抛开原来的服务商重新搭一套。

但这样做的问题很明显:完全没用到已经付费的那条跨境隧道,等于重新买了一遍”跨境”这件事,原来的钱白花了。

第二个尝试:iWAN 的”管道模式”

翻 Panabit 的资料时发现一个叫”管道模式”(pipe mode) 的功能:给两个客户端配相同的管道 ID、相反的管道方向,服务端会自动把这两个客户端的流量互相转发,不用额外配路由。这个思路是对的——复用同一个服务端账号,让服务端帮忙把流量转发到 VPS 这个”第二客户端”上——但需要服务商那边开通”第二客户端”名额并协调管道 ID,操作成本比预想的高。

真正的答案:管理后台里其实就是境外网关本身

转折点是发现自己手上这个 Panabit 管理后台,”虚拟专网”菜单下除了”iWAN服务”/“iWAN客户端”,还有 IPSec 和 L2TP 两个选项。一开始以为这台设备是境内客户端,打算在这台设备上再接一条隧道到 VPS,并且琢磨怎么让这条新隧道”强制走 iWAN 线路”才能复用跨境段。

结果去看”承载线路”下拉框,只有 WAN 一个选项,没有 iWAN。百思不得其解之后才意识到想错了方向:这台管理后台本身就是服务商在境外落地的那台网关,iWAN 隧道在这里已经终结了。它自己的 WAN 就是境外本地的出口线路,当然不会有”iWAN 线路”可选——因为到了这一步,跨境这件事已经做完了。

这个认知纠正之后,整个架构一下子就清楚了:

1
2
3
4
境内客户端 --(已付费的 iWAN 隧道,跨境)-->
服务商境外网关(就是这个管理后台)
--(新建一条 L2TP,走它自己的 WAN)-->
我的 VPS --(NAT)--> 公网

真正要做的事情,不是让某条隧道”穿过” iWAN,而是在这台境外网关的策略路由里,把原本从 iWAN 隧道进来、默认会被 NAT 到网关自己 WAN 口的流量,截胡改道到新建的这条去 VPS 的隧道。

为什么是 L2TP 不是 IPSec

一开始打算用 IPSec,服务商技术支持却建议用 L2TP。原因其实很实际:IPSec两端要精确匹配一长串 IKE 参数(加密算法、DH 分组、生命周期……),不同厂商实现之间经常卡在参数不一致上,Panabit 自己的论坛上都有专门吐槽 IPSec 连接问题的帖子。L2TP 走标准的 PPP 用户名密码认证,兼容性好得多,排障成本低一个量级。至于”裸 L2TP 没加密”这件事——反正这条连接是从 iWAN 隧道落地之后才发起的,暴露面本来就比直接裸奔在公网小,够用。

性能上两者差距也不大:L2TP 没有加密开销,IPSec 每个包都要走一遍 AES+HMAC,但现在随便一台 VPS 都有 AES-NI 硬件加速,个人用的单条出口隧道这个量级根本感知不到差别。真正的差距在包头开销和兼容性,不在吞吐。

VPS 端:一个 pm2 管理的 L2TP 服务器

VPS 这一侧需要跑一个纯 L2TP(不带 IPSec) 服务端,我写了个一键部署脚本deploy_l2tp_exit.sh,支持安装/更新/卸载,x86_64/arm64 通用xl2tpd/ppp/pm2 都是走包管理器和 npm 装的,不涉及架构相关的二进制):

1
2
3
4
5
6
7
8
# 安装
sudo bash deploy_l2tp_exit.sh --username <账号> --password <密码>

# 更新(复用已保存的参数重新生成配置+重启)
sudo bash deploy_l2tp_exit.sh --update

# 卸载
sudo bash deploy_l2tp_exit.sh --uninstall

用 pm2 而不是 systemd 管这个进程,单纯是手头已经用 pm2 管理其它几个服务,统一用一套工具看状态/看日志方便。做法是让 xl2tpd -D 跑在前台(等价于Debian 官方 xl2tpd.service 里 ExecStart=/usr/sbin/xl2tpd -D 的方式),交给 pm2 管重启和开机自启。

脚本自动做的事情:

  • 装 xl2tpd/ppp/iptables,以及 pm2 本身(Node.js + npm 全局包)
  • 生成 xl2tpd.conf/options.xl2tpd/写 chap-secrets
  • 开 ip_forward,加出口 NAT(MASQUERADE)
  • 主动把 PPP 的 MTU/MRU 收紧到 1200,并加 TCPMSS clamp 规则——这条 L2TP 是叠在已经 1400 MTU 左右的 iWAN 隧道上面的,嵌套隧道比单层隧道更容易出 现”ping 能通、TCP/HTTPS 大包直接卡死”的 MTU 黑洞问题,提前把 MTU 往下 压一截 + 加 MSS clamp 是双保险

境外网关:新建 L2TP 客户端 + 一条策略路由

在”虚拟专网 → L2TP”里新建一个连接,对端填 VPS 的公网 IP 和端口、账号密码跟 VPS 上保持一致,”承载线路”保持 WAN 不用管(前面说过,原因是这台设备本身已经在境外了)。

连上之后,关键是策略路由那张表。默认策略通常长这样:

序号 源接口 源地址 目标地址 动作 目标线路
1 any iWAN网段 iWAN网段 路由 iwan-server
20000 iwan-server iWAN网段 any NAT WAN
30000 any any any NAT WAN

序号 20000 这条就是”服务商默认出口”的真身——把 iWAN 隧道进来的流量 NAT到网关自己 WAN 口出网。要做的就是在它前面插一条优先级更高的规则:

字段 填什么
序号 比 20000 小,比如 10000
源接口 iwan-server
源地址 自己账号实际分配到的隧道 IP(建议用具体 /32,不要整个网段,避免把其他账号的流量也带偏)
目标地址 any(或者只想部分流量走 VPS 就填具体网段)
动作 NAT(不是”路由”——要跨到 VPS 的地址空间需要先做源地址转换,不然到了 VPS 那边源 IP 还是内网地址,对不上 VPS 侧按隧道网段做的 MASQUERADE 规则)
目标线路 刚建的这条 L2TP 连接

保存之后用这个账号测一下出口 IP,变成 VPS 自己的公网 IP 就算通了。

实测踩过的坑

整个过程里真正费时间的不是想清楚架构,是下面这几个一个接一个冒出来的坑,记录一下免得以后再踩:

1. Debian 装完 xl2tpd 包会自动拉起同名 systemd 服务,跟 pm2 抢 pidfile

apt install xl2tpd 装完之后,包的安装后置脚本会自动把它注册成 systemd服务并启动。于是出现两个 xl2tpd 互相打架——systemd 那个先占住了 pidfile,pm2 拉起来的这份一启动就发现”已经有实例在跑”直接退出,pm2 又把它重启一遍,死循环到重启次数上限。pm2 logs 里一直刷:

1
consider_pidfile: There's already a xl2tpd server running.

解决办法是在注册 pm2 进程之前,先 systemctl stop/disable/mask xl2tpd——注意要 mask 不能只 disable,否则下次 apt upgrade 触发的包维护脚本还是有可能把它重新拉起来。

2. ppp 2.5.x 移除了几个老掉牙的串口选项

options.xl2tpd 里常见的 crtscts/lock/local 这几个选项,本来是给真实串口调制解调器用的硬件流控/锁文件/载波检测,pppol2tp 走的是伪 tty 根本用不上。但新版本的 ppp (2.5.x,Debian 13/trixie 这类新发行版自带)彻底不认这几个选项了,写在配置文件里会导致 pppd 直接报错退出:

1
2
/usr/sbin/pppd: In file /etc/ppp/options.xl2tpd: unrecognized option 'crtscts'
xl2tpd[xxxxx]: child_handler : pppd exited for call xxxxx with code 2

最坑的地方是 xl2tpd 这边日志看着一切正常——隧道建立了、会话也建立了(Call established)——紧接着 pppd 就因为一个选项直接退出,第一次看到以为是认证或者网络问题,排查方向完全错了。把这三个选项从配置里删掉就好。

3. 装了 Docker 的机器,FORWARD 链默认策略是 DROP

这个是最隐蔽的一个。L2TP 连上了,境外网关那边能看到这条线路”流出速率”在跳,但”流入速率”一直是 0——相当于单向通,包发过去了,回包却永远收不到。

查了半天发现是 VPS 上装了 Docker,Docker 会把 FORWARD 链的默认策略改成 DROP,只放行自己管理的 docker0/容器网络(iptables -L FORWARD能看到专门的 DOCKER-USER/DOCKER-FORWARD 链)。我们自己加的 NAT 规则挂在 POSTROUTING,但包根本到不了那一步,在 FORWARD 链这里就被默认策略吞掉了。解决办法是显式插入放行规则,而且要用 -I(插到链最前面)而不是 -A(追加到最后),避免排在 Docker/ufw 自己加的 DROP 规则后面不生效:

1
2
iptables -I FORWARD -s 192.168.99.0/24 -o eth0 -j ACCEPT
iptables -I FORWARD -d 192.168.99.0/24 -i eth0 -m state --state ESTABLISHED,RELATED -j ACCEPT

4. 普通 iptables 规则重启就没了

上面这些规则默认不会持久化,VPS 一重启全部清空。装 iptables-persistent(Debian/Ubuntu),跑一次 netfilter-persistent save 把当前规则(包括Docker 自己那些)落盘,重启后自动恢复。

5. 单核 VPS 上部署脚本”卡死”

在一台单核 VPS 上跑部署脚本,卡在”安装依赖”这一步一动不动,什么输出都没有。排查下来是两个问题叠在一起:

  • 开机自动触发的 unattended-upgrades/apt-daily 后台更新占着 dpkg 锁,
    单核机器跑得很慢,锁等很久才释放;
  • 脚本最初用 apt-get ... -qq 把所有输出都静默了,导致”其实在正常跑只是
    慢”和”真的卡死”从终端上完全看不出区别,白白让人怀疑是不是死循环了。

更麻烦的是,如果这时候等得不耐烦直接 Ctrl+C 掉,会把 dpkg 中断在”半配置”状态,之后随便跑什么 apt 命令都会先报一句:

1
E: dpkg was interrupted, you must manually run 'dpkg --configure -a' to correct the problem.

修法是先手动 dpkg --configure -a 把状态修好,再重新执行部署脚本;脚本现在也去掉了 -qq、加了锁等待提示,并且在装包前自动跑一遍dpkg --configure -a 兜底(没有残留状态时是无副作用的空操作)。

小结

这件事从头到尾最大的认知偏差,是一开始把”哪一端是境内客户端、哪一端是境外落地点”搞反了——一旦搞清楚管理后台本身就是境外网关,剩下的就是常规的”L2TP + NAT + 策略路由”操作,没有任何魔法。反倒是 VPS 这一侧看似简单的L2TP 服务端,被 Debian 新版本的一堆默认行为(systemd 自启、ppp 2.5 的兼容性变化、Docker 改 iptables 默认策略)接连绊了好几跤,这些坑单独拎出来看都不算疑难杂症,连在一起排查的时候却挺容易把人带偏方向。

最终效果:境内设备产生的流量,经过已经付费的 iWAN 隧道到达服务商境外网关,再经过一条自建的 L2TP 隧道落到自己的 VPS,由 VPS 做最后一跳 NAT 出网——跨境这段没有浪费,出口 IP 也完全掌握在自己手里。