Keep learning, keep living...

0%

之前的文章<<Kubernetes网络和CNI>><<Kubernetes flannel网络分析>>介绍了Kubernetes集群的pod网络的通信过程。

pod本质上非固定的,经常发生变化,而pod IPpod销毁和创建的时候会发生变更,因而不能直接对外提供服务。Kubernetes通过service资源来对外提供服务,serviceIP是固定的,它自动绑定一组pod并根据不同实现将流量转发到这些pod中, 并在流量转发的过程中实现负载均衡(load balance)。

Kubernetesnode节点上的主要组件有kube-proxykubelet, kubelet会调用相关的CNI实现完成POD网络的通信。而kube-proxy则负责上述的servicePOD之间的流量转发。实际上,在之前文章的实验环境里,即使把node节点上的kube-proxy组件都停止,也不会影响pod网络通信。

service本质就是将一组pod通过固定IP暴露给使用者,可以由ip:port:protocol来标识。

service主要以下几种类型:

  • ClusterIP: 用Kubernetes集群内部IP暴露服务,也就是说只有在Kubernetes集群内才可以访问这个service。这是默认的service类型。ClusterIP的范围是在kube-apiserver启动时通过-service-cluster-ip-range参数指定的。这些IP只能在kubernetes集群内进行访问。service的相关信息是在yaml文件中定义的,最终暴露的信息可表示为:

    1
    spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
  • NodePort: 在Kubernetes集群的所有node节点上使用相同的固定port来暴露服务。这种类型会自动创建ClusterIP类型的服务,NodePortservice会将流量转发到ClusterIP类型的服务。服务的使用者可以使用NodeIP:NodePort来访问该服务。这种类型服务暴露的信息可以表示为:

    1
    2
    <NodeIP>:spec.ports[*].nodePort:spec.ports[*].protocol
    spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
  • LoadBalancer: 是通过kubernetes集群外部设施所提供的IP来暴露服务。NodePortClusterIP类型的服务会被自动创建。不同的LoadBalancer负责实现外部IP:portNodePort服务的映射。这种类型暴露的信息可以表示为:

    1
    2
    3
    spec.loadBalancerIp:spec.ports[*].port:spec.ports[*].protocol
    <NodeIP>:spec.ports[*].nodePort:spec.ports[*].protocol
    spec.clusterIp:spec.ports[*].port:spec.ports[*].protocol
阅读全文 »

flannelcoreos开源的Kubernetes CNI实现。它使用etcd或者Kubernetes API存储整个集群的网络配置。每个kubernetes节点上运行flanneld组件,它从etcd或者Kubernetes API获取集群的网络地址空间,并在空间内获取一个subnet,该节点上的容器IP都从这个subnet中分配,从而保证不同节点上的IP不会冲突。flannel通过不同的backend来实现跨主机的容器网络通信,目前支持udp,vxlan,host-gw等一系列backend实现。本文介绍vxlan backend下的容器通信过程。

flannelv0.9.0版本上对vxlan的实现作了改动。源码中有一段非常详细的注释介绍了不同版本的设计与实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Some design notes and history:
// VXLAN encapsulates L2 packets (though flannel is L3 only so don't expect to be able to send L2 packets across hosts)
// The first versions of vxlan for flannel registered the flannel daemon as a handler for both "L2" and "L3" misses
// - When a container sends a packet to a new IP address on the flannel network (but on a different host) this generates
// an L2 miss (i.e. an ARP lookup)
// - The flannel daemon knows which flannel host the packet is destined for so it can supply the VTEP MAC to use.
// This is stored in the ARP table (with a timeout) to avoid constantly looking it up.
// - The packet can then be encapsulated but the host needs to know where to send it. This creates another callout from
// the kernal vxlan code to the flannel daemon to get the public IP that should be used for that VTEP (this gets called
// an L3 miss). The L2/L3 miss hooks are registered when the vxlan device is created. At the same time a device route
// is created to the whole flannel network so that non-local traffic is sent over the vxlan device.
//
// In this scheme the scaling of table entries (per host) is:
// - 1 route (for the configured network out the vxlan device)
// - One arp entry for each remote container that this host has recently contacted
// - One FDB entry for each remote host
//
// The second version of flannel vxlan removed the need for the L3MISS callout. When a new remote host is found (either
// during startup or when it's created), flannel simply adds the required entries so that no further lookup/callout is required.
//
//
// The latest version of the vxlan backend removes the need for the L2MISS too, which means that the flannel deamon is not
// listening for any netlink messages anymore. This improves reliability (no problems with timeouts if
// flannel crashes or restarts) and simplifies upgrades.
//
// How it works:
// Create the vxlan device but don't register for any L2MISS or L3MISS messages
// Then, as each remote host is discovered (either on startup or when they are added), do the following
// 1) create routing table entry for the remote subnet. It goes via the vxlan device but also specifies a next hop (of the remote flannel host).
// 2) Create a static ARP entry for the remote flannel host IP address (and the VTEP MAC)
// 3) Create an FDB entry with the VTEP MAC and the public IP of the remote flannel daemon.
//
// In this scheme the scaling of table entries is linear to the number of remote hosts - 1 route, 1 arp entry and 1 FDB entry per host
//
// In this newest scheme, there is also the option of skipping the use of vxlan for hosts that are on the same subnet,
// this is called "directRouting"

