影视聚合站 科技 文章内容

手工模拟实现 Docker 容器网络

发布时间:2022-01-01 12:55:20来源:ImportNew

如今服务器虚拟化技术已经发展到了深水区。现在业界已经有很多公司都迁移到容器上了。我们的开发写出来的代码大概率是要运行在容器上的。因此深刻理解容器网络的工作原理非常的重要。只有这样将来遇到问题的时候才知道该如何下手处理。

网络虚拟化,其实用一句话来概括就是用软件来模拟实现真实的物理网络连接。比如Docker就是用纯软件的方式在宿主机上模拟出来的独立网络环境。我们今天来徒手打造一个虚拟网络,实现在这个网络里访问外网资源,同时监听端口提供对外服务的功能。

看完这一篇后,相信你对Docker虚拟网络能有进一步的理解。好了,我们开始!

Linux下的veth是一对儿虚拟网卡设备,和我们常见的lo很类似。在这儿设备里,从一端发送数据后,内核会寻找该设备的另一半,所以在另外一端就能收到。不过veth只能解决一对一通信的问题。

如果有很多对儿veth需要互相通信的话,就需要引入bridge这个虚拟交换机。各个veth对儿可以把一头连接在bridge的接口上,bridge可以和交换机一样在端口之间转发数据,使得各个端口上的veth都可以互相通信。

每个虚拟网卡设备、进程、socket、路由表等等网络栈相关的对象默认都是归属在init_net这个缺省的namespace中的。不过我们希望不同的虚拟化环境之间是隔离的,用Docker来举例,那就是不能让A容器用到B容器的设备、路由表、socket等资源,甚至连看一眼都不可以。只有这样才能保证不同的容器之间复用资源的同时,还不会影响其它容器的正常运行。而且它们之间、和宿主机之间都可以互相通信。

这里有一个问题没有解决,那就是虚拟出来的网络环境和外部网络的通信。还拿Docker容器来举例,你启动的容器里的服务肯定是需要访问外部的数据库的。还有就是可能需要暴露比如80端口对外提供服务。

我们今天的文章主要就是解决这两个问题的,一是从虚拟网络中访问外网,二是在虚拟网络中提供服务供外网使用。解决它们需要用到路由和NAT技术。

Linux是在发送数据包的时候,会涉及到路由过程。这个发送数据包既包括本机发送数据包,也包括途径当前机器的数据包的转发。

先来看本机发送数据包。所谓路由其实很简单,就是该选择哪张网卡(虚拟网卡设备也算)将数据写进去。到底该选择哪张网卡呢,规则都是在路由表中指定的。Linux中可以有多张路由表,最重要和常用的是local和main。

local路由表中统一记录本地,确切的说是本网络命名空间中的网卡设备IP的路由规则。

#iproutelisttablelocallocal10.143.x.ydeveth0protokernelscopehostsrc10.143.x.ylocal127.0.0.1devloprotokernelscopehostsrc127.0.0.1

其它的路由规则,一般都是在main路由表中记录着的。可以用iproutelisttablelocal查看,也可以用更简短的route-n。

再看途径当前机器的数据包的转发。除了本机发送以外,转发也会涉及路由过程。如果Linux收到数据包以后发现目的地址并不是本地的地址的话,就可以选择把这个数据包从自己的某个网卡设备上转发出去。这个时候和本机发送一样,也需要读取路由表。根据路由表的配置来选择从哪个设备将包转走。

不过值得注意的是,Linux上转发功能默认是关闭的。也就是发现目的地址不是本机IP地址默认是将包直接丢弃。需要做一些简单的配置,然后Linux才可以干像路由器一样的活儿,实现数据包的转发。

Linux内核网络栈在运行上基本上是一个纯内核态的东西,但为了迎合各种各样用户层不同的需求,内核开放了一些口子出来供用户层来干预。其中iptables就是一个非常常用的干预内核行为的工具,它在内核里埋下了五个钩子入口,这就是俗称的五链。

Linux在接收数据的时候,在IP层进入ip_rcv中处理。再执行路由判断,发现是本机的话就进入ip_local_deliver进行本机接收,最后送往TCP协议层。在这个过程中,埋了两个HOOK,第一个是PRE_ROUTING。这段代码会执行到iptables中pre_routing里的各种表。发现是本地接收后接着又会执行到LOCAL_IN,这会执行到iptables中配置的input规则。

在发送数据的时候,查找路由表找到出口设备后,依次通过__ip_local_out、ip_output等函数将包送到设备层。在这两个函数中分别过了OUTPUT和POSTROUTING开的各种规则。

如果是转发过程,Linux收到数据包发现不是本机的包可以通过查找自己的路由表找到合适的设备把它转发出去。那就先是在ip_rcv中将包送到ip_forward函数中处理,最后在ip_output函数中将包转发出去。在这个过程中分别过了PREROUTING、FORWARD和POSTROUTING三个规则。

