1.前情回顾
我在《veth pair细讲》 这篇文章中搭建了一个网络拓扑,用来模拟宿主机和隔离空间之间的网络通信。 而且我还将veth pair的两端设置成了不同网段的IP,虽然我知道实际没有人会这么做。如果已经熟悉上一篇的网络拓补, 可以直接查看docker端口映射章节
我们知道基于目前的网络拓补,如果我想在宿主机上访问这个独立空间的服务,是不通的。
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服务了,如图所示:
我们来看看加上路由后,数据包从发送到返回的完整解密过程:
- 去程:宿主机 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!
- 回程:ns1 宿主机(回复 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 请求时的真实心理活动:
- 收到广播包: ns1 的网卡收到一个二层广播包:“谁的 IP 是 192.168.1.2?请把你的 MAC 地址告诉 10.0.0.1!”
- 校验目标 IP: ns1 检查数据包里的目标 IP,发现:“192.168.1.2 就是我自己!”
- 做出回应: 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
二层通信过程:
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) 内核会认为:“这不是发给我的信,我不是路由器,别想借我的道!” 直接丢弃(Drop)该数据包!
先查看当前是否开启了转发模式
sysctl net.ipv4.ip_forward
下图表示当前服务器并没有开启转发模式
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 被 iptables DNAT 改写成 192.168.1.2:8080 宿主机查路由表发现192.168.1.2 走 veth-host 沿着 veth pair 网线塞进 ns1 的 veth-ns 送达 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 目标 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 网卡吐出去
参考下这段描述:
为了欺骗客户端,让客户端误以为“一直是宿主机在亲自跟我通信”,否则 ,客户端会因为“收件人与发件人对不上”而判定该数据包非法,直接丢弃,端口映射彻底失效!。
上面这个反向的过程是交给Conntrack来进行自动处理的。
从其他机器(比如 client: 172.17.194.200)访问的真实流程
假设你从局域网另一台机器 172.17.194.200 敲命令: curl http://172.17.194.141:9999
- 外部机器在发包时就已经确定了源 IP
客户端机器(172.17.194.200)在它自己那边就把信封封好了:
源 IP:172.17.194.200
目标 IP:172.17.194.141:9999
这个信封通过物理网线吐到了宿主机的 eth0 网卡上。
- 触发 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。
- 宿主机做路由查询(Routing Lookup)
改完目标 IP 之后,宿主机内核拿着新目标 192.168.1.2 去查路由表:
- 查 main 路由表:192.168.1.0/24 dev veth-host。
- 内核判定:“这不是发给本机的包,而是需要转发(Forward)给 veth-host 的包!”
- 数据包走 FORWARD 链,然后从 veth-host 吐给 ns1。
- 送达 ns1 与回包
ns1 收到包,看到的信封是:源 IP: 172.17.194.200 目标 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
配置完伪装之后可以访问