Docker 容器如何访问外部网络端以及口映射原理?

举报
山河已无恙 发表于 2024/03/28 19:37:41 2024/03/28
【摘要】 写在前面整理 Docker 容器如何访问外部网络端以及口映射原理做简单分享理解不足小伙伴帮忙指正 不必太纠结于当下,也不必太忧虑未来,当你经历过一些事情的时候,眼前的风景已经和从前不一样了。——村上春树我们知道正常情况下,在 Docker 中启动一个容器,这个容器可以自动的访问外部网络,今天我们就来看看 docker 中的容器是如何访问外部网络的?默认情况下,当我们什么配置都不做,docke...

写在前面


  • 整理 Docker 容器如何访问外部网络端以及口映射原理做简单分享
  • 理解不足小伙伴帮忙指正

不必太纠结于当下,也不必太忧虑未来,当你经历过一些事情的时候,眼前的风景已经和从前不一样了。——村上春树


我们知道正常情况下,在 Docker 中启动一个容器,这个容器可以自动的访问外部网络,今天我们就来看看 docker 中的容器是如何访问外部网络的?

默认情况下,当我们什么配置都不做,docker 会为每个创建的容器使用 Bridge Network 类型的网络,同时 docker 默认使用过 bridge 的网络驱动

可以通过下面的命令来验证

liruilonger@cloudshell:~$ docker network inspect bridge --format='{{.Driver}}'
bridge
liruilonger@cloudshell:~$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "cd77486c39955f3d2369fe32e1f5b9b65d81c1a07bb677b085cec72b8fb52440",
        "Created": "2024-03-26T13:03:43.742084591Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {},
        "Options": {
            "com.docker.network.bridge.default_bridge": "true",
            "com.docker.network.bridge.enable_icc": "true",
            "com.docker.network.bridge.enable_ip_masquerade": "true",
            "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
            "com.docker.network.bridge.name": "docker0",
            "com.docker.network.driver.mtu": "1460"
        },
        "Labels": {}
    }
]
liruilonger@cloudshell:~$
liruilonger@cloudshell:~$ docker info | grep -i network
  Network: bridge host ipvlan macvlan null overlay

现在我们启动一个 nginx 容器

liruilonger@cloudshell:~$ docker run -d -p 2024:80 --name mynginxs nginx
704b4427a24d56e6a2cc999fcf95125c73e665cb90029b191febc405f90a789a
liruilonger@cloudshell:~$

映射端口访问正常

在这里插入图片描述在这里插入图片描述

同时在容器内部访问 外部网站正常

liruilonger@cloudshell:~$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                  NAMES
704b4427a24d   nginx     "/docker-entrypoint.…"   2 minutes ago   Up 2 minutes   0.0.0.0:2024->80/tcp   mynginxs
liruilonger@cloudshell:~$ docker exec -it 704b4427a24d bash
root@704b4427a24d:/# curl baidu.com
<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>
root@704b4427a24d:/#

现在我么来看看容器访问 baidu.com 是如何发生的?在这之前,我需要看一下当前容器的网络配置

liruilonger@cloudshell:~$ docker inspect 704b4427a24d

之所以能够实现访问外网,下面的配置必不可少

        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "29735aa89eefbbbc03beb8f120aab0d0898de7b46959cf560739748458a1f8ca",
            "SandboxKey": "/var/run/docker/netns/29735aa89eef",
            "Ports": {
                "80/tcp": [
                    {
                        "HostIp": "0.0.0.0",
                        "HostPort": "2024"
                    }
                ]
            },
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "c8e13b9e448504121192937ac4e4619c3dbdcc58fd26b89a601f3bba61dd9f21",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "02:42:ac:11:00:02",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "MacAddress": "02:42:ac:11:00:02",
                    "NetworkID": "cd77486c39955f3d2369fe32e1f5b9b65d81c1a07bb677b085cec72b8fb52440",
                    "EndpointID": "c8e13b9e448504121192937ac4e4619c3dbdcc58fd26b89a601f3bba61dd9f21",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "DriverOpts": null,
                    "DNSNames": null
                }
            }
        }