v0.9.0之前版本的实现主要依赖vxlan内核模块的L2MISSL3MISS消息机制。L2MISS是指vxlan设备在ARP表中找不到内层IP对应的MAC地址时会给用户态程序发送netlink消息。L3MISS是指vxlan设备在FDB表中找不到VXLAN协议内层MAC地址所属的VTEPIP地址时会给用户态程序发送netlink消息。之前的文章<<动态维护FDB表项实现VXLAN通信>>介绍过相关概念和操作。本文主要分析v0.9.0版本上的实现方式。

阅读全文 »

Linux进程在运行中异常中止或者崩溃,Linux会将进程当时的内存信息、寄存器信息、函数调用堆栈等信息存储到文件系统的一个文件中,这个过程叫做core dump, 生成的core文件可以使用gdb来分析诊断程序存在的问题。最近对core dump的生成过程及相应进程退出的过程有些疑惑。究竟是崩溃进程的内核task退出前生成core文件还是崩溃的task先退出之后kernel检测到进程崩溃后额外来生成的呢?带着这个疑问去做些了实验和代码分析。

默认情况下,core dump文件被命名为core.pid, 如core.12345。内核参数/proc/sys/kernel/core_pattern可以定义core dump文件名模板。具体模板参数可以参考文档。但内核有个小巧特性,如果core_pattern的第一个字符为管道符:|, 内核会执行|之后的配置的可执行程序,并通过pipecore dump内容传给这个用户态程序。

ABRT:Automated Bug Reporting Tool服务收集应用崩溃的core文件就是使用这个机制,abrt-ccpp服务会将core_pattern设置为:

1
|/usr/libexec/abrt-hook-ccpp %s %c %p %u %g %t e %P %I %h
阅读全文 »

Security Boot机制是UEFI的一个特性,用于确保固件所加载的代码是可信的。它通过将可信根存放在固件中,在加载阶段校验所加载的二进制可信。当前实现主要是基于X.509证书的公私钥体系。固件使用可信的公钥来校验所加载的bootloader, bootloader再来校验内核或者第二阶段的bootloader, 内核校验所加载的内核模块,这样一级级完成校验。

在开启了Security Boot机制的CentOS8上,加载我们未签名的内核模块会失败:

1
2
[root@localhost virtualdev]# insmod ./virtualdev.ko
insmod: ERROR: could not insert module ./virtualdev.ko: Required key not available

接下来我们来通过签名我们的内核模块完成加载。要签名和校验我们的内核模块,我们需要一对公钥私钥。私钥用于签名,公钥需要加载到系统固件的MOK: Machine Owner Key列表中用于校验被签名的模块。

阅读全文 »

之前的文章<<Kubernetes环境中NFQUEUE与MARK机制冲突>>介绍了我们使用NFQUEUE机制将数据包送往用户态进行安全检测。之前程序逻辑是将来自虚拟网络设备的数据包直接放行。而当把逻辑修改为对POD虚拟网卡的流量也进行检测时,POD网络就无法连通了。排查发现数据包送上用户态之后,并没有收到用户态程序的裁决信息。

