背景
某次在试图从容器内访问到本地的数据库时,发现在本机上并没有 docker0
这个网桥。学习了一波 Docker
网络相关的知识后作出了以下总结。
相信下面这张图大家没少看过,Docker
相较于 VM
更加轻量,不需要虚拟出自己的操作系统层,具有更多的优点。
上图来源于网络,后文中也将对来源于网络的图做出说明
然而笔者想当然的就以为 Docker
在所有系统下都是这种架构。查阅相关资料后发现,Docker
在 linux
环境下确实是这样运行的,但是在 Windows
和 macOS
系统下,Docker
会先启一个 VM
,然后在该 VM
上运行 Docker
环境。新版的 Docker for Mac
已经不再使用 VirtualBox
提供虚拟机环境,而是使用更轻量的 HyperKit
,设计上更为巧妙,本文就不展开讲这个点了。下图是 Docker for Mac
的架构图。
因此 docker0
网桥是在虚拟机内,而不是在宿主机内。可想而知,运行在虚拟机中与运行在本机上肯定是有区别的,因此 Docker for Mac
实际上自带了 k8s
环境,并且通过端口转发到宿主机,以屏蔽用户对 VM
的感知。然而要实现容器到宿主机的通信,还需要进行另一番调研。
下面先介绍一下 Docker
支持的几种网络模式。
Docker 支持的网络模式
Network | Description |
---|---|
none | no networking in the container |
bridge | Connect the container to the bridge via veth interfaces |
host | Use the host's network stack inside the container |
Container:<nameid> | Use the network stack of another container, specified via its name or id |
Network | Connects the container to a user created network(using |
NETWORK: NONE
这种网络模式下无法联网,仅有一个 loopback
回环网络,没有其他网卡。这种类型的网络无法联网,但可以通过 link
容器来实现容器间通信,这种网络模式可以很好的保证容器的安全性。同时,这种网络模式下用户自己自行创建网络,可以实现更为灵活复杂的网络。
NETWORK: BRIDGE
这是容器默认的网络模式。桥接模式会为 Docker Container
创建独立的网络堆栈,保证容器内的进程组使用独立的网络环境,从而实现容器间、容器与宿主机之间的网络栈隔离。同时,桥接模式可以通过宿主机上的 docker0
网桥来实现宿主机与容器之间的网络通信。
桥接模式会在主机上创建两个虚拟网络接口设备,一个附加在宿主机上的 docker0
网桥内,并命名为 veth0
,另一个附加在 Docker Container
所属的 namespace
的下,并命名为 eth0
。
通过 veth pair
技术,其特性可以保证无论哪一个 veth
接收到网络报文,都会将报文传输给另一方,这就实现了从容器到宿主机的网络连通性。然而上面也提到了 Docker
需要运行在 linux
环境下,所以我们无法在主机上看到 docker0
网桥,这个网桥位于虚拟机中。
NETWORK: HOST
这种网络模式下容器将跟主机共享网络堆栈,因此容器可以直接使用宿主机的 eh0
实现与外界的通信,并且主机所有的接口都可以被容器访问及使用。
在这种模式下,容器将获取更高的网络性能,因为它使用主机的网络栈,不需要通过 Docker
守护进程进行一层虚拟化。但是,这种模式也会导致容器网络环境隔离性弱化,即容器不再拥有隔离的、独立的网络栈。容器会与宿主机竞争网络栈的使用,同时容器也不再拥有所有的端口资源,因为部分端口已经被宿主机本身的服务占用,还有部分端口用于桥接模式容器的端口映射。
NETWORK: CONTAINER
这种网络模式下,容器将和另一个容器共享网络堆栈,因此,同样需要注意端口冲突等问题。该模式下,两个容器与其他容器以及宿主机之间存在网络隔离。
USER-DEFINED NETWORK
开发者可以使用 Docker
网络驱动程序或者外部的网络驱动程序来创建网络,也可以把多个容器连接到同一个网络下。在这里不展开介绍。
解决方法
方案一(仅在 macOS/Windows 下可用)
Docker
本身针对这种场景已经提出了一些解决方案。从版本 18.03 开始,Docker for Mac
提供一个特殊的 DNS name
以便用户从容器内访问到本机, 这个 DNS name
被解析至主机在 docker
内使用的内部 IP
。同时其他版本的 Docker Desktop for Mac
也有对应的 host
,如下所示:
- Docker for Mac v 18.03 and above
host.docker.internal
- Docker for Mac v 17.12 to v 18.02
docker.for.mac.host.internal
- Docker for Mac v 17.06 to v 17.11
docker.for.mac.localhost
- Docker for Mac 17.05 and below
Docker
不提供,需要自己进行配置,具体配置方法可以参考 Janne Annala 的回答
https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach
如上图所示,可以通过 host.docker.internal
直接访问到宿主机内的服务
方案二(仅在 Linux 下可用)
使用桥接模式。
可以看到宿主机下已经有了 docker0
网桥,且地址为 172.17.0.1
。
进入容器并查看容器内的网络接口信息和路由表可以发现 eth0
和宿主机中 docker0
的网段相同,且已经将 docker0
的接口地址设置为了默认网关,即匹配到的请求将通过 172.17.0.1
转发到目标地址。
那么通过该地址即可实现从容器到宿主机的访问。
方案三(仅在 Linux 下可用)
使用 host
模式启动服务就可以直接访问本机上的服务。利弊如上所述,若在生产环境使用该模式还需要自己再多做相关调研。
延展阅读
读到这的读者可能就会想,那容器内部需要访问外界,或者外界需要访问服务内部该咋办?好像刚刚提到的知识点不足以支撑这一流程呀?docker0
网段和宿主机的网段不同,外界无法得知容器 IP
更无法直接访问到容器内部。
这里就要引入另一个概念 NAT(Network Address Translation)
,是一种用于重写源IP地址或目的IP地址的技术。
外界访问容器内部
前提条件:容器运行时通过 -P 或 -p 指令主动暴露端口并将端口映射至主机上
- 外界直接请求
host_ip:port_0
- 通过
DNAT
将请求的目的地址修改为container_ip:port_1
- 宿主机将请求转发给
veth pair
veth pair
将请求通过veth
转发至容器内部的eth0
- 回包时也通过
docker0
转发至宿主机的eth0
发送回包
通过检测数据包可以看到请求的目的 IP 被修改为了对应的容器 IP ,以完成外界对容器内部的访问 7.png 8.png
容器内部访问外界
- 容器内发出请求,此时会随机选取一个未被占用的端口
port0
作为源端口 - 请求通过
eth0
转发至docker0
网桥处的veth
docker0
网球将请求转发至宿主机的eth0
处- 宿主机处理请求时通过
SNAT
将请求源地址修改为host_ip:port_1
并转发出去 - 外界回包时发送至宿主机的
eth0
处 - 按照
iptables
规则,宿主机将请求转发至容器内部
查看 iptables 规则可知,从 172.17.0.0 网段出去访问外网的请求都会交由 MASQUERADE 处理。而 MASQUERADE 的处理就是将请求的源 ip 替换成宿主机的 ip 并发出去,也就是做了一次 NAT 处理。 9.png 通过检测数据包可知,请求的源 ip 确实被从 docker0 网段的容器 ip:172.17.0.2 修改为 eth1:10.12.91.17 10.png 11.png