写点什么

cilium 原理之 sock_connect

作者:沃趣科技
  • 2023-08-10
    浙江
  • 本文字数:4937 字

    阅读完需:约 16 分钟

cilium 原理之sock_connect

1.背景

在集群网络使用 cilium 之后,最明显的情况就是:服务暴露 vip+port,在集群内怎么测试都正常,但集群外访问可能是有问题的。而这就在于 cilium 所使用的 ebpf 科技。


2.引子:curl 请求的路程


相对底层一点的语言,比如 c 语言,在创建一个 tcp 连接时,主要分两步(其它语言可能会更简单):


    int socket_desc;    struct sockaddr_in server;		//Create socket	socket_desc = socket(AF_INET , SOCK_STREAM , 0);
server.sin_addr.s_addr = inet_addr("1.1.1.1"); server.sin_family = AF_INET; server.sin_port = htons( 80 );
//Connect to remote server if (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)
复制代码


一个连接的创建,主要分两个步骤:


  1. 创建 socket 对象

  2. 发起 connect 连接


而实际上,在内核层,它经历的步骤会非常多。可以通过 perf 工具来查看:


perf trace  -e 'net:*' -e 'sock:*' -e 'syscalls:*'  curl 1.1.1.1 -s >& /dev/stdout 
复制代码


上面的输出很多,而 syscalls:sys_enter_socket 前面的很长一段,是 curl 程序打开本身加载动态链接库需要的系统调用。


而本次需要关心的是以下这部分(截取的部分内容):


   108.294 curl/15819 syscalls:sys_enter_socket(family: INET, type: STREAM)   108.351 curl/15819 syscalls:sys_exit_socket(__syscall_nr: 41, ret: AX25)   108.939 curl/15819 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16)   108.991 curl/15819 sock:inet_sock_set_state(skaddr: 0xffff902527424c80, oldstate: 7, newstate: 2, dport: 80, family: 2, protocol: 6, saddr: 0x7f176658943b, daddr: 0x7f176658943f, saddr_v6: 0x7f1766589443, daddr_v6: 0x7f1766589453)   109.090 curl/15819 net:net_dev_queue(skbaddr: 0xffff9024f0a2d4e8, len: 74, name: "enp1s0")   109.140 curl/15819 net:net_dev_start_xmit(name: "enp1s0", skbaddr: 0xffff9024f0a2d4e8, protocol: 2048, ip_summed: 3, len: 74, network_offset: 14, transport_offset_valid: 1, transport_offset: 34, gso_segs: 1, gso_type: 1)
复制代码


从上面可以看出,在定义 socket 后,接着就是 connect 连接,而在 sock:inet_sock_set_state 这一步,有输出地址相关信息,但输出的是内存地址,无法直接查看。能通过 bcc 工具集中的 tcplife 来查看。


# 一个终端中运行:tcplife -D 12345# 另一个终端中运行:curl 1.1.1.1:12345
复制代码


虽然访问的是不存在的地址,但内核也会基于默认路由,走默认网关,将报文发送到 enp1s0 网卡上。而在 sock:inet_sock_set_state 可以抓取到源地址与目的地址信息。 既然我们能在 sock:inet_sock_set_state 点挂载程序,抓取报文信息,那我们是否可以在挂载点,修改 socket 的目的地址与目的端口信息?