在对NFQUEUE的源码实现了进行草略分析后,发现NFQUEUE机制是支持network namespace的。POD虚拟网络设备的数据包送往用户态的队列是在POD独有的network namespace中创建的,和默认的network namespace:init_net中的队列是完全独立的。我们的用户态程序运行是在init_net中运行,而PODnetwork namespace中并没有用户态程序在读取队列获取数据包,因而数据包会被丢弃。

和之前文章同样,通过简化程序来进行实验。实验的Kubernetes环境有3个node, 容器组网使用flannel

我们创建了两个busyboxpod

1
2
kubectl run busybox1 --image=busybox --command -- sleep 3600
kubectl run busybox2 --image=busybox --command -- sleep 3600

他们分别位于node1node2上:

1
2
3
4
[root@master1 scripts]# kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox1-77bb94599d-x89z4 1/1 Running 12 22h 10.230.96.4 node1 <none> <none>
busybox2-7d76b658b6-h5r2k 1/1 Running 10 22h 10.230.12.2 node2 <none> <none>

我们从busybox2中访问busybox1,网络连通正常:

1
2
3
4
5
6
7
8
[root@master1 scripts]# kubectl exec -ti busybox2-7d76b658b6-h5r2k -- ping -c2 10.230.96.4
PING 10.230.96.4 (10.230.96.4): 56 data bytes
64 bytes from 10.230.96.4: seq=0 ttl=62 time=1.076 ms
64 bytes from 10.230.96.4: seq=1 ttl=62 time=0.770 ms

--- 10.230.96.4 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.770/0.923/1.076 ms
阅读全文 »

之前的文章<<QEMU虚拟机内识别ivshmem设备>>介绍了在虚拟机内通过用户态程序访问ivshmem设备的共享内存。在虚拟机之间或者宿主机与虚拟机之间通过共享内存进行通信的情形下,共享内存的两端必须依赖轮询方式来实现通知机制。这种方式是ivshmem提供的ivshmem-plain的使用方式。除此之外,ivshmem还提供了ivshmem-doorbell的使用方式,它提供了基于中断的通知机制。

ivshmem-doorbell提供了两种中断方式,一种是传统的基于INTx的中断, 它主要使用BAR0Interrupt MaskInterrupt Status两个寄存器;另一种是基于MSI-X的中断,它主要使用BAR0IVPositionDoorbell两个寄存器。参考共享的设备端叫做peerIVPosition寄存器存储该peer的数字标识符(0-65535), 称做peer_id。该寄存器为只读寄存器。而Doorbell寄存器为只写寄存器。ivshmem-doorbell支持多个中断向量,写入Doorbell寄存器则触发共享该内存的某个peer的某个中断。Doorbell32位,低16位为peer_id,而高16位为中断向量号(这里是从0开始的顺序号,而非PCI驱动在Guest虚拟机内部所申请的向量号)。

使用ivshmem-doorbell机制需要运行ivshmem-serverivshmem-server根据参数创建共享内存,并通过监听本地UNIX DOMAIN SOCKET等待共享内存的peer来连接。添加了ivshmem-doorbell设备的QEMU进程会连接该socket, 从而获取ivshmem-server所分配的一个peer_idivshmem-doorbell支持多个中断向量,ivshmem-server会为ivshmem虚拟PCI设备支持的每个中断向量创建一个eventfd,并将共享内存以及为所有客户端中断向量所创建的eventfd都通过SCM_RIGHTS机制传递给所有客户端进程。这样所有的peer便都具备了独立的两两之间的通知通道。之后在虚拟机内通过触发ivshmem虚拟PCI设备的DOORBELL寄存器的写入,虚拟机的QEMU进程便会通过DOORBELL寄存器中的peer_id和中断向量号来找到相应的eventfd,从而通知到对端的QEMU进程来产生相应的PCI中断。

要使用中断机制,用户态程序是无能为力的,需要编写相应的PCI驱动来实现。本文通过一个简单的PCI驱动示例来说明ivshmem-doorbellMSI-X中断机制的使用。

阅读全文 »

