ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

告别iptables:用cproxy+wstunnel搭建稳定内网穿透隧道

告别iptables:用cproxy+wstunnel搭建稳定内网穿透隧道 这两年在家庭内网和远程访问这件事上我差不多把该踩的坑都踩了一遍。最早为了把自己家里的 NAS 和虚拟机暴露到公网我老老实实研究 iptables前前后后写了两屏规则转发链、回流、地址伪装全用上结果换个内网网段就得从头理一遍。后来我把方案换成了 cproxy wstunnel 这条组合线内网穿透的体验可以说提升了一个台阶外面设备访问家里服务就像访问一个普通公网端口内网一侧不用在每个应用上改配置隧道断了还会自动重连。这篇文章就把这套透明转发的搭建过程、底层思路和实测排错完整写出来给正在 iptables 和各类端口转发工具之间纠结的朋友一个参考。1. 我为什么把 iptables 方案整个丢掉先说明白iptables 本身是个极其强大的工具Linux 上几乎所有网络转发和拦截都绕不开它。但“强大”和“好用”是两回事。我需要做的事情本质上是通过一台有公网地址的服务器把流量送回家里的内网服务。这个场景在笔记本、手机上特别常见出差的时候想看一眼家里机器的 SSH或者打开路由器后台都属于典型的内网穿透需求。用 iptables 做这件事思路是用户请求先到公网服务器然后服务器把它“嫁给”内网机器的某个端口。看起来只有一条 DNAT 规则的事实际运行起来完全不是那么回事。我当时写过一套规则包含了 PREROUTING 链上的流量标记、FORWARD 链上的过滤放行、POSTROUTING 链上的 SNAT还要配合ip_forward开关、conntrack 状态跟踪。规则一多顺序就变得异常敏感换一个网卡改一个服务端口可能就要把整张表重排一遍。更现实的问题是 iptables 规则的重载。机器一旦重启规则全部消失必须依赖iptables-save和iptables-restore把策略恢复回来。如果两条规则之间有点逻辑关联保存和恢复的顺序只要错一步服务就是半通不通的状态。我印象很深的一次是为了让一个服务在重启后自动恢复写了 init 脚本结果因为restore执行时机比网卡初始化早所有规则直接失效排查了整整一个下午。还有一个让我下决心放弃的点iptables 本质上是在内核网络栈里做数据包层面的“搬运工”它不关心你传输的是 HTTP、SSH 还是数据库协议也不管上层连接是否断掉。链路一旦抖动TCP 连接断了就断了没有重连机制没有会话保持。可真实网络环境里运营商链路切换、NAT 会话老化、机房小抖动都会发生。我用 iptables 的时候每遇到一次断连就要手动重连体验非常割裂。对比一下我现在的方案差别就很明显了维度传统 iptables 端口转发cproxy wstunnel 隧道方案规则维护多条链多张表顺序敏感写一份端侧配置即可重启恢复需要 save/restore 和脚本编排systemd 托管开机自启断线处理无感知TCP 断了就断了心跳检测加自动重连多服务扩展每加一个端口都要加规则在同一隧道内增加转发项调试难度靠iptables -L -v和数据包计数看日志和隧道状态相对直观当然我不是说 iptables 一无是处它确实是最底层的通用方案。但在“稳定访问内网服务”这个具体需求里更上层的隧道方案明显更省心。这也是我写下这篇文章的初衷如果你也只需要解决内网穿透这一件事没必要把精力耗在数据包规则里。2. 一条链路看懂 cproxy 和 wstunnel 各干各的活先讲清楚这套方案的角色分工很多人上来就抄命令结果两端装反了半天连不上。wstunnel 是一个基于 WebSocket 的隧道工具它负责在公网服务器和内网机器之间建立一条端到端的 WebSocket 通道。cproxy 则负责把本地要穿透的流量“接住”然后塞进这条通道。这里最核心的概念是WebSocket 隧道本质上就是一条长连接。它把 TCP 的字节流切成 WebSocket 帧再包裹在普通 HTTPS 流量中传输。听起来可能抽象打个比方就很直观了内网的服务藏在一条小巷里直接叫外卖小哥送进去不可能因为外人根本不知道门牌号。wstunnel 做的事情是在巷子口租了一个收发室外面所有包裹都先送到收发室再由收发室统一转交到巷子里的各家各户。cproxy 则是这个收发室的自动化分拣员它知道哪一份包裹要送给哪一户。具体到流量流向大概是这样用户从公网的笔记本发起请求比如 SSH 连接tunnel.example.com:8022。这个请求首先到达公网服务器上的 wstunnel 服务端。因为 wstunnel 已经在公网侧监听 8022 端口它接收请求后把数据放入已经建立的 WebSocket 隧道一路送回内网机器的 cproxy 端。cproxy 收到数据后根据本地的转发配置把连接原样转给127.0.0.1:22这个真实服务。回程数据就反着走一遍。整个过程对使用方是透明的或者说是“无感”的。你 SSH 到公网 IP 的 8022 端口实际上进入的是你家里的另一台机器。这一点和 iptables 想实现的效果一模一样但实现位置完全不同iptables 是在服务器的内核网络层面做重定向而 cproxy wstunnel 是在用户态应用层做转发。用户态做的好处是重连、心跳、多路复用这些逻辑可以写得很灵活不用去搞内核参数和规则链。还有一点值得说为什么选 WebSocket 而不是直接打一条裸 TCP 长连接因为裸 TCP 长连接在跨网络环境里经常被中间链路提前回收一旦连接断开又没有自动恢复机制整个隧道就瘫痪了。WebSocket 协议本身带了 ping/pong 心跳帧还有状态码、关闭握手这些规范性机制配合成熟的重连库长连接的存活率会高很多。如果服务端还开了 TLS那整个通道就是加密的即使流量经过一些不可信的链路里面跑的数据也不太容易被窥探。这里顺带提醒一下整套方案里有一个关键前提两端必须约定同一个 WebSocket 地址和同一个认证凭据。很多人配置失败不是工具不会用而是客户端填了ws://服务端监听的是wss://协议不一致导致握手立刻失败。后文部署部分我会把这个坑详细展开。3. 服务端部署 wstunnel先把门槛挪到公网服务端是整个隧道的外口所有的公网请求都先到这里集合。这部分部署到位了后面的内网侧配置才有意义。3.1 安装 wstunnel 并生成一张证书我习惯把 wstunnel 装在/opt/wstunnel这个独立目录下面避免散落在一堆 Go 工具里。安装方法很简单去项目的 GitHub Releases 页面下载对应架构的压缩包一般一个机器一个二进制文件就够了。下载后解压再做一个软链接让命令可以直接使用mkdir -p /opt/wstunnel tar -xzf wstunnel_linux_amd64.tar.gz -C /opt/wstunnel ln -s /opt/wstunnel/wstunnel /usr/local/bin/wstunnel wstunnel version如果你用的不是 x86_64 架构比如 ARM 的机器那下载wstunnel_linux_arm64就对了解压步骤完全一样。接着处理证书。因为服务端要监听wss://也就是 WebSocket over TLS必须有一张受信任的证书否则客户端要么连不上要么得跳过证书校验那就不安全了。我自己用的是域名证书申请流程走 certbot一条命令就能搞定apt install certbot -y certbot certonly --standalone -d tunnel.example.com这里有个细节certbot 的 standalone 模式会占用 80 端口假如这台机器上已经跑了 Nginx 或者其他 Web 服务就得先停掉或者改用 Webroot 模式。证书生成后位置一般在/etc/letsencrypt/live/tunnel.example.com/目录下里面包含fullchain.pem和privkey.pem两个关键文件。3.2 用 systemd 把服务端托起来不建议用nohup或者直接在前台跑 wstunnel一旦 SSH 会话退出服务就没了。写成 systemd 服务是最正规的重启自启、崩溃自动拉起运维体验好很多。我用的服务单元文件大概是这样的[Unit] Descriptionwstunnel server Afternetwork-online.target [Service] ExecStart/usr/local/bin/wstunnel server wss://0.0.0.0:443 --tls-cert /etc/letsencrypt/live/tunnel.example.com/fullchain.pem --tls-key /etc/letsencrypt/live/tunnel.example.com/privkey.pem Restartalways RestartSec3 Userwstunnel Groupwstunnel [Install] WantedBymulti-user.target使用前先创建专用账号避免服务以 root 身份跑useradd -r -s /usr/sbin/nologin wstunnel mkdir -p /etc/wstunnel chown wstunnel:wstunnel /etc/wstunnel systemctl daemon-reload systemctl enable --now wstunnel-server有一个需要特别注意的问题即使命令写的是监听 443Userwstunnel这个普通账号也可能没有权限去读 Lets Encrypt 的证书目录。我一开始就遇到服务反复重启日志报权限不足后来把证书复制到/etc/wstunnel/下然后改属主为 wstunnel问题就解决了。启动后验证一下监听状态确认端口真的在听ss -lntp | grep 443看到LISTEN状态说明服务端已经就位。3.3 服务端这一步为什么值得单独打磨很多人会觉得服务端不就是一个进程嘛跑起来就行。其实服务端决定的是整个隧道的底子连接数增加、断线重连、证书续期这些问题全都会集中到这里。systemd 单元里Restartalways保证了进程即使被异常杀掉3 秒后能自动拉起来这对长连接服务来说非常关键。还有云平台的安全组端口放行必须做到位。如果你是云服务器443 入口不但要在系统层面放行还要在云控制台的入方向规则里记录 443/TCP否则外面的流量根本到不了 wstunnel。我自己就在这两层权限上卡过系统规则写了云平台没放行从客户端发起的握手请求一直超时。4. 内网侧部署 cproxy把流量接进隧道服务端弄好之后就可以开始配置家里那台 Linux 主机了。cproxy 在这个环节担任客户端角色它负责主动向外发起 WebSocket 连接并且把本地的服务请求送进隧道。4.1 客户端安装与基础配置下载 cproxy 的二进制文件同样放到一个固定目录。我在内网主机上用的是 Linux amd64 版本mkdir -p /opt/cproxy tar -xzf cproxy_linux_amd64.tar.gz -C /opt/cproxy ln -s /opt/cproxy/cproxy /usr/local/bin/cproxy配置方面cproxy 不同分支和版本的字段名会有差异我贴一个自己常用的配置内容供参考具体字段名以你下载版本的 README 为准server wss://tunnel.example.com:443 token 这里填一个足够长的随机字符串 listen_port 8022 target_host 127.0.0.1 target_port 22 auto_reconnect true heartbeat_seconds 20这个配置表达的意思其实非常直白cproxy 主动连接到公网服务器的 WebSocket 地址并把公网上的 8022 端口映射到本机的 22 端口。也就是说公网侧访问tunnel.example.com:8022流量会被送回本机的 SSH 服务。按我的习惯token 会在两端手动保持同一份相当于一把共用的钥匙避免别人随便连进来。配置写好之后先在前台启动一次观察日志输出cproxy -c cproxy.conf看到 WebSocket 连接建立成功、隧道协商完成的日志后就说明链路打通了。确认没问题再系统化托管同样写一个 systemd 单元模式和服务端类似这里就不重复贴完整文件了。4.2 从公网侧做一次完整验证配置完成立刻从一台公网上的机器做验证。比如我用另一台 VPS 发起 SSH 连接ssh -p 8022 usertunnel.example.com -v正常情况下这次连接看起来是连到公网服务器的 8022 端口但实际上到达的是家里那台主机的 SSH。端口对了、用户密码对就能直接登录。我第一回跑通这个流程的时候盯着终端里跳过的 SSH 协议细节有点不太真实的感觉就像隔着整个城市在操作身边的电脑。除了 SSH还可以顺手验证几个常见服务。假如家里有 Web 界面比如路由器后台或者某个监控面板同样可以映射出来。只需要在配置里增加端口转发项重新加载配置外部设备就能通过http://tunnel.example.com:9000这类地址直接访问。4.3 一个端口穿透多服务的扩展思路有人会问难道每个服务都要占一个公网端口吗如果家里服务比较多确实会有端口管理的问题。我的做法是做一个统一的入口把最常用的 Web 服务合并到一个端口下通过路径区分。比如公网只开放 443隧道后面对接的是内网一台跑反向入口的机器这台机器再根据 Host 头或者路径把请求分发给不同服务。这样做的好处是公网上只需要暴露少数几个端口风险面小管理也方便。至于那些无法合并的服务比如 SSH、某种数据库端口就单独映射一个公网端口。下面是几个映射示例公网端口隧道目标用途8022192.168.1.5:22远程 SSH 管理内网服务器9001192.168.1.5:3000访问内网监控面板9443192.168.1.1:443管理家庭路由器后台每次增加一条映射只需要改 cproxy 配置并重启服务不用像 iptables 那样处理整张规则表。5. 实测数据与踩坑记录延迟、吞吐、断线重连这些事方案跑通只是第一步真实用下来你会发现延迟、吞吐和断线处理才是决定体验的关键。这块我把自己的实测数据和踩坑经历都写出来。5.1 延迟与吞吐到底能到什么水平我在一个普通住宅网络环境下测过内网机器到 VPS 的物理网络延迟大约是 42 毫秒经过 cproxy wstunnel 隧道之后交互延迟大概在 47 到 55 毫秒之间浮动。这个增量对于 SSH 操作和 Web 页面访问来说体感几乎可以忽略。但如果你追求极致性能要注意一点隧道转发在用户态做CPU 占用会比内核态转发高尤其大文件传输、大量并发连接时建议留意一下服务端和内网主机的 CPU 负载。吞吐方面实际传输速度受三个因素影响带宽上限、WebSocket 帧封装开销、以及单条 TCP 连接上的流控。我测下来普通千兆内网加一个中等带宽的机房带宽实际能跑到的吞吐大约是线路带宽的 85% 到 90%。如果是高频小包场景比如同步数据库日志帧过多反而会有瓶颈。这时候可以调整客户端的心跳间隔和并发连接复用尽量减少每笔数据的握手开销。5.2 隧道能通但 SSH 卡死多半是 MTU 在捣乱这是我最开始遇到的拦路虎。隧道建立成功也能看到欢迎语但输入几个字符以后终端就“冻”住了过一个会又恢复。深挖之后发现是 MTU 问题WebSocket 隧道封装本身有额外开销如果底层网络的 MTU 是 1500经过隧道封装后实际可用载荷其实达不到 1500大包就会被卡在中间链路表现为偶发卡顿。解决方案很直接把走隧道的那条虚拟网卡或者接口的 MTU 调低比如调到 1400 或者 1380。不同网络的极限值不一样可以用ping带-M do参数逐步测出来从 1400 往下减减到一定值不再丢包就找到一个能用的 MTU。这个值写进配置重启服务SSH 就流畅了。5.3 掉线自动重连的真相靠心跳也靠 systemd隧道长连接最怕的就是网络闪断。我实测过如果只是几秒的链路切换TCP 连接本身可能还没死透这时候 WebSocket 层的心跳帧能及时发现异常客户端会主动发起重连新连接建立后所有流量自动切换到新通道上应用层几乎无感。但如果主机完全重启比如家里停电又来电那 cr 进程也随着系统重启了这时候就必须靠 systemd 的Restartalways把它拉起来。特别提醒一个细节systemd 的Restart和RestartSec是两回事前者决定是否重启后者决定间隔。写在 systemd 单元里的RestartSec3是隔三秒重试如果目标服务本来就处于未恢复状态比如网络还没就绪那这个间隔要适当加大否则系统会在开机阶段疯狂重启进程日志刷得飞快。我后来把RestartSec加大到 5 秒并且给服务加了Afternetwork-online.target开机后等服务网络稳定再拉起就再也没出现过空转重试。5.4 证书过期是穿透服务沉默的常见原因很多人会忽略证书有有效期这回事。我见过太多案例内网一侧死活连不上排查半天发现是证书过期了。和直连公网服务不同隧道没连上时你不会收到明晃晃的浏览器安全警告因为双方是程序在握手报错大概率只出现在 wstunnel 日志里。建议把证书自动续期加上配置好 certbot 的定时续期任务后再配合 systemd 的PartOf或手动重启服务。严格说wstunnel 进程在启动时会读取证书文件续期之后如果不重启它读到的还是旧证书文件句柄所以我后来在证书续期脚本里加了重启 wstunnel 服务的动作才算真正一劳永逸。有一个排错表格可以供参考遇到问题基本能对应到原因症状最可能原因处理办法客户端连接一直超时云平台安全组未放行 443在云控制台放行对应 TCP 端口握手报 404服务端不是 wss 协议检查服务端监听地址和证书配置连接成功但发数据就卡隧道接口 MTU 过 大手动调低 MTU 并测试服务端日志报权限不足systemd 用户无证书读取权限调整证书目录属主或复制到专用目录断线后长时间不恢复心跳周期过长或重连机制未开启配置心跳并确认 auto_reconnect本机服务能通但外部访问失败cproxy 中 target 地址写错确认是 127.0.0.1 还是局域网实际 IP5.5 多连接并发时的注意事项如果同时有多个应用走同一个隧道比如你在外面一边 SSH 登录路由器一边访问家里的监控画面两股流量共用一条 WebSocket 连接。wstunnel 对这种复用场景支持得不错它会把不同本地连接区分开互不干扰。但有一点我需要提醒内网机器的文件句柄数和内存限制会直接影响同一时刻能承载的连接数。如果连接很多记得调高ulimit限制排查时如果发现连接一多就报Too many open files基本就是这个原因。6. 这套方案还能怎么延伸以及几句实在话最后聊点扩展用法。cproxy wstunnel 不是只能做 SSH 穿透和 Web 访问它也可以配合 UDP 场景使用wstunnel 新版本对一些 UDP 流量的支持已经比较完整这意味着你可以把某些局域网内的自定义协议、DNS 查询这类流量也搬出去。不过说实话UDP 转发对网络质量要求更高实现起来也比 TCP 复杂非必要我不太建议贸然开。另一个用法是结合定时监控。既然隧道常驻我写了一台内网机器的监控脚本定期检测隧道连接的存活状态如果发现重连次数异常增多就自动发一条通知。这个思路很简单隧道是你访问家庭服务的唯一入口它的健康状态比单个服务更重要。把这条路盯好剩下的服务反而不容易出大问题。还有一点实际操作中的经验公网的入口端口不要直接用原本的服务端口比如内网 SSH 是 22公网对应端口就给 8022 或更大一些至少在端口扫描时不至于一眼暴露。此项算不上安全措施但能减少大量盲目扫描的流量。像这种隧道方案本质上就是把网络的控制权从系统内核层“踢给”了应用层所以维护起来更灵活也更适合在多个设备间复用。现在我出差在外用手机连一下tunnel.example.com:8022就能回到家里任何一台 Linux 机器过程稳定配置也清爽。如果你正在为 iptables 的规则头疼可以考虑花一个晚上把 wstunnel 服务端和内网侧的 cproxy 搭起来跑通一次你就知道这套方案在各种远程访问任务里到底能省多少心。最后说一句实在话隧道工具本身没有对错关键在于你是否只把它用于自己拥有管理权限或者经过明确授权的设备。公网暴露的端口尽量只开自己需要的服务能加白名单就加白名单能上密钥认证就不靠密码。技术提供了一个方便之门但门后那套房子的安全始终要靠你自己把关。
返回列表