综上所述,iptables里的五个链在内核网络模块中的位置就可以归纳成如下这幅图。

数据接收过程走的是1和2,发送过程走的是4、5,转发过程是1、3、5。有了这张图,我们能更清楚地理解iptable和内核的关系。

在iptables中,根据实现的功能的不同,又分成了四张表。分别是raw、mangle、nat和filter。其中nat表实现我们常说的NAT(NetworkAddressTranslation)功能。其中nat又分成SNAT(SourceNAT)和DNAT(DestinationNAT)两种。

SNAT解决的是内网地址访问外部网络的问题。它是通过在POSTROUTING里修改来源IP来实现的。

DNAT解决的是内网的服务要能够被外部访问到的问题。它在通过PREROUTING修改目标IP实现的。

基于以上的基础知识,我们用纯手工的方式搭建一个可以和Docker类似的虚拟网络。而且要实现和外网通信的功能。

我们先来创建一个虚拟的网络环境出来,其命名空间为net1。宿主机的IP是10.162的网段,可以访问外部机器。虚拟网络为其分配192.168.0的网段,这个网段是私有的,外部机器无法识别。

这个虚拟网络的搭建过程如下。先创建一个netns出来,命名为net1。

#ipnetnsaddnet1

创建一个veth对儿(veth1-veth1_p),把其中的一头veth1放在net1中,给它配置上IP,并把它启动起来。

#iplinkaddveth1typevethpeernameveth1_p#iplinksetveth1netnsnet1#ipnetnsexecnet1ipaddradd192.168.0.2/24devveth1#IP#ipnetnsexecnet1iplinksetveth1up

创建一个bridge,给它也设置上ip。接下来把veth的另外一端veth1_p插到bridge上面。最后把网桥和veth1_p都启动起来。

#brctladdbrbr0#ipaddradd192.168.0.1/24devbr0#iplinksetdevveth1_pmasterbr0#iplinksetveth1_pup#iplinksetbr0up

这样我们就在Linux上创建出了一个虚拟的网络。

现在假设我们上面的net1这个网络环境中想访问外网。这里的外网是指的虚拟网络宿主机外部的网络。

我们假设它要访问的另外一台机器IP是10.153.*.*,这个10.153.*.*后面两段由于是我的内部网络,所以隐藏起来了。你在实验的过程中,用自己的IP代替即可。

我们直接来访问一下试试:

#ipnetnsexecnet1ping10.153.*.*connect:Networkisunreachable

提示网络不通,这是怎么回事?用这段报错关键字在内核源码里搜索一下:

//file:arch/parisc/include/uapi/asm/errno.h#defineENETUNREACH229/*Networkisunreachable*///file:net/ipv4/ping.cstaticintping_sendmsg(structkiocb*iocb,structsock*sk,structmsghdr*msg,size_tlen){...rt=ip_route_output_flow(net,&fl4,sk);if(IS_ERR(rt)){err=PTR_ERR(rt);rt=NULL;if(err==-ENETUNREACH)IP_INC_STATS_BH(net,IPSTATS_MIB_OUTNOROUTES);gotoout;}...out:returnerr;}

在ip_route_output_flow这里的返回值判断如果是ENETUNREACH就退出了。这个宏定义注释上来看报错的信息就是“Networkisunreachable”。

这个ip_route_output_flow主要是执行路由选路。所以我们推断可能是路由出问题了,看一下这个命名空间的路由表。

#ipnetnsexecnet1route-nKernelIProutingtableDestinationGatewayGenmaskFlagsMetricRefUseIface192.168.0.00.0.0.0255.255.255.0U000veth1

怪不得,原来net1这个namespace下默认只有192.168.0.*这个网段的路由规则。我们ping的IP是10.153.*.*,根据这个路由表里找不到出口。自然就发送失败了。

我们来给net添加上默认路由规则,只要匹配不到其它规则就默认送到veth1上,同时指定下一条是它所连接的bridge(192.168.0.1)。

#ipnetnsexecnet1route-nKernelIProutingtableDestinationGatewayGenmaskFlagsMetricRefUseIface192.168.0.00.0.0.0255.255.255.0U000veth1

#ipnetnsexecnet1routeadddefaultgw192.168.0.1veth1

再ping一下试试。

#ipnetnsexecnet1ping10.153.*.*-c2PING10.153.*.*(10.153.*.*)56(84)bytesofdata.---10.153.*.*pingstatistics---2packetstransmitted,0received,100%packetloss,time999ms

额好吧,仍然不通。上面路由帮我们把数据包从veth正确送到了bridge这个网桥上。接下来网桥还需要bridge转发到eth0网卡上。所以我们得打开下面这两个转发相关的配置

