Microsoft MVP성태의 닷넷 이야기
Linux: 126. eBPF (bpf2go) - tcp_sendmsg 예제 [링크 복사], [링크+제목 복사],
조회: 681
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
13120정성태8/19/202221647Windows: 209. Windows NT Service에서 UI를 다루는 방법 [3]
13119정성태8/18/202221464.NET Framework: 2044. .NET Core/5+ 프로젝트에서 참조 DLL이 보관된 공통 디렉터리를 지정하는 방법
13118정성태8/18/202217038.NET Framework: 2043. WPF Color의 기본 색 영역은 (sRGB가 아닌) scRGB [2]
13117정성태8/17/202221949.NET Framework: 2042. C# 11 - 파일 범위 내에서 유효한 타입 정의 (File-local types)파일 다운로드1
13116정성태8/4/202222106.NET Framework: 2041. C# - Socket.Close 시 Socket.Receive 메서드에서 예외가 발생하는 문제파일 다운로드1
13115정성태8/3/202223089.NET Framework: 2040. C# - ValueTask와 Task의 성능 비교 [1]파일 다운로드1
13114정성태8/2/202223170.NET Framework: 2039. C# - Task와 비교해 본 ValueTask 사용법파일 다운로드1
13113정성태7/31/202222813.NET Framework: 2038. C# 11 - Span 타입에 대한 패턴 매칭 (Pattern matching on ReadOnlySpan<char>)
13112정성태7/30/202224379.NET Framework: 2037. C# 11 - 목록 패턴(List patterns) [1]파일 다운로드1
13111정성태7/29/202223820.NET Framework: 2036. C# 11 - IntPtr/UIntPtr과 nint/nuint의 통합파일 다운로드1
13110정성태7/27/202221613.NET Framework: 2035. C# 11 - 새로운 연산자 ">>>" (Unsigned Right Shift)파일 다운로드1
13109정성태7/27/202223921VS.NET IDE: 177. 비주얼 스튜디오 2022를 이용한 (소스 코드가 없는) 닷넷 모듈 디버깅 - "외부 원본(External Sources)" [1]
13108정성태7/26/202221576Linux: 53. container에 실행 중인 Golang 프로세스를 디버깅하는 방법 [1]
13107정성태7/25/202224461Linux: 52. Debian/Ubuntu 계열의 docker container에서 자주 설치하게 되는 명령어 [1]
13106정성태7/24/202218556오류 유형: 819. 닷넷 6 프로젝트의 "Conditional compilation symbols" 기본값 오류
13105정성태7/23/202223290.NET Framework: 2034. .NET Core/5+ 환경에서 (프로젝트가 아닌) C# 코드 파일을 입력으로 컴파일하는 방법 - 두 번째 이야기 [1]
13104정성태7/23/202227018Linux: 51. WSL - init에서 systemd로 전환하는 방법
13103정성태7/22/202221910오류 유형: 818. WSL - systemd-genie와 관련한 2가지(systemd-remount-fs.service, multipathd.socket) 에러
13102정성태7/19/202221333.NET Framework: 2033. .NET Core/5+에서는 구할 수 없는 HttpRuntime.AppDomainAppId
13101정성태7/15/202235762도서: 시작하세요! C# 10 프로그래밍
13100정성태7/15/202223683.NET Framework: 2032. C# 11 - shift 연산자 재정의에 대한 제약 완화 (Relaxing Shift Operator)
13099정성태7/14/202221799.NET Framework: 2031. C# 11 - 사용자 정의 checked 연산자파일 다운로드1
13098정성태7/13/202219473개발 환경 구성: 647. Azure - scale-out 상태의 App Service에서 특정 인스턴스에 요청을 보내는 방법 [1]
13097정성태7/12/202219051오류 유형: 817. Golang - binary.Read: invalid type int32
13096정성태7/8/202221850.NET Framework: 2030. C# 11 - UTF-8 문자열 리터럴
13095정성태7/7/202219057Windows: 208. AD 도메인에 참여하지 않은 컴퓨터에서 Kerberos 인증을 사용하는 방법
... 31  32  33  34  35  36  [37]  38  39  40  41  42  43  44  45  ...