首先启用 ip 转发功能,并且确保 iptables 能够将分组转发至本地:

# 启用 ip 转发功能
sysctl -w net.ipv4.ip_forward=1

# 允许 kernel 将流量导至本地
sysctl -w net.ipv4.conf.all.route_localnet=1

此时这台主机实质上就相当于一台路由器了,然后可以通过 iptables 命令行工具配置 NAT 规则:

# 将所有目的地的端口是 1186 的 packet 转发给 127.0.0.1:1186
# 在 nat 表中,-j DNAT 会自动配置相应的网络地址转换(NAT)规则
iptables -t nat \\
  -I PREROUTING \\
  -p tcp \\
  --dport 1186 \\
  -j DNAT \\
  --to-destination 127.0.0.1:1186

这里用一个例子稍微解释一下 NAT:具体来说,一个 IP Packet 含有原地址信息,类似于 hostA:port1, 还有目的地址信息,类似于 hostB:port2.

你可以理解为:这是一个发送方是在 hostA:port1 并且接收方是在 hostB:port2 的 packet.

现在我们假设在 hostB 这台机器上,有一个进程在监听 127.0.0.1:port3 这个 socket, 可以把这个进程看作是一个提供某种服务的进程(例如 SQL 数据库,或者 HTTP 服务器等等)。

我们想把 port3 暴露出去。所谓「暴露出去」指的是:要能够在 hostA 这台机子上访问到 hostB 主机上 port3 的服务。

为什么现在不行呢?不是已经监听了 port3 端口吗?不可用直接在 hostA 通过 hostB:port3 这个套接字直接访问该服务吗?

当一个目的地址为 hostB:port3 的 packet 到达 hostB 时,由于 hostB:port3127.0.0.1:port3 这两个套接字不匹配,所以这个 packet 携带的数据最终不会被转到正在监听套接字 127.0.0.1:port3 的进程,所以外网访问不了 127.0.0.1:port3 上提供的服务,这也正是我们为什么想尝试把 port3 「暴露出去」。

这就引出了 NAT(网络地址转换),当一个目的地址为 hostB:port3 的 packet 到达 hostB 时,在 PREROUTING 阶段,我们将它的目的地址修改为 127.0.0.1:port3 ,当然,这个修改后的 packet 不能直接让 port3 的进程收到,而是要把发送方的地址也做相应的修改,否则,由于回复时会对调发送方和接收方的位置,所以当 hostB 想要回复 hostA 时, hostA 会看到发送方的地址为 127.0.0.1:port3(也就是 hostB 修改过的接收方地址),从而双方的通信无法继续。

所以,在 NAT 时,不仅要修改目的地址,还要修改原地址也就是发送方地址,挑一个没有使用的端口 port4 , 把发送方修改为 127.0.0.1:port4, 然后当那个进程回复消息时,会发给 port4, NAT 早已监听这个地址,于是再把目的地改成原来的发送方也就是 hostA:port1, NAT 内部会维护一个哈希表记录修改后的发送方 127.0.0.1:port4 到原始的发送方 `hostA:por1``这样的对应关系。

接下来我们可以在启用了 NAT 的那台机器上 (hostB) 做一个测试:

ncat -l 127.0.0.1 1186  # 监听 127.0.0.1:1186

然后在外网主机 (hostA) 连接到这台主机 (hostB):

ncat hostB 1186  # 连接到 hostB 的 1186 端口

这时应该能进行双向通信。

另外,也可以通过:

nmap -p 1186 hostB

来进行验证,如果成功地暴露了 1186 端口,那么应该看到 "open" 字样。