#sysctlnet.ipv4.conf.all.forwarding=1#iptables-PFORWARDACCEPT

不过这个时候,还存在一个问题。那就是外部的机器并不认识192.168.0.*这个网段的ip。它们之间都是通过10.153.*.*来进行通信的。设想下我们工作中的电脑上没有外网IP的时候是如何正常上网的呢?外部的网络只认识外网IP。没错,那就是我们上面说的NAT技术。

我们这次的需求是实现内部虚拟网络访问外网,所以需要使用的是SNAT。它将namespace请求中的IP(192.168.0.2)换成外部网络认识的10.153.*.*,进而达到正常访问外部网络的效果。

#iptables-tnat-APOSTROUTING-s192.168.0.0/24!-obr0-jMASQUERADE

来再ping一下试试,欧耶,通了!

#ipnetnsexecnet1ping10.153.*.*PING10.153.*.*(10.153.*.*)56(84)bytesofdata.64bytesfrom10.153.*.*:icmp_seq=1ttl=57time=1.70ms64bytesfrom10.153.*.*:icmp_seq=2ttl=57time=1.68ms

这时候我们可以开启tcpdump抓包查看一下,在bridge上抓到的包我们能看到还是原始的源IP和目的IP。

再到eth0上查看的话,源IP已经被替换成可和外网通信的eth0上的IP了。

至此,容器就可以通过宿主机的网卡来访问外部网络上的资源了。我们来总结一下这个发送过程。

我们再考虑另外一个需求,那就是把在这个命名空间内的服务提供给外部网络来使用。

和上面的问题一样,我们的虚拟网络环境中192.168.0.2这个IP外界是不认识它的。只有这个宿主机知道它是谁。所以我们同样还需要NAT功能。

这次我们是要实现外部网络访问内部地址,所以需要的是DNAT配置。DNAT和SNAT配置中有一个不一样的地方就是需要明确指定容器中的端口在宿主机上是对应哪个。比如在docker的使用中,是通过-p来指定端口的对应关系。

#dockerrun-p8000:80...

我们通过如下这个命令来配置DNAT规则

#iptables-tnat-APREROUTING!-ibr0-ptcp-mtcp--dport8088-jDNAT--to-destination192.168.0.2:80

这里表示的是宿主机在路由之前判断一下如果流量不是来自br0,并且是访问tcp的8088的话,那就转发到192.168.0.2:80。

在net1环境中启动一个Server

#ipnetnsexecnet1nc-lp80

外部选一个ip,比如10.143.*.*,telnet连一下10.162.*.*8088试试,通了!

#telnet10.162.*.*8088Trying10.162.*.*...Connectedto10.162.*.*.Escapecharacteris'^]'.

开启抓包,#tcpdump-ieth0host10.143.*.*。可见在请求的时候,目的是宿主机的IP的端口。

但数据包到宿主机协议栈以后命中了我们配置的DNAT规则,宿主机把它转发到了br0上。在bridge上由于没有那么多的网络流量包,所以不用过滤直接抓包就行,#tcpdump-ibr0。

在br0上抓到的目的IP和端口是已经替换过的了。

bridge当然知道192.168.0.2是veth1。于是,在veth1上监听80的服务就能收到来自外界的请求了!我们来总结一下这个接收过程:

现在业界已经有很多公司都迁移到容器上了。我们的开发写出来的代码大概率是要运行在容器上的。因此深刻理解容器网络的工作原理非常的重要。这有这样将来遇到问题的时候才知道该如何下手处理。

本文开头我们先是简单介绍了veth、bridge、namespace、路由、iptables等基础知识。Veth实现连接,bridge实现转发,namespace实现隔离,路由表控制发送时的设备选择,iptables实现nat等功能。

接着基于以上基础知识,我们采用纯手工的方式搭建了一个虚拟网络环境。

这个虚拟网络可以访问外网资源,也可以提供端口服务供外网来调用。这就是Docker容器网络工作的基本原理。

整个实验我打包写成一个Makefile,放到了这里:https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test07

最后,我们再扩展一下。今天我们讨论的问题是Docker网络通信的问题。Docker容器通过端口映射的方式提供对外服务。外部机器访问容器服务的时候,仍然需要通过容器的宿主机IP来访问。

在Kubernets中,对跨主网络通信有更高的要求,要不同宿主机之间的容器可以直接互联互通。所以Kubernets的网络模型也更为复杂。

-EOF-

推荐阅读点击标题可跳转

1、

2、

3、

看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

点赞和在看就是最大的支持❤️

© 2016-2022 ysjhz.com Inc.

站点统计| 举报| Archiver| 手机版| 小黑屋| 影视聚合站 ( 皖ICP备16004362号-1 )