通过docker run可以方便创建一个隔离网络的容器,但我们不忍好奇,docker是如何帮我们创建容器网络,底层工作原理是什么,究竟用了什么技术实现,下面就创建一个空白容器,逐步搭建起容器网络。
要求
手工创建容器网络,并完成:
- 能curl www.baidu.com
- 容器监听5000端口并映射到节点1 5000端口,节点2能通过192.168.1.1:5000访问到。
思路
- 创建虚拟网卡对veth pair,一个挂在容器、另一个挂点节点上。虚拟网卡对类似一根网卡,网线一头连在容器,网线另一头连在节点。
- 容器配置默认网卡,把全部的IP包都通过veth-docker网卡转发给节点虚拟网卡veth-node(10.10.1.1)。
- 访问外网:通过iptables SNAT把容器访问外网的IP的源地址从 10.10.1.2:port1 改成192.168.10.3:port2,并通过节点网卡ens33发送出去;节点网卡收到回包,再把目标地址从 192.168.10.3:port2 改成 10.10.1.2:port1 发给容器。
- 监听绑定5000端口:通过iptables DNAT把收到ip包的目标地址从192.168.10.3:5000改成10.10.1.2:5000,再通过节点的虚拟网卡veth-node转发到容器;容器回包从虚拟网卡发到节点后,把源地址从10.10.1.2:5000改成192.168.10.3:5000再发到外网,从而实现容器监听绑定5000端口。
准备
- 两台虚拟机,网络互通并可以访问互联网,下面实践中两台虚拟机分别命名成:节点1(192.168.10.3/24)、节点2(192.168.10.3/24)。
- 节点1安装docker,并支持ip_forward。
创建veth虚拟网卡
创建虚拟网卡对veth-pair,两端分别命名veth-node、veth-docker,veth-node挂在节点、veth-docker挂在容器
[root@master ~]# ip link add veth-node type veth peer name veth-docker
节点网卡veth-node配置
在docker中,为了多容器网络互通,会创建一个虚拟网桥并分配ip。节点虚拟网卡veth-node加入虚拟网桥,不用单独分配ip。由于这次实践目标不是多容器网络互通,简单起见就不加入虚拟网桥。 设置节点虚拟网卡ip成10.10.1.1/24,并启用网卡。
# 设置节点
[root@master ~]# ip addr add 10.10.1.1/24 dev veth-node
[root@master ~]# ip link set veth-node up
容器网卡veth-docker配置
创建docker容器
创建容器,这里使用jaydu666/docker-network镜像,镜像集成ping、curl工具用于验证外网访问;同时启动一个简答的node.js的web服务监听5000端口,用于验证监听绑定节点5000端口。jaydu666/docker-network镜像构建见附页2。
# 拉取镜像
[root@master centos-tool]# docker pull jaydu666/docker-network
# 创建容器并记录容器id
[root@master ~]# docker container run --name docker-network -d --rm -it --net=none jaydu666/docker-network
挂载veth-docker网卡到容器
容器有独立网络命名空间(netns),这个默认在节点上不可见。可以通过把进程命名空间下的网络命名空间(/proc/{容器进程id}/ns/net) 链接到 节点网络命名空间下(/var/run/netns/),显示出容器网络命名空间。再把虚拟网卡veth-docker挂到容器网络命名空间下。
挂载前容器网络
通过docker container exec docker-network ip addr查看挂载前容器网络
[root@master ~]# docker container exec docker-network ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
挂载
通过docker inspect查看容器进程id,链接容器进程id下的网络命名空间到节点网络命名空间。
# 查看容器pid,并把pid赋值给container_pid
[root@master ~]# container_pid=`docker inspect -f '{{.State.Pid}}' docker-network`
# 创建网络空间
[root@master netns]# mkdir -p /var/run/netns; rm /var/run/netns/docker_network_ns; ln -s /proc/${container_pid}/ns/net /var/run/netns/docker_network_ns
# 挂载网卡
[root@master ~]# ip link set veth-docker netns docker_network_ns
挂载后容器网络 通过docker container exec docker-network ip addr查看挂载后容器网络,看到veth-docker虚拟网卡已经挂载到容器上
[root@master ~]# docker container exec docker-network ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
10: veth-docker@if11: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
link/ether fa:d3:66:13:a3:75 brd ff:ff:ff:ff:ff:ff link-netnsid 0
设置容器虚拟网卡IP并启用
通过ip netns exec在容器网络命名空间下,使用ip addr设置容器虚拟网卡网络为10.10.1.2/24,并用ip link启用网卡。
# 设置veth-docker的Ip
[root@master ~]# ip netns exec docker_network_ns ip addr add 10.10.1.2/24 dev veth-docker
# 启动veth-docker网卡
[root@master ~]# ip netns exec docker_network_ns ip link set veth-docker up
配置默认网关(路由规则)
配置默认网关:全部的IP包都通过veth-docker网卡,转发给10.10.1.1
配置前查看路由规则
通过ip route查看容器网络路由规则
# 查看路由规则
[root@master ~]# ip netns exec docker_network_ns ip route
10.10.1.0/24 dev veth-docker proto kernel scope link src 10.10.1.2
配置规则 通过ip route添加容器默认路由规则
[root@master ~]# ip netns exec docker_network_ns ip route add default via 10.10.1.1 dev veth-docker
配置后查看路由规则
通过ip route查看容器网络路由规则
[root@master ~]# ip netns exec docker_network_ns ip route list
default via 10.10.1.1 dev veth-docker
10.10.1.0/24 dev veth-docker proto kernel scope link src 10.10.1.2
节点上配置nat转发规则
在节点上为容器配置snat、dnat转发
临时配置允许转发网络包
通过sysctl临时配置允许转发网络包,重启就会失败,可以通过修改/etc/sysctl.conf永久生效。
sysctl -w net.ipv4.ip_forward=1
访问外网
通过配置SNAT转发规则来访问外网。以下两种方式都可以,相对snat方式,MASQUERADE方式不用指定转换后的ip,通过-o参数的网卡自动获取ip。
[root@master ~]# iptables -t nat -A POSTROUTING -s 10.10.1.0/24 -o ens33 -j MASQUERADE
# 或者
[root@master ~]# iptables -t nat -A POSTROUTING -s 10.10.1.0/24 -j SNAT --to-source 192.168.10.3
绑定节点5000端口
通过配置DNAT绑定节点5000端口。
[root@master ~]# iptables -t nat -A PREROUTING -d 192.168.10.3/32 -p tcp --dport 5000 -j DNAT --to-destination 10.10.1.2:5000
验证结果
通过访问外网、节点2访问容器web简易服务端结果,验证容器网络搭建结果。为了验证绑定节点5000端口,需要在容器启动个web简易服务端并监听5000端口,验证节点2是否可通过节点1的ip和端口 访问到容器服务端。
访问外网
在容器内使用curl访问百度网站,验证是否正常访问外网
[root@master ~]# docker container exec docker-network curl -s www.baidu.com
5000端口绑定测试
slave1是节点2,在节点2 curl 192.168.10.3:5000 查看是否正确访问到
[root@slave1 ~]# curl 192.168.10.3:5000
总结
- 容器通过虚拟网卡veth与其所在节点(物理机)通讯
- 通过在节点配置iptables的nats实现转发到容器
附页1
为了方便实验,整理以上创建容器网络命令到以下脚本: 脚本仓库
附页2
docker-network镜像构建链接:镜像仓库
app.js
const http = require("http");
const os = require("os");
var handler = function(request, response) {
response.writeHead(200, {'Content-type': "text/plain;charset=UTF-8"});
response.end(`[${new Date().toLocaleString()}][${os.hostname()}] hello world.\n`);
};
var www = http.createServer(handler);
www.listen(5000);
Dockerfile
From node
RUN apt-get update -y && apt-get -y install iproute2 iputils-ping curl
Add app.js /app.js
ENTRYPOINT ["node", "app.js"]
参考资料
[1] Docker入门之-网络(三):容器如何与外部世界通信 - CodeAntenna
[2] 实例:创建一个点到点连接 - Docker — 从入门到实践
[3] Network namespaces to the Internet with veth and NAT
[4] linux - Why SNAT works by setting one rules in tables without the rules in PREROUTING chain? - Super User