• 深入解析docker内核网桥


    今天做虚拟桌面,朋友问我,为什么vnc 连接另一个docker 容器一直超时,原因是在docker 启动的时候没有组网,那么接下来我就要解析下docker的内核网络。

    我们思考几个问题,带你了解linux 中docker 网络实现的基本原理。

    文章分析的基石来源于 Docker 的moby源码:

    https://github.com/moby/moby

    一、docker 网络组成

    docker 默认有主机模式和网桥模式,这里只说网桥模式,我们说一下docker内核的网桥代码

    先简单说一下容器,容器底层调用的是linux 的 clone,我们使用clone的时候有一些独特的标志位

    CLONE_NEWNES,

    CLONE_NEWUTS,

    CLONE_NEWIPC,

    CLONE_NEWPID,

    CLONE_NEWNET 

    使用linux内核的这些标志位,我们都可以创建出不同NS的进程,其中我们需要注意的是CLONE_NEWNET 

    二、Docker 桥接模式分析

    Docker Bridge 实现方式如下:

    1)我们实现要考虑不同的两个进程 netns 是不一样的,那么这两个不同的netns之间如何通信?

    我们可以使用linux 的 veth 接口 ,申请两个veth 设备假设是veth0 和 veth1。而veth pair 设备确保无论哪一个收到报文都会发送给另一端

    2)我们把veth0 附加到 docker daemon 创建的docker0 网桥上。保证宿主机有能力把报文发送到veth0

    3) 然后我们把 veth1 添加到容器所属的命名空间下,veth1 在Docker 容器里看来就是eth0。一方面保证网络报文发往veth0,可以被veth1收到,实现宿主机到docker 容器之间连通性。

    1)问题反思?

    docker 网桥是如何实现的?

    docker 又是怎么通过建立linux 设备veth的?

    docker 创建这些基础网络组件的过程中,到底是用了linux 内核的哪些特性?

    2)docker 网桥是如何实现的?

    go 部分

    我们翻阅Docker 源码,发现这么一段

    位于源码的

     daemon/daemon_unix.go

    1. // --ip processing
    2. if cfg.DefaultIP != nil {
    3. netOption[bridge.DefaultBindingIP] = cfg.DefaultIP.String()
    4. }
    5. _, err = controller.NewNetwork("bridge", "bridge", "",
    6. libnetwork.NetworkOptionEnableIPv6(cfg.EnableIPv6),
    7. libnetwork.NetworkOptionDriverOpts(netOption),
    8. libnetwork.NetworkOptionIpam("default", "", v4Conf, v6Conf, nil),
    9. libnetwork.NetworkOptionDeferIPv6Alloc(deferIPv6Alloc))

    libnetwork/controller.go 

    1. func (c *Controller) addNetwork(n *Network) error {
    2. d, err := n.driver(true)
    3. if err != nil {
    4. return err
    5. }
    6. // Create the network
    7. if err := d.CreateNetwork(n.id, n.generic, n, n.getIPData(4), n.getIPData(6)); err != nil {
    8. return err
    9. }
    10. n.startResolver()
    11. return nil
    12. }

    最后我们发现网桥的实现最后到了CreateNetwork中,我们在追踪进入CreateNetwork中

    libnetwork/drivers/bridge/bridge_linux.go 

    CreateNetwork 的实现我们发现具体关键代码位于

    1. // If the bridge interface doesn't exist, we need to start the setup steps
    2. // by creating a new device and assigning it an IPv4 address.
    3. bridgeAlreadyExists := bridgeIface.exists()
    4. if !bridgeAlreadyExists {
    5. bridgeSetup.queueStep(setupDevice)
    6. bridgeSetup.queueStep(setupDefaultSysctl)
    7. }

    再继续看 setupDevice,一切水落石处,Docker 网桥创建的关键在于 setupDevice

    1. func setupDevice(config *networkConfiguration, i *bridgeInterface) error {
    2. // We only attempt to create the bridge when the requested device name is
    3. // the default one. The default bridge name can be overridden with the
    4. // DOCKER_TEST_CREATE_DEFAULT_BRIDGE env var. It should be used only for
    5. // test purpose.
    6. var defaultBridgeName string
    7. if defaultBridgeName = os.Getenv("DOCKER_TEST_CREATE_DEFAULT_BRIDGE"); defaultBridgeName == "" {
    8. defaultBridgeName = DefaultBridgeName
    9. }
    10. if config.BridgeName != defaultBridgeName && config.DefaultBridge {
    11. return NonDefaultBridgeExistError(config.BridgeName)
    12. }
    13. // Set the bridgeInterface netlink.Bridge.
    14. i.Link = &netlink.Bridge{
    15. LinkAttrs: netlink.LinkAttrs{
    16. Name: config.BridgeName,
    17. },
    18. }
    19. // Set the bridge's MAC address. Requires kernel version 3.3 or up.
    20. hwAddr := netutils.GenerateRandomMAC()
    21. i.Link.Attrs().HardwareAddr = hwAddr
    22. log.G(context.TODO()).Debugf("Setting bridge mac address to %s", hwAddr)
    23. if err := i.nlh.LinkAdd(i.Link); err != nil {
    24. log.G(context.TODO()).WithError(err).Errorf("Failed to create bridge %s via netlink", config.BridgeName)
    25. return err
    26. }
    27. return nil
    28. }
    linux 接口部分:

    既然我们找到了库的实现,那么我们继续思考netlink库为什么能调在linux 系统里创建网桥,它到底是使用了kernel 开发的哪些能力?

    分析go netlink 库我发现了一个函数叫linkModify,他的一段代码实现如下:

    1. _, err := req.Execute(unix.NETLINK_ROUTE, 0)
    2. if err != nil {
    3. return err
    4. }

    底层使用的是 

    1. func getNetlinkSocket(protocol int) (*NetlinkSocket, error) {
    2. fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, protocol)
    3. if err != nil {
    4. return nil, err
    5. }
    6. s := &NetlinkSocket{
    7. fd: int32(fd),
    8. }
    9. s.lsa.Family = unix.AF_NETLINK
    10. if err := unix.Bind(fd, &s.lsa); err != nil {
    11. unix.Close(fd)
    12. return nil, err
    13. }
    14. return s, nil
    15. }

    也就是说Docker 的网络库最终使用的是

    netlink socket API:
     socket()函数
            socket域(地址族)是AF_NETLINK
            socket类型是SOCK_RAW或SOCK_DGRAM,因为netlink是一种面向数据的服务
            netlink协议类型定义在netlink.h(以下以NETLINK_ROUTE为例),也可以自定义
    AF_NETLINK 无疑是linux 中内核态和用户态通信的重要手段,他和ioctl的最大区别是,AF_NETLINK是一种协议族,提供了一种标准化的机制,用于用户态程序与内核之间进行网络相关的通信。它定义了不同的协议类型,如NETLINK_ROUTENETLINK_SELINUX等,以支持特定的网络操作和功能。使用AF_NETLINK,用户态程序可以发送特定类型的消息给内核,以请求或传递网络相关的信息。

    docker 是使用AF_NETLINK 做的,而我们使用brctl 却是使用的ioctl

    docker veth 设备是如何实现的

    1. case *Veth:
    2. data := linkInfo.AddRtAttr(nl.IFLA_INFO_DATA, nil)
    3. peer := data.AddRtAttr(nl.VETH_INFO_PEER, nil)
    4. nl.NewIfInfomsgChild(peer, unix.AF_UNSPEC)
    5. peer.AddRtAttr(unix.IFLA_IFNAME, nl.ZeroTerminated(link.PeerName))
    6. if base.TxQLen >= 0 {
    7. peer.AddRtAttr(unix.IFLA_TXQLEN, nl.Uint32Attr(uint32(base.TxQLen)))
    8. }
    9. if base.NumTxQueues > 0 {
    10. peer.AddRtAttr(unix.IFLA_NUM_TX_QUEUES, nl.Uint32Attr(uint32(base.NumTxQueues)))
    11. }
    12. if base.NumRxQueues > 0 {
    13. peer.AddRtAttr(unix.IFLA_NUM_RX_QUEUES, nl.Uint32Attr(uint32(base.NumRxQueues)))
    14. }
    15. if base.MTU > 0 {
    16. peer.AddRtAttr(unix.IFLA_MTU, nl.Uint32Attr(uint32(base.MTU)))
    17. }
    18. if link.PeerHardwareAddr != nil {
    19. peer.AddRtAttr(unix.IFLA_ADDRESS, []byte(link.PeerHardwareAddr))
    20. }
    21. if link.PeerNamespace != nil {
    22. switch ns := link.PeerNamespace.(type) {
    23. case NsPid:
    24. val := nl.Uint32Attr(uint32(ns))
    25. peer.AddRtAttr(unix.IFLA_NET_NS_PID, val)
    26. case NsFd:
    27. val := nl.Uint32Attr(uint32(ns))
    28. peer.AddRtAttr(unix.IFLA_NET_NS_FD, val)
    29. }
    30. }

    我们发现还是AF_NETLINK 实现的

    三、快速组网组成一个测试网络

    创建网桥

    ip link add br0 type bridge

    启动网桥

    ip link set br0 up

    创建网络命名空间ns1 和ns2 

    1. ~# ip netns add ns1
    2. ~# ip netns add ns2

     创建 veth 对

    1. ~# ip link add veth0 type veth peer br-veth0
    2. ~# ip link add veth1 type veth peer br-veth1

    查看创建的 veth 对,通过查看此时 veth对 在 root名称空间下

    ip address show

    将创建的 veth对 的一端插入到指定的名称空间 

    1. ~# ip link set veth0 netns ns1
    2. ~# ip link set veth1 netns ns2

    通过进入不同的名称空间查看网卡的一端

    1. ~# ip netns exec ns1 ip a
    2. ~# ip netns exec ns2 ip a

    将创建的 veth对 的另一端插入到 br0 的桥接网卡

    1. ~# ip link set br-veth0 master br0
    2. ~# ip link set br-veth1 master br0

     启动网卡 veth0 veth1 br-veth0 br-veth1,并配置 ip 地址

    1. // 开启网桥部分
    2. ~# ip link set br-veth0 up
    3. ~# ip link set br-veth1 up
    4. // 开启ns里的veth
    5. ~# ip netns exec ns1 ip link set veth0 up
    6. ~# ip netns exec ns2 ip link set veth1 up
    7. // 设置容器内的veth 设备ip
    8. ~# ip netns exec ns1 ifconfig veth0 192.168.100.10/24
    9. ~# ip netns exec ns2 ifconfig veth1 192.168.100.20/24

    四、总结

    Docker 创建网桥和Veth设备都是通过AF_NETLINK 套接字实现的,我需要去读 <精通linux内核网络>这本书去调研一下

  • 相关阅读:
    前后端分类 (增加,查询)
    KT142C-sop16语音芯片ic的串口指令详细说明_默认9600指令可设
    synchronized下的 i+=2 和 i++ i++执行结果居然不一样
    Google ProtoBuf介绍
    企业级智能PDF及文档处理SDK GdPicture.NET 14.2 Crack
    新型双功能螯合剂NOTA及其衍生物CAS号:147597-66-8p-SCN-Bn-NOTA
    图计算:基于SparkGrpahX计算聚类系数
    Win10_触摸板使用_系统自带使用教程
    Redis多容器高并发场景 , 设置缓存的时候,要考虑多容器加锁的场景。(incr计数和redis分布式锁区别)
    数据结构系列——栈 stack
  • 原文地址:https://blog.csdn.net/qq_32783703/article/details/133652250