Docker端口映射咋做的,模拟下。

0 阅读11分钟

1.前情回顾

我在《veth pair细讲》 这篇文章中搭建了一个网络拓扑,用来模拟宿主机和隔离空间之间的网络通信。 而且我还将veth pair的两端设置成了不同网段的IP,虽然我知道实际没有人会这么做。如果已经熟悉上一篇的网络拓补, 可以直接查看docker端口映射章节

image.png

我们知道基于目前的网络拓补,如果我想在宿主机上访问这个独立空间的服务,是不通的。

curl http://192.168.1.2:8080

访问不通的理由在于 在宿主机上访问 http://192.168.1.2:8080 宿主机会查找自己的路由表。

我们看看宿主机目前的路由条目:

[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         172.17.207.253  0.0.0.0         UG    100    0        0 eth0
10.0.0.0        0.0.0.0         255.255.255.0   U     0      0        0 veth-host
172.17.192.0    0.0.0.0         255.255.240.0   U     100    0        0 eth0

路由条目只配置了三条路由规则:一条默认路由和两条直连路由,但是没有配置到达192.168.1.0/24的路由条目,那么就会匹配默认路由,直接将包发送到了外网网关,导致数据包从eth0 溜走。

我们可以通过命令查看数据包到底是从哪个地方走的。 ip route get 192.168.1.2

[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# ip route get 192.168.1.2
192.168.1.2 via 172.17.207.253 dev eth0 src 172.17.194.141 uid 0 
    cache 

dev eth0: 说明数据包直接从eth0网卡发送出去了

via 172.17.207.253: 说明发送到了网关,下一跳的位置。

那么显然数据就直接丢了,所以要想能通信,你必须得告诉宿主机,“去往 192.168.1.2 的包,强行从 veth-host 网卡丢出去!”

ip route add 192.168.1.0/24 dev veth-host

配置完毕之后会多一条路由

[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
192.168.1.0     0.0.0.0         255.255.255.0   U     0      0        0 veth-host

此时你在观察:

[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# ip route get 192.168.1.2
192.168.1.2 dev veth-host src 10.0.0.1 uid 0 
    cache 

你会发现,数据包直接发送到dev veth-host

在 namespace (ns1) 里也教它路由: 告诉 ns1: “去往 10.0.0.1 的包,强行从 veth-ns1 网卡丢出去!”

ip netns exec ns1 ip route add 10.0.0.0/24 dev veth-ns1

这样配置后双向的路由就通了。 你就可以在宿主机上访问隔离空间的8080服务了,如图所示:

image.png

我们来看看加上路由后,数据包从发送到返回的完整解密过程:

  1. 去程:宿主机 →\rightarrow ns1 (ping 192.168.1.2)
[宿主机内核]
  │
  ├── 1. 检查三层路由:要给 192.168.1.2 发包。
  │    └─► 匹配到你加的路由:192.168.1.2 dev veth-host
  │    └─► 内核结论:“好!从 veth-host 扔出去,不需要去找网关!”
  │
  ├── 2. 准备二层封装(ARP 寻找 MAC 地址):
  │    └─► 宿主机在 veth-host 上广播 ARP 询问:“谁是 192.168.1.2?把 MAC 告诉我!”
  │    └─► 数据包顺着 veth pair 流到了 ns1。
  │
[ns1 空间]
  │
  ├── 3. ns1 的 veth-ns1 收到 ARP 请求:
  │    └─► ns1 一看:“192.168.1.2 就是我自己的 IP 啊!”
  │    └─► ns1 回复 ARP 响应:“我是 192.168.1.2,我的 MAC 地址是 xx:xx:xx:xx!”
  │
  └── 4. 宿主机收到 MAC,成功把 ICMP Echo Request 请求包塞进 veth pair,送达 ns1!
  1. 回程:ns1 →\rightarrow 宿主机(回复 Ping 包)
[ns1 空间]
  │
  ├── 1. ns1 准备给宿主机 (10.0.0.1) 回包:
  │    └─► 匹配到你在 ns1 内加的路由:10.0.0.1 dev veth-ns1
  │    └─► ns1 内核结论:“10.0.0.1 也在我的直连网卡 veth-ns1 上,不用找网关!”
  │
  ├── 2. ARP 寻找 MAC:
  │    └─► ns1 广播询问:“谁是 10.0.0.1?”
  │    └─► 宿主机收到后回复:“10.0.0.1 是我,MAC 地址是 yy:yy:yy:yy!”
  │
  └── 3. 回包顺着 veth pair 成功传回宿主机,Ping 通完成!

1.1 聊聊ARP

ARP(Address Resolution Protocol,地址解析协议)是一个二层(数据链路层)协议,它的本质职责非常纯粹,只有一句话:

“谁在局域网里喊我的 IP,我就用我的 MAC 地址回答谁。”

我们来看看 ns1(网卡 IP 是 192.168.1.2)收到 ARP 请求时的真实心理活动:

  1. 收到广播包: ns1 的网卡收到一个二层广播包:“谁的 IP 是 192.168.1.2?请把你的 MAC 地址告诉 10.0.0.1!”
  2. 校验目标 IP: ns1 检查数据包里的目标 IP,发现:“192.168.1.2 就是我自己!”
  3. 做出回应: ns1 根本不会去检查询问者的 IP(10.0.0.1)和自己是不是在同一个网段,它只知道“有人在问我的 IP”,于是立刻封包回应:“192.168.1.2 的 MAC 地址是 AA:BB:CC:DD:EE:FF!

核心结论:

收到 ARP 请求的网卡,只看目标 IP 是不是自己。只要目标 IP 是自己,无论发起方是哪个网段的 IP,它都会老老实实回复自己的 MAC 地址!

1.2 veth pair 也是有mac地址的

veth pair 虽然是虚拟网卡,但 Linux 内核为它们模拟了非常完整的二层以太网网卡功能,包括独立的 MAC 地址 和完整的 ARP 协议栈

我们可以通过命令查看两端的MAC地址:

[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# ip link show veth-host
3: veth-host@if4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 6e:2b:13:de:69:92 brd ff:ff:ff:ff:ff:ff link-netns ns1
[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# ip netns exec ns1 ip link show veth-ns1
4: veth-ns1@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 1e:16:65:72:96:8c brd ff:ff:ff:ff:ff:ff link-netnsid 0

我们发现veth-host的mac地址是 link/ether 6e:2b:13:de:69:92

veth-ns1的mac地址是: link/ether 1e:16:65:72:96:8c

我们可以通过arp -n 来查看mac地址的缓存

arp -n 用于查看操作系统当前缓存的 ARP 表(地址解析协议缓存),也就是局域网内 IP 地址与 MAC 地址(物理地址)的映射关系。

[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# arp -n
Address                  HWtype  HWaddress           Flags Mask            Iface
192.168.1.2              ether   1e:16:65:72:96:8c   C                     veth-host
172.17.207.253           ether   ee:ff:ff:ff:ff:ff   C                     eth0

二层通信过程:

image.png

1.3 同网段和跨网段一般的请求流程

应用程序发起请求:目标 IP = X
                                 │
                                 ▼
                     【第一步:必须先查路由表】
                                 │
            ┌────────────────────┴────────────────────┐
            ▼                                         ▼
   【情况 A:匹配到同网段直连路由】             【情况 B:匹配到异网段网关路由】
 (比如 192.168.1.0/24 dev veth-host)          (比如 default via 172.17.194.1 dev eth0)
            │                                         │
            ▼                                         ▼
 【第二步:路由决策】                        【第二步:路由决策】
  * 选定出口网卡:veth-host                   * 选定出口网卡:eth0
  * 判定为直连:无需网关                      * 判定为非直连:下一跳为网关 (172.17.194.1)
            │                                         │
            ▼                                         ▼
 【第三步:二层封装】                        【第三步:二层封装】
  * ARP 询问:目标 IP (192.168.1.2) 的 MAC     * ARP 询问:网关 IP (172.17.194.1) 的 MAC
  * 目的 MAC = 对方网卡的 MAC                  * 目的 MAC = 网关的 MAC
            │                                         │
            ▼                                         ▼
       直接二层发送                               交给网关转发

2 docker端口映射

现在架构是这样的:

  • 独立空间 (ns1) 内部跑了一个 8080 服务,监听 192.168.1.2:8080。
  • 宿主机(ECS) 只能通过私网 IP 192.168.1.2:8080 访问它。
  • 但外部用户/客户端 根本无法直连 192.168.1.2 这个私网地址,外界只能看到你的宿主机公网/物理网卡 IP 172.17.194.141。

现在我想做一个映射 就似乎我访问宿主机172.17.194.141的9999端口,将来可以直接访问到隔离的8080服务。以此来印证端口映射的实现。

curl http://172.17.194.141:9999

第一步要开启转发模式,默认linux内核是关闭转发模式的.

如果目标 IP 不是自己(比如经过 DNAT 改写后的 192.168.1.2) →\to 内核会认为:“这不是发给我的信,我不是路由器,别想借我的道!” →\to 直接丢弃(Drop)该数据包!

先查看当前是否开启了转发模式

sysctl net.ipv4.ip_forward

下图表示当前服务器并没有开启转发模式

image.png

1.开启转发模式

sudo sysctl -w net.ipv4.ip_forward=1

2.进行DNAT 目标地址转换:

sudo iptables -t nat -A PREROUTING -d 172.17.194.141 -p tcp --dport 9999 -j DNAT --to-destination 192.168.1.2:8080

执行完毕之后,网络数据流就变成了整个样子

1.去程:外界访问 172.17.194.141:9999 →\to 被 iptables DNAT 改写成 192.168.1.2:8080 →\to 宿主机查路由表发现192.168.1.2 走 veth-host→\to 沿着 veth pair 网线塞进 ns1 的 veth-ns →\to 送达 8080 服务。

在宿主机上进行访问

在宿主机上直接curl http://172.17.194.141:9999 数据包是从内向外发出的,根本不会经过网卡的接收入口。PREROUTING 链它的任务是拦截从网卡硬件(如 eth0)外部接收进来的数据包。 所以数据包会绕过PRETOUTING链。

但是会经过OUTPUT链条,所以在output链上配置DNAT,

sudo iptables -t nat -A OUTPUT -p tcp -d 172.17.194.141 --dport 9999 -j DNAT --to-destination 192.168.1.2:8080

OUTPUT 链专门处理本机自己产生的出站数据包

比如本机访问外部,数据包流向

本机进程(curl / ping / ssh / 业务程序)
        ↓
     OUTPUT 链          ← 本机发出的包在这里被检查
        ↓
     路由判断
        ↓
   POSTROUTING 链
        ↓
     网卡发出去

配置上述规则后,会触发OUTPUT链的DNAT(改写目标)

将目标IP 改写: 源 IP: 172.17.194.141 →\to 目标 IP: 192.168.1.2:8080

因为目标 IP 发生了改变,内核重新检索路由表

发现 192.168.1.2/24 网段关联在 veth-host 网卡上,决定将包从 veth-host 送出。

这样就直接送达了ns1命名空间

回程

ns1回包的时候,因为是回复给宿主机的IP,所以需要配置路由让ns1知道怎么将数据包返回

sudo ip netns exec ns1 ip route add 172.17.192.0/20 dev veth-ns1

直接告诉 ns1,凡是发往 172.17.192.0/20 网段的包,都从自己的 veth-ns1 网卡吐出去

参考下这段描述:

image.png

为了欺骗客户端,让客户端误以为“一直是宿主机在亲自跟我通信”,否则 ,客户端会因为“收件人与发件人对不上”而判定该数据包非法,直接丢弃,端口映射彻底失效!。

上面这个反向的过程是交给Conntrack来进行自动处理的。

从其他机器(比如 client: 172.17.194.200)访问的真实流程

假设你从局域网另一台机器 172.17.194.200 敲命令: curl http://172.17.194.141:9999

  1. 外部机器在发包时就已经确定了源 IP

客户端机器(172.17.194.200)在它自己那边就把信封封好了:

源 IP:172.17.194.200

目标 IP:172.17.194.141:9999

这个信封通过物理网线吐到了宿主机的 eth0 网卡上。

  1. 触发 Netfilter NAT 表的 PREROUTING 链(核心差异点!)

数据包刚从 eth0 网卡进来,在宿主机内核还没有做任何路由查询之前,立刻被拦截:

匹配到了你的 PREROUTING 链规则: -t nat -A PREROUTING -d 172.17.194.141 -p tcp --dport 9999 -j DNAT --to-destination 192.168.1.2:8080

DNAT 发生:目标 IP 被修改为 192.168.1.2:8080。

源 IP 依然不变:信封上的源 IP 仍然是客户端的 172.17.194.200。

  1. 宿主机做路由查询(Routing Lookup)

改完目标 IP 之后,宿主机内核拿着新目标 192.168.1.2 去查路由表:

  • 查 main 路由表:192.168.1.0/24 dev veth-host。
  • 内核判定:“这不是发给本机的包,而是需要转发(Forward)给 veth-host 的包!”
  • 数据包走 FORWARD 链,然后从 veth-host 吐给 ns1。
  1. 送达 ns1 与回包

ns1 收到包,看到的信封是:源 IP: 172.17.194.200 →\to 目标 IP: 192.168.1.2:8080。

回包要求:ns1 需要给 172.17.194.200 回包。

如果 ns1 里面加了路由 ip route add 172.17.192.0/20 dev veth-ns1(或者有默认网关),回包就能顺利沿原路退回给宿主机,再由宿主机自动 Un-DNAT 退给外部客户端!

MASQUERADE 规则

sudo iptables -t nat -A POSTROUTING -d 192.168.1.2 -p tcp --dport 8080 -j MASQUERADE

-j MASQUERADE:执行 源地址伪装 动作(自动将源 IP 替换为数据包出口网卡 veth-host 的 IP 10.0.0.1)

加上 MASQUERADE 后(外部访问成功): 外部客户端 10.8.0.50 访问宿主机。

宿主机做 DNAT(改目标为 192.168.1.2),接着在 POSTROUTING 做 MASQUERADE(把源 IP 改为 10.0.0.1)。

ns1 收到请求,以为是 10.0.0.1 找它,查路由表命中现成的 10.0.0.0/24 dev veth-ns1 路由,顺利回包给宿主机!

宿主机收到回包后,自动反向把目标改回 10.8.0.50,把数据交还给外部客户端。

这样,无需在 ns1 内部配置复杂的静态路由或默认网关,任何外部客户端都能正常访问了!

我删除一下在ns1配置的路由

sudo ip netns exec ns1 ip route del 172.17.192.0/20 dev veth-ns1

删除完毕之后,剩下两条


[root@iZ2ze4kh3gm5ijgppvs77sZ ~]# sudo ip netns exec ns1 ip route
10.0.0.0/24 dev veth-ns1 scope link 
192.168.1.0/24 dev veth-ns1 proto kernel scope link src 192.168.1.2 

配置完伪装之后可以访问

image.png