通过上面的配置信息,可以找到有用的信息

IP 地址为: "IPAddress": "172.17.0.2", 网关为: "Gateway": "172.17.0.1"

简单梳理一下流程:

  1. 首先在容器内发起对 baidu.com 的访问请求
  2. 请求首先被容器中网络命名空间(/var/run/docker/netns/29735aa89eef)对应的网络栈接收
  3. 容器内的网络栈将检查目标地址是否在容器网络的子网范围内。由于 baidu.com 不在容器网络内,网络栈确定需要将请求发送到容器外部网络
  4. 所以容器要找网关 172.17.0.1 把请求发出去。这里的网关地址实际上是在安装 docker 是默认创建的桥虚拟接设备 docker0

通过下面的命令我们可以看到

liruilonger@cloudshell:~$ ifconfig  docker0
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1460
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:87:21:0a:8b  txqueuelen 0  (Ethernet)
        RX packets 23  bytes 2710 (2.6 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 23  bytes 4437 (4.3 KiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

liruilonger@cloudshell:~$

实际上在创建 容器之后,docker 会默认帮我们做一些事

  • 会创建一个容器对应的 Linux 网络命名空间
  • 创建一对 veth pair,将其中一个端口连接到根命名空间中的网桥docker0上,另一个端口放置在容器命名空间中。
  • 在容器命名空间中配置 IP 地址(172.17.0.2),并将该设备激活。
  • 在根命名空间中启用 IP 转发功能(通过设置 net.ipv4.ip_forward=1),同时在容器命名空间配置默认网关(172.17.0.1)。
  • 配置 NAT 规则 SNAT,将容器网络命名空间中的流量转发的源IP地址转化为根命名空间中的IP地址

可以通过 sudo iptables -t nat -nL 命令查到POSTROUTING 链中配置的 SNAT 规则

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0

它将源地址为 172.17.0.0/16(Docker 桥接网络的子网)的所有数据包的源地址修改为主机的 IP 地址,并将目标地址设置为 0.0.0.0/0,表示任何目标地址。这个规则允许位于 Docker 桥接网络中的容器访问外部网络和互联网资源。

  • 目标命名空间中的流量将通过默认网关走网桥 IP 地址转发到根命名空间中,并通过根命名空间中的网络设备连接到互联网。
  1. 所以在到了网关地址对应的 Linux 网桥设备 docker0 之后,因为默认开启了 ipv4 转发,即可以简单理解为把宿主机当交换机, docker0 的流量会直接转发到外部网络
liruilonger@cloudshell:~$ ip route
default via 10.88.0.1 dev eth0 
10.88.0.0/16 dev eth0 proto kernel scope link src 10.88.0.4 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
liruilonger@cloudshell:~$
  1. Docker 宿主机的网络栈接收到请求后,宿主机的网络配置设置了 SNAT,它将转换容器内部的源 IP 地址为宿主机的 IP 地址,宿主机上的网络栈将根据自己的路由表和网络配置,将请求转发到外部网络,同时以便响应返回时能正确到达容器
  2. 之后的请求就是宿主机和公网的通行,这里不多描述

所以一般情况下,容器访问外部网络,需要两个因素:

  • ip_forward(开启 IPV4 转发)
  • SNAT/MASQUERADE(配置 SNAT/MASQUERADE)

所以如果发现容器内访问不了外部网络,则需要确认系统的ip_forward是否已打开。或者检查docker daemon启动的时候--ip-forward参数是不是被设置成false了,如果是的话,则需要设置--ip-forward=true重新启动 Docker,Docker 会打开主机的 ip forward。

即从容器网段出来访问外部网络的包,都要做一次MASQUERADE,即出去的包都用主机的IP地址替换源地址

下面为当前容器宿主机所有链上的 nat 表的防火墙规则

liruilonger@cloudshell:~$ sudo iptables -t nat -nL
Chain PREROUTING (policy ACCEPT)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            169.254.169.254      tcp dpt:80 to:127.0.0.1:900
DOCKER     all  --  0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
DNAT       tcp  --  0.0.0.0/0            169.254.169.254      tcp dpt:80 to:127.0.0.1:900
DNAT       tcp  --  0.0.0.0/0            169.254.169.254      tcp dpt:8080 to:169.254.169.254:80
DOCKER     all  --  0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT)
target     prot opt source               destination
MASQUERADE  all  --  172.17.0.0/16        0.0.0.0/0
MASQUERADE  tcp  --  172.17.0.2           172.17.0.2           tcp dpt:80

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:2024 to:172.17.0.2:80
liruilonger@cloudshell:~$

这里我们顺便看一下,容器端口映射的原理,实际上主要在 DOCKER 这条自定义链上配置了 DNAT

Chain DOCKER (2 references)
target     prot opt source               destination
RETURN     all  --  0.0.0.0/0            0.0.0.0/0
DNAT       tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:2024 to:172.17.0.2:80
liruilonger@cloudshell:~$

第二个规则是针对源地址为0.0.0.0/0,目标地址为0.0.0.0/0目标端口为2024的TCP数据包。这个规则将数据包的目标地址修改为172.17.0.2:80,即将数据包重定向到172.17.0.2的端口80

这里实际上进行了端口映射的操作,也就是 DNAT 发生的地方,它有两处引用

分别是PREROUTING链和OUTPUT链,意味着从外面发到本机和本地进程访问本机(由 iptables 匹配规则ADDRTYPE match dst-type LOCAL指定)的 2024 端口的包目的地址都会被修改成 172.17.0.2:80。

关于 docker 的端口映射, 除了使用docker ps命令给出容器的端口映射关系,还可以使用docker port命令查看容器的端口在主机上的映射

这里简单分享一些 DNAT 和 SNAT 的知识

SNAT/DNAT 认知

DNAT

DNAT根据指定条件 修改数据包的目标IP地址和目标端口 。DNAT 的原理和我们上文讨论的端口转发原理差不多,差别是端口转发不修改IP地址。使用iptables做目的地址转换的一个典型例子如下:

iptables -t nat -A PREROUTING -d 1.2.3.4  -p tcp -dport 80 -j DNAT  --to-destination 10.20.30.40:8080 
  • -j DNAT 表示目的地址转换
  • -d 1.2.3.4 -p tcp --dport 80 表示匹配的包,条件是访问目的地址和端口为1.2.3.4:80的TCP包
  • --to-destination 表示将该包的目的地址和端口修改成 10.20.30.40:8080。

同样,DNAT不修改协议。如果要匹配网卡,可以用 -i eth0 指定收到包的网卡(i 是 input 的缩写)。需要注意的是,DNAT 只发生在 nat表的 PREROUTING 链和 OUTPUT,这也是我们要指定收到包的网卡而不是发出包的网卡的原因

当涉及转发的目的IP地址是外机时,需要确保启用 ip forward 功能,即把 Linux :

echo 1 > /proc/sys/net/ipv4/ip_forward

SNAT/ 网络地址欺骗

神秘的网络地址欺骗其实是SNAT的一种。SNAT 根据指定条件修改数据包的源IP地址,即 DNAT 的逆操作。与 DNAT 的限制类似,SNAT 策略只能发生在 nat 表的 POSTROUTING 链 和 INPUT 链。

ipttables -t nat -A POSTROUTING -s 192.168.26.12 -o eth0 -j SNAT -to-source 10.127.16.1 
  • -j SNAT表示源地址转换
  • -s 192.168.1.12 表示匹配的包源地址是 192.168.1.12,
  • --to-source 表示将该包的源地址修改成 10.172.16.1。与DNAT类似
  • -o eth0(o是output的缩写)匹配发包的网卡

至于网络地址伪装,与SNAT类似,其实就是一种特殊的源地址转换,报文从哪个网卡出就用该网卡上的IP地址替换该报文的源地址,具体用哪个IP地址由内核决定。下面这条规则的意思是:源地址是 10.8.0.0/16 的报文都做一次 Masq

iptable -t nat -A POSTROUTING -s 10.8.0.0/16 -j MASQUERADE
在这里插入图片描述在这里插入图片描述

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)



© 2018-2024 liruilonger@gmail.com, All rights reserved. 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。