深入解析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

	// --ip processingif cfg.DefaultIP != nil {netOption[bridge.DefaultBindingIP] = cfg.DefaultIP.String()}_, err = controller.NewNetwork("bridge", "bridge", "",libnetwork.NetworkOptionEnableIPv6(cfg.EnableIPv6),libnetwork.NetworkOptionDriverOpts(netOption),libnetwork.NetworkOptionIpam("default", "", v4Conf, v6Conf, nil),libnetwork.NetworkOptionDeferIPv6Alloc(deferIPv6Alloc))

libnetwork/controller.go 

func (c *Controller) addNetwork(n *Network) error {d, err := n.driver(true)if err != nil {return err}// Create the networkif err := d.CreateNetwork(n.id, n.generic, n, n.getIPData(4), n.getIPData(6)); err != nil {return err}n.startResolver()return nil
}

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

libnetwork/drivers/bridge/bridge_linux.go 

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

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

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

func setupDevice(config *networkConfiguration, i *bridgeInterface) error {// We only attempt to create the bridge when the requested device name is// the default one. The default bridge name can be overridden with the// DOCKER_TEST_CREATE_DEFAULT_BRIDGE env var. It should be used only for// test purpose.var defaultBridgeName stringif defaultBridgeName = os.Getenv("DOCKER_TEST_CREATE_DEFAULT_BRIDGE"); defaultBridgeName == "" {defaultBridgeName = DefaultBridgeName}if config.BridgeName != defaultBridgeName && config.DefaultBridge {return NonDefaultBridgeExistError(config.BridgeName)}// Set the bridgeInterface netlink.Bridge.i.Link = &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: config.BridgeName,},}// Set the bridge's MAC address. Requires kernel version 3.3 or up.hwAddr := netutils.GenerateRandomMAC()i.Link.Attrs().HardwareAddr = hwAddrlog.G(context.TODO()).Debugf("Setting bridge mac address to %s", hwAddr)if err := i.nlh.LinkAdd(i.Link); err != nil {log.G(context.TODO()).WithError(err).Errorf("Failed to create bridge %s via netlink", config.BridgeName)return err}return nil
}
linux 接口部分:

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

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

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

底层使用的是 

func getNetlinkSocket(protocol int) (*NetlinkSocket, error) {fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, protocol)if err != nil {return nil, err}s := &NetlinkSocket{fd: int32(fd),}s.lsa.Family = unix.AF_NETLINKif err := unix.Bind(fd, &s.lsa); err != nil {unix.Close(fd)return nil, err}return s, nil
}

也就是说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 设备是如何实现的

case *Veth:data := linkInfo.AddRtAttr(nl.IFLA_INFO_DATA, nil)peer := data.AddRtAttr(nl.VETH_INFO_PEER, nil)nl.NewIfInfomsgChild(peer, unix.AF_UNSPEC)peer.AddRtAttr(unix.IFLA_IFNAME, nl.ZeroTerminated(link.PeerName))if base.TxQLen >= 0 {peer.AddRtAttr(unix.IFLA_TXQLEN, nl.Uint32Attr(uint32(base.TxQLen)))}if base.NumTxQueues > 0 {peer.AddRtAttr(unix.IFLA_NUM_TX_QUEUES, nl.Uint32Attr(uint32(base.NumTxQueues)))}if base.NumRxQueues > 0 {peer.AddRtAttr(unix.IFLA_NUM_RX_QUEUES, nl.Uint32Attr(uint32(base.NumRxQueues)))}if base.MTU > 0 {peer.AddRtAttr(unix.IFLA_MTU, nl.Uint32Attr(uint32(base.MTU)))}if link.PeerHardwareAddr != nil {peer.AddRtAttr(unix.IFLA_ADDRESS, []byte(link.PeerHardwareAddr))}if link.PeerNamespace != nil {switch ns := link.PeerNamespace.(type) {case NsPid:val := nl.Uint32Attr(uint32(ns))peer.AddRtAttr(unix.IFLA_NET_NS_PID, val)case NsFd:val := nl.Uint32Attr(uint32(ns))peer.AddRtAttr(unix.IFLA_NET_NS_FD, val)}}

