learn-tech/专栏/周志明的架构课/52_Linux网络虚拟化(上):信息是如何通过网络传输被另一个程序接收到的?.md
2024-10-16 06:37:41 +08:00

18 KiB
Raw Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        52 _ Linux网络虚拟化信息是如何通过网络传输被另一个程序接收到的
                        你好,我是周志明。从这节课开始,我会用两讲的时间带你学习虚拟化网络方面的知识点。

如果不加任何限定“虚拟化网络”其实是一项内容十分丰富研究历史十分悠久的计算机技术它完全不依附于虚拟化容器而是作为计算机科学中一门独立的分支。像是网络运营商经常提起的“网络功能虚拟化”Network Function VirtualizationNFV还有网络设备商和网络管理软件提供商经常提起的“软件定义网络”Software Defined NetworkingSDN等等这些都属于虚拟化网络的范畴。

不过,对于我们这样普通的软件开发者来说,一般没有什么必要去完全理解和掌握虚拟化网络,因为这需要储备大量开发中不常用到的专业知识,而且还会消耗大量的时间成本。

所以在课程里我们讨论的虚拟化网络是狭义的它特指“如何基于Linux系统的网络虚拟化技术来实现的容器间网络通信”更通俗一点说就是只关注那些为了相互隔离的Linux网络名称空间可以相互通信而设计出来的虚拟化网络设施。

另外我还要说明的是在这个语境中的“虚拟化网络”就是直接为容器服务的说它是依附于容器而存在的也完全可行。所以为了避免混淆我在课程中会尽量回避“虚拟化网络”这个范畴过大的概念而是会以“容器间网络”和“Linux网络虚拟化”为题来展开讲解。

好了下面我们就从Linux下网络通信的协议栈模型以及程序如何干涉在协议栈中流动的信息来开始了解吧。

Linux系统下的网络通信模型

如果抛开虚拟化只谈网络的话那我认为首先应该了解的知识就是Linux系统的网络通信模型即信息是如何从程序中发出通过网络传输再被另一个程序接收到的。

从整体上看Linux系统的通信过程无论是按理论上的OSI七层模型还是以实际上的TCP/IP四层模型来解构都明显地呈现出“逐层调用逐层封装”的特点这种逐层处理的方式与栈结构比如程序执行时的方法栈很类似所以它通常被称为“Linux网络协议栈”简称“网络栈”有时也称“协议栈”。

下图就体现了Linux网络通信过程与OSI或者TCP/IP模型的对应关系也展示了网络栈中的数据流动的路径你可以看一下

在图中传输模型的左侧我特别标示出了网络栈在用户与内核空间的部分也就是说几乎整个网络栈应用层以下都位于系统内核空间之中而Linux系统之所以采用这种设计主要是从数据安全隔离的角度出发来考虑的。

由内核去处理网络报文的收发无疑会有更高的执行开销比如数据在内核态和用户态之间来回拷贝的额外成本所以就会损失一些性能但是这样能够保证应用程序无法窃听到或者去伪造另一个应用程序的通信内容。当然针对特别关注收发性能的应用场景也有直接在用户空间中实现全套协议栈的旁路方案比如开源的Netmap以及Intel的DPDK都能做到零拷贝收发网络数据包。

另外,图中传输模型的箭头展示的是数据流动的方向,它体现了信息从程序中发出以后,到被另一个程序接收到之前经历的几个阶段,下面我来给你一一分析下。

Socket

应用层的程序是通过Socket编程接口来和内核空间的网络协议栈通信的。Linux Socket是从BSD Socket发展而来的现在的Socket已经不局限于作为某个操作系统的专属功能而是成为了各大主流操作系统共同支持的通用网络编程接口是网络应用程序实际上的交互基础。

在这里应用程序通过读写收、发缓冲区Receive/Send Buffer来与Socket进行交互在Unix和Linux系统中出于“一切皆是文件”的设计哲学对Socket的操作被实现为了对文件系统socketfs的读写访问操作通过文件描述符File Descriptor来进行。

