Microsoft MVP성태의 닷넷 이야기
Linux: 126. eBPF (bpf2go) - tcp_sendmsg 예제 [링크 복사], [링크+제목 복사],
조회: 435
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 

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,
};




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 10/14/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
14025정성태10/14/2025436Linux: 126. eBPF (bpf2go) - tcp_sendmsg 예제
14024정성태10/14/2025484오류 유형: 984. Whisper.net - System.Exception: 'Cannot dispose while processing, please use DisposeAsync instead.'
14023정성태10/12/2025988닷넷: 2369. C# / Whisper 모델 - 동영상의 음성을 인식해 자동으로 SRT 자막 파일을 생성 [1]파일 다운로드1
14022정성태10/10/20251888닷넷: 2368. C# / NAudio - (AI 학습을 위해) 무음 구간을 반영한 오디오 파일 분할파일 다운로드1
14021정성태10/6/20252483닷넷: 2367. C# - Youtube 동영상 다운로드 (YoutubeExplode 패키지) [1]파일 다운로드1
14020정성태10/2/20252130Linux: 125. eBPF - __attribute__((preserve_access_index)) 활용 사례
14019정성태10/1/20252305Linux: 124. eBPF - __sk_buff / sk_buff 구조체
14018정성태9/30/20251679닷넷: 2366. C# - UIAutomationClient를 이용해 시스템 트레이의 아이콘을 열거하는 방법파일 다운로드1
14017정성태9/29/20252150Linux: 123. eBPF (bpf2go) - BPF_PROG_TYPE_SOCKET_FILTER 예제 - SEC("socket")
14016정성태9/28/20252376Linux: 122. eBPF - __attribute__((preserve_access_index)) 사용법
14015정성태9/22/20251875닷넷: 2365. C# - FFMpegCore를 이용한 MP4 동영상으로부터 MP3 음원 추출 예제파일 다운로드1
14014정성태9/17/20251893닷넷: 2364. C# - stun.l.google.com을 사용해 공용 IP 주소와 포트를 알아내는 방법파일 다운로드1
14013정성태9/14/20252539닷넷: 2363. C# - Whisper.NET Library를 이용해 음성을 텍스트로 변환 및 번역하는 예제파일 다운로드1
14012정성태9/9/20252772닷넷: 2362. C# - Windows.Media.Ocr: 윈도우 운영체제에 포함된 OCR(Optical Character Recognition)파일 다운로드1
14011정성태9/7/20253404닷넷: 2361. C# - Linux 환경의 readlink 호출
14010정성태9/1/20253243오류 유형: 983. apt update 시 "The repository 'http://deb.debian.org/debian buster Release' does not have a Release file." 오류
14009정성태8/28/20253675닷넷: 2360. C# 14 - (11) Expression Tree에 선택적 인수와 명명된 인수 허용파일 다운로드1
14008정성태8/26/20254227닷넷: 2359. C# 14 - (10) 복합 대입 연산자의 오버로드 지원파일 다운로드1
14007정성태8/25/20254657닷넷: 2358. C# - 현재 빌드에 적용 중인 컴파일러 버전 확인 방법 (#error version)
14006정성태8/23/20254954Linux: 121. Linux - snap 패키지 관리자로 설치한 소프트웨어의 디렉터리 접근 제한
14005정성태8/21/20253921오류 유형: 982. sudo: unable to load /usr/libexec/sudo/sudoers.so: libssl.so.3: cannot open shared object file: No such file or directory
14004정성태8/21/20254566오류 유형: 981. dotnet 실행 시 No usable version of the libssl was found
14003정성태8/21/20254783닷넷: 2357. C# 14 - (9) 새로운 지시자 추가 (Ignored directives)
14002정성태8/20/20254935오류 유형: 980. C# - appsettings.json 파일의 설정값이 적용 안 된다면?
14001정성태8/19/20259732닷넷: 2356. .NET SDK 10 - 단일 소스 코드 파일을 빌드/실행하는 기능을 "dotnet" 명령어에 추가 [1]
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...