我们发现还是AF_NETLINK 实现的

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

创建网桥

ip link add br0 type bridge

启动网桥

ip link set br0 up

创建网络命名空间ns1 和ns2 

~# ip netns add ns1
~# ip netns add ns2

 创建 veth 对

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

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

ip address show

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

~# ip link set veth0 netns ns1
~# ip link set veth1 netns ns2

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

~# ip netns exec ns1 ip a
~# ip netns exec ns2 ip a

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

~# ip link set br-veth0 master br0
~# ip link set br-veth1 master br0

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

// 开启网桥部分
~# ip link set br-veth0 up
~# ip link set br-veth1 up// 开启ns里的veth
~# ip netns exec ns1 ip link set veth0 up
~# ip netns exec ns2 ip link set veth1 up// 设置容器内的veth 设备ip
~# ip netns exec ns1 ifconfig veth0 192.168.100.10/24
~# ip netns exec ns2 ifconfig veth1 192.168.100.20/24

四、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/114809.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Java基础面试四十六】、 List<? super T>和List<? extends T>有什么区别?

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 面试官&#xff1a;问题 参考答案&#x…

计算机算法分析与设计(15)---贪心算法(虚拟汽车加油问题和最优分解问题)

文章目录 一、虚拟汽车加油问题1.1 问题描述1.2 思路分析1.3 代码编写 二、最优分解问题2.1 问题描述2.2 思路分析2.3 代码编写 一、虚拟汽车加油问题 1.1 问题描述 一辆虚拟汽车加满油后可行驶 n n n km。旅途中有若干加油站。设计一个有效算法&#xff0c;指出应在哪些加油…

MyBatisPlus实现连表操作、批量处理

1、实现连表查询 正常来说单靠mybatisplus无法实现连表查询&#xff0c;只能靠单表sql然后进行拼接形成连表查询&#xff0c;或者使用xml文件去编写sql语句来实现连表查询。但他又给我们提供了一个插件MyBatis-Plus-Join&#xff0c;用来弥补mybatisplus再连表上的不足&#…

Apache Jmeter测压工具快速入门

Jmeter测压工具快速入门 一、Jmeter介绍二、Jmeter On Mac2.1 下载2.2 安装2.2.1 环境配置2.2.2 初始化设置 2.3 测试2.3.1 创建JDBC Connection Configuration2.3.2 创建线程组2.3.3 创建JDBC Request2.3.4 创建结果监控2.3.5 运行结果 2.4 问题记录2.4.1 VM option UseG1GC异…

【C语言】每日一题(旋转数组)

旋转数组&#xff0c;链接奉上 目录 方法:创建额外的数组&#xff1a;整体思路&#xff1a;代码实现&#xff1a; 数组反转&#xff1a;整体思路&#xff1a;代码实现&#xff1a;小插曲&#xff1a; 方法: 创建额外的数组&#xff1a; 整体思路&#xff1a; 创建一个额外的…

oracle实现搜索不区分大小写

<if test"code ! null and code ! ">and upper(code) like upper(%${code}%) </if>关键字upper

51单片机的时钟系统

1.简介 51内置的时钟系统可以用来计时&#xff0c;与主程序分割开来&#xff0c;在计时过程中不会终端主程序&#xff0c;还可以通过开启时钟中断来执行相应的操作。 2.单片机工作方式 单片机内部有两个十六位的定时器T0和T1。每个定时器有两种工作方式选择&#xff0c;分别…

Redis-Sentinel高可用架构学习

Redis-Sentinel高可用架构 Redis主从复制过程&#xff1a; 主从同步原理 Redis Sentinel&#xff08;哨兵&#xff09;高可用集群方案&#xff1a;Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案。 当用Redis做Master-slave的高可用方案时&#xff0c;假如master宕机了…

STM32F4_照相机

目录 前言 1. BMP编码 2. JPEG编码 前言 我们所要实现的照相机&#xff0c;支持BMP图片格式的照片和JPEG图片格式的照片。 1. BMP编码 BMP文件是由文件头、位图信息头、颜色信息和图形数据四部分构成。 1. BMP文件头&#xff08;14个字节&#xff09;&#xff1a;BMP文件…