TCP/UDP

传输层协议族里最重要的协议无疑就是传输控制协议Transmission Control ProtocolTCP和用户数据报协议User Datagram ProtocolUDP两种它们也是在Linux内核中被直接支持的协议。此外还有流控制传输协议Stream Control Transmission ProtocolSCTP、数据报拥塞控制协议Datagram Congestion Control ProtocolDCCP等等。当然了不同的协议处理流程大致都是一样的只是封装的报文和头、尾部信息会有些不一样。

这里我以TCP协议为例内核发现Socket的发送缓冲区中有新的数据被拷贝进来后会把数据封装为TCP Segment报文常见的网络协议的报文基本上都是由报文头Header和报文体Body也叫荷载“Payload”两部分组成。

接着系统内核将缓冲区中用户要发送出去的数据作为报文体然后把传输层中的必要控制信息比如代表哪个程序发、由哪个程序收的源、目标端口号用于保证可靠通信重发与控制顺序的序列号、用于校验信息是否在传输中出现损失的校验和Check Sum等信息封装入报文头中。

IP

网络层协议最主要的就是网际协议Internet ProtocolIP其他的还会有因特网组管理协议Internet Group Management ProtocolIGMP、大量的路由协议EGP、NHRP、OSPF、IGRP、……等等。

这里我就以IP协议为例它会把来自上一层即前面例子中的TCP报文的数据包作为报文体然后再次加入到自己的报文头中比如指明数据应该发到哪里的路由地址、数据包的长度、协议的版本号等等这样封装成IP数据包后再发往下一层。关于TCP和IP协议报文的内容我曾在“负载均衡”这节课中详细讲解过你可以去回顾复习下。

Device

Device即网络设备它是网络访问层中面向系统一侧的接口。不过这里所说的设备跟物理硬件设备并不是同一个概念Device只是一种向操作系统端开放的接口它的背后既可能代表着真实的物理硬件也可能是某段具有特定功能的程序代码比如即使不存在物理网卡也依然可以存在回环设备Loopback Device

许多网络抓包工具比如tcpdump、Wirshark就是在此处工作的我在前面第38讲介绍微服务流量控制的时候曾提到过的网络流量整形通常也是在这里完成的。

Device主要的作用是抽象出统一的界面让程序代码去选择或影响收发包出入口比如决定数据应该从哪块网卡设备发送出去还有就是准备好网卡驱动工作所需的数据比如来自上一层的IP数据包、下一跳Next Hop的MAC地址这个地址是通过ARP Request得到的等等。

Driver

网卡驱动程序Driver是网络访问层中面向硬件一侧的接口网卡驱动程序会通过DMA把主存中的待发送的数据包复制到驱动内部的缓冲区之中。数据被复制的同时也会把上层提供的IP数据包、下一跳的MAC地址这些信息加上网卡的MAC地址、VLAN Tag等信息一并封装成为以太帧Ethernet Frame并自动计算校验和。而对于需要确认重发的信息如果没有收到接收者的确认ACK响应那重发的处理也是在这里自动完成的。

好了,上面这些阶段就是信息从程序中对外发出时,经过协议栈的过程了,而接收过程则是从相反方向进行的逆操作。

这里你需要记住,程序发送数据做的是层层封包,加入协议头,传给下一层;而接受数据则是层层解包,提取协议体,传给上一层。你可以通过类比来理解数据包的接收过程,我就不再啰嗦一遍数据接收的步骤了。

干预网络通信的Netfilter框架

到这里,我们似乎可以发现,网络协议栈的处理是一套相对固定和封闭的流程,在整套处理过程中,除了在网络设备这层,我们能看到一点点程序以设备的形式介入处理的空间以外,其他过程似乎就没有什么可供程序插手的余地了。

然而事实并非如此从Linux Kernel 2.4版开始内核开放了一套通用的、可供代码干预数据在协议栈中流转的过滤器框架这就是Netfilter框架。