ivshmem:(Inter-VM shared memory device)QEMU提供的一种宿主机与虚拟机之间或多个虚拟机之间共享内存的特殊设备。它有两种形式:

  • ivshmem-plain: 简单的共享内存区域
  • ivshmem-doorbell: 除了共享内存,还能提供基于中断的通信机制

这种设备在虚拟机内部表现为PCI设备,共享的内存区域则以PCI BAR的形式存在。ivshmemPCI设备提供3个BAR:

  • BAR0: 设备寄存器
  • BAR1: MSI-X
  • BAR2: 共享内存区域

简单共享内存的场景只使用BAR2就足够了。如果需要基于中断实现额外通信,需要用到BAR0BAR1。这可能需要编写内核驱动在虚拟机内处理中断,宿主机上QEMU进程在启动前需要先启动ivshmem server, 然后让QEMU进程连接到serverunix socket

具体可以参考官方文档

本文只讨论ivshmem-plain模式。宿主机上添加ivshmem设备后,虚拟机应用如何找到相应的ivshmem设备呢?

Linux的/sys/bus/pci/devices/目录会列出所有的PCI设备,ivshmem设备也会包含在其中。PCI设备都存在vendor号和device两个标识,vendor表示厂商,device表示厂商内的设备类型。ivshmem设备的vendor号为0x1af4, device号为0x1110,PCI设备的vendordevice号可在这里进行查询。

虚拟机中应用可通过遍历该目录下的具体设备,通过读取vendordevice文件来识别ivshmem设备。

但如果有两种应用都需要使用一个独立的ivshmem设备,虚拟机应用如何识别出应该使用哪个ivshmem设备呢?

因为每个PCI设备都可以由BDF:(Bus, Device, Function)来唯一标识,简单做法可以为每个应用预留好固定BDF地址。BDF地址中,BUS占用8位,Device占用5位,Function占用3位。比如,预留总线pci0的最后两个设备地址0000:00:1e.00000:00:1f.0

有时候无法预留,不同虚拟机上的ivshmem地址可能不同。这种情况可以通过与宿主机上的应用约定好相应的固定内容做为signature写入共享内存头部,虚拟机应用读取共享内存头部的signature信息来识别相应设备。

阅读全文 »

在Kubernetes节点上安装我们的流量检测模块之后所有的Pod会断网。经分析是由于流量检测模块的NFQUEUE机制与kube-proxy使用的iptablesmark机制冲突的原因。

在Linux内核中,网络数据包是由sk_buff结构来表示的,一般数据包简写作SKBmarksk_buff结构的一个字段, 如(include/linux/skbuff.h):

1
2
3
4
5
6
7
8
struct sk_buff {
...
union {
__u32 mark;
__u32 reserved_tailroom;
};
...
}

mark并不是网络协议结构的部分,不会存在于任一层协议头中,而是Linux网络子系统用于在主机内部传递状态信息的标记机制。各种网络应用可以根据自身需要使用该字段来实现自身的状态传递。

这个mark机制主要用在netfilter框架中,所以也叫nfmark。除了它之外,内核中还有conntrack模块也有自己的mark机制,一般叫做ctmark

之前的文章<<基于IPTABLES MARK机制实现策略路由>>也介绍过iptablesMARK模块,可以用于修改和匹配数据包的mark值。

NFQUEUE机制可以在内核中将数据通过NFQUEUE通道将数据包送往用户态,在用户态进行安全检测,再将裁决(verdict)结果送回内核。之前的文章<<NFQUEUE和libnetfilter_queue实例分析>>介绍了libnetfilter_queue库的简单用法。我们的流量检测程序会使用libnetfilter_queue库中的nfq_set_verdict2在返回verdict的同时,设置数据包的mark值,以传递更多的信息给内核模块,函数原型如下:

1
2
3
4
5
6
7
int nfq_set_verdict2(struct nfq_q_handle *  qh,
uint32_t id,
uint32_t verdict,
uint32_t mark,
uint32_t data_len,
const unsigned char* buf
)

这就会导致数据包sk_buff结构的mark值被设置。而kube-proxy实现也依赖iptablesmark机制, 会在主机上添加如下iptables规则:

1
2
3
-A KUBE-MARK-DROP -j MARK --set-xmark 0x8000/0x8000
...
-A KUBE-FIREWALL -m comment --comment "kubernetes firewall for dropping marked packets" -m mark --mark 0x8000/0x8000 -j DROP