答案是肯定的。但 cilium 是在 cgroup/connect4 进行修改的(和上面从 perf 查出来的不同,但可以通过 bcc 的工具来验证。cgroup 是高版本内核才有的特殊,具体可参考链接,里面有标识内核版本的特性。


那么,这是如何查到的呢?


[root@c7-1 ~]# bpftool prog |grep sock1653: type 18  name sock6_connect  tag d526fd1cb49a372e  gpl1657: cgroup_sock  name sock6_post_bind  tag e46a7916c9c72e67  gpl1661: type 18  name sock6_sendmsg  tag 19094f9c26d4dddf  gpl1665: type 18  name sock6_recvmsg  tag 282bf4c10eff7f73  gpl1669: type 18  name sock4_connect  tag 57eae2cf019378cc  gpl1673: cgroup_sock  name sock4_post_bind  tag ddd7183184f2e6e9  gpl1677: type 18  name sock4_sendmsg  tag 570ef9d580ce0589  gpl1681: type 18  name sock4_recvmsg  tag 0bdebe7409ceb49f  gpl[root@c7-1 ~]# bpftool prog |grep connect1653: type 18  name sock6_connect  tag d526fd1cb49a372e  gpl1669: type 18  name sock4_connect  tag 57eae2cf019378cc  gpl
复制代码


在有运行 cilium 的机器上,使用 bpftool 工具查询挂载的程序,发现与 socket 相关的就是这些。


再到 cilium 的源代码中,查看对应的代码段定义:


github.com/cilium/cilium/bpf$ grep -i "__section(" *.cbpf_host.c:__section("from-netdev")bpf_host.c:__section("from-host")bpf_host.c:__section("to-netdev")bpf_host.c:__section("to-host")bpf_lxc.c:__section("from-container")bpf_lxc.c:__section("mydebug1")bpf_lxc.c:__section("mydebug2")bpf_lxc.c:__section("to-container")bpf_network.c:__section("from-network")bpf_overlay.c:__section("from-overlay")bpf_overlay.c:__section("to-overlay")bpf_sock.c:__section("cgroup/connect4")bpf_sock.c:__section("cgroup/post_bind4")bpf_sock.c:__section("cgroup/bind4")bpf_sock.c:__section("cgroup/sendmsg4")bpf_sock.c:__section("cgroup/recvmsg4")bpf_sock.c:__section("cgroup/getpeername4")bpf_sock.c:__section("cgroup/post_bind6")bpf_sock.c:__section("cgroup/bind6")bpf_sock.c:__section("cgroup/connect6")bpf_sock.c:__section("cgroup/sendmsg6")bpf_sock.c:__section("cgroup/recvmsg6")bpf_sock.c:__section("cgroup/getpeername6")bpf_xdp.c:__section("from-netdev")
复制代码


由此,cilium 使用的科技就很明显了。


3.手写 ebpf

1.ebpf 程序实现


在看 cilium 源码实现之前,先手写一个最简单的修改目的地址与端口的程序。因为 cilium 本身框架很复杂,代码也有相关,所以先以最简单的(写死的)程序入手。代码可以参考 cilium 源码。


#include <bpf/ctx/unspec.h>#include <bpf/api.h>
#define SKIP_POLICY_MAP 1#define SKIP_CALLS_MAP 1
#define SYS_REJECT 0#define SYS_PROCEED 1
# define printk(fmt, ...) \ ({ \ const char ____fmt[] = fmt; \ trace_printk(____fmt, sizeof(____fmt), \ ##__VA_ARGS__); \ })
__section("cgroup/connect4")int sock4_connect(struct bpf_sock_addr *ctx ){ if (ctx->user_ip4 != 0x04030201) { // des ip is 1.2.3.4 return SYS_PROCEED; } printk("aa %x ", ctx->user_ip4); ctx->user_ip4=0x19280a0a; // set to 10.10.40.25 printk("set ok %x,%x", ctx->user_ip4, ctx->user_port); return SYS_PROCEED;}
BPF_LICENSE("Dual BSD/GPL");
复制代码


程序说明:

  1. 判断目标 ip 是 1.2.3.4 才处理(对应 16 进制顺序相反,是因为系统为小端模式)。

  2. 输出目的 ip,方便 debug。

  3. 修改目的 ip 为指定的 ip。

  4. 输出设置的结果。


入参 bpf_sock_addr,可从 cilium 的源码中找到相关定义。


mysock.c


/* User bpf_sock_addr struct to access socket fields and sockaddr struct passed * by user and intended to be used by socket (e.g. to bind to, depends on * attach type). */struct bpf_sock_addr {	__u32 user_family;	/* Allows 4-byte read, but no write. */	__u32 user_ip4;		/* Allows 1,2,4-byte read and 4-byte write.				 * Stored in network byte order.				 */	__u32 user_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.				 * Stored in network byte order.				 */	__u32 user_port;	/* Allows 1,2,4-byte read and 4-byte write.				 * Stored in network byte order				 */	__u32 family;		/* Allows 4-byte read, but no write */	__u32 type;		/* Allows 4-byte read, but no write */	__u32 protocol;		/* Allows 4-byte read, but no write */	__u32 msg_src_ip4;	/* Allows 1,2,4-byte read and 4-byte write.				 * Stored in network byte order.				 */	__u32 msg_src_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.				 * Stored in network byte order.				 */	__bpf_md_ptr(struct bpf_sock *, sk);};
复制代码


2.程序加载

基于 k8s 部署 cilium 后,cilium 会在容器中初始化好环境,我们可以直接使用,省去编译环境、cgroupv2 配置的麻烦。


将上面的文件,复制到 cilium 的容器中(本样例中使用的 cilium 版本为 1.12.7)。



file=./mysock.c
clang -O2 -target bpf -std=gnu89 -nostdinc -emit-llvm -g -Wall -Wextra -Werror -Wshadow -Wno-address-of-packed-member -Wno-unknown-warning-option -Wno-gnu-variable-sized-type-not-at-end -Wdeclaration-after-statement -Wimplicit-int-conversion -Wenum-conversion -I. -I/run/cilium/state/globals -I/var/lib/cilium/bpf -I/var/lib/cilium/bpf/include -D__NR_CPUS__=8 -DENABLE_ARP_RESPONDER=1 -DCALLS_MAP=cilium_calls_lb -c $file -o - | llc -march=bpf -mcpu=v2 -mattr=dwarfris -filetype=obj -o mysock.o
bpftool cgroup detach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytestrm -f /sys/fs/bpf/tc/globals/mytest
tc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest
复制代码


3.测试


开启四个终端,分别执行如下命令(直接在主机上执行):


# command 1cat /sys/kernel/debug/tracing/trace_pipe# command 2tcpconnect -P 80# command 3tcplife -D 80# command 4curl 1.2.3.4:80
复制代码


因为我们会变更目的 ip,所以就基于端口来抓包。


用 tcplife 抓包,抓的是上面 perf 的 sock:inet_sock_set_state 时的状态。


用 tcpconnect 抓的是 connect() syscall 时的状态。


4.自己搭建 ebpf 环境

1.挂载 cgroup2


mkdir -p /run/cilium/cgroupv2mount -t cgroup2  none /run/cilium/cgroupv2/
复制代码


2.加载 ebp 程序


因为 centos8 自带的 tc 与 bpftool 版本有点低,所以使用 cilium 中已经适配好的版本。


docker run -it --name=mytest --network=host --privileged -v $PWD:/hosts/ -v /sys/fs/bpf:/sys/fs/bpf -v /run/cilium/cgroupv2/:/run/cilium/cgroupv2 cilium:v1.12.7 bash
cd /hosts/# 可以直接用之前编译好的文件tc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest
复制代码


很香!你会发现,功能已经实现了。


5.cilium 逻辑讲解


  • 框架已定型,通过在 ebpf 中,获取目的 ip 与目的端口,然后基于映射规则将目的 ip 与端口进行修改,从而实现 vip 到目的地址的转换。


  • 由于它是在 connect 阶段做的转换,类似在调用 connect 函数时注册一个回调函数,和 dnat 是不同的,所以不需要在回包时转换还原。


cilium service list
复制代码


这个命令可以查看 cilium 基于 service 配置的映射规则,ebpf 程序再从这个规则中找到合适的 bacend,并修改目的地址,然后完成转换。


6.展望

1.这个功能可以做什么?


服务暴露关心的主要是两点:1. vip 的高可用。2.负载均衡。而这两点,通过本文所介绍的方式都是可以实现的。


1.vip 的高可用


  • vip 的高可用,其本质就是在服务异常时,可以切换到服务 b(这里暂不考虑有状态服务分主备的情况)。


  • 当我们在客户端运行 ebpf 程序时,就不需要这个 vip 了。在应用时可以配置一个虚拟的地址,比如 1.2.3.4,由 ebpf 程序来决定转换到哪个实际的后端服务。而且当服务 a 异常后,可以变更映射规则,切换到服务 b。这一切对应用都是透明的。


2.负载均衡


  • 既然可以将目的地址映射到服务 a,那么基于服务 a,b,c 之间做负载均衡也是可行的。包括设置权重、熔断等。


  • 如 istio,需要在客户端注入 sidecar,运行 envoy 程序,其实也是相类似的逻辑,只不过它是通过代理实现。除了解析目的地址外,它还支持解析数据包,比如解析 http 协议,在异常时自动重试,实现服务切换应用无感知。相对于应用来说,只是卡顿了一下。


  • ebpf 程序能够满足大部分场景,而且很高效。


作者:

沃趣科技产品研发部

发布于: 刚刚阅读数: 3
用户头像

沃趣科技

关注

中立的企业级数据库云 2022-02-23 加入

专注数据库云生态领域,期待与各位一起探索技术奥秘,乐承分享乃永恒之道。

评论

发布
暂无评论
cilium 原理之sock_connect_数据库_沃趣科技_InfoQ写作社区