Netfilter框架是Linux防火墙和网络的主要维护者罗斯迪·鲁塞尔Rusty Russell提出并主导设计的它围绕网络层IP协议的周围埋下了五个钩子Hooks每当有数据包流到网络层经过这些钩子时就会自动触发由内核模块注册在这里的回调函数程序代码就能够通过回调来干预Linux的网络通信。

下面我给你介绍一下这五个钩子分别都是什么:

PREROUTING来自设备的数据包进入协议栈后就会立即触发这个钩子。注意如果PREROUTING钩子在进入IP路由之前触发了就意味着只要接收到的数据包无论是否真的发往本机也都会触发这个钩子。它一般是用于目标网络地址转换Destination NATDNAT。 INPUT报文经过IP路由后如果确定是发往本机的将会触发这个钩子它一般用于加工发往本地进程的数据包。 FORWARD报文经过IP路由后如果确定不是发往本机的将会触发这个钩子它一般用于处理转发到其他机器的数据包。 OUTPUT从本机程序发出的数据包在经过IP路由前将会触发这个钩子它一般用于加工本地进程的输出数据包。 POSTROUTING从本机网卡出去的数据包无论是本机的程序所发出的还是由本机转发给其他机器的都会触发这个钩子它一般是用于源网络地址转换Source NATSNAT

Netfilter允许在同一个钩子处注册多个回调函数所以数据包在向钩子注册回调函数时必须提供明确的优先级以便触发时能按照优先级从高到低进行激活。而因为回调函数会有很多个看起来就像是挂在同一个钩子上的一串链条所以钩子触发的回调函数集合就被称为“回调链”Chained Callbacks这个名字也导致了后续基于Netfilter设计的Xtables系工具比如下面我要介绍的iptables都使用到了“链”Chain的概念。

那么虽然现在看来Netfilter只是一些简单的事件回调机制而已但这样一套简单的设计却成为了整座Linux网络大厦的核心基石Linux系统提供的许多网络能力比如数据包过滤、封包处理设置标志位、修改TTL等、地址伪装、网络地址转换、透明代理、访问控制、基于协议类型的连接跟踪、带宽限速等等它们都是在Netfilter的基础之上实现的。

而且以Netfilter为基础的应用也有很多其中使用最广泛的毫无疑问要数Xtables系列工具比如iptables、ebtables、arptables、ip6tables等等。如果你用过Linux系统来做过开发的话那我估计至少这里面的iptables工具你会或多或少地使用过它常被称为是Linux系统“自带的防火墙”。

但其实iptables实际能做的事情已经远远超出了防火墙的范畴严谨地讲iptables比较贴切的定位应该是能够代替Netfilter多数常规功能的IP包过滤工具。

要知道iptables的设计意图是因为Netfilter的钩子回调虽然很强大但毕竟要通过程序编码才够能使用并不适合系统管理员用来日常运维而它的价值就是以配置去实现原本用Netfilter编码才能做到的事情。

一般来说iptables会先把用户常用的管理意图总结成具体的行为预先准备好然后就会在满足条件的时候自动激活行为比如以下几种常见的iptables预置的行为

DROP直接将数据包丢弃。 REJECT给客户端返回Connection Refused或Destination Unreachable报文。 QUEUE将数据包放入用户空间的队列供用户空间的程序处理。 RETURN跳出当前链该链里后续的规则不再执行。 ACCEPT同意数据包通过继续执行后续的规则。 JUMP跳转到其他用户自定义的链继续执行。 REDIRECT在本机做端口映射。 MASQUERADE地址伪装自动用修改源或目标的IP地址来做NAT LOG在/var/log/messages文件中记录日志信息。 ……

当然这些行为本来能够被挂载到Netfilter钩子的回调链上但iptables又进行了一层额外抽象它不是把行为与链直接挂钩而是会根据这些底层操作的目的先总结为更高层次的规则。

