本文总结三大平台的 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

三个常见的坑

  1. 只支持 TCP。 portproxy 没有 UDP 转发能力,需要 UDP 就上 WSL 里的 socat(见下一节)。

  2. 依赖「IP Helper」服务。 很多精简系统或优化脚本会把 iphlpsvc 关掉,此时规则加得上去但完全不生效。检查和启动:

    sc query iphlpsvc
    
    Set-Service -Name iphlpsvc -StartupType Automatic; Start-Service iphlpsvc
    
  3. 报「请求的地址在其上下文中无效」。 通常是监听端口落进了 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 -

两个必须知道的限制:

  1. rdr 只对经过本机的流量生效,本机自己发起到 127.0.0.1 的连接不会被重定向,所以「本机访问 8080 转到 80」这种最常见的需求它办不到;
  2. 这样加载会替换掉当前的 pf 规则集,正经做法是写进 /etc/pf.anchors/ 再用 anchor 引用。

结论:日常需求直接用 socat,pf 留给「本机当网关帮别的设备转发」这种场景。

顺手记一下 macOS 查端口占用(更详细的可以看我之前那篇 Mac 查看端口占用和释放端口):

sudo lsof -i tcp:8080

三个平台怎么选

WindowsLinuxmacOS
工具netsh interface portproxy(自带)socat + systemdsocat + launchd
需要安装不需要apt install socatbrew install socat
UDP❌ 不支持✅✅
重启后保留✅ 自动(存注册表)需要 systemctl enable需要 LaunchDaemon
依赖IP Helper 服务—Homebrew
查端口占用netstat -ano | findstr :端口ss -tulnplsof -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-*' 清理。

相关文章