对于不合法的报文,kube-proxy会给相应报文标记0x8000/0x8000, 之后通过KUBE-FIREWALL规则链将数据包丢弃。

如果我们的流量检测程序所设置的mark值设置为kube-proxy所依赖的0x8000位,就会导致数据包被丢弃。

阅读全文 »

在一些服务器安全场景中,需要通过网络连接关联到相关进程。例如,在安全溯源场景中,通过威胁情报可以判断某台主机上存在恶意连接,这时就需要追查这些恶意连接是由哪个进程以及哪个可执行文件来发起的。又或者,在微隔离场景中,我们不仅仅需要知道IP:PortIP:Port之间的访问关系,我们还需要额外增加进程级别的信息,也就是哪个进程通过IP:Port在访问IP:Port的哪个进程。

要解决这种网络连接与进程关联的问题,在用户态的可行办法主要是通过读取/proc/net/tcp以及/proc/[pid]/fd这两种文件来构建相应的映射结构。

通过读取文件/proc/net/tcp可获取系统的TCP连接信息:

1
2
3
4
5
6
[root@centos3 tcpconn]# cat /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000:006F 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 13841 1 ffff9fc7da7c8000 100 0 0 10 0
1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 16863 1 ffff9fc7da7c87c0 100 0 0 10 0
2: 0100007F:0019 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 17951 1 ffff9fc7da7c9f00 100 0 0 10 0
3: 0F02000A:0016 0202000A:F3FE 01 00000000:00000000 02:000AF352 00000000 0 0 23961 4 ffff9fc7da7c8f80 20 4 25 10 -1

从其中可获取TCP连接四元组及对应socket的inode号。

而从/proc/[pid]/fd中可以获取进程所有的文件描述符:

1
2
3
4
5
6
7
[root@centos3 tcpconn]# ls -l /proc/823/fd
total 0
lr-x------. 1 root root 64 Jul 24 15:06 0 -> /dev/null
lrwx------. 1 root root 64 Jul 24 15:06 1 -> socket:[16389]
lrwx------. 1 root root 64 Jul 24 15:06 2 -> socket:[16389]
lrwx------. 1 root root 64 Jul 24 15:06 3 -> socket:[16863]
lrwx------. 1 root root 64 Jul 24 15:06 4 -> socket:[16939]

其中也可以获取相应的inode号.

这样,我们就可以从/proc/net/tcp建立起网络连接五元组->inode的映射, 再从/proc/pid/fd建立起连接inode->进程的映射。从而实现网络连接关联到相应进程。

但这种方式整个映射关系的建立依赖周期性读取两种proc文件,缺乏实时性,对于瞬时连接相应的数据可以无法实时获取到,从而无法关联到进程。

阅读全文 »

CNI: Container Network Interface是配置Linux容器网络接口的一种规范。它将容器运行时和容器网络实现解耦,使容器网络实现成为可插拔的插件。在不同的容器运行时环境中,容器网络实现可以复用。而在不同的网络环境中,也可以灵活的插拔不同的网络实现。

CNI规范主要涉及容器运行时CNI插件两个角色,规范约定了二者的交互方式。容器运行时在容器实例创建时调用CNI插件的ADD接口以创建容器络连接所需的资源网络连接,当容器删除时调用CNIDEL接口移除所创建的相应资源完成资源释放。不同的CNI插件按照规范所统一定义的接口、参数、响应实现不同的网络方案。

官方提供了开发库libcni容器运行时可以使用该库来集成CNI能力,并且还提供了一系列的CNI插件参考实现

目前CNI规范的最新版本为0.4.0

从具体实现上看,容器运行时会先创建好相应的network namespace, 然后CNI插件负责将网络接口插入到容器实例的network namespace、在宿主机上做必要的网络操作(如绑定IP到接口、建立路由等)以实现容器网络连通。

一般设计中,主体模块与插件之间的交互会采用RPC、二进制兼容的动态库加载等手段。CNI规范则指定CNI插件实现为可执行程序,相应接口参数通过环境变量与标准输入流传给CNI插件,类似于早年WEB领域的CGI模式。

阅读全文 »