我举个例子假设你挂载规则的目的是为了实现网络地址转换NAT那就应该对符合某种特征的流量比如来源于某个网段、从某张网卡发送出去、在某个钩子上比如做SNAT通常在POSTROUTING做DNAT通常在PREROUTING进行MASQUERADE行为这样具有相同目的的规则就应该放到一起才便于管理所以也就形成了“规则表”的概念。

iptables内置了五张不可扩展的规则表其中的security表并不常用很多资料只计算了前四张表我们来看看

raw表用于去除数据包上的连接追踪机制Connection Tracking。 mangle表用于修改数据包的报文头信息比如服务类型Type Of ServiceToS、生存周期Time to LiveTTL以及为数据包设置Mark标记典型的应用是链路的服务质量管理Quality Of ServiceQoS。 nat表用于修改数据包的源或者目的地址等信息典型的应用是网络地址转换Network Address Translation。 filter表用于对数据包进行过滤控制到达某条链上的数据包是继续放行、直接丢弃或拒绝ACCEPT、DROP、REJECT典型的应用是防火墙。 security表用于在数据包上应用SELinux这张表并不常用。

这五张规则表是有优先级的raw→mangle→nat→filter→security也就是前面我列举出的顺序。这里你要注意在iptables中新增规则时需要按照规则的意图指定要存入到哪张表中如果没有指定就默认会存入filter表。此外每张表能够使用到的链也有所不同具体表与链的对应关系如下所示

那么你从名字上其实就能看出预置的五条链是直接源自于Netfilter的钩子它们与五张规则表的对应关系是固定的用户不能增加自定义的表或者修改已有表与链的关系但可以增加自定义的链。

新增的自定义链与Netfilter的钩子没有天然的对应关系换句话说就是不会被自动触发只有显式地使用JUMP行为从默认的五条链中跳转过去才能被执行。

可以说iptables不仅仅是Linux系统自带的一个网络工具它在容器间通信中也扮演着相当重要的角色。比如Kubernetes用来管理Sevice的Endpoints的核心组件kube-proxy就依赖iptables来完成ClusterIP到Pod的通信也可以采用IPVSIPVS同样是基于Netfilter的这种通信的本质就是一种NAT访问。

当然对于Linux用户来说前面提到的内容可能都是相当基础的网络常识但如果你平常比较少在Linux系统下工作就可能需要一些用iptables充当防火墙过滤数据、充当作路由器转发数据、充当作网关做NAT转换的实际例子来帮助理解了这些操作在网上也很容易就能找到这里我就不专门去举例说明了。

小结

Linux目前提供的八种名称空间里网络名称空间无疑是隔离内容最多的一种它为名称空间内的所有进程提供了全套的网络设施包括独立的设备界面、路由表、ARP表IP地址表、iptables/ebtables规则、协议栈等等。

虚拟化容器是以Linux名称空间的隔离性为基础来实现的那解决隔离的容器之间、容器与宿主机之间乃至跨物理网络的不同容器间通信问题的责任就很自然地落在了Linux网络虚拟化技术的肩上。这节课里我们暂时放下了容器编排、云原生、微服务等等这些上层概念走进Linux网络的底层世界去学习了一些与设备、协议、通信相关的基础网络知识。

最后我想说的是到目前为止我给你介绍的Linux下网络通信的协议栈模型以及程序如何干涉在协议栈中流动的信息它们与虚拟化都没有产生什么直接联系而是整个Linux网络通信的必要基础。在下节课我们就要开始专注于跟网络虚拟化密切相关的内容了。

一课一思

说实话,今天的内容其实很适合以实现业务功能为主、平常并不直接接触网络设备的普通开发人员,而如果你是做平台基础设施开发或者运维的,那学习这节课可能就会觉得有点太基础或啰嗦了,因为这些都是基本的工作技能。

所以在最后,我想来了解一下,如果你是一名程序员,那你是否经常有机会接触这些网络方面的知识呢?如果有,你都用它们来做什么?欢迎给我留言。

另外,如果你觉得有收获,也欢迎把今天的内容分享给更多的朋友。感谢你的阅读,我们下一讲再见。