numpy矩阵画框框

在n>5(n是奇数)的nn数组中&#xff0c;用*画外方框和内接菱形。 (本笔记适合熟悉numpy的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&#xff0c;不仅仅是基础那…

c++中的继承

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、继承的概念及定义1、继承的概念2、继承的定义2.1 定义格式2.2 继承关系和访问限定符2.3 继承基类成员访问方式的变化 二、基类和派生类对象赋值转换三、继承…

【27】c++设计模式——>迭代器模式(遍历双向链表)(2)

//实现双向链表 #pragma once #include<iostream> #include<string> #include<vector> using namespace std;class Iterator; class ForwardIterator; class ReverseIterator;//链表的最小组成部分是一个节点&#xff0c;先实现一个节点 struct Node //c中st…

在Espressif-IDE中使用Wokwi仿真ESP32

陈拓 2023/10/17-2023/10/19 1. 概述 在Espressif-IDE v2.9.0版本之后可直接在IDE中使用Wokwi模拟器。 1.1 什么是 Wokwi 模拟器&#xff1f; Wokwi 是一款在线电子模拟器&#xff0c;支持模拟各种开发板、元器件和传感器&#xff0c;例如乐鑫产品 ESP32。 Wokwi 提供基于浏…

html表格标签

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body><!--表格table 行 tr 列 td --> <table border"1px"><tr> <!--colsp…

逻辑漏洞详解

原理&#xff1a; 没有固定的概念&#xff0c;一般都是不符合常识的情况。比如任意用户注册&#xff0c;短信炸弹&#xff0c;占用资源&#xff0c;交易支付、密码修改、密码找回、越权修改、越权查询、突破限制。 根据实际业务逻辑进行比对&#xff0c;购物的可以根据数量&a…

gulp打包vue3+jsx+less插件

最终转换结果如下 在根目录下添加gulpfile.js文件&#xff0c;package.json添加命令npm run gulp var gulp require(gulp) var babel require(gulp-babel) var less require(gulp-less) var del require(del); var spawn require(child_process).spawn;const outDir &…

【FPGA零基础学习之旅#16】嵌入式块RAM-双口ram的使用

&#x1f389;欢迎来到FPGA专栏~双口ram的使用 ☆* o(≧▽≦)o *☆嗨~我是小夏与酒&#x1f379; ✨博客主页&#xff1a;小夏与酒的博客 &#x1f388;该系列文章专栏&#xff1a;FPGA学习之旅 文章作者技术和水平有限&#xff0c;如果文中出现错误&#xff0c;希望大家能指正…

X32位汇编和X64位区别无参函数分析(一)

前言 一、X32汇编函数无参无返回分析 二、X64汇编函数无参无返回分析 总结 前言 提示&#xff1a;以下是个人学习总结&#xff1a;如有错误请大神指出来&#xff0c;只供学习参考&#xff0c;本内容使用使用VS2017开发工具&#xff1a;语言是C&#xff0c;需要一些常见的汇编指…

手机知识:安卓内存都卷到24GB了,为何iPhone还在固守8GB

目录 一、系统机制 二、生态差异 三、总结 在刚刚过去的9月&#xff0c;年货iPhone 15系列正式发布&#xff0c;标准版不出意外还是挤药膏&#xff0c;除了镜头、屏幕有些升级&#xff0c;芯片用iPhone 14 Pro系列的&#xff0c;内存只有6GB&#xff1b;即使是集钛合金机身、…

【大数据】Kafka 实战教程(一)

Kafka 实战教程&#xff08;一&#xff09; 1.Kafka 介绍1.1. 主要功能1.2. 使用场景1.3 详细介绍1.3.1 消息传输流程1.3.2 Kafka 服务器消息存储策略1.3.3 与生产者的交互1.3.4 与消费者的交互 2.Kafka 生产者3.Kafka 消费者3.1 Kafka 消费模式3.1.1 At-most-once&#xff08;…