eBPF (bpf2go) - tcp_sendmsg 예제
지난 글에서는 소켓 모니터링을 위한 별도의 BPF_PROG_TYPE_SOCKET_FILTER 유형을 살펴봤고,
eBPF (bpf2go) - BPF_PROG_TYPE_SOCKET_FILTER 예제 - SEC("socket")
; https://www.sysnet.pe.kr/2/0/14017
이번에는 TCP 전용으로 제공하는 tcp_sendmsg, tcp_recvmsg 커널 함수를 가로채는 예제를 다뤄볼 텐데요, 아래의 질문에 나온 코드로 시작해 보겠습니다.
TCP Packet Payload Sniffing with eBPF Kprobe Attachment to tcp_sendmsg
; https://stackoverflow.com/questions/79112904/tcp-packet-payload-sniffing-with-ebpf-kprobe-attachment-to-tcp-sendmsg
위의 예제를 참고해
BPF_PROG_TYPE_KPROBE(
kprobe/kretprobe) 유형으로는 이렇게 코딩할 수 있는데요,
SEC("kprobe/tcp_sendmsg")
int kprobe__tcp_sendmsg(struct pt_regs *ctx) {
struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
struct msghdr *msg = (struct msghdr *)PT_REGS_PARM2(ctx);
if (sk == NULL || msg == NULL) {
return 0;
}
__be16 lport = 0;
__u16 dport = 0;
bpf_probe_read_kernel(&lport, sizeof(lport), &sk->__sk_common.skc_num);
bpf_probe_read_kernel(&dport, sizeof(dport), &sk->__sk_common.skc_dport);
dport = bpf_ntohs(dport);
bpf_printk("kprobe__tcp_sendmsg: lport = %d, dport = %d\n", lport, dport);
}
테스트를 위해 C# 소켓 클라이언트 코드를 작성한 후,
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect("20.249.98.255", 80);
// Prepare HTTP GET request
string request = "GET / HTTP/1.1\r\nHost: www.sysnet.pe.kr\r\nConnection: close\r\n\r\n";
byte[] requestBytes = Encoding.ASCII.GetBytes(request);
Console.WriteLine($"Socket payload size: {requestBytes.Length} bytes");
// Send request
socket.Send(requestBytes);
// Receive response
var buffer = new byte[4096];
int bytesReceived;
var response = new StringBuilder();
do
{
bytesReceived = socket.Receive(buffer);
response.Append(Encoding.ASCII.GetString(buffer, 0, bytesReceived));
} while (bytesReceived > 0);
Console.WriteLine($"{socket.LocalEndPoint} <==> {socket.RemoteEndPoint}");
socket.Close();
실행하면 C# App 콘솔에는 대략 이런 출력이 나오고,
$ dotnet run
Socket payload size: 61 bytes
192.168.100.50:59718 <==> 20.249.98.255:80
eBPF 측 출력에서도 위의 포트 번호에 대한 로그가 찍히는 것을 확인할 수 있습니다.
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
testapp-2629028 [002] ...31 3694779.211927: bpf_trace_printk: kprobe__tcp_sendmsg: lport = 59718, dport = 80
물론,
BPF_PROG_TYPE_TRACING(
fentry/fexit) 유형으로 작성해도 코드를 거의 그대로 활용할 수 있습니다.
SEC("fentry/tcp_sendmsg")
int BPF_PROG(fentry_tcp_sendmsg, struct sock *sk, struct msghdr *msg)
{
__be16 lport = 0;
__u16 dport = 0;
bpf_probe_read_kernel(&lport, sizeof(lport), &sk->__sk_common.skc_num);
bpf_probe_read_kernel(&dport, sizeof(dport), &sk->__sk_common.skc_dport);
dport = bpf_ntohs(dport);
bpf_printk("fentry_tcp_sendmsg: lport = %d, dport = %d\n", lport, dport);
return 0;
}
기왕 알아본 김에,
TCP Packet Payload Sniffing with eBPF Kprobe Attachment to tcp_sendmsg 질문에 나온 패킷 페이로드 접근도 시도해 봤는데요, 우선 패킷의 전체 길이는 다음과 같이 구할 수 있었습니다.
struct iov_iter* iter = &(msg->msg_iter);
if (iter == NULL) {
return 0;
}
__u32 nr_segs = 0;
bpf_probe_read(&nr_segs, sizeof(nr_segs), &iter->nr_segs);
if (nr_segs == 0) {
return 0;
}
if (iter->iter_type != ITER_IOVEC) {
return 0;
}
__u64 iov_len;
bpf_probe_read(&iov_len, sizeof(__u64), &(iter->count));
bpf_printk("fentry_tcp_sendmsg: nr_segs = %d, iov_len = %d\n", nr_segs, iov_len);
역시 위에서 다룬 C# 예제 코드와 함께 실행하면 이런 로그가 나오는데요,
testapp-2629028 [002] ...11 3695873.126880: bpf_trace_printk: fentry_tcp_sendmsg: lport = 48428, dport = 80
testapp-2629028 [002] ...11 3695873.126882: bpf_trace_printk: fentry_tcp_sendmsg: nr_segs = 1, iov_len = 61
61 바이트 값이 C# 쪽에서 전송한 페이로드 크기와 정확히 일치합니다.
string request = "GET / HTTP/1.1\r\nHost: www.sysnet.pe.kr\r\nConnection: close\r\n\r\n";
byte[] requestBytes = Encoding.ASCII.GetBytes(request);
Console.WriteLine($"Socket payload size: {requestBytes.Length} bytes"); // requestBytes.Length == 61
그런데, 문제는 패킷 내용을 가져오는 것에는 실패한다는 점입니다. 제가 시도한 코드는 우선 iov 필드를 접근한 후,
const struct iovec *iov = (const struct iovec *)BPF_CORE_READ(iter, iov); // 커널 6.11(~ 6.14) 버전 환경이라면 eBPF 로드 단계에서 오류가 발생합니다.
if (iov == NULL) {
return 0;
}
bpf_printk("iovec == %p\n", iov);
iovec 구조체의,
struct iovec {
void *iov_base;
__kernel_size_t iov_len;
};
필드를 접근해 봤는데요,
void *iovbase;
bpf_probe_read(&iovbase, sizeof(iovbase), &iov[0].iov_base);
__u64 iovlen = 0;
bpf_probe_read(&iovlen, sizeof(iov_len), &iov[0].iov_len);
bpf_printk("iov = %p, iovbase = %p, iov_len = %d\n", iov, iovbase, iovlen);
// 출력 예: iov = 000000008ecaac37, iovbase = 00000000367bdf95, iov_len = 825184340
출력 결과가 좀 이상합니다. 게다가 여러 번 테스트를 해도, iov 포인터 값은 매번 바뀌는 반면 iovbase와 iov_len 값은 변함없이 저 값으로 나옵니다. iov_len 값 자체도 이상하지만, iovbase는 포인터이기 때문에 매번 다른 값이 나와야 정상입니다.
실제로 iovbase 주소를 통해 데이터를 읽어오려 하면,
char* ptr = (char*)iovbase;
bpf_printk("data: %c\n", ptr[0]);
eBPF 프로그램 로드 단계에서 permission denied 오류가 발생합니다.
load program: permission denied: 84: (71) r3 = *(u8 *)(r7 +0): R7 invalid mem access 'scalar' (112 line(s) omitted)
혹시나 싶어 bpf_probe_read_kernel() 함수로 읽어보면,
char data[4];
long ret = bpf_probe_read_kernel(data, 1, iovbase);
bpf_printk("ret == %d\n", ret); // 출력 결과: -34
/*
$ sudo apt install moreutils
$ errno 34
ERANGE 34 Numerical result out of range
*/
아마도 정상적인 포인터 주소가 아니므로 읽기에 실패해 ERANGE 오류가 나는 것 같습니다. 더 이상, 딱히 제 지식으로는 더 해볼 것이 없군요. ^^;
검색해 보면,
sock_sendmsg(): implicit function calls by Kernel
; https://stackoverflow.com/questions/21623814/sock-sendmsg-implicit-function-calls-by-kernel
Extract packet data using BPF from struct msghr
; https://stackoverflow.com/questions/76483747/extract-packet-data-using-bpf-from-struct-msghr
tcp_sendmsg가 아닌 "sock_sendmsg" 함수를 가로채서 시도한 사례가 있는데요, 이상하게 제 환경(Ubuntu 22.04, 24.04) 환경에서는 sock_sendmsg 함수가 동작하지 않습니다. 대신 이름을 바꾸는 경우,
SEC("fentry/sock_sendmsg_my")
int test_sock_sendmsg(void *ctx) {
bpf_printk("test_sock_sendmsg\n");
return 0;
}
eBPF 프로그램 로드 단계에서 "program myf_sock_sendmsg: attach Tracing/TraceFEntry: fentry sock_sendmsg_my not supported" 오류가 발생하는 걸로 봐서 sock_sendmsg 함수가 커널에서 없어진 것은 아닌 듯합니다. 암튼, 정상적인 이름으로 로딩을 해도 "test_sock_sendmsg" 메시지가 전혀 찍히지 않습니다.
참고로 현재 tcp_sendmsg의 커널 소스 코드는 이런데,
// https://github.com/torvalds/linux/blob/master/net/ipv4/tcp.c#L1388
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
int ret;
lock_sock(sk);
ret = tcp_sendmsg_locked(sk, msg, size);
release_sock(sk);
return ret;
}
EXPORT_SYMBOL(tcp_sendmsg);
과거에는 kiocb 구조체 포인터가 첫 번째 인자로 들어왔던 적이 있었던 것 같습니다.
// https://www.cubrid.org/blog/3826497
// int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t size)
$ cat vmlinux.h | grep -A 13 "struct kiocb {"
struct kiocb {
struct file *ki_filp;
loff_t ki_pos;
void (*ki_complete)(struct kiocb *, long int, long int);
void *private;
int ki_flags;
u16 ki_hint;
u16 ki_ioprio;
union {
unsigned int ki_cookie;
struct wait_page_queue *ki_waitq;
};
};
기타 소켓과 관련된 타입은 아래에 정리해 봤습니다.
$ cat vmlinux.h | grep -A 12 "struct socket {"
struct socket {
socket_state state;
short int type;
long unsigned int flags;
struct file *file;
struct sock *sk;
const struct proto_ops *ops;
long: 64;
long: 64;
long: 64;
struct socket_wq wq;
};
$ cat vmlinux.h | grep -A 9 "enum sock_type {"
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
$ cat vmlinux.h | grep -B 6 "} socket_state;"
typedef enum {
SS_FREE = 0,
SS_UNCONNECTED = 1,
SS_CONNECTING = 2,
SS_CONNECTED = 3,
SS_DISCONNECTING = 4,
} socket_state;
$ cat vmlinux.h | grep -A 105 "struct sock {"
struct sock {
struct sock_common __sk_common;
socket_lock_t sk_lock;
atomic_t sk_drops;
int sk_rcvlowat;
struct sk_buff_head sk_error_queue;
struct sk_buff *sk_rx_skb_cache;
struct sk_buff_head sk_receive_queue;
struct {
atomic_t rmem_alloc;
int len;
struct sk_buff *head;
struct sk_buff *tail;
} sk_backlog;
int sk_forward_alloc;
unsigned int sk_ll_usec;
unsigned int sk_napi_id;
int sk_rcvbuf;
int sk_wait_pending;
struct sk_filter *sk_filter;
union {
struct socket_wq *sk_wq;
struct socket_wq *sk_wq_raw;
};
struct xfrm_policy *sk_policy[2];
struct dst_entry *sk_rx_dst;
int sk_rx_dst_ifindex;
u32 sk_rx_dst_cookie;
struct dst_entry *sk_dst_cache;
atomic_t sk_omem_alloc;
int sk_sndbuf;
int sk_wmem_queued;
refcount_t sk_wmem_alloc;
long unsigned int sk_tsq_flags;
union {
struct sk_buff *sk_send_head;
struct rb_root tcp_rtx_queue;
};
struct sk_buff *sk_tx_skb_cache;
struct sk_buff_head sk_write_queue;
__s32 sk_peek_off;
int sk_write_pending;
__u32 sk_dst_pending_confirm;
u32 sk_pacing_status;
long int sk_sndtimeo;
struct timer_list sk_timer;
__u32 sk_priority;
__u32 sk_mark;
long unsigned int sk_pacing_rate;
long unsigned int sk_max_pacing_rate;
struct page_frag sk_frag;
netdev_features_t sk_route_caps;
netdev_features_t sk_route_nocaps;
netdev_features_t sk_route_forced_caps;
int sk_gso_type;
unsigned int sk_gso_max_size;
gfp_t sk_allocation;
__u32 sk_txhash;
u8 sk_padding: 1;
u8 sk_kern_sock: 1;
u8 sk_no_check_tx: 1;
u8 sk_no_check_rx: 1;
u8 sk_userlocks: 4;
u8 sk_pacing_shift;
u16 sk_type;
u16 sk_protocol;
u16 sk_gso_max_segs;
long unsigned int sk_lingertime;
struct proto *sk_prot_creator;
rwlock_t sk_callback_lock;
int sk_err;
int sk_err_soft;
u32 sk_ack_backlog;
u32 sk_max_ack_backlog;
kuid_t sk_uid;
u8 sk_prefer_busy_poll;
u16 sk_busy_poll_budget;
spinlock_t sk_peer_lock;
struct pid *sk_peer_pid;
const struct cred *sk_peer_cred;
long int sk_rcvtimeo;
ktime_t sk_stamp;
u16 sk_tsflags;
int sk_bind_phc;
u8 sk_shutdown;
atomic_t sk_tskey;
atomic_t sk_zckey;
u8 sk_clockid;
u8 sk_txtime_deadline_mode: 1;
u8 sk_txtime_report_errors: 1;
u8 sk_txtime_unused: 6;
struct socket *sk_socket;
void *sk_user_data;
void *sk_security;
struct sock_cgroup_data sk_cgrp_data;
struct mem_cgroup *sk_memcg;
void (*sk_state_change)(struct sock *);
void (*sk_data_ready)(struct sock *);
void (*sk_write_space)(struct sock *);
void (*sk_error_report)(struct sock *);
int (*sk_backlog_rcv)(struct sock *, struct sk_buff *);
void (*sk_destruct)(struct sock *);
struct sock_reuseport *sk_reuseport_cb;
struct bpf_local_storage *sk_bpf_storage;
struct callback_head sk_rcu;
};
$ cat vmlinux.h | grep -A 60 "struct sock_common {"
struct sock_common {
union {
__addrpair skc_addrpair;
struct {
__be32 skc_daddr;
__be32 skc_rcv_saddr;
};
};
union {
unsigned int skc_hash;
__u16 skc_u16hashes[2];
};
union {
__portpair skc_portpair;
struct {
__be16 skc_dport;
__u16 skc_num;
};
};
short unsigned int skc_family;
volatile unsigned char skc_state;
unsigned char skc_reuse: 4;
unsigned char skc_reuseport: 1;
unsigned char skc_ipv6only: 1;
unsigned char skc_net_refcnt: 1;
int skc_bound_dev_if;
union {
struct hlist_node skc_bind_node;
struct hlist_node skc_portaddr_node;
};
struct proto *skc_prot;
possible_net_t skc_net;
struct in6_addr skc_v6_daddr;
struct in6_addr skc_v6_rcv_saddr;
atomic64_t skc_cookie;
union {
long unsigned int skc_flags;
struct sock *skc_listener;
struct inet_timewait_death_row *skc_tw_dr;
};
int skc_dontcopy_begin[0];
union {
struct hlist_node skc_node;
struct hlist_nulls_node skc_nulls_node;
};
short unsigned int skc_tx_queue_mapping;
short unsigned int skc_rx_queue_mapping;
union {
int skc_incoming_cpu;
u32 skc_rcv_wnd;
u32 skc_tw_rcv_nxt;
};
refcount_t skc_refcnt;
int skc_dontcopy_end[0];
union {
u32 skc_rxhash;
u32 skc_window_clamp;
u32 skc_tw_snd_nxt;
};
};
$ cat vmlinux.h | grep -A 12 "struct msghdr {"
struct msghdr {
void *msg_name;
int msg_namelen;
struct iov_iter msg_iter;
union {
void *msg_control;
void *msg_control_user;
};
bool msg_control_is_user: 1;
__kernel_size_t msg_controllen;
unsigned int msg_flags;
struct kiocb *msg_iocb;
};
$ cat vmlinux.h | grep -A 22 "struct iov_iter {"
struct iov_iter {
u8 iter_type;
bool nofault;
bool data_source;
size_t iov_offset;
size_t count;
union {
const struct iovec *iov;
const struct kvec *kvec;
const struct bio_vec *bvec;
struct xarray *xarray;
struct pipe_inode_info *pipe;
};
union {
long unsigned int nr_segs;
struct {
unsigned int head;
unsigned int start_head;
};
loff_t xarray_start;
};
};
$ cat vmlinux.h | grep -A 7 "enum iter_type {"
enum iter_type {
ITER_IOVEC = 0,
ITER_KVEC = 1,
ITER_BVEC = 2,
ITER_PIPE = 3,
ITER_XARRAY = 4,
ITER_DISCARD = 5,
};
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]