runc中本身太多的网络配置,如果非要介绍的话,那么只有三个点相关:
- 在config.json中配置network namespace。
- 使用PreStart hook来配置网络——实际上意义不大
- 默认配置lo网卡
这三点中只有第三点是属于直接相关的代码逻辑,另两点只是可以进行配置。
首先看第三点,如果在config.json中配置network namespace,但是没有指定path那么系统会默认创建loopback回路网卡,默认配置项在这里加入,网卡的创建是在init流程中,具体代码逻辑在https://github.com/opencontainers/runc/blob/v1.0.0-rc8/libcontainer/standard_init_linux.go#L80 换句话说,如果使用runc的默认配置,那么attach到容器中执行ifconfig将只会看到lo一个网卡。
~ # ifconfig
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
关于第二点,runc官网文档的介绍可以通过prestart,poststop来管理container的网络配置,可以将容器网络的创建脚本写在prestart中,网络的销毁配置在poststop中达到网络的配置,config.json配置方式
~ # ifconfig
"hooks": {
"prestart": [
{
"path": "/home/env/gopath/bin/netns",
"args": [
"netns",
"create",
"--ip",
"176.10.0.1/24"
]
}
],
"poststop": [
{
"path": "/home/env/gopath/bin/netns",
"args": [
"netns",
"rm"
]
}
]
},
注上面的脚本用了网上的一个开源项目,项目路径:https://github.com/genuinetools/netns 但是需要注意,prestart实际是在namespace挂载完成,rootfs的mount传播设置好,dev下所有的设备都挂载完成后,change root前执行。而且这个执行的调用init进程通过管道通知start进程执行调用,换句话说就是这个调用的namespace空间实际与runc相同的namespace中,而不是在init相同的namespace中。
这里是介绍第一点,因为他也是docker等网络设置的原理相似。假如host网络可以访问internet,那么runc通过默认的config.json配置出来的container实际与host间网络是隔离的,并且除了回路网卡外,没有网络配置,网络及host机器以及外界都是不通的,要想实现网络可访问,有两种选择:
A). 不挂载network namespace, 当使用rootless容器器,第二种方案由于权限问题,会导致无法连通网络,所以只能采用这种方式,这也是spec命令中如果–rootless后要移除network namespace的原因,代码见这里
修改config.json, 去掉namespace中的network配置
"namespaces": [
{
"type": "pid"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
}
]
启动容器后,查看网卡信息
runc run container
ifconfig
eth0 Link encap:Ethernet HWaddr FA:16:3E:44:2A:2D
inet addr:10.0.0.11 Bcast:10.0.0.255 Mask:255.255.255.0
inet6 addr: fe80::f816:3eff:fe44:2a2d/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:220113532 errors:0 dropped:0 overruns:0 frame:0
TX packets:200823070 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:125843384251 (117.2 GiB) TX bytes:35319134918 (32.8 GiB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
inet6 addr: ::1/128 Scope:Host
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:14768870216 errors:0 dropped:0 overruns:0 frame:0
TX packets:14768870216 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:3214996363063 (2.9 TiB) TX bytes:3214996363063 (2.9 TiB)
我们可以看到,实际显示出来的网卡信息是host的网络信息并且网络是联通的,我们分别的容器里和启动容器的外部查看namespace情况
~ # ls -l /proc/self/ns/
total 0
lrwxrwxrwx 1 root root 0 Jun 29 10:15 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jun 29 10:15 ipc -> ipc:[4026532344]
lrwxrwxrwx 1 root root 0 Jun 29 10:15 mnt -> mnt:[4026532342]
lrwxrwxrwx 1 root root 0 Jun 29 10:15 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Jun 29 10:15 pid -> pid:[4026532345]
lrwxrwxrwx 1 root root 0 Jun 29 10:15 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jun 29 10:15 uts -> uts:[4026532343]
total 0
dr-x--x--x 2 root root 0 Jun 29 18:15 ./
dr-xr-xr-x 9 root root 0 Jun 29 18:15 ../
lrwxrwxrwx 1 root root 0 Jun 29 18:15 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jun 29 18:15 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jun 29 18:15 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jun 29 18:15 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Jun 29 18:15 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jun 29 18:15 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jun 29 18:15 uts -> uts:[4026531838]
我们可以发现net的ID是一样的,也就是说容器与host间没有网络隔离。容器的网络配置将和host上一样,这样只要主机上能访问的网络,容器内部都能访问。
B). 挂载一个已有网络已配置好的network namepace。配置好网络的访问。
这个相对来讲就要复杂一些,打通host与namespace间的网络,核心实现方式就是桥接。方法见下:
1) 创建一个网卡runc0,并配置其地址为176.10.0.1
sudo brctl addbr runc0
sudo ip link set runc0 up
sudo ip addr add 176.10.0.1/16 dev runc0
2) 创建一个bridge,把其中的一段连接到runc0
sudo ip link add name veth-host type veth peer name veth-guest
sudo ip link set veth-host up
sudo brctl addif runc0 veth-host
3) 创建一个名为runc的network namespace, 把bridge的另一端加入到runc中,然后重命名另一端名字为eth1, 配置上ip地址
sudo ip netns add runc
sudo ip link set veth-guest netns runc
sudo ip netns exec runc ip link set veth-guest name eth1
sudo ip netns exec runc ip addr add 176.10.0.101/16 dev eth1
sudo ip netns exec runc ip link set eth1 up
到这里已经在host上有一个网卡,namespace下有另一个网卡,并且这两个网卡通过一个bridge进行了连通,他们之间已经可以进行通信了。
接下来要做得是将namespace中的数据能转发到host的runc0 网卡上,需要将namespace中的默认路由的网关设置成runc0的ip
sudo ip netns exec runc ip route add default via 192.168.10.1
到这一步,host与namespace中的进程已经可以通信了。因为在添加ip设置的时候,系统实际默认向route表中加了一条地址转发规则。
ip netns exec runc route
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
default 176.10.0.1 0.0.0.0 UG 0 0 0 eth1
176.10.0.0 * 255.255.0.0 U 0 0 0 eth1
route
176.10.0.0 * 255.255.0.0 U 0 0 0 runc0
最后需要做是让runc0中的外网访问数据能通过host的默认端口(比如eth0)发出去,并将该网段的数据能分发到runc0.
这里主要要做两件事情,1是配置SNAT,将176.10开头的源地址修改为eth0网卡的地址,2配置filter,使eth0与runc0间数据能否互相通信。
iptables -t nat -I POSTROUTING 1 --source 176.10.0.1/16 -o eth0 -j MASQUERADE
iptables -t filter -A FORWARD -o eth0 -i runc0 -j ACCEPT
iptables -t filter -A FORWARD -i eth0 -o runc0 -j ACCEPT
这样namespace中的进程就可以访问eth0能访问的空间了,接下配置好了这个链路以及network namespace后,需要将该network namespace配置给runc,该network namespace的路径为/run/netns/runc,config.json配置如下
"namespaces": [
{
"type": "pid"
},
{
"type": “network”,
"path": "/run/netns/runc"
},
{
"type": "ipc"
},
{
"type": "uts"
},
{
"type": "mount"
}
]
再通过runc启动容器后,在容器内部测试网络OK,整个网络原理和docker类似。