Docker容器无法连接外部网络原因排查
Docker是当前最常用的容器运行时引擎,在使用Kubernetes的过程中,我们使用Docker来负责底层的容器的启动、停止。在用户新安装Docker后的使用过程中,发现通过docker run
命令启动的容器,使用默认的bridge网络的情况下,容器无法连接到外部网络,针对这个现象进行排查。
缩小问题范围
使用 docker run -it --rm alpine:3.6 /bin/sh
启动一个容器,采用bridge网络,在容器内ping
外部网络的IP,我们发现是无法ping通,该命令会hang住。退出该容器,再尝试使用host网络启动容器,docker run -it --rm --network=host alpine:3.6 /bin/sh
,这次我们发现是可以ping
通外部网络的,说明是docker的默认bridge网络有问题,缩小范围。
容器使用bridge网络的情况下,在ping
外部网络的情况下,如果发送的不是Docker启动创建的docker0
的网桥,会进行SNAT,然后使用宿主机的网卡出去,那么怀疑是SNAT可能有问题,因此查看iptables中和Docker相关的规则。命令结果如下:
# iptables -t nat -nvL POSTROUTING
Chain POSTROUTING (policy ACCEPT 845 packets, 57086 bytes)
pkts bytes target prot opt in out source destination
1414 96107 POSTROUTING_direct all -- * * 0.0.0.0/0 0.0.0.0/0
1414 96107 POSTROUTING_ZONES_SOURCE all -- * * 0.0.0.0/0 0.0.0.0/0
1414 96107 POSTROUTING_ZONES all -- * * 0.0.0.0/0 0.0.0.0/0
根据上面结果,我们发现缺少了一条-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
的iptables规则。我们怀疑是用户在配置Docker的时候,配置出错导致的该问题。
排查过程
根据上面的章节,我们发现是iptable的SNAT规则有问题,那么我们就要看Docker是如何来生成该规则的。
查看配置
首先查看官方的文档,我们看到官方文档有一段关于SNAT的相关配置:
--iptables=false
prevents the Docker daemon from adding iptables rules. If multiple daemons manage iptables rules, they may overwrite rules set by another daemon. Be aware that disabling this option requires you to manually add iptables rules to expose container ports. If you prevent Docker from adding iptables rules, Docker will also not add IP masquerading rules, even if you set--ip-masq
totrue
. Without IP masquerading rules, Docker containers will not be able to connect to external hosts or the internet when using network other than default bridge.
可以看到对于缺少的这条规则来说,需要dockerd在启动的时候有两项配置--iptables=true
和--ip-masq=true
,否则Docker不会在创建默认网桥的时候生成该规则,从而容器无法访问外部网络,但是Docker默认以上两个配置项默认均为true
。我们让用户登录出现该问题的机器上,查看/etc/docker/daemon.json
里的上述两项配置,发现确实是因为设置了--ip-masq=false
,这个配置是我们在Kubernetes的环境上开启的,用户在新安装的时候,由于不清楚Docker的相关配置,直接拷贝了Kubernetes环境上的配置,导致覆盖了Docker默认的配置,从而出现了容器内部无法访问外部网络的情况。
到这里问题已经排查清楚,下面我们看下dockerd
是如何根据--ip-masq
这个参数生成-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
这条iptables规则的。
查看日志
我们把dockerd
的debug模式开启,在--ip-masq
为false
的情况下,我们重启dockerd
的服务,看到日志如下:
通过dockerd的启动日志,看到没有关于设置-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
的。接着看下在--ip-masq
为true
的情况下,dockerd
服务的日志:
通过日志,我们可以看到在启用--ip-masq
这个配置的情况下,dockerd
服务的日志会先删除,再重新生成上述两行防火墙规则。下面看下源码是如何处理这个规则的。
查看源码
下载对应环境中17.06.2-ce的Docker代码,我们可以搜索MASQUERADE
或者POSTROUTING
关键字,可以发现在libnetwork/drivers/bridge/setup_ip_tables.go
里是有相关设置的。
func setupIPTablesInternal(bridgeIface string, addr net.Addr, icc, ipmasq, hairpin, enable bool) error {
var (
address = addr.String()
natRule = iptRule{table: iptables.Nat, chain: "POSTROUTING", preArgs: []string{"-t", "nat"}, args: []string{"-s", address, "!", "-o", bridgeIface, "-j", "MASQUERADE"}}
hpNatRule = iptRule{table: iptables.Nat, chain: "POSTROUTING", preArgs: []string{"-t", "nat"}, args: []string{"-m", "addrtype", "--src-type", "LOCAL", "-o", bridgeIface, "-j", "MASQUERADE"}}
skipDNAT = iptRule{table: iptables.Nat, chain: DockerChain, preArgs: []string{"-t", "nat"}, args: []string{"-i", bridgeIface, "-j", "RETURN"}}
outRule = iptRule{table: iptables.Filter, chain: "FORWARD", args: []string{"-i", bridgeIface, "!", "-o", bridgeIface, "-j", "ACCEPT"}}
)
// Set NAT.
if ipmasq {
if err := programChainRule(natRule, "NAT", enable); err != nil {
return err
}
}
...
}
可以看到只有上述ipmasq
为true
的时候,该POSTROUTING
的规则才会被创建。而我们继续向上追溯,setupIPTablesInternal
这个函数是在setup_ip_tables.go
中setupIPTables
进行调用,该函数如下:
// setup_ip_tables.go
func (n *bridgeNetwork) setupIPTables(config *networkConfiguration, i *bridgeInterface) error {
var err error
d := n.driver
d.Lock()
driverConfig := d.config
d.Unlock()
// Sanity check.
if driverConfig.EnableIPTables == false {
return errors.New("Cannot program chains, EnableIPTable is disabled")
}
...
if config.Internal {
...
} else {
if err = setupIPTablesInternal(config.BridgeName, maskedAddrv4, config.EnableICC, config.EnableIPMasquerade, hairpinMode, true); err != nil {
return fmt.Errorf("Failed to Setup IP tables: %s", err.Error())
}
n.registerIptCleanFunc(func() error {
return setupIPTablesInternal(config.BridgeName, maskedAddrv4, config.EnableICC, config.EnableIPMasquerade, hairpinMode, false)
})
}
return nil
}
我们看到ipmasq
对应的是config.EnableIPMasquerade
,我们继续查找config.EnableIPMasquerade
引用的地方,发现针对该配置的设置是在初始化bridge的时候进行的。
接下来我们看下从dockerd启动到初始化bridge及网络的这个过程中,相关ipmasq
的处理。
具体的启动实现流程
首先dockerd读取命令行参数并调用daemonCli.start(opts)
,该函数先读取/etc/docker/daemon.json
的文件,并与命令行参数进行比对及配置合并,然后会执行NewDaemon
函数进行daemon的初始化,主要的daemon的逻辑在这个函数完成。
NewDaemon首先执行verfiyDaemonSettings函数进行参数的校验处理
func verifyDaemonSettings(conf *config.Config) error {
...
// 如果iptables为false,即使ip-masq为true,该ip-masq仍然失效,这个和上面官方文档的说明是一致的。
if !conf.BridgeConfig.EnableIPTables && conf.BridgeConfig.EnableIPMasq {
conf.BridgeConfig.EnableIPMasq = false
}
...
return nil
}
执行数据恢复
配置处理好后,会执行日志驱动、线程数等设置,并且根据数据目录(/var/lib/docker
)初始化相应的组件的存储信息,以上执行完后,会执行数据恢复的步骤,该步骤是为了把之前本机的容器网络、容器配置好并启动,函数为d.restore()
,下面进入该函数。
// daemon/daemon.go
func (daemon *Daemon) restore() error {
var (
currentDriver = daemon.GraphDriverName()
containers = make(map[string]*container.Container)
)
logrus.Info("Loading containers: start.")
dir, err := ioutil.ReadDir(daemon.repository)
if err != nil {
return err
}
for _, v := range dir {
id := v.Name()
// 读取`/var/lib/docker/containers`中所有文件目录,获取到所有的容器信息,加载到内存中,
// 每次加载完一个容器,会打印`Loaded container `字样的日志
}
removeContainers := make(map[string]*container.Container)
restartContainers := make(map[*container.Container]chan struct{})
activeSandboxes := make(map[string]interface{})
for id, c := range containers {
// 查看上述加载的容器,并把老版本的容器进行配置或volumes的迁移,用于兼容老版本启动的容器
...
}
var wg sync.WaitGroup
var mapLock sync.Mutex
for _, c := range containers {
wg.Add(1)
go func(c *container.Container) {
defer wg.Done()
//启动多协程,对状态为`Running`和`Paused`的容器进行不同的处理,处理包括:mount、remove等操作
...
}(c)
}
wg.Wait()
// 初始化网络
daemon.netController, err = daemon.initNetworkController(daemon.configStore, activeSandboxes)
if err != nil {
return fmt.Errorf("Error initializing network controller: %v", err)
}
...
logrus.Info("Loading containers: done.")
return nil
}
进入initNetworkController方法,查看具体的实现。
// daemon/daemon_unix.go
func (daemon *Daemon) initNetworkController(config *config.Config, activeSandboxes map[string]interface{}) (libnetwork.NetworkController, error) {
// 处理network的配置,把默认的bridge的网络插件添加到配置中
netOptions, err := daemon.networkOptions(config, daemon.PluginStore, activeSandboxes)
if err != nil {
return nil, err
}
// 创建网络的控制器
// 1. 默认把`/var/lib/docker/network/files/local-kv.db`作为网络的数据库存储容器网络相关信息。
// 2. 初始化docker支持的所有网络类型,如:bridge、host、none等。
// 3. 清理之前docker创建的iptables规则,并处理FORWARD规则。
// 该部分的实现内容可以参考`libnetwork.New(netOptions...)`的实现
controller, err := libnetwork.New(netOptions...)
if err != nil {
return nil, fmt.Errorf("error obtaining controller instance: %v", err)
}
// Initialize default network on "null"
...
// Initialize default network on "host"
...
// 清理无用的bridge网络
if n, err := controller.NetworkByName("bridge"); err == nil {
if err = n.Delete(); err != nil {
return nil, fmt.Errorf("could not delete the default bridge network: %v", err)
}
}
if !config.DisableBridge {
// Initialize default driver "bridge"
if err := initBridgeDriver(controller, config); err != nil {
return nil, err
}
} else {
removeDefaultBridgeInterface()
}
return controller, nil
}
func initBridgeDriver(controller libnetwork.NetworkController, config *config.Config) error {
bridgeName := bridge.DefaultBridgeName
if config.BridgeConfig.Iface != "" {
bridgeName = config.BridgeConfig.Iface
}
netOption := map[string]string{
bridge.BridgeName: bridgeName,
bridge.DefaultBridge: strconv.FormatBool(true),
netlabel.DriverMTU: strconv.Itoa(config.Mtu),
// config.BridgeConfig.EnableIPMasq 这里对应了1中的verifyDaemonSettings的校验函数。
bridge.EnableIPMasquerade: strconv.FormatBool(config.BridgeConfig.EnableIPMasq),
bridge.EnableICC: strconv.FormatBool(config.BridgeConfig.InterContainerCommunication),
}
// --ip processing
if config.BridgeConfig.DefaultIP != nil {
netOption[bridge.DefaultBindingIP] = config.BridgeConfig.DefaultIP.String()
}
...
// NewNetwork这个方法是初始化bridge,进入该函数
// Initialize default network on "bridge" with the same name
_, err = controller.NewNetwork("bridge", "bridge", "",
libnetwork.NetworkOptionEnableIPv6(config.BridgeConfig.EnableIPv6),
libnetwork.NetworkOptionDriverOpts(netOption),
libnetwork.NetworkOptionIpam("default", "", v4Conf, v6Conf, nil),
libnetwork.NetworkOptionDeferIPv6Alloc(deferIPv6Alloc))
if err != nil {
return fmt.Errorf("Error creating default \"bridge\" network: %v", err)
}
return nil
}
NewNetwork这个方法是初始化bridge,进入该函数,发现调用github.com/docker/libnetwork/drivers/bridge/bridge.go
中的createNetwork
方法,进入到方法中,我们看到这个createNetwork
方法最终调用了我们在一开始看到的setupIPTables
这个方法进行iptables规则的处理。
// github.com/docker/libnetwork/drivers/bridge/bridge.go
func (d *driver) createNetwork(config *networkConfiguration) error {
...
network := &bridgeNetwork{
id: config.ID,
endpoints: make(map[string]*bridgeEndpoint),
config: config,
portMapper: portmapper.New(d.config.UserlandProxyPath),
driver: d,
}
...
// Conditionally queue setup steps depending on configuration values.
for _, step := range []struct {
Condition bool
Fn setupStep
}{
...
// Setup IPTables.
{d.config.EnableIPTables, network.setupIPTables},
...
} {
if step.Condition {
bridgeSetup.queueStep(step.Fn)
}
}
return nil
}
到这里,我们就了解了docker是如何处理--ip-masq
这个参数并如何根据这个参数进行iptables规则的设置。
libnetwork.New(netOptions…)的实现
// github.com/docker/libnetwork/controller.go
// New creates a new instance of network controller.
func New(cfgOptions ...config.Option) (NetworkController, error) {
c := &controller{
id: stringid.GenerateRandomID(),
cfg: config.ParseConfigOptions(cfgOptions...),
sandboxes: sandboxTable{},
svcRecords: make(map[string]svcInfo),
serviceBindings: make(map[serviceKey]*service),
agentInitDone: make(chan struct{}),
networkLocker: locker.New(),
}
// 创建读取网络数据库的client
if err := c.initStores(); err != nil {
return nil, err
}
drvRegistry, err := drvregistry.New(c.getStore(datastore.LocalScope), c.getStore(datastore.GlobalScope), c.RegisterDriver, nil, c.cfg.PluginGetter)
if err != nil {
return nil, err
}
// 添加并初始化所有网络类型,getInitializers的函数如下,可以看到这些是docker支持的网络类型。
// 我们主要关注在bridge的类型,即bridge.Init这个方法。
/*
func getInitializers(experimental bool) []initializer {
in := []initializer{
{bridge.Init, "bridge"},
{host.Init, "host"},
{macvlan.Init, "macvlan"},
{null.Init, "null"},
{remote.Init, "remote"},
{overlay.Init, "overlay"},
}
if experimental {
in = append(in, additionalDrivers()...)
}
return in
}
*/
for _, i := range getInitializers(c.cfg.Daemon.Experimental) {
var dcfg map[string]interface{}
// External plugins don't need config passed through daemon. They can
// bootstrap themselves
if i.ntype != "remote" {
dcfg = c.makeDriverConfig(i.ntype)
}
if err := drvRegistry.AddDriver(i.ntype, i.fn, dcfg); err != nil {
return nil, err
}
}
...
return c, nil
}
// bridge.Init方法的实现。
// Init registers a new instance of bridge driver
func Init(dc driverapi.DriverCallback, config map[string]interface{}) error {
d := newDriver()
if err := d.configure(config); err != nil {
return err
}
c := driverapi.Capability{
DataScope: datastore.LocalScope,
ConnectivityScope: datastore.LocalScope,
}
return dc.RegisterDriver(networkType, d, c)
}
func (d *driver) configure(option map[string]interface{}) error {
var (
config *configuration
err error
natChain *iptables.ChainInfo
filterChain *iptables.ChainInfo
isolationChain *iptables.ChainInfo
)
...
if config.EnableIPTables {
if _, err := os.Stat("/proc/sys/net/bridge"); err != nil {
if out, err := exec.Command("modprobe", "-va", "bridge", "br_netfilter").CombinedOutput(); err != nil {
logrus.Warnf("Running modprobe bridge br_netfilter failed with message: %s, error: %v", out, err)
}
}
// 清理之前docker相关的iptables规则,在前面章节的日志可以看到有删除iptables规则
removeIPChains()
natChain, filterChain, isolationChain, err = setupIPChains(config)
if err != nil {
return err
}
// Make sure on firewall reload, first thing being re-played is chains creation
iptables.OnReloaded(func() { logrus.Debugf("Recreating iptables chains on firewall reload"); setupIPChains(config) })
}
// 这里是处理FORWARD规则的地方,会去读取/proc/sys/net/ipv4/ip_forward中的值进行处理
if config.EnableIPForwarding {
err = setupIPForwarding(config.EnableIPTables)
if err != nil {
logrus.Warn(err)
return err
}
}
...
return nil
}
数据存储在本地db文件中的内容
docker默认使用boltdb来存储网络相关的配置信息,那么我们看下在这个db文件中到底保存的数据格式是什么,对于我们以后看docker的一些源码实现或者自身组件的实现可能会有帮助。
我们通过代码,可以读取存储在本机的docker网络配置,我们读取了Key为docker/network/v1.0/bridge
的值,该key为docker的默认bridge的信息,信息格式如下:
{
"BridgeIfaceCreator": 2,
"Internal": false,
"DefaultBridge": true,
"EnableICC": true,
"EnableIPv6": false,
"Mtu": 1500,
"DefaultGatewayIPv6": "<nil>",
"EnableIPMasquerade": true,
"DefaultGatewayIPv4": "<nil>",
"AddressIPv4": "172.17.0.1/16",
"ContainerIfacePrefix": "",
"ID": "6ce3c0c4d5c392d732a06a7ea9d6293bf04201056d563f7a6ad5ef9d8b3822db",
"BridgeName": "docker0",
"DefaultBindingIP": "0.0.0.0"
}
可以看到EnableIPMasquerade
我们设置的true
,从而使得前文所说的-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
规则被创建。
// 读取boltdb的代码示例
package main
import (
"fmt"
"log"
"time"
"github.com/docker/libkv"
"github.com/docker/libkv/store"
"github.com/docker/libkv/store/boltdb"
)
func init() {
// Register boltdb store to libkv
boltdb.Register()
}
func main() {
client := "./local-kv.db"
// Initialize a new store
kv, err := libkv.NewStore(
store.BOLTDB,
[]string{client},
&store.Config{
Bucket: "libnetwork",
ConnectionTimeout: 10 * time.Second,
},
)
if err != nil {
log.Fatalf("Cannot create store: %v", err)
}
pair, err := kv.List("docker/network/v1.0/bridge")
for _, p := range pair {
fmt.Println(p.Key)
fmt.Println(string(p.Value))
}
}
总结
实现流程如下:
dockerd
首先根据配置文件和命令行参数获取--iptables
和--ip-masq
的值- 进行参数校验,校验后决定
ip-masq
是否启用 - 在
dockerd
初始化bridge网络的时候,先清理旧的iptables规则,然后依次添加新的iptables规则 - 如果启用
ip-masq
,那么创建POSTROUTING
的规则。