当前位置: 首页 > news >正文

BPF 获取 LVS FullNat 模式下的 Client IP - liyan

观测服务的请求调用需求是客观存在的。一般是需要观测服务的主动发起的调用信息,但是偶尔也会遇到需要观测服务被调用信息的需求。但是一般待采集的服务都是挂载在LVS下面的。这就势必涉及到LVS预设的工作模式下,一般都是FULLNET,需要的real client ip的信息获取方式。
笔者通过调研,实现了一种通过BPF来观测挂载在LVS下的RS被调用TCP连接信息的方式。本文中关于toa的操作及代码定义均引用自Huawei/TCP_option_address。

一、效果

先看下采集效果:

upload successful

二、LVS FullNat

关于LVSNat,DR,Tun以及FullNat模式的介绍已经有了很多的资料,比如这篇文章就介绍的很详细。这里笔者附上FullNat模式下的示意图:

upload successful

如图所示,如果需要在RS上获取CIP,就涉及到TOA信息的解析。TOA (tcp optional address)是利用tcp协议option字段来传递信息的一种工作方式。关于TOA的约定笔者并没有找到官方的RFC文档。只有一些结构的定义。

/* MUST be 4 bytes alignment */
struct toa_data {__u8 opcode;__u8 opsize;__u16 port;__u32 ip;
};

同时,rfc793里对TCP header的约定如下,理论上toa_data应该写在Options字段中。

    0                   1                   2                   3   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|          Source Port          |       Destination Port        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                        Sequence Number                        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    Acknowledgment Number                      |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|  Data |           |U|A|P|R|S|F|                               || Offset| Reserved  |R|C|S|S|Y|I|            Window             ||       |           |G|K|H|T|N|N|                               |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|           Checksum            |         Urgent Pointer        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    Options                    |    Padding    |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                             data                              |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+TCP Header Format

一般来说,将real-client ip写入tcp option字段的操作是在LVS上进行的。而解析并且方便RS操作,主要是需要在getname的时候需要返回real-client ip以便于做进一步的业务逻辑,比如按照IP限流等,是RStoa模块在操作的。一般是在tcp握手的第三个SYN报文处理时,toa.ko通过tcp_v4_syn_recv_sock处理的hook函数方式来触发toa数据的处理。
这里附一段这里的逻辑:

static struct sock *
tcp_v4_syn_recv_sock_toa(struct sock *sk, struct sk_buff *skb,struct request_sock *req, struct dst_entry *dst)
#endif
{struct sock *newsock = NULL;TOA_DBG("tcp_v4_syn_recv_sock_toa called
");/* call orginal one */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4,4,0)newsock = tcp_v4_syn_recv_sock(sk, skb, req, dst, req_unhash, own_req);
#elsenewsock = tcp_v4_syn_recv_sock(sk, skb, req, dst);
#endif/* set our value if need */if (NULL != newsock && NULL == newsock->sk_user_data) {newsock->sk_user_data = get_toa_data(skb);if (NULL != newsock->sk_user_data)TOA_INC_STATS(ext_stats, SYN_RECV_SOCK_TOA_CNT);elseTOA_INC_STATS(ext_stats, SYN_RECV_SOCK_NO_TOA_CNT);TOA_DBG("tcp_v4_syn_recv_sock_toa: set ""sk->sk_user_data to %p
",newsock->sk_user_data);}return newsock;
}static void *get_toa_data(struct sk_buff *skb)
{struct tcphdr *th;int length;unsigned char *ptr;struct toa_data tdata;void *ret_ptr = NULL;unsigned char buff[(15 * 4) - sizeof(struct tcphdr)];TOA_DBG("get_toa_data called
");if (NULL != skb) {th = tcp_hdr(skb);length = (th->doff * 4) - sizeof(struct tcphdr);ptr = skb_header_pointer(skb, sizeof(struct tcphdr),length, buff);if (!ptr)return NULL;while (length > 0) {int opcode = *ptr++;int opsize;switch (opcode) {case TCPOPT_EOL:return NULL;case TCPOPT_NOP:	/* Ref: RFC 793 section 3.1 */length--;continue;default:opsize = *ptr++;if (opsize < 2)	/* "silly options" */return NULL;if (opsize > length)/* don't parse partial options */return NULL;if (TCPOPT_TOA == opcode &&  // 254TCPOLEN_TOA == opsize) {  // 8memcpy(&tdata, ptr - 2, sizeof(tdata));TOA_DBG("find toa data: ip = ""%u.%u.%u.%u, port = %u
",NIPQUAD(tdata.ip),ntohs(tdata.port));memcpy(&ret_ptr, &tdata,sizeof(ret_ptr));TOA_DBG("coded toa data: %p
",ret_ptr);return ret_ptr;}ptr += opsize - 2;length -= opsize;}}}return NULL;
}

可以看到,这里首先调用了原有的tcp_v4_syn_recv_sock函数,并且在sk_user_data未被占用的情况下,通过get_toa_data的方式,从原始的skb中将toa信息解析出来,并将数据赋值给sk->sk_user_data
虽然这部分逻辑并不完全理解,但是从逻辑来看,只要读取sk_user_data并且判断其中是否有符合条件的值,即可获取real-client ip
至此,基本的逻辑就梳理出来了。对应的BPF处理逻辑也就很清晰了。

三、BPF 逻辑

直接上代码:

#define INADDR_LOOPBACK      0x7f000001 /* 127.0.0.1   */
#define INADDR_LOOPBACK_HOST INADDR_LOOPBACK
#define INADDR_LOOPBACK_NET  0x0100007f /* 127.0.0.1   */#define ns2sec(ns) ((ns) / (1000 * 1000 * 1000))
#ifndef memcpy
#define memcpy(dest, src, n) __builtin_memcpy((dest), (src), (n))
#endif#define MERGE_SEC 10typedef struct {u8  opcode;u8  opsize;u16 port;u32 ip;
} toa_data_t;// 一般 toa 模块里只会填充一个 toa 数据
#define TCP_OPTION_LEN 1struct tcp_event {u32        raddr;u32        laddr;u16        rport;u16        lport;int        err;u64        toa_addr;toa_data_t toa_data;u64        sec;u64        ns;
};
typedef struct tcp_event tcp_event_t;
const struct tcp_event*  unused_0x01 __attribute__((unused));struct {__uint(type, BPF_MAP_TYPE_LRU_HASH);__uint(key_size, sizeof(tcp_event_t));__uint(value_size, sizeof(u64)); // timestamp__uint(max_entries, 1024);
} tcp_event_map SEC(".maps");struct {__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);__uint(max_entries, 1024);
} events SEC(".maps");enum toa_type {ipopt_toa        = 254, // IP_v4 客户端 IP,目前仅考虑
};#define _AF_INET     2 /* internetwork: UDP, TCP, etc. */
#define _IPPROTO_TCP 6SEC("kretprobe/inet_csk_accept")
int kretprobe__inet_csk_accept(struct pt_regs* ctx) {u64          start_ns = bpf_ktime_get_ns();tcp_event_t  event    = {};struct sock* sk       = (struct sock*)PT_REGS_RC(ctx);if (sk == NULL) {return 0;}struct sock_common sk_common = {};bpf_probe_read(&sk_common, sizeof(sk_common), (const void*)(sk));if (sk_common.skc_family != _AF_INET) {return 0;}// 不处理本地回环if (sk_common.skc_rcv_saddr == INADDR_LOOPBACK_NET ||sk_common.skc_daddr == INADDR_LOOPBACK_NET) {return 0;}event.laddr = bpf_ntohl(sk_common.skc_rcv_saddr);event.raddr = bpf_ntohl(sk_common.skc_daddr);event.lport = sk_common.skc_num;event.rport = bpf_ntohs(sk_common.skc_dport);int        err;toa_data_t toa_data[TCP_OPTION_LEN] = {};err = BPF_CORE_READ_INTO(&toa_data, sk, sk_user_data);if (err) {return 0;}u8 i = 0;
#pragma unrollfor (i = 0; i < TCP_OPTION_LEN; i++) {if (toa_data[i].opcode != ipopt_toa) {continue;}memcpy(&event.toa_data, &toa_data[i], sizeof(toa_data_t));}u32 raddr = event.raddr;if (event.toa_data.ip != 0 && event.toa_data.port != 0) {// 挂载在 lvs 时,DS 的 IP 会发生变更。这里也给聚合掉。event.raddr = 0;}// remote port 都不要event.toa_data.port = 0;event.rport         = 0;u64  sec            = 0;u64  now_ns         = bpf_ktime_get_ns();u64* last_ns        = (u64*)bpf_map_lookup_elem(&tcp_event_map, &event);if (last_ns != NULL) {sec = ns2sec((now_ns - *last_ns));if (sec <= MERGE_SEC) {return 0;}} else {sec = 99;}bpf_map_update_elem(&tcp_event_map, &event, &now_ns, BPF_ANY);event.sec   = sec;event.raddr = raddr;u64 end_ns  = bpf_ktime_get_ns();event.ns    = end_ns - start_ns;bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));return 0;
}

以上,周末愉快。

http://www.jsqmd.com/news/481755/

相关文章:

  • 解决RDK X5(ARM64架构)板卡Remote-SSH运行Antigravity AI崩溃(SIGILL):Samba网络盘本地挂载方案
  • 强烈安利! AI论文工具,千笔AI VS 灵感风暴AI,专科生必备神器!
  • centos 安装docker并构建golang镜像 - liyan
  • 狡猾的北狐狸
  • 老王-三观稳则人生稳
  • centos 构建 local-k8s - liyan
  • 老王-老祖宗没说完的后半句
  • 2026涂塑钢管市场评测:哪些公司口碑较好?IPN8710防腐钢管/圆孔滤水钢管 ,涂塑钢管制造厂家推荐排行榜单 - 品牌推荐师
  • 老王-缺心眼是边界感缺失
  • clisp编译 - liyan
  • 老王-家兴不在运在德
  • challenges of bpf tracing go - liyan
  • 泛型体系实战
  • 电机设计仿真:Maxwell ANSYS 五相电机设计
  • Openclaw 附录B 常用Skills清单
  • 053店铺租赁租凭平台系统-springboot+vue
  • Openclaw 附录C 配置模板与自定义参考
  • babyRE
  • 云上OpenClaw实操视频教程合集
  • LeetCode:70. 爬楼梯
  • 青岛东泉环保智能设备有限公司电话查询:供应商联络方式与注意事项 - 品牌推荐
  • 零人类公司编排框架Paperclip的安装
  • CWT-CNN-GRU基于连续小波变换和卷积神经网络-门控循环单元故障诊断MATLAB代码
  • CSDN博客汇总(101-200篇)
  • OpenClaw 核心技术框架深度
  • COMSOL模拟含裂缝地层流动与传热耦合及油藏数值模拟:注入井与生产井的交叉裂缝流动考虑
  • L2-020
  • 【后端必看】什么是 Elasticsearch?都要学什么?
  • 聊聊rpa机器人开发课程,无锡文友信息推荐及费用多少 - myqiye
  • Openclaw 附录A 命令速查表