本文总结三大平台的 TCP/UDP 端口转发方法:Windows 用系统自带的 netsh interface portproxy,Linux 和 macOS 用 socat(并注册成开机自启的服务)。文末还有平台对比和排错清单。
命令参数又多又容易记错,所以我在下面放了一个命令生成器,填好地址端口就能直接复制。
什么时候需要端口转发
我自己最常遇到的几种场景:
- 服务只监听
127.0.0.1,想让局域网里的其他设备也能访问; - VPN 抢走了全部路由(比如 Cisco AnyConnect 默认关闭 split tunnel),本机访问不到局域网设备,需要借道一个能通的机器;
- WSL2 里跑的服务,需要从 Windows 宿主机或局域网访问,而 WSL 的 IP 每次重启都会变;
- 端口被占用或被规范限制,想用另一个端口对外提供同一个服务。
需要区分两个概念:这里说的都是本机层面的端口转发(用户态代理),也就是「进来的连接原样接到另一个地址端口」;如果要让整台机器充当网关替一个网段做转发,那是 NAT 的活,Linux 上应该用 nftables/iptables 的 DNAT 并打开 net.ipv4.ip_forward,不在本文范围内。
🧰 命令生成器
支持多条规则一起生成,切换标签页即可得到对应平台的命令,每个代码块右上角可以一键复制。
生成器完全在浏览器本地运行,不会把你填的地址发到任何地方。也可以单独打开:端口转发命令生成器。
Windows:netsh interface portproxy
Windows 不用装任何东西,系统自带的 netsh interface portproxy 就够了,规则写进注册表,重启后依然生效。所有命令都需要管理员权限的 PowerShell 或 CMD。
创建转发
netsh interface portproxy add v4tov4 listenaddress=127.0.0.1 listenport=1085 connectaddress=100.122.123.54 connectport=1085
参数含义:
| 参数 | 含义 |
|---|---|
v4tov4 | 监听和目标都是 IPv4;另有 v4tov6、v6tov4、v6tov6 |
listenaddress | 本机监听地址,127.0.0.1 只允许本机访问,0.0.0.0 对所有网卡开放 |
listenport | 本机监听端口 |
connectaddress | 转发目的地地址(IP 或主机名) |
connectport | 转发目的地端口 |
查看与删除
netsh interface portproxy show all
netsh interface portproxy delete v4tov4 listenaddress=127.0.0.1 listenport=1085
删除时的 listenaddress 必须和添加时写的完全一致,写成 0.0.0.0 去删一条 127.0.0.1 的规则会报找不到。想一次清空所有规则:
netsh interface portproxy reset
对局域网开放时要放行防火墙
listenaddress 用了 0.0.0.0 或具体网卡 IP 时,还要让 Windows 防火墙放行入站:
netsh advfirewall firewall add rule name="portproxy 8080" dir=in action=allow protocol=TCP localport=8080
三个常见的坑
只支持 TCP。
portproxy没有 UDP 转发能力,需要 UDP 就上 WSL 里的socat(见下一节)。依赖「IP Helper」服务。 很多精简系统或优化脚本会把
iphlpsvc关掉,此时规则加得上去但完全不生效。检查和启动:sc query iphlpsvcSet-Service -Name iphlpsvc -StartupType Automatic; Start-Service iphlpsvc报「请求的地址在其上下文中无效」。 通常是监听端口落进了 Hyper-V / WSL 动态保留的排除区间,换个端口就好。查看被保留的区间:
netsh interface ipv4 show excludedportrange protocol=tcp
顺手记一下查端口占用:
netstat -ano | findstr :8080
tasklist | findstr <上一条命令查到的 PID>
Linux:socat
netsh 那种「配置一下就常驻」的东西 Linux 上没有直接对应物,最顺手的是 socat:一条命令一个转发,再交给 systemd 保活。
安装
sudo apt update && sudo apt install -y socat
RHEL 系用 sudo dnf install -y socat,Alpine 用 apk add socat。
一条命令跑起来
socat TCP-LISTEN:180,bind=127.0.0.1,reuseaddr,fork TCP:10.0.0.1:80
TCP-LISTEN:180监听本地 180 端口;bind=127.0.0.1只监听回环地址,只有本机能连;去掉它就等于监听所有网卡;fork每来一条连接就 fork 一个子进程处理,不加的话只能服务一条连接,断开后进程就退出了;reuseaddr允许立刻重新绑定端口,避免重启服务时撞上TIME_WAIT;TCP:10.0.0.1:80目标地址端口。
需要 UDP 转发时不要照抄 UDP-LISTEN,用 RECVFROM/SENDTO 这一对更合适:
socat -T30 UDP4-RECVFROM:5353,bind=127.0.0.1,reuseaddr,fork UDP4-SENDTO:10.0.0.1:5353
-T30 是空闲 30 秒回收,UDP 无连接,必须靠超时清理。
注册成 systemd 服务
推荐做法:一条转发规则一个 unit。 好处是某条转发挂掉只重启它自己,systemctl status 一眼能看出是哪条断了,也能单独停用。
sudo tee /etc/systemd/system/socat-180.service >/dev/null <<'EOF'
[Unit]
Description=socat forward 127.0.0.1:180 -> 10.0.0.1:80
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
ExecStart=/usr/bin/socat TCP-LISTEN:180,bind=127.0.0.1,reuseaddr,fork TCP:10.0.0.1:80
Restart=always
RestartSec=2
DynamicUser=yes
NoNewPrivileges=yes
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
EOF
几点说明:
DynamicUser=yes让服务用临时非特权用户运行,转发进程没必要是 root;- 监听端口小于 1024 时才需要
AmbientCapabilities=CAP_NET_BIND_SERVICE,否则可以删掉这行; Restart=always+RestartSec=2保证目标端偶发不可用时会自动重连。
加载并设为开机自启:
sudo systemctl daemon-reload && sudo systemctl enable --now socat-180.service
多条规则就重复上面的模板(文件名换成 socat-<端口>.service),最后一次性启用:
sudo systemctl enable --now socat-180.service socat-1899.service socat-9899.service
也可以一个 unit 塞多条(但不推荐)
网上流传的写法是在一个 unit 里用 & 起多个 socat:
[Service]
ExecStart=/bin/bash -c "/usr/bin/socat TCP-LISTEN:180,bind=127.0.0.1,reuseaddr,fork TCP:10.0.0.1:80 & \
/usr/bin/socat TCP-LISTEN:9899,bind=127.0.0.1,reuseaddr,fork TCP:10.0.0.1:9899"
Restart=always
ExecStop=/bin/bash -c "pkill -f '/usr/bin/socat TCP-LISTEN'"
能用,但有两个明显缺点:单条转发挂掉 systemd 察觉不到(只要前台那条还活着,服务状态就是 active),以及 ExecStop 里的 pkill -f 会顺手杀掉机器上所有 socat TCP-LISTEN 进程,包括不属于这个服务的。规则超过一条就用上面的一 unit 一规则写法。
查看与排错
systemctl status socat-180.service
sudo ss -tulnp | grep :180
journalctl -u socat-180.service -e
日志里最常见的是 Address already in use(端口被占)和 Connection refused(目标端没在听),先用 ss 和 nc -vz 10.0.0.1 80 分别确认两头。
macOS
macOS 上同样推荐 socat,用 Homebrew 装,用 launchd 保活。
socat + launchd
brew install socat
先手动验证一下(Ctrl+C 结束):
socat TCP-LISTEN:8080,bind=127.0.0.1,reuseaddr,fork TCP:10.0.0.1:80
确认没问题后写成 LaunchDaemon 常驻:
sudo tee /Library/LaunchDaemons/local.socat.8080.plist >/dev/null <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>local.socat.8080</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/socat</string>
<string>TCP-LISTEN:8080,bind=127.0.0.1,reuseaddr,fork</string>
<string>TCP:10.0.0.1:80</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>
EOF
sudo launchctl bootstrap system /Library/LaunchDaemons/local.socat.8080.plist
注意 socat 的路径:Apple 芯片是 /opt/homebrew/bin/socat,Intel 机型是 /usr/local/bin/socat,写错 launchd 会静默失败,用 sudo launchctl print system/local.socat.8080 能看到退出状态。
卸载:
sudo launchctl bootout system/local.socat.8080 && sudo rm /Library/LaunchDaemons/local.socat.8080.plist
系统自带的 pf(有坑,选读)
macOS 自带 pf 防火墙,可以用 rdr 做端口重定向:
echo "rdr pass inet proto tcp from any to any port 8080 -> 127.0.0.1 port 80" | sudo pfctl -Ef -
两个必须知道的限制:
rdr只对经过本机的流量生效,本机自己发起到127.0.0.1的连接不会被重定向,所以「本机访问 8080 转到 80」这种最常见的需求它办不到;- 这样加载会替换掉当前的 pf 规则集,正经做法是写进
/etc/pf.anchors/再用 anchor 引用。
结论:日常需求直接用 socat,pf 留给「本机当网关帮别的设备转发」这种场景。
顺手记一下 macOS 查端口占用(更详细的可以看我之前那篇 Mac 查看端口占用和释放端口):
sudo lsof -i tcp:8080
三个平台怎么选
| Windows | Linux | macOS | |
|---|---|---|---|
| 工具 | netsh interface portproxy(自带) | socat + systemd | socat + launchd |
| 需要安装 | 不需要 | apt install socat | brew install socat |
| UDP | ❌ 不支持 | ✅ | ✅ |
| 重启后保留 | ✅ 自动(存注册表) | 需要 systemctl enable | 需要 LaunchDaemon |
| 依赖 | IP Helper 服务 | — | Homebrew |
| 查端口占用 | netstat -ano | findstr :端口 | ss -tulnp | lsof -i tcp:端口 |
跨机器的临时需求还有个更省事的选择:SSH 隧道。本地 8080 转到跳板机能访问的 10.0.0.1:80,一条命令就行,三个平台通用:
ssh -N -L 127.0.0.1:8080:10.0.0.1:80 user@jump-host
安全提醒
端口转发本质是在绕过网络隔离,所以:
- 默认写
bind=127.0.0.1/listenaddress=127.0.0.1,只有确实要给局域网用时才放开成0.0.0.0; - 转发出去的服务如果没有认证(管理后台、数据库、Redis 之类),放开监听地址等于把它交给整个局域网;
- 不要把内网服务通过公网服务器转发暴露出去,除非前面有认证和 TLS;
- 规则会一直留着,建议记一下自己加过什么,定期用
netsh interface portproxy show all/systemctl list-units 'socat-*'清理。