podman 容器内无法访问网络
podman是什么
前几天将自己的开发环境切到了centos 8,为了兼容之前一些业务程序的编译,所以我需要一个centos 7容器。在yum install docker之后发现,centos 8的docker被一个叫podman的程序给替代了。去简单了解了下,podman是redhat一直主推的本地容器解决方案,能够提供和k8s的pod相同的容器组织方式,多个容器可以放在同一个pod中,同一个pod中的容器共用网络。
podman系列主要包含三个命令podman、buildah、skopeo,其中podman本身负责运行、停止、管理容器,buildah负责构建容器镜像、skopeo负责与remote repo交互,拉取或推送镜像。但我们使用时不必这么麻烦,redhat为了方便用户从docker迁移到podman,在podman上几乎实现了大多数docker的常用命令,podman会替你转调buildah和skopeo,你甚至可以直接 alias docker=podman,然后像使用docker一样使用podman。podman系列的的安装方法如下:
yum module list | grep container
# yum install podman buildah skopeo
yum install @container-tools
相比于docker,podman系列抛弃了server端,不再有类似dockerd的后台进程,执行命令时不再需要将命令和包拷贝到server端,在build image时,能够节省一笔拷贝的开销。podman不再要求root权限,可以创建rootfull和rootless两种类型的容器,虽然两者的容器运行时都是基于runc。podman引入了k8s中pod的概念,可以将多个容器放到同一个pod中,共享网络;默认的pod基础容器和k8s一样执行/pause命令,并支持用户自定义基础容器。
如何简单的使用podman可以参考: https://thenewstack.io/deploy-a-pod-on-centos-with-podman/
问题
现在回到我的问题。安装好podman之后,按照如下步骤,开始搭建编译容器。在yum install时,发现容器不能访问外网
podman pull centos:7
cat > Dockerfile <<EOF
FROM centos:7
ENV container docker
RUN (cd /lib/systemd/system/sysinit.target.wants/; for i in *; do [ $i == \
systemd-tmpfiles-setup.service ] || rm -f $i; done); \
rm -f /lib/systemd/system/multi-user.target.wants/*;\
rm -f /etc/systemd/system/*.wants/*;\
rm -f /lib/systemd/system/local-fs.target.wants/*; \
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
rm -f /lib/systemd/system/basic.target.wants/*;\
rm -f /lib/systemd/system/anaconda.target.wants/*;
VOLUME [ "/sys/fs/cgroup" ]
CMD ["/usr/sbin/init"]
EOF
podman build -t c7-systemd .
podman run -it -d --name mybuild -v /data/share:/data/share c7-systemd
podman exec -it mybuild bash
yum install gcc-c++ libstdc++-static openssl-devel
问题具体表现是:
- 直接ping外网域名,返回bad address
- ping 域名对应的ip正常
- curl 域名对应的ip返回no route to host
- 容器内的网络配置文件,一切正常
(venv) ▸ root@new1 ~ $podman run --rm alpine ping www.baidu.com
ping: bad address 'www.baidu.com'
(venv) ▸ root@new1 ~ $nslookup www.baidu.com
Server: 10.11.56.22
Address: 10.11.56.22#53
Non-authoritative answer:
www.baidu.com canonical name = www.a.shifen.com.
Name: www.a.shifen.com
Address: 14.215.177.39
Name: www.a.shifen.com
Address: 14.215.177.38
(venv) ▸ root@new1 ~ $podman run --rm alpine ping 14.215.177.39
PING 14.215.177.39 (14.215.177.39): 56 data bytes
64 bytes from 14.215.177.39: seq=0 ttl=47 time=12.559 ms
64 bytes from 14.215.177.39: seq=1 ttl=47 time=10.266 ms
(venv) ▸ root@new1 ~ $podman run --rm alpine curl 14.215.177.39
curl: (7) Failed connect to 10.28.36.104:80; No route to host
容器的网络, CNI
podman容器有三种网络模式:bridge、host和none。启动容器时,不显式指定的话,使用的是bridge网络;可以通过--net=
参数执行为host或none模式。host模式下,容器共享宿主机的网络;none模式表示容器不存在网络栈,仅有本地回环,无法与外界或其他pod通信。
对于我们上面的问题,一个最简单的解决方法就是使用host网络模式重新启动容器。当时我也是这样做的,但正常情况下bridge模式为什么不行呢?这问题也得有个答案。
(venv) ▸ root@new1 ~ $podman run --rm --net=host alpine ping www.baidu.com
PING www.baidu.com (14.215.177.38): 56 data bytes
64 bytes from 14.215.177.38: seq=0 ttl=48 time=9.428 ms
64 bytes from 14.215.177.38: seq=1 ttl=48 time=9.328 ms
众所周知,容器的网络实现一般基于CNI(Container Networking Interface)。podman也不例外,基于CNI来实现其bridged network stack。CNI是容器的网络标准,类似的还有CRI(Container runtime interface)、CSI(Container storage interface),是k8s为了规范底层pod的实现方式,制定的几种标准接口。
podman使用的CNI项目是CNI的一个使用最广泛的实现,该项目基于iptables来实现bridged network stack。CNI通过podman提供的信息,以及描述podman所需网桥信息的默认配置/etc/cni/net.d/87-podman-bridge.conflist来创建虚拟网桥。
CNI项目使用iptables来实现各种网络栈,这就存在一个问题,当其他程序对容器宿主机的netfilter规则进行修改,可能会影响到podman容器的网络访问。比如iptables规则的修改、firewalld规则的修改都会影响容器的网络访问。所以我们需要去分析当前的iptables配置和firewalld配置。
问题解决
检查iptables规则
iptables采取黑名单策略,INPUT、FORWARD、OUTPUT chain的默认规则都是ACCEPT,对于没有显式配置DROP规则的网络报,是采取放通的处理方式。
并且CNI创建了自己的chain CNI-FORWARD,并且,针对容器ip 10.88.0.46(这是一个运行中的容器)放通了所有的进出流量。iptables的规则没有问题。
(venv) ▸ root@new1 ~ $iptables -L
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy ACCEPT)
target prot opt source destination
CNI-FORWARD all -- anywhere anywhere /* CNI firewall plugin rules */
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
Chain CNI-FORWARD (1 references)
target prot opt source destination
CNI-ADMIN all -- anywhere anywhere /* CNI firewall plugin rules */
ACCEPT all -- anywhere 10.88.0.46 ctstate RELATED,ESTABLISHED
ACCEPT all -- 10.88.0.46 anywhere
Chain CNI-ADMIN (1 references)
target prot opt source destination
检查firewalld的配置
firewalld和iptables一样,都是基于netfilter实现的,但firewalld比较新,优化了很多iptables的问题。其中一个就是,firewalld采取白名单策略,仅让符合配置中规则的流量通过。
firewalld以zone作为配置管理的单位,可以为不同的区域配置不同的放通规则。可以通过配置入站ip地址范围或者配置网络接口来将网络包,分配到不同的zone进行处理。
# 列出所有的zone
▸ root@new1 ~ $firewall-cmd --get-zones
block dmz drop external home internal public trusted work
默认情况下,所有的interface都会被放入public zone。配置了interface或者ip范围的zone,被称为active zone。默认情况下,只有public zone是活跃的。
# 列出active zone
▸ root@new1 ~ $firewall-cmd --get-active-zones
public
interfaces: enp0s3 enp0s8
以这个zone的配置为例,简单解释下每行的含义
▸ root@new1 ~ $firewall-cmd --zone=public --list-all
public (active)
target: default
icmp-block-inversion: no
interfaces: enp0s3 enp0s8
sources:
services: cockpit dhcpv6-client ssh
ports:
protocols:
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
简单逐行做下解释:
- public (active) 表示public区域是active的
- target: default 当包与public区域匹配,而没有被下述规则所处理,的包的处理方式,除default,还有其他方式
ACCEPT:通过这个包。
%%REJECT%%:拒绝这个包,并返回一个拒绝的回复。
DROP:丢弃这个包,不回复任何信息。
default:不做任何事情。该区域不再管它,返回上一层处理 - icmp-block-inversion: no
- interfaces: enp0s3 enp0s8 列出这个zone上关联的interface
- sources: 列出这个区域上的源,现在是空,如果有的话,是类似33.233.233.0/24这种格式
- services: cockpit dhcpv6-client ssh 列出允许通过防火墙的服务,可以通过firewall-cmd --get-services列出所有服务
- ports: 列出允许通过防火墙的target port,当要放通一个没有定义的服务时,可以使用端口
- protocols:
- masquerade: no 表示这个区域不支持ip伪装。允许的话,将允许ip转发,计算机作为路由器需要开启这个功能
- icmp-blocks: 要阻塞的icmp流量名单
- rich rules: 高级规则
通过ip addr看到podman创建的接口是cni-podman0
4: cni-podman0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default qlen 1000
link/ether 5e:a4:d6:86:c8:b6 brd ff:ff:ff:ff:ff:ff
inet 10.88.0.1/16 brd 10.88.255.255 scope global cni-podman0
valid_lft forever preferred_lft forever
inet6 fe80::5ca4:d6ff:fe86:c8b6/64 scope link
valid_lft forever preferred_lft forever
现在,我们找到了问题所在,podman所创建的接口cni-podman0,没有任何firewall zone与之匹配,默认的行为是拒绝来自cni-podman0的包,所以解决方法是将cni-podman0加入到trusted zone中。
▸ root@new1 ~ $firewall-cmd --zone=trusted --add-interface=cni-podman0
success
▸ root@new1 ~ $firewall-cmd --get-active-zones
public
interfaces: enp0s3 enp0s8
trusted
interfaces: cni-podman0
trusted zone的配置如下,其target是ACCEPT,只要将interface 或者source加入该zone,来自该interface或source的包就会被接受。
▸ root@new1 ~ $firewall-cmd --zone=trusted --list-all
trusted (active)
target: ACCEPT
icmp-block-inversion: no
interfaces: cni-podman0
sources:
services:
ports:
protocols:
masquerade: no
forward-ports:
source-ports:
icmp-blocks:
rich rules:
在容器中curl一下外网域名,做下验证,一切正常了
▸ root@new1 ~ $podman run --rm c7-systemd curl www.baidu.com
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc>........... </body> </html>