BPF 获取 LVS FullNat 模式下的 Client IP

搞项目。

观测服务的请求调用需求是客观存在的。一般是需要观测服务的主动发起的调用信息,但是偶尔也会遇到需要观测服务被调用信息的需求。但是一般待采集的服务都是挂载在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文档。只有一些结构的定义。

1
2
3
4
5
6
7
/* MUST be 4 bytes alignment */
struct toa_data {
	__u8 opcode;
	__u8 opsize;
	__u16 port;
	__u32 ip;
};

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    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数据的处理。
这里附一段这里的逻辑:

 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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);
#else
	newsock = 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);
		else
			TOA_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 &&  // 254
				    TCPOLEN_TOA == opsize) {  // 8
					memcpy(&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 逻辑

直接上代码:

  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
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#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 10

typedef struct {
  u8  opcode;
  u8  opsize;
  u16 port;
  u32 ip;
} toa_data_t;

// 一般 toa 模块里只会填充一个 toa 数据
#define TCP_OPTION_LEN 1

struct 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 6

SEC("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 unroll
  for (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;
}

以上,周末愉快。

Hello, World!
使用 Hugo 构建
主题 StackJimmy 设计