Technology
现代 Linux 端口转发怎么选:SSH、socat、nftables、firewalld 不是一回事
Linux 端口转发最容易写成一堆命令拼图:SSH 一条、socat 一条、firewalld 再来两条,看起来都能通,实际上它们解决的问题根本不一样。真正该先问的不是“怎么转”,而是“这条流量到底应该在什么作用域内转、谁能碰到、出了事怎么收回”。
把这件事拆开后,选择就清楚了:临时调试优先 SSH,单次桥接可以用 socat,网关或宿主机级别的永久规则用 nftables,RHEL/CentOS/Fedora 这类发行版里再用 firewalld 把 zone/policy 和持久化管理起来。旧教程里那种一把梭的全局 masquerade,最好默认当成风险项,而不是范本。
先把场景分开:本地、远程、动态代理、单次桥接、持久转发
SSH 的 -L、-R、-D 解决的是安全隧道里的端口转发,不是防火墙策略。它们的优势是快、可撤销、对变更影响小,适合临时排障、连数据库、临时露出某个内部服务,或者给浏览器挂一个 SOCKS 代理。
-L是本地转发:把本机某个监听端口,转到远端主机可达的地址和端口。-R是远程转发:把远端主机上的监听端口,反向打回本地。-D是动态转发:在本机起一个 SOCKS 入口,应用自己决定下一跳。
这三种都依赖 SSH 会话,适合“我现在要看一下这个端口”“先把流量打通再说”的场景,不适合把一台机器长期改造成基础转发节点。OpenBSD 的 ssh(1) 手册里也明确写了 -L、-R、-D 的作用域,以及 GatewayPorts、ExitOnForwardFailure 这些控制项。ExitOnForwardFailure 很关键:你用 -f 后台跑的时候,如果转发根本没建立成功,应该直接失败,而不是假装已经上线。
ssh -N -f -o ExitOnForwardFailure=yes -L 127.0.0.1:8080:10.0.0.20:80 [email protected]上面这类写法的重点不是参数多,而是边界清楚:只绑在 127.0.0.1,只给本机用,失败就退出。反过来,如果你把监听地址放成 0.0.0.0 或者让 GatewayPorts 放开,再配上 -R,那就已经是在讨论暴露面,不是在讨论调试命令了。
socat 介于 SSH 和防火墙之间。它很适合做一次性的 TCP/UNIX socket 桥接、协议探测、应用排障,尤其是你想快速确认“是不是后端服务本身坏了”。但它不是默认的长期生产方案,因为它本身没有防火墙那套策略模型,也没有像 SSH 那样天然的加密和认证边界。
socat TCP-LISTEN:18080,bind=127.0.0.1,fork TCP:10.0.0.20:80这条命令的含义很直白:在本机回环口起一个监听,收到连接后转给内网主机。它适合单次观察、临时对接,或者作为 systemd service 包起来做受控桥接;如果你准备让它长期挂着,就应该补权限、日志、重启策略、来源限制,别把一个调试工具当成网关能力。
真正需要长期存在的转发,应该落到 nftables 的 stateful NAT
一旦需求变成“这台机器本来就是网关”“这条端口映射要稳定存在”“要把外网到内网的方向固定下来”,就不要再用 SSH 或 socat 这种会话级工具硬撑了。这个层面应该看 nftables。
nftables 的 NAT 是有状态的:dnat 负责把进入流量改写到目标地址,通常放在 prerouting;snat 或 masquerade 则放在 postrouting。它和“只改地址、不管回包路径”的静态思路不是一回事。对现代 Linux 来说,NAT 应该按连接跟踪来理解,而不是按老式直觉理解。
table inet nat {
chain prerouting {
type nat hook prerouting priority -100; policy accept;
tcp dport 8080 dnat to 10.0.0.20:80
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
ip saddr 10.0.0.0/24 oifname "eth0" masquerade
}
}这个例子里最值得注意的不是语法,而是 scope。dnat 只改目标地址,masquerade 只在出接口做源地址伪装,二者都应该只对明确的流量段生效。不要把一条规则写成“全局一把梭”,更不要把一个网卡上所有流量都混成一个桶。
nftables wiki 里对 masquerade 的定义很清楚:它本质上是 SNAT 的特例,会自动使用出接口地址。这个特性在动态公网地址、拨号、云主机弹性网卡之类的场景里很方便,但也正因为“自动”,它更需要收窄匹配条件。否则你会把本来不该改写的出站流量也一并改掉,排错时很难第一时间看出来。
还有一个常被忽略的点:iptables 和 nftables 不要混着玩。现代发行版里它们可能通过兼容层并存,但你如果一边在 nft 里写 NAT,一边又用旧的 iptables 习惯去加规则,最后看到的不是“更兼容”,而是规则视图分裂、优先级难判、运维同事接手时完全对不上。生产里最好统一到一个后端,别把两套模型叠在一起。
firewalld 适合做管理入口,但要知道它管到哪一层
在 RHEL 系发行版里,firewalld 往往比直接手写 nftables 更常见,因为它把 zone、policy、服务、端口转发和持久化封装成了更适合日常运维的接口。问题是,很多旧教程只记住了 --add-masquerade 和 --add-forward-port,却没说清楚它们的作用域。
firewalld 官方文档和 firewall-cmd 手册页写得很明确:--add-masquerade 只启用 IPv4 masquerade;--add-forward-port 是 IPv4 forward port;如果你给 toaddr,IP forwarding 会被隐式启用。IPv6 的转发端口要走 rich language,而不是把 IPv4 的命令原样抄过去。
# 先确认当前 zone
firewall-cmd --get-active-zones
# 在指定 zone 上做永久 masquerade
firewall-cmd --permanent --zone=public --add-masquerade
# 做一个 IPv4 forward-port
firewall-cmd --permanent --zone=public --add-forward-port=port=8080:proto=tcp:toaddr=10.0.0.20:toport=80
# 重新载入生效
firewall-cmd --reload但这里最重要的不是命令能不能跑,而是你有没有选对 zone。firewalld 的核心价值是按来源、接口和服务来分区管理,而不是把一台机器所有入口都写成一个“默认放行区”。如果一个转发规则应该只对某个网卡、某个来源网段、某个内网出口生效,就把它放进对应的 zone 或 policy 里,不要让它滑成全局策略。
policy 的作用尤其适合表达“从哪里到哪里允许转发”。它比把规则零碎地摊在一个 zone 上更适合做安全审计,因为你能清楚看到入口域和出口域的关系。也就是说,zone 管边界,policy 管关系,masquerade 和 forward-port 只是落在这个关系上的动作。
旧的全局 masquerade 风险,主要不是“会不会通”,而是“通得太多”
很多老文档里会直接写 --add-masquerade,看起来像是在“开启转发”,其实它表达的不是一件事。masquerade 解决的是源地址改写,不是策略授权;它一旦放到过大的范围里,就会把很多你没打算暴露的流量一起带出去。
典型风险有几个:
- 来源范围过大:本来只想让一个内网段出网,结果整个 zone 都被伪装了。
- 接口范围不清:规则没有绑定具体出口,后续换接口或加网卡时很容易误伤。
- 回包路径不透明:连接能通,不代表你知道它是怎么通的,排障时只看到“能访问”,看不到哪条路径真的在走。
- 与其他 NAT 规则叠加:再加一层 nftables 或旧 iptables 后,来源追踪会越来越乱。
所以正确的做法不是“禁用 masquerade”,而是把它缩到最小可接受范围:明确 source net、明确 output interface、明确只在需要转发的 zone 或 policy 上启用。能不用全局 masquerade,就不要用全局 masquerade。
持久化、回滚和验证,要当成发布流程而不是附属动作
端口转发最怕的是“今天能通,明天不知道谁改坏了”。所以不管你用哪种方式,最终都要回答三个问题:怎么持久化、怎么回滚、怎么验证。
SSH 方案的持久化最简单,通常就是 systemd user service、shell 脚本或者自动化任务,但回滚也最直接:停掉进程、撤掉服务、删掉临时密钥或跳板配置。它适合可快速恢复的临时链路。
socat 方案如果要长期运行,建议包进 systemd,至少保证日志可查、失败可重启、配置能单独回收。验证时别只看进程在不在,要实际连一下入口端口,确认目标服务返回的是你期望的协议。
nftables 和 firewalld 则应该把“临时试验”和“永久规则”分开。先用临时规则验证路径,再写 permanent 配置,最后 reload。回滚也要反过来做:先删永久规则,再 reload,必要时保留临时窗口做回退确认,不要直接把生产防线连根拔掉。
# 验证监听/转发是否真的存在
ss -ltnp | grep 8080
# 验证连通性
curl -v http://127.0.0.1:8080/
# 如果是网关转发,顺手看路由和连接跟踪
ip route
conntrack -L 2>/dev/null | head验证不能只盯“端口开了没有”,还要看方向是否正确、来源是否受限、IPv4/IPv6 是否符合预期、回包是不是走了同一条设计好的路径。很多线上事故的症状都很朴素:端口开了,但暴露面扩大了;服务通了,但外网也能直连到本不该开放的地址;IPv4 通了,IPv6 其实没管住。
一个实用的选择顺序
如果你需要的是“我现在要临时查问题”,先用 SSH -L 或 -D。如果是“我想把一个 socket 临时桥接过去”,用 socat。如果你要的是稳定的主机级转发或网关级 NAT,就上 nftables;如果你在 RHEL 系生态里需要更运维友好的入口,就让 firewalld 负责 zone/policy 和持久化,再把底层转发逻辑收束到明确的规则集里。
别把这几个工具混成一类,更别继续沿用旧教程里的全局 masquerade 思路。现代 Linux 的端口转发不是“会不会转”的问题,而是“谁能转、在哪转、转到哪、什么时候能收回”的问题。把作用域、IPv4/IPv6、IP forwarding、持久化和回滚都说清楚,才算真的把这件事做好。