500代码行代码手写docker-设置网络命名空间

这篇具有很好参考价值的文章主要介绍了500代码行代码手写docker-设置网络命名空间。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

(4)500代码行代码手写docker-设置网络命名空间

本系列教程主要是为了弄清楚容器化的原理,纸上得来终觉浅,绝知此事要躬行,理论始终不及动手实践来的深刻,所以这个系列会用go语言实现一个类似docker的容器化功能,最终能够容器化的运行一个进程。

本章的源码已经上传到github,地址如下:

https://github.com/HobbyBear/tinydocker/tree/chapter4

前文我们已经为容器替换了新的根文件系统,但是由于我们启动容器的时候是在一个新的网络命名空间,目前的容器还不能访问外部网络,我们需要在这一节,让容器能够访问外部网络,并且能够实现同一个主机上的容器能够网络互通。

这节代码运行效果

500代码行代码手写docker-设置网络命名空间

容器互通的原理

在正式开始编码之前,我将基于最简单的情况,则同一个主机上的容器能够通过ip互相访问的情况,简单的介绍下,容器网络互联的原理,我们是在一个新的网络命名空间 启动的子进程,不同网络命名空间拥有自己的防火墙,路由表,网络设备,所以需要对新生成的网络命名空间进行配置。让网络命名空间内部的网络包能够从网络命名空间内部出去到达主机上。

在linux上,可以用veth虚拟网络设备去连接两个不同网络命名空间,veth设备是成队出现,分别连接到不同的命名空间中, 从veth设备一端进入的网络包能够到达veth设备的另一端, 但在配置容器网络时并不是将veth设备直接连接在另一端的网络命名空间内,因为如果主机上容器过多的话,采用直接两两相连的方式,将会让网络拓扑过于复杂。所以一般是将veth设备连接到一个叫做网桥bridge的虚拟网络设备上,通过它对网络包进行转发。

关于veth设备和bridge的原理和使用,我之前出过一期视频讲解,可以去哪里深入的学习下:

容器网络原理之包流转路径

之前也出过许多对容器网络讲解的系列视频,如果有对容器网络不熟悉的同学,请看这里:

k8s容器互联 flannel host-gw 原理

k8s容器互联 flannel vxlan 模式原理

容器系列 笔记分享

实现ipam(ip地址分配管理)

现在,来让我们实现下关于容器网络配置的逻辑,首先容器在创建的时候,得先为它分配一个ip地址,本质上就是为它内部的veth设备分配一个ip地址。这就涉及到如何分配ip地址的问题,这里有两个问题需要解决:
1,当知道一个网络的网段后,如何知道网段内部哪个ip进行了分配,哪个ip没有进行分配。
2,如果知道了这个网段内某个ip没有被分配,如何根据偏移量计算最终没有被分配的ip,比如我知道第8个ip没有被分配,网络网段为192.168.0.0/24 ,那么第8个ip是多少?

首先来看下ip存储的问题,也就是看哪些ip进行了分配,哪些没有进行分配。

bitmap 存储ip地址分配记录

这是一个看某个值是否存在的问题,可以通过bitmap去存储,这在快速判断ip是否存在的前提下,也能极大的降低存储成本。

如下所示,如果第一个字节的第1位和第3位被置为1了,说明在这个网段内,第一个ip和第3个ip都被占用了。
500代码行代码手写docker-设置网络命名空间

一个byte是8个bit,也就可以表示8个ip是否被占用,而一个网段中的ip个数=2的N次方个,其中N=32-网段位数。

用一个实际的例子举例,比如子网掩码是255.255.0.0,说明网段是前面的16位,那么ip个数就是由后16位bit数表示,排除掉其中主机号全为0的网络号和主机号全为1的广播号,可用ip数=2的16次方-2 ,要表示那么多的ip数就需要 (2的16次方-2)/8 大小的字节 约等于8kb,转换成字节数组长度就是8192。

具体实现如下:

一个bitmap用一个byte数组表示

type bitMap struct {
	Bitmap []byte
}

bitmap的方法也就3个,

1, 查看第n个ip是不是被分配。

func (b *bitMap) BitExist(pos int) bool {
	aIndex := arrIndex(pos)
	bIndex := bytePos(pos)
	return 1 == 1&(b.Bitmap[aIndex]>>bIndex)
}

arrIndex和bytePos 方法实现如下:

func arrIndex(pos int) int {
	return pos / 8
}

func bytePos(pos int) int {
	return pos % 8
}

我们最终是要找到这地n个ip所在的bit位,然后查找该bit位是否被置为1,置为1就代表这第n个ip是被分配了。用n/8得到的就是第n个ip所在bit位的字节数组的索引,用n%8得到的余数就是在字节里的第几个bit位,如何取出对应的bit位呢?

首先是b.Bitmap[aIndex]得到对应的字节,然后将该字节右移对应的bit位数,这样第n个ip的bit位就变到了第一个bit位上。整个过程像下面这样:

500代码行代码手写docker-设置网络命名空间
与运算是双1结果才是1,所以如果最后一个bit位是1则最后与运算的结果就是数字1,如果最后一位bit位是0,则最后运算的结果就是0。

2,设置第n个ip被分配。

设置第n个ip被分配,即设置它对应的bit位为1,首先还是要找到这第n个ip在数组中的位置,然后取出对应字节byte,通过位运算设置其对应的bit位。

func (b *bitMap) BitSet(pos int) {
	aIndex := arrIndex(pos)
	bIndex := bytePos(pos)
	b.Bitmap[aIndex] = b.Bitmap[aIndex] | (1 << bIndex)
}

零bit的或位运算不会改变原bit位值大小,而1的bit的或位运算会将原来bit位置为1,利用这个特性便可以很容易的写出来上面的代码。

整个过程如图所示:
500代码行代码手写docker-设置网络命名空间

3,释放第n个ip的分配记录。

释放第n个ip原理和前面类似,设置第n个ip对应的bit位为0。

func (b *bitMap) BitClean(pos int) {
	aIndex := arrIndex(pos)
	bIndex := bytePos(pos)
	b.Bitmap[aIndex] = b.Bitmap[aIndex] & (^(1 << bIndex))
}

零bit的与位运算会让原bit位置为0,而1的与位运算不会改变原bit位的值,知道了这个特性再看上述代码应该就很容易了,其中^ 运算符为取反的意思,这样00000100 就会变为 11111011,这样与原bit位进行与位运算就能将索引为2的bit位置为0了。

通过ip偏移计算ip地址

通过上述bitmap的实现可以解决ip分配的存储问题,但还有一个问题要解决,那就是目前只知道了第几个ip没有分配,如何通过这个ip偏移量获取到具体的ip地址?现在来解决这个问题。

一个网段里第一个ip的主机号全为0,被称为网络号,其ip偏移为0,拿192.168.0.0/16网段举例,第一个ip就是192.168.0.0,第二个ip地址其ip偏移量为1,ip地址是192.168.0.1,以此类推,可以得到下面的公式:

ip地址=ip网络号+ip地址偏移

所以关键就是要得到一个ip的ip网络号,用ipv4举例,在golang里面ip类型本质上就是一个长度为4字节数组

type IP []byte

所以现在要把这个4字节的数组转换为32位整形,可以像下面这样转换

func ipToUint32(ip net.IP) uint32 {
	if ip == nil {
		return 0
	}
	ip = ip.To4()
	if ip == nil {
		return 0
	}
	return binary.BigEndian.Uint32(ip)
}

func (bigEndian) Uint32(b []byte) uint32 {
	_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
	return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
}

由于ip地址是大端排序,网段号排在字节数组前面,所以binary.BigEndian进行转换。

这样获取ip的逻辑就是一个简单的加法了

firstIP := ipToUint32(ip.Mask(cidr.Mask))
ip = uint32ToIP(firstIP + uint32(pos))

通过ip地址计算ip偏移

关于ip的分配还有最后一个比较关键的点,那就是释放ip,前面已经提到我们已经可以办到释放第n个ip了,其中n就是ip的偏移量,那么如何通过ip地址去计算ip的偏移量呢?
其实很容易,拿当前ip减去网络号就是ip偏移量了

ip偏移量=当前ip-网络号ip

这里的具体代码我就不再展示了。

创建网络设备实现容器互联

知道如何为容器分配ip地址了,还需要在网络命名空间内 创建新的网络设备,然后设置上这个ip。为了让整个逻辑变的简单,我们创建一个默认的网络,让容器创建的时候自动在这个默认的网络下,并为其分配ip。

整个过程分为两个阶段,一个是程序启动的时候,会去检查主机上是否存在这个默认网络需要的配置,如果有则不再创建相关网络设备,我将这个阶段称为网络初始化阶段,第二个阶段是容器创建时候,需要为容器创建相关网络配置的阶段。我们挨个来看看。

🐮🐮🐮 当你写这部分代码时,最好用我这章源码上的根文件系统,因为原始的根文件系统其实是不包含ifconfig,ping命令的,到时候不好调试路由以及测试网络联通信,源码上的根文件系统我把这两个命令都下载好了。 ❗️❗️不过,你也可以用nsenter 命令进入容器的网络命名空间然后使用主机上的ping ifconfig 命令进行网络调试,类似这样nsenter -t 容器进程号 -n ping www.baidu.com

网络初始化阶段

前面提到同一台主机上实现容器之间的网络互联,其本质是实现不同网络命名空间之间的互联,实现方式则是不同网络命名空间都用veth设备连接到一个网桥虚拟网络设备上,通过它们则可以实现网络互联。

所以在初始化阶段,首先就看看主机上的网桥设备是不是创建好了,并且设置一个网段作为默认网络将会使用的网段。

代码如下:

func Init() error {
	// 对默认网络进行初试化
	err := NetMgr.LoadConf()
	if err != nil {
		return fmt.Errorf("load netMgr conf err=%s", err)
	}
	if NetMgr.Storage[defaultNetName] == nil {
		if err := BridgeDriver.CreateNetwork(defaultNetName, defaultSubnet, BridgeNetworkType); err != nil {
			return fmt.Errorf("err=%s", err)
		}
		if err := IpAmfs.SetIpUsed(defaultSubnet); err != nil {
			return err
		}
	}
	return nil
}

为了实现主机上对默认网络的存储,我将默认网络的信息通过json序列化方式存储到了主机文件上。NetMgr.LoadConf则是对主机上存储的默认网络信息进行加载。加载之后判断是否存在默认网络,不存在则去创建一个默认网络,而创建一个默认网络的步骤本质上就是创建网桥设备,并设置网桥ip为defaultSubnet,然后将defaultSubnet的ip标记为使用。以便后续为容器分配ip地址时,不会占用网桥的ip地址。

创建容器时的网络配置逻辑

接着着重来看下创建容器时如何进行相关的网络配置,我们需要在主机上创建一个veth设备,然后将这个veth设备一端连接到主机的网桥上,然后将另一端连接到容器的网络命名空间内部。

需要注意的是,在主机为容器配置好网络连接前,容器的子进程还必须进行等待,当主机为容器配置好网络设备后,子进程才真正的开始执行用户将要执行的程序。这里父子进程间的通信,我采用发送信号的方式,子进程等待父进程发送SIGUSR2信号,其代码如下

func main(){
.....

log.Info("Wait  SIGUSR2 signal arrived ....")
		// 等待父进程网络命名空间设置完毕
		network.WaitParentSetNewNet()
		........
		err := syscall.Exec(cmd, os.Args[3:], os.Environ())
		if err != nil {
			log.Error("exec proc fail %s", err)
			return
		}
....

}		
func WaitParentSetNewNet() {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGUSR2)
	<-sigs
	log.Info("Received SIGUSR2 signal, prepare run container")
}		

而父进程在用cmd.start命名启动子进程后则开始为容器子进程配置网络设备,代码如下

func ConfigDefaultNetworkInNewNet(pid int) error {
	// 为容器分配ip
	ip, err := IpAmfs.AllocIp(defaultSubnet)
	if err != nil {
		return fmt.Errorf("ipam alloc ip fail %s", err)
	}

	// 主机上创建 veth 设备,并连接到网桥上
	vethLink, networkConf, err := BridgeDriver.CrateVeth(defaultNetName)
	if err != nil {
		return fmt.Errorf("create veth fail err=%s", err)
	}
	// 主机上设置子进程网络命名空间 配置
	if err := BridgeDriver.setContainerIp(vethLink.PeerName, pid, ip, networkConf.BridgeIp); err != nil {
		return fmt.Errorf("setContainerIp fail err=%s peername=%s pid=%d ip=%v conf=%+v", err, vethLink.PeerName, pid, ip, networkConf)
	}
	// 通知子进程设置完毕
	return noticeSunProcessNetConfigFin(pid)
}

着重看下BridgeDriver.setContainerIp方法如何为对容器的网络命名空间进程配置。

func (b *bridgeDriver) setContainerIp(peerName string, pid int, containerIp net.IP, gateway *net.IPNet) error {
	peerLink, err := netlink.LinkByName(peerName)
	if err != nil {
		return fmt.Errorf("fail config endpoint: %v", err)
	}
	loLink, err := netlink.LinkByName("lo")
	if err != nil {
		return fmt.Errorf("fail config endpoint: %v", err)
	}
	// 进入容器的网络命名空间
	defer enterContainerNetns(&peerLink, pid)()
	containerVethInterfaceIP := *gateway
	containerVethInterfaceIP.IP = containerIp
	// 为容器内部的veth设备设置ip
	if err = setInterfaceIP(peerName, containerVethInterfaceIP.String()); err != nil {
		return fmt.Errorf("%v,%s", containerIp, err)
	}
	// 将veth设备设置为up状态
	if err := netlink.LinkSetUp(peerLink); err != nil {
		return fmt.Errorf("netlink.LinkSetUp fail  name=%s err=%s", peerName, err)
	}
	if err := netlink.LinkSetUp(loLink); err != nil {
		return fmt.Errorf("netlink.LinkSetUp fail  name=%s err=%s", peerName, err)
	}
	_, cidr, _ := net.ParseCIDR("0.0.0.0/0")
	defaultRoute := &netlink.Route{
		LinkIndex: peerLink.Attrs().Index,
		Gw:        gateway.IP,
		Dst:       cidr,
	}
	// 在容器的网络命名空间内配置路由信息
	if err = netlink.RouteAdd(defaultRoute); err != nil {
		return fmt.Errorf("router add fail %s", err)
	}
	return nil
}

整个过程其实比较清晰,通过veth设备名找到veth设备后,就进入了容器的网络命名空间,然后设备veth设备ip,设置路由表信息。关键在主机上要如何才能进入容器的网络命名空间呢?

答案是采用setns系统调用,setns系统调用可以让当前线程进程进入指定的命名空间内部,这段逻辑是在上述enterContainerNetns方法中,解释如下:文章来源地址https://www.toymoban.com/news/detail-458791.html

func enterContainerNetns(vethLink *netlink.Link, pid int) func() {
//  根据子进程pid查询网络命名空间的fd文件描述符
	f, err := os.OpenFile(fmt.Sprintf("/proc/%d/ns/net", pid), os.O_RDONLY, 0)
	if err != nil {
		fmt.Println(fmt.Errorf("error get container net namespace, %v", err))
	}

	nsFD := f.Fd()
	// 锁住当前线程,因为setns是让当前线程进指定命名空间,go的协程在运行时可以被挂载到不同的线程上去进行执行,为了规避这种情况,将当前协程与当前线程进行了绑定。
	runtime.LockOSThread()

	// 修改veth peer 另外一端移到容器的namespace中
	if err = netlink.LinkSetNsFd(*vethLink, int(nsFD)); err != nil {
		log.Error("error set link netns , %v", err)
	}

	// 获取当前的网络namespace
	origns, err := netns.Get()
	if err != nil {
		log.Error("error get current netns, %v", err)
	}

	// 设置当前线程到新的网络namespace,并在函数执行完成之后再恢复到之前的namespace
	if err = netns.Set(netns.NsHandle(nsFD)); err != nil {
		log.Error("error set netns, %v", err)
	}
	return func() {
		netns.Set(origns)
		origns.Close()
		runtime.UnlockOSThread()
		f.Close()
	}
}

到了这里,关于500代码行代码手写docker-设置网络命名空间的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Docker】Linux网络命名空间

    Namespace是Linux提供的一种对于系统全局资源的隔离机制;从进程的视角来看,同一个namespace中的进程看到的是该namespace自己独立的一份全局资源,这些资源的变化只在本namespace中可见,对其他namespace没有影响。容器就是采用namespace机制实现了对网络,进程空间等的隔离。不同

    2024年02月06日
    浏览(34)
  • supervisorctl(-jar)启动配置设置NACOS不同命名空间

    由于需要在上海服务器上面配置B测试环境,原本上面已有A测试环境,固需要将两套权限系统分开 可以使用不同的命名空间来隔离启动服务 注:本文章均不涉及公司机密 命名空间默认会有一个public,并且不能删除,此时我们新增一个名为hzces的命名空间 此处选择hzces的命名空

    2024年02月11日
    浏览(39)
  • 【Docker】Linux网桥连接多个命名空间

    veth实现了点对点的虚拟连接,可以通过veth连接两个namespace,如果我们需要将3个或者多个namespace接入同一个二层网络时,就不能只使用veth了。 在物理网络中,如果需要连接多个主机,我们会使用bridge(网桥),或者又称为交换机。Linux也提供了网桥的虚拟实现。下面我们试验

    2024年02月05日
    浏览(55)
  • 虹科教程 | Linux网络命名空间与虹科PROFINET协议栈的GOAL中间件结合使用

    PROFINET是由PI推出的开放式工业以太网标准,它使用TCP/IP等IT标准,并由IEC 61158和IEC 61784 标准化,具有实时功能,并能够无缝集成到现场总线系统中。凭借其技术的开放性、灵活性和性能优势,PROFINET可应用于过程/工厂自动化、运动控制等领域。通过PROFINET,可实现确定性响应

    2024年02月13日
    浏览(49)
  • 思科模拟器:acl网络访问控制 命名设置(extended)

    环境: 1. 全局互通 2.阻止1.1访问2.1ftp服务 3.阻止3.1访问2.2www 网站访问 进入r2全局模式 ip access-list extended asd ip access-list extended + 名字( 这个名字随便取,相当于一个变量,往里写入变量值,等会把这个变量使用在接口上 ) deny tcp host 192.168.1.1 host 192.168.2.1 eq ftp 拒绝1.1访问2

    2024年02月11日
    浏览(37)
  • 【C++】命名空间 namespace 与 标准流 iostream ( 命名空间概念简介 | 命名空间定义 | 命名空间使用 | iostream 中的命名空间分析 )

    命名空间 namespace 又称为 名字空间 , 名称空间 , 名域 , 作用域 , 是 C++ 语言 对 C 语言 的扩展 之一 ; C++ 中的 命名空间 namespace 指的是 标识符 的 可见范围 , C++ 标准库中的 所有 标识符 , 都定义在 std 命名空间中 ; 命名空间 英文名称是 \\\" namespace \\\" , name 是 名字 , 名称 的意思 ,

    2024年02月12日
    浏览(44)
  • (神经网络)MNIST手写体数字识别MATLAB完整代码

            在此次实验中,笔者针对 MNIST 数据集,利用卷积神经网络进行训练与测试,提 出了一系列的改进方法,并对这些改进的方法进行了逐一验证,比较了改进方法与浅层 神经网络的优劣。         首先,笔者对实验中所用的 MNIST 数据集进行了简单的介绍;接着,

    2024年02月03日
    浏览(46)
  • 卷积神经网络CNN原理+代码(pytorch实现MNIST集手写数字分类任务)

    前言 若将图像数据输入全连接层,可能会导致丧失一些位置信息 卷积神经网络将图像按照原有的空间结构保存,不会丧失位置信息。 卷积运算: 1.以单通道为例: 将将input中选中的部分与kernel进行数乘 : 以上图为例对应元素相乘结果为211,并将结果填入output矩阵的左上角

    2024年02月04日
    浏览(63)
  • 入门深度学习——基于全连接神经网络的手写数字识别案例(python代码实现)

    1.1 问题导入 如图所示,数字五的图片作为输入,layer01层为输入层,layer02层为隐藏层,找出每列最大值对应索引为输出层。根据下图给出的网络结构搭建本案例用到的全连接神经网络 1.2 手写字数据集MINST 如图所示,MNIST数据集是机器学习领域中非常经典的一个数据集,由6

    2024年02月03日
    浏览(46)
  • vs2022 命名空间“System”中不存在类型或命名空间名“Printing”

    在监控打印机状态的时候,需要用到System.Printing。 System.Printing 命名空间 | Microsoft Learn 在微软官网有这个命名空间,其中提到 不支持命名空间中的System.Printing类在Windows服务或 ASP.NET 应用程序或服务中使用。 尝试从其中一种应用程序类型内使用这些类可能会导致意外问题,例

    2024年02月05日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包