#keywords sk_buff, skb, socket, buffer, skb_push, skb_pull, skb_put, frag_list, GSO, TSO, skb_segment, VLAN, BPF, __sk_buff, flow_dissector, RSS, RPS, encapsulation, tunnel, kmem_cache, netns #title Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여 [wiki:Home 대문] / [wiki:CategoryProgramming 프로그래밍] / [wiki:skbuff Linux Kernel의 skbuff에 대하여] ---- == [wiki:skbuff Linux Kernel의 skbuff(Socket buffer descriptors)에 대하여] == * 작성자 조재혁([mailto:minzkn@minzkn.com]) * 고친과정 2020년 12월 07일 : 처음씀 [[TableOfContents]] 최근 정리 내용은 [[HTML(리눅스 커널 정리 (Linux 커널 개발자를 위한 종합 한글 레퍼런스))]] 을 참고하세요. == 개요 == |Linux packet journey,napi, hardware queue,skb| 참고 영상 || || [[Play(https://youtu.be/6Fl1rsxk4JQ)]] || Linux kernel 에서 sk_buff (skb) 자료형은 network packet 을 처리하는데 중요한 부분입니다. 이 문서는 이를 설명하기 위해서 작성되었습니다. 이 문서는 저 혼자 모든 것을 이해하고 작성한 것이 절대로 아니며 수 많은 검색과 분석을 통하여 먼저 선두에서 정보를 공유해주신 수많은 이름 모를 선배님들의 발자취에 의해서 작성한 것입니다. 개인 적인 관점에서의 해석이 틀릴 수 있으며 이러한 부분을 알려주시면 내용을 갱신하겠습니다. sk_buff (skb) 는 대략 다음과 같은 흐름내에서 데이터를 다루는게 목적입니다. 여기서 패킷의 인입(Input)/포워딩(Forward)/출력(Output) 이 어디서 일어나는지를 중심으로 파악되고 있어야 할 필요가 있습니다. (아래 그림은 IPSec packet 관점에서 그린것이므로 IPSec 용어만 빼고 일반 네트워크 패킷으로 가정하여 보시면 됩니다.) [attachment:VirtualPrivateNetwork/An_IPsec_Packet_flow_2017.07.18_v0.4_(Linux_kernel_v3.8_vanilla_source_기준).png] * 인입(Input)에는 크게 두 가지로 나뉩니다. (위 그림상에서는 좌측 상단 구름) == sk_buff 자료구조 == [[attachment:sk_buff-structure-0-20220812.png]] Linux 커널 네트워크 스택의 핵심 자료구조 sk_buff: 메모리 레이아웃, 데이터 조작 함수, clone/copy, 소켓(struct sock) 연동, 소켓 옵션, raw socket, 체크섬 오프로드, GSO/GRO, zero-copy 전송까지 종합 분석합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다. '''주의''' 전제 조건: 네트워크 스택 (https://minzkn.com/linuxkernel/pages/networking-overview.html)과 네트워크 디바이스 드라이버 (https://minzkn.com/linuxkernel/pages/net-device-driver.html) 문서를 먼저 읽으세요. 고성능 패킷 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다. '''팁''' 일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다. 일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연과 처리량이 달라지는지 명확해집니다. === 핵심 요약 === * 메모리 레이아웃 — {{{head}}}, {{{data}}}, {{{tail}}}, {{{end}}} 4개 포인터로 버퍼 관리. {{{skb_push/pull/put}}}로 데이터 영역 조작. * 참조 모델 — {{{clone}}}은 메타데이터만 복사하고 버퍼 공유, {{{copy}}}는 완전 복사. 참조 카운트 관리가 핵심. * 소켓 메모리 — {{{sk_rmem_alloc}}}/{{{sk_wmem_alloc}}}이 {{{truesize}}} 기반 소켓 버퍼 제한 구현. * 헤더 포인터 — {{{mac_header}}}, {{{network_header}}}, {{{transport_header}}}로 L2/L3/L4 헤더 오프셋 추적. * 수명주기 — 할당 → 프로토콜 처리 → 소켓 전달 → 사용자 복사 → 해제. 각 단계에서 다른 함수와 상태 변화. * skb 확장 — {{{skb_ext}}}로 conntrack, IPsec secpath, bridge NF 등 가변 메타데이터를 skb에 동적 연결. 5.x+에서 메모리 효율 향상. * page_pool — 최신 고성능 드라이버(6.x+)는 page_pool로 DMA 매핑 캐시와 페이지 재활용을 구현해 할당/해제 비용 최소화. * XDP 인터페이스 — {{{xdp_buff}}}는 skb 할당 이전 단계로 동작. XDP_PASS 시 {{{build_skb()}}}를 통해 sk_buff로 변환되어 일반 스택 진입. === 단계별 이해 === 1. 구조체 이해 4개 포인터(head, data, tail, end)와 3개 헤더 포인터(mac, network, transport)의 관계를 먼저 익힙시다. 이게 sk_buff의 핵심입니다. 2. 데이터 조작 함수 {{{skb_push}}}(헤더 추가), {{{skb_pull}}}(헤더 제거), {{{skb_put}}}(데이터 추가)의 동작을 코드로 직접 연습합니다. 3. 할당 함수 선택 {{{alloc_skb}}}(일반), {{{netdev_alloc_skb}}}(드라이버 수신), {{{napi_alloc_skb}}}(NAPI), {{{page_pool_alloc_pages}}}(6.x 고성능) 차이점을 파악합니다. 4. 수명주기 추적 수신 경로(NIC → 드라이버 → L2 → L3 → L4 → 소켓)와 전송 경로의 함수 호출 체인을 따라가며 이해합니다. 5. 확장 시스템 학습 {{{skb_ext}}}, {{{page_pool}}}, XDP ↔ sk_buff 변환 등 현대 커널의 최적화 메커니즘을 파악해 고성능 드라이버 코드를 읽는 눈을 키웁니다. 6. 실전 디버깅 연습 {{{perf trace -e skb:kfree_skb}}}로 드롭 원인을 추적하고, {{{/proc/net/softnet_stat}}}으로 CPU별 처리량을 분석하며, {{{dropwatch}}}로 병목 지점을 찾아봅니다. '''정보''' 관련 표준: IEEE 802.3 (Ethernet 프레임), RFC 791 (IPv4), RFC 8200 (IPv6) — sk_buff는 이 표준들이 정의하는 패킷 구조를 커널 내부에서 표현합니다. 종합 목록은 참고자료 — 표준 & 규격 (https://minzkn.com/linuxkernel/pages/references.html#std-networking) 섹션을 참고하세요. === 개요 === {{{struct sk_buff}}}(흔히 skb라 부름)는 Linux 네트워크 스택에서 모든 패킷을 표현하는 자료구조입니다. 수신 패킷이든 전송 패킷이든, L2 프레임이든 L4 세그먼트든, 커널 내에서 네트워크 데이터가 이동할 때는 항상 sk_buff가 사용됩니다. O'Reilly의 "Understanding Linux Network Internals"에서 Christian Benvenuti는 sk_buff를 "네트워크 스택의 왕(king of networking)"이라 표현했습니다. 이 자료구조를 이해하지 않고는 Linux 네트워크 코드를 읽을 수 없습니다. * 헤더 파일: {{{}}} * 주요 소스: {{{net/core/skbuff.c}}} * 구조체 크기: x86_64에서 약 232바이트 (커널 6.x 기준) === struct sk_buff 주요 필드 === {{{#!plain [코드: C] /* include/linux/skbuff.h (주요 필드만 발췌) */ struct sk_buff { union { struct { struct sk_buff *next; /* 리스트 내 다음 skb */ struct sk_buff *prev; /* 리스트 내 이전 skb */ }; struct rb_node rbnode; /* TCP retransmit queue용 */ }; struct sock *sk; /* 소속 소켓 */ struct net_device *dev; /* 수신/전송 네트워크 디바이스 */ unsigned int len; /* 전체 데이터 길이 (linear + frags) */ unsigned int data_len; /* 비선형(paged) 데이터 길이 */ __u16 mac_len; /* MAC 헤더 길이 */ __u16 hdr_len; /* 클론 시 writable 헤더 길이 */ __be16 protocol; /* 패킷 프로토콜 (ETH_P_IP 등) */ __u32 priority; /* QoS 우선순위 */ sk_buff_data_t transport_header; /* L4 헤더 오프셋 */ sk_buff_data_t network_header; /* L3 헤더 오프셋 */ sk_buff_data_t mac_header; /* L2 헤더 오프셋 */ sk_buff_data_t tail; /* 데이터 끝 */ sk_buff_data_t end; /* 할당된 버퍼 끝 */ unsigned char *head; /* 할당된 버퍼 시작 */ unsigned char *data; /* 실제 데이터 시작 */ unsigned int truesize; /* 실제 메모리 사용량 */ refcount_t users; /* 참조 카운트 */ }; }}} ==== 자주 사용되는 추가 필드 ==== 위의 핵심 필드 외에도, sk_buff에는 프로토콜 처리와 성능 최적화에 쓰이는 중요한 필드들이 있습니다: {{{#!plain [코드: C] struct sk_buff { /* ... 위의 핵심 필드들 ... */ char cb[48]; /* 프로토콜별 제어 블록 (Control Buffer) */ __u32 hash; /* 패킷 해시 (RSS, flow steering) */ __u8 pkt_type:3; /* PACKET_HOST, PACKET_BROADCAST 등 */ __u8 ip_summed:2; /* 체크섬 오프로드 상태 */ __u32 mark; /* netfilter/tc 마킹 (iptables -j MARK) */ __u16 queue_mapping; /* 멀티큐 NIC 큐 인덱스 */ unsigned int napi_id; /* NAPI 구조체 ID (busy polling) */ union { __u32 tstamp; /* 수신 타임스탬프 */ u64 skb_mstamp_ns; /* 고해상도 타임스탬프 */ }; __u8 cloned:1; /* clone 여부 */ __u8 nohdr:1; /* 페이로드 참조만 (헤더 없음) */ __u8 peeked:1; /* MSG_PEEK으로 이미 확인됨 */ }; }}} ||필드||크기||용도||접근 방법|| ||{{{cb[48]}}}||48바이트||프로토콜 레이어가 임시 데이터 저장 (TCP: {{{tcp_skb_cb}}})||{{{TCP_SKB_CB(skb)}}}, {{{IPCB(skb)}}}|| ||{{{hash}}}||32비트||수신 패킷의 flow hash (RSS, RPS에 활용)||{{{skb_get_hash(skb)}}}|| ||{{{pkt_type}}}||3비트||패킷 수신 유형: HOST, BROADCAST, MULTICAST, OTHERHOST||직접 접근|| ||{{{ip_summed}}}||2비트||체크섬 오프로드 모드 (NONE/UNNECESSARY/COMPLETE/PARTIAL)||직접 접근|| ||{{{mark}}}||32비트||netfilter, tc, 라우팅 결정에 사용되는 패킷 마크||직접 접근|| ||{{{queue_mapping}}}||16비트||멀티큐 NIC에서 TX/RX 큐 선택||{{{skb_get_queue_mapping(skb)}}}|| ||{{{napi_id}}}||32비트||NAPI 인스턴스 식별 (SO_BUSY_POLL 연동)||직접 접근|| '''정보''' {{{cb[48]}}}은 각 프로토콜 레이어가 자신만의 구조체로 캐스팅하여 사용합니다. 예를 들어 TCP는 {{{TCP_SKB_CB(skb)}}}로 {{{struct tcp_skb_cb}}}에 시퀀스 번호, 타임스탬프 등을 저장합니다. 한 레이어가 cb를 사용하면 다음 레이어에서 덮어쓰므로, 레이어 간 데이터 전달에는 사용하지 마세요. ==== 체크섬 오프로드와 ip_summed ==== {{{ip_summed}}} 필드는 NIC 하드웨어의 체크섬 오프로드 상태를 나타내는 핵심 플래그입니다: ||값||의미 (RX)||의미 (TX)|| ||{{{CHECKSUM_NONE}}}||HW 미지원, SW 검증 필요||SW가 체크섬 계산 완료|| ||{{{CHECKSUM_UNNECESSARY}}}||HW 검증 완료, 유효함||체크섬 불필요 (loopback 등)|| ||{{{CHECKSUM_COMPLETE}}}||HW가 전체 체크섬 제공||사용 안 함|| ||{{{CHECKSUM_PARTIAL}}}||사용 안 함||HW에 체크섬 계산 위임|| {{{#!plain [코드: C] /* 수신: 드라이버에서 체크섬 상태 설정 */ void my_driver_rx(struct sk_buff *skb, bool csum_ok) { if (csum_ok) { skb->ip_summed = CHECKSUM_UNNECESSARY; /* SW 검증 생략 */ } else { skb->ip_summed = CHECKSUM_NONE; /* SW 검증 필요 */ } } /* 전송: CHECKSUM_PARTIAL 설정 예 */ skb->ip_summed = CHECKSUM_PARTIAL; skb->csum_start = skb_transport_header(skb) - skb->head; skb->csum_offset = offsetof(struct udphdr, check); }}} '''팁''' 경험적 팁: 실제 네트워크 드라이버 개발 시, {{{CHECKSUM_UNNECESSARY}}}를 설정하면 TCP/UDP 프로토콜 스택에서 {{{__skb_checksum_validate_needed()}}}를 건너뛰어 CPU 사이클을 크게 절약합니다. 하지만 일부 buggy NIC에서는 가짜 양수(false positive)가 발생할 수 있어, 문제 발생 시 {{{ethtool -K eth0 rx-checksumming off}}}로 비활성화하고 테스트하세요. === skb 할당과 해제 === sk_buff 할당 함수는 사용 상황에 따라 여러 변형이 있습니다: ||함수||컨텍스트||특징|| ||{{{alloc_skb(size, gfp)}}}||일반 (프로세스/softirq)||기본 할당 함수. kmalloc으로 linear 버퍼 할당|| ||{{{netdev_alloc_skb(dev, len)}}}||NAPI/irq 수신 경로||NET_SKB_PAD headroom 자동 확보, per-CPU 캐시 활용|| ||{{{napi_alloc_skb(napi, len)}}}||NAPI poll 내부||NAPI 전용 per-CPU 페이지 캐시, 최적 성능|| ||{{{build_skb(data, frag_size)}}}||사전 할당 버퍼||이미 할당된 버퍼에 skb 메타데이터만 생성|| ||{{{__alloc_skb(size, gfp, flags)}}}||내부 API||SKB_ALLOC_FCLONE, SKB_ALLOC_RX 등 플래그 지정|| {{{#!plain [코드: C] /* 일반적인 전송 경로 할당 */ struct sk_buff *skb = alloc_skb(MAX_HEADER + payload_len, GFP_KERNEL); if (!skb) return -ENOMEM; skb_reserve(skb, MAX_HEADER); /* L2/L3/L4 헤더용 headroom */ /* NAPI 수신 경로 할당 (드라이버 내) */ struct sk_buff *skb = napi_alloc_skb(napi, 256); /* 헤더만 linear */ /* 페이로드는 page fragment로 추가 */ skb_add_rx_frag(skb, 0, page, offset, size, truesize); /* build_skb: XDP, 고성능 드라이버에서 사용 */ void *buf = page_address(page); struct sk_buff *skb = build_skb(buf, PAGE_SIZE); if (!skb) { put_page(page); return; } skb_reserve(skb, headroom); }}} 해제 함수도 상황에 따라 구분됩니다: ||함수||용도||tracepoint|| ||{{{kfree_skb(skb)}}}||패킷 드롭 (에러/필터링)||{{{skb:kfree_skb}}} 발생 (원인 추적 가능)|| ||{{{consume_skb(skb)}}}||정상적 소비 완료||{{{skb:consume_skb}}} 발생|| ||{{{dev_kfree_skb_any(skb)}}}||드라이버 (irq/process 모두)||컨텍스트에 따라 지연 해제 가능|| ||{{{dev_consume_skb_any(skb)}}}||드라이버 정상 소비||irq-safe한 consume_skb|| ||{{{kfree_skb_reason(skb, reason)}}}||드롭 원인 명시 (6.x+)||드롭 원인을 enum으로 기록|| '''주의''' {{{kfree_skb()}}}와 {{{consume_skb()}}}의 차이는 기능적으로 동일하지만, tracepoint가 다릅니다. 정상 경로에서 {{{kfree_skb()}}}를 사용하면 드롭 모니터링 도구가 오탐을 보고합니다. 반드시 의미에 맞는 함수를 사용하세요. === 메모리 레이아웃 === sk_buff의 데이터 영역은 4개의 핵심 포인터로 관리됩니다: {{{#!plain [SVG 텍스트 변환: sk_buff 메모리 레이아웃 다이어그램] 캡션: head~end: 할당된 버퍼 전체, data~tail: 현재 유효 데이터, end 뒤에 skb_shared_info 텍스트 요소: - sk_buff 메모리 레이아웃 - headroom - 데이터 영역 - (len - data_len) - tailroom - head - data - tail - end - skb_ - shared_ - info - (frags[]) }}} 이 레이아웃은 불변식 {{{head ≤ data ≤ tail ≤ end}}}를 항상 유지합니다. 이 조건이 깨지면 커널 패닉 또는 메모리 손상으로 이어집니다. {{{skb_push()}}}/{{{skb_pull()}}}/{{{skb_put()}}} 계열 함수는 호출 전에 이 불변식을 검증하며, 위반 시 {{{skb_over_panic}}} / {{{skb_under_panic}}}을 트리거합니다. ==== 메모리 관련 주요 매크로 ==== sk_buff 버퍼 크기를 계산할 때 자주 사용되는 매크로입니다: ||매크로||정의 (개념)||용도|| ||{{{SKB_DATA_ALIGN(X)}}}||{{{ALIGN(X, SMP_CACHE_BYTES)}}}||SMP 캐시 라인 단위(보통 64B)로 올림 정렬. {{{end}}} 포인터 계산에 사용.|| ||{{{SKB_WITH_OVERHEAD(X)}}}||{{{X - SKB_DATA_ALIGN(sizeof(struct skb_shared_info))}}}||할당 크기 X에서 {{{skb_shared_info}}}를 뺀 실제 사용 가능한 linear 데이터 크기.|| ||{{{SKB_TRUESIZE(X)}}}||{{{SKB_DATA_ALIGN(X + sizeof(struct sk_buff)) + SKB_DATA_ALIGN(sizeof(struct skb_shared_info))}}}||X바이트 데이터를 담는 sk_buff를 할당할 때 실제로 필요한 총 메모리 크기. {{{skb->truesize}}} 초기값으로 사용.|| ||{{{SKB_MAX_HEAD(X)}}}||{{{SKB_WITH_OVERHEAD(PAGE_SIZE - X)}}}||헤더용 headroom X를 예약한 뒤 한 페이지 내에서 사용할 수 있는 linear 데이터 최대 크기.|| '''팁''' truesize 사용 예: {{{alloc_skb(size, gfp)}}}는 내부적으로 {{{SKB_TRUESIZE(size)}}}로 {{{skb->truesize}}}를 초기화합니다. 소켓의 수신 버퍼 제한({{{sk_rmem_alloc}}})은 이 값을 누적해 추적하므로, 페이지 프래그먼트를 직접 추가할 때는 {{{skb_add_rx_frag()}}}의 {{{truesize}}} 인자를 실제 할당 크기(예: {{{PAGE_SIZE}}})로 정확히 넘겨야 합니다. === 데이터 조작 함수 === sk_buff의 데이터 영역을 조작하는 4대 함수: ||함수||동작||용도|| ||{{{skb_reserve(skb, len)}}}||data와 tail을 len만큼 뒤로||할당 직후 headroom 확보|| ||{{{skb_put(skb, len)}}}||tail을 len만큼 뒤로||데이터 끝에 추가 (전송 시)|| ||{{{skb_push(skb, len)}}}||data를 len만큼 앞으로||헤더 추가 (L4→L3→L2)|| ||{{{skb_pull(skb, len)}}}||data를 len만큼 뒤로||헤더 제거 (수신 시 L2→L3→L4)|| {{{#!plain [코드: C] /* 전형적인 전송 경로에서의 skb 조작 순서 */ struct sk_buff *skb = alloc_skb(1500 + headroom, GFP_KERNEL); /* 1. headroom 확보 */ skb_reserve(skb, headroom); /* data, tail 이동 → headroom 공간 */ /* 2. 페이로드 추가 */ unsigned char *p = skb_put(skb, payload_len); /* tail 이동 */ memcpy(p, payload_data, payload_len); /* 3. TCP 헤더 추가 */ struct tcphdr *th = skb_push(skb, sizeof(*th)); /* data 앞으로 이동 */ skb_reset_transport_header(skb); /* 4. IP 헤더 추가 */ struct iphdr *ih = skb_push(skb, sizeof(*ih)); /* data 더 앞으로 */ skb_reset_network_header(skb); /* 5. Ethernet 헤더 추가 */ struct ethhdr *eh = skb_push(skb, ETH_HLEN); skb_reset_mac_header(skb); }}} === Clone/Copy 메커니즘 === 여러 곳에서 같은 패킷을 참조해야 할 때 (예: 패킷 미러링, tcpdump 캡처, netfilter bridge), 세 가지 복사 전략을 선택할 수 있습니다: {{{#!plain [SVG 텍스트 변환: skb_clone vs pskb_copy vs skb_copy 다이어그램] 캡션: clone은 메타데이터만 복사하고 버퍼를 공유, pskb_copy는 linear 데이터만 복사, skb_copy는 전체 독립 복사 텍스트 요소: - skb_clone vs pskb_copy vs skb_copy - skb_clone - sk_buff (원본) - sk_buff (clone) - 공유 데이터 버퍼 - dataref = 2 - pskb_copy - sk_buff (copy) - linear (원본) - linear (복사) - 공유 paged frags (refcount++) - skb_copy - 전체 버퍼 (원본) - 전체 버퍼 (복사) - 비교 요약 - 메타만 복사 - 버퍼 100% 공유 - 가장 빠름 - 데이터 수정 불가 - linear 헤더 복사 - paged data 공유 - 중간 비용 - 헤더 수정 가능 - 전체 완전 복사 - 독립적 버퍼 - 가장 느림 - 자유로운 수정 }}} {{{#!plain [코드: C] /* clone: sk_buff 메타데이터만 복사, 데이터 버퍼는 공유 (refcount 증가) */ struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC); /* clone->data == skb->data (같은 버퍼 참조) */ /* skb_shared_info.dataref 증가됨 */ /* pskb_copy: linear 헤더만 복사, paged data는 page refcount 증가 */ struct sk_buff *pcopy = pskb_copy(skb, GFP_ATOMIC); /* 헤더를 수정해야 하지만 페이로드는 그대로인 경우 최적 */ /* copy: 메타데이터 + linear + paged 데이터 모두 완전 복사 */ struct sk_buff *copy = skb_copy(skb, GFP_ATOMIC); /* copy->data != skb->data (독립적 버퍼) */ /* skb_share_check: 공유 여부 확인 후 필요 시 clone */ skb = skb_share_check(skb, GFP_ATOMIC); if (!skb) return NET_RX_DROP; /* 이제 skb를 독점적으로 소유 — 안전하게 메타데이터 수정 가능 */ }}} '''팁''' 선택 기준: 패킷을 읽기만 한다면 {{{skb_clone()}}}, 헤더만 수정해야 한다면 {{{pskb_copy()}}}, 페이로드까지 수정해야 한다면 {{{skb_copy()}}}를 사용하세요. Netfilter NAT는 {{{pskb_copy()}}}를 주로 사용합니다. === 프래그먼트와 scatter-gather === 대용량 패킷은 linear 데이터 외에 paged fragments를 사용합니다: {{{#!plain [코드: C] /* skb_shared_info: end 포인터 바로 뒤에 위치 */ struct skb_shared_info { __u8 nr_frags; /* fragment 수 */ __u8 tx_flags; unsigned short gso_size; /* GSO 세그먼트 크기 */ unsigned short gso_segs; /* GSO 세그먼트 수 */ unsigned short gso_type; /* GSO 타입 */ struct sk_buff *frag_list; /* 연결된 skb 리스트 */ skb_frag_t frags[MAX_SKB_FRAGS]; /* page fragment 배열 */ atomic_t dataref; /* 데이터 공유 참조 카운트 */ }; /* fragment 접근 */ struct skb_shared_info *si = skb_shinfo(skb); for (int i = 0; i < si->nr_frags; i++) { skb_frag_t *frag = &si->frags[i]; struct page *page = skb_frag_page(frag); unsigned int offset = skb_frag_off(frag); unsigned int size = skb_frag_size(frag); } }}} === 고급 데이터 조작 === 패킷 처리 시 데이터 레이아웃을 변경해야 하는 상황이 자주 발생합니다. 아래 함수들은 각각 다른 상황에서 사용됩니다: ||함수||동작||사용 시나리오|| ||{{{skb_linearize(skb)}}}||모든 paged fragment를 linear 영역으로 합침||레거시 드라이버, fragment 미지원 코드|| ||{{{pskb_may_pull(skb, len)}}}||len 바이트까지 linear 영역에 확보||프로토콜 헤더 파싱 전 (필수 패턴)|| ||{{{pskb_expand_head(skb, nhead, ntail, gfp)}}}||headroom/tailroom 확장 (필요 시 버퍼 재할당)||encapsulation 헤더 추가 (tunnel, VLAN)|| ||{{{skb_cow_head(skb, headroom)}}}||공유 skb의 헤더를 안전하게 쓰기 가능하게||clone된 skb의 헤더 수정 전|| ||{{{skb_make_writable(skb, len)}}}||len 바이트까지 쓰기 가능하게 (clone 해제+linearize)||netfilter에서 패킷 내용 수정 전|| {{{#!plain [코드: C] /* pskb_may_pull: 프로토콜 헤더 파싱의 필수 패턴 */ static int my_protocol_rcv(struct sk_buff *skb) { struct my_hdr *hdr; /* linear 영역에 최소 헤더 크기만큼 확보 */ if (!pskb_may_pull(skb, sizeof(*hdr))) goto drop; hdr = (struct my_hdr *)skb_transport_header(skb); /* 이제 hdr-> 필드에 안전하게 접근 가능 */ /* 가변 길이 헤더라면 두 번째 pull */ if (!pskb_may_pull(skb, hdr->hdr_len)) goto drop; hdr = (struct my_hdr *)skb_transport_header(skb); /* 포인터 재취득! */ /* ... 처리 ... */ } /* skb_cow_head: 터널 encapsulation 전 headroom 확보 */ static int my_tunnel_xmit(struct sk_buff *skb, struct net_device *dev) { int hdr_len = sizeof(struct iphdr) + sizeof(struct gre_hdr); /* headroom이 부족하거나 skb가 공유 상태이면 재할당 */ if (skb_cow_head(skb, hdr_len + LL_RESERVED_SPACE(dev))) { kfree_skb(skb); return NETDEV_TX_OK; } /* 이제 안전하게 헤더 추가 가능 */ skb_push(skb, hdr_len); /* ... GRE + IP 헤더 설정 ... */ } /* skb_linearize: fragment가 있는 skb를 하나의 연속 버퍼로 */ if (skb_is_nonlinear(skb)) { if (skb_linearize(skb)) goto drop; /* 메모리 부족 */ /* 이제 모든 데이터가 head~tail 사이에 연속으로 존재 */ } }}} '''주의''' {{{pskb_may_pull()}}} 호출 후에는 반드시 헤더 포인터를 재취득해야 합니다. 내부적으로 버퍼 재할당이 일어날 수 있어 이전 포인터가 무효화됩니다. 이 실수는 커널 네트워크 코드에서 가장 흔한 버그 패턴 중 하나입니다. === sk_buff 리스트 관리 === {{{#!plain [코드: C] /* sk_buff_head: 이중 연결 리스트의 head (sentinel) */ struct sk_buff_head { struct sk_buff *next; struct sk_buff *prev; __u32 qlen; /* 큐 내 skb 수 */ spinlock_t lock; /* 동시성 보호 */ }; /* 초기화 */ struct sk_buff_head my_queue; skb_queue_head_init(&my_queue); /* 큐 조작 */ skb_queue_tail(&my_queue, skb); /* 큐 끝에 추가 */ skb_queue_head(&my_queue, skb); /* 큐 앞에 추가 */ struct sk_buff *s = skb_dequeue(&my_queue); /* 큐 앞에서 제거 */ skb_queue_purge(&my_queue); /* 전체 비우기 */ }}} === 소켓과 sk_buff의 관계 === sk_buff의 {{{sk}}} 필드는 해당 패킷이 소속된 소켓(struct sock)을 가리킵니다. 이 연결은 소켓 메모리 계산, 소켓 옵션 적용, 프로토콜별 처리의 기반이 됩니다. ==== struct sock 계층 구조 ==== 커널 소켓은 3단계 계층으로 구성됩니다: {{{#!plain [SVG 텍스트 변환: 소켓 구조체 계층과 sk_buff의 관계 다이어그램] 캡션: struct socket → struct sock → 프로토콜별 sock. sk_buff는 sk->sk_receive_queue 등에 연결되며 skb->sk로 소켓을 역참조 텍스트 요소: - 소켓 구조체 계층과 sk_buff의 관계 - struct socket - BSD 소켓 인터페이스 - file, ops, sk 포인터 - struct sock (sk) - 프로토콜 무관 공통 계층 - sk_receive_queue, sk_write_queue - sk_rmem_alloc, sk_wmem_alloc - struct tcp_sock / udp_sock - 프로토콜별 확장 - inet_sock ⊃ sock 내장 - sk - struct sk_buff - skb->sk → sock - skb->destructor - skb->truesize - skb->sk - sk_receive_queue - sk_receive_queue (RX) - 수신 skb 대기열 - sk_write_queue (TX) - 전송 skb 대기열 - sk_backlog (overflow) - 소켓 lock 중 수신 대기 - sk_error_queue - ICMP 에러, MSG_ERRQUEUE }}} {{{#!plain [코드: C] /* 소켓 구조체 계층 (간략) */ struct socket { /* BSD 소켓 (사용자 공간 인터페이스) */ socket_state state; /* SS_UNCONNECTED, SS_CONNECTED 등 */ struct file *file; /* VFS file (fd와 연결) */ struct sock *sk; /* 네트워크 레이어 소켓 */ const struct proto_ops *ops; /* connect, sendmsg 등 */ }; struct sock { /* 프로토콜 무관 공통 소켓 */ struct sk_buff_head sk_receive_queue; /* 수신 skb 큐 */ struct sk_buff_head sk_write_queue; /* 전송 skb 큐 */ struct sk_buff_head sk_error_queue; /* 에러 큐 (ICMP 등) */ struct { struct sk_buff *head, *tail; } sk_backlog; /* backlog 큐 (lock 중 수신) */ atomic_t sk_rmem_alloc; /* 수신 큐 메모리 사용량 */ atomic_t sk_wmem_alloc; /* 전송 큐 메모리 사용량 */ int sk_rcvbuf; /* SO_RCVBUF 값 */ int sk_sndbuf; /* SO_SNDBUF 값 */ unsigned long sk_flags; /* 소켓 플래그 */ struct proto *sk_prot; /* 프로토콜 핸들러 */ void (*sk_data_ready)(struct sock *sk); /* 수신 알림 */ /* ... */ }; /* 프로토콜별 확장 (임베디드 패턴) */ struct inet_sock { /* IPv4/IPv6 공통 */ struct sock sk; /* 공통 sock 내장 */ __be32 inet_saddr; /* 소스 IP */ __be32 inet_daddr; /* 목적지 IP */ __be16 inet_sport; /* 소스 포트 */ __be16 inet_dport; /* 목적지 포트 */ __u8 tos; /* IP_TOS 옵션 */ __u8 min_ttl; /* IP_MINTTL 옵션 */ __s16 uc_ttl; /* IP_TTL 옵션 (-1 = 기본값) */ struct ip_options_rcu *inet_opt; /* IP 옵션 */ /* ... */ }; struct tcp_sock { struct inet_connection_sock inet_conn; /* inet_sock ⊃ sock 내장 */ u32 snd_una; /* 가장 오래된 미확인 시퀀스 */ u32 snd_nxt; /* 다음 전송 시퀀스 */ u32 rcv_nxt; /* 다음 수신 기대 시퀀스 */ u32 mss_cache; /* MSS (최대 세그먼트 크기) */ /* ... 수십 개 TCP 전용 필드 ... */ }; struct udp_sock { struct inet_sock inet; int pending; /* cork 상태 */ __u8 encap_type; /* UDP encap (VXLAN 등) */ /* ... */ }; /* 캐스팅 매크로 */ #define inet_sk(sk) ((struct inet_sock *)(sk)) #define tcp_sk(sk) ((struct tcp_sock *)(sk)) #define udp_sk(sk) ((struct udp_sock *)(sk)) }}} ==== skb↔sk 바인딩과 소켓 메모리 관리 ==== {{{skb->sk}}}가 설정되면 해당 skb의 메모리 사용량이 소켓에 과금됩니다. 이 메커니즘이 {{{SO_RCVBUF}}}/{{{SO_SNDBUF}}} 제한을 실현합니다: {{{#!plain [코드: C] /* skb를 소켓에 연결 — 메모리 과금 시작 */ static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk) { skb->sk = sk; skb->destructor = sock_rfree; /* 해제 시 콜백 */ atomic_add(skb->truesize, &sk->sk_rmem_alloc); /* → 수신 큐 메모리 사용량에 truesize만큼 추가 */ } static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk) { skb->sk = sk; skb->destructor = sock_wfree; /* 해제 시 콜백 */ refcount_add(skb->truesize, &sk->sk_wmem_alloc); /* → 전송 큐 메모리 사용량에 truesize만큼 추가 */ } /* skb 해제 시: destructor 콜백이 메모리 차감 */ void sock_rfree(struct sk_buff *skb) { struct sock *sk = skb->sk; atomic_sub(skb->truesize, &sk->sk_rmem_alloc); /* → 수신 큐 메모리 사용량에서 차감 */ } /* 수신 큐 과부하 확인 — 소켓 버퍼 제한 */ static inline bool sk_rmem_schedule(struct sock *sk, struct sk_buff *skb, int size) { /* sk_rmem_alloc + size > sk_rcvbuf 이면 false → 패킷 드롭 */ return __sk_mem_schedule(sk, size, SK_MEM_RECV); } }}} ||필드/콜백||방향||역할|| ||{{{sk_rmem_alloc}}}||RX||수신 큐에 쌓인 skb의 총 truesize (SO_RCVBUF와 비교)|| ||{{{sk_wmem_alloc}}}||TX||전송 중인 skb의 총 truesize (SO_SNDBUF와 비교)|| ||{{{sock_rfree}}}||RX||skb 해제 시 sk_rmem_alloc 차감|| ||{{{sock_wfree}}}||TX||skb 해제 시 sk_wmem_alloc 차감, 전송 대기 프로세스 wakeup|| ||{{{skb_orphan(skb)}}}||양방향||skb↔sk 연결 해제 (destructor 호출 후 sk=NULL)|| '''정보''' {{{skb->truesize}}}는 skb 구조체 크기 + 할당된 데이터 버퍼 크기를 합산한 값입니다. 소켓의 메모리 추적은 이 값 기반이므로, truesize가 실제와 어긋나면 SO_RCVBUF 제한이 오작동합니다. 너무 작으면 메모리 과다 사용, 너무 크면 조기 패킷 드롭이 발생합니다. ==== 소켓 옵션(setsockopt)과 sk_buff ==== 사용자 공간의 {{{setsockopt()}}} 호출은 {{{struct sock}}} 필드를 변경하고, 이것이 skb 생성·처리에 직접 반영됩니다: ||소켓 옵션||레벨||sock/skb 영향|| ||{{{SO_RCVBUF}}}||SOL_SOCKET||{{{sk->sk_rcvbuf}}} 설정 → 수신 큐 skb 총량 제한|| ||{{{SO_SNDBUF}}}||SOL_SOCKET||{{{sk->sk_sndbuf}}} 설정 → 전송 큐 skb 총량 제한|| ||{{{SO_MARK}}}||SOL_SOCKET||{{{sk->sk_mark}}} → {{{skb->mark}}}로 복사 (netfilter/tc/라우팅)|| ||{{{SO_PRIORITY}}}||SOL_SOCKET||{{{sk->sk_priority}}} → {{{skb->priority}}}로 복사 (QoS)|| ||{{{SO_BINDTODEVICE}}}||SOL_SOCKET||{{{sk->sk_bound_dev_if}}} → skb의 {{{dev}}} 제한|| ||{{{SO_TIMESTAMP}}}||SOL_SOCKET||수신 skb에 타임스탬프 기록, {{{recvmsg()}}} cmsg로 전달|| ||{{{SO_BUSY_POLL}}}||SOL_SOCKET||{{{sk->sk_napi_id}}} + {{{skb->napi_id}}}로 busy polling|| ||{{{IP_TOS}}}||SOL_IP||{{{inet->tos}}} → 전송 skb IP 헤더 TOS 필드|| ||{{{IP_TTL}}}||SOL_IP||{{{inet->uc_ttl}}} → 전송 skb IP 헤더 TTL 필드|| ||{{{IP_HDRINCL}}}||SOL_IP||raw socket: 사용자가 IP 헤더를 직접 제공|| ||{{{TCP_NODELAY}}}||SOL_TCP||Nagle 알고리즘 해제 → 소량 데이터 즉시 skb 전송|| ||{{{TCP_CORK}}}||SOL_TCP||skb 전송 지연 (cork), uncork 시 한번에 전송|| ||{{{UDP_CORK}}}||SOL_UDP||여러 sendmsg를 하나의 skb로 합쳐 전송|| ||{{{UDP_GRO}}}||SOL_UDP||수신 UDP GRO 활성화 → 여러 패킷이 하나의 큰 skb로|| {{{#!plain [코드: C] /* 전송 경로에서 sock 옵션 → skb 필드 복사 과정 */ static void ip_copy_addrs(struct iphdr *iph, const struct flowi4 *fl4) { /* flowi4는 routing lookup 입력: sock의 IP/포트에서 구성 */ iph->saddr = fl4->saddr; iph->daddr = fl4->daddr; } /* ip_queue_xmit: TCP 전송 시 sock 옵션 적용 */ int __ip_queue_xmit(struct sock *sk, struct sk_buff *skb, ...) { struct inet_sock *inet = inet_sk(sk); /* SK 옵션 → skb 필드 전파 */ skb->priority = sk->sk_priority; /* SO_PRIORITY */ skb->mark = sk->sk_mark; /* SO_MARK */ /* IP 헤더 필드: inet_sock에서 가져옴 */ iph->tos = inet->tos; /* IP_TOS */ iph->ttl = ip_select_ttl(inet, ...); /* IP_TTL 또는 기본값 */ /* ... */ } /* SO_RCVBUF 설정과 수신 큐 제한의 관계 */ /* 사용자 공간 */ int bufsize = 262144; /* 256KB */ setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)); /* 커널: sk->sk_rcvbuf = min(bufsize * 2, sysctl_rmem_max) * → 실제 커널 값은 요청값의 2배 (overhead 고려) * * 수신 시: atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf 이면 * → 새 패킷 드롭 (ENOMEM) */ }}} '''주의''' {{{SO_RCVBUF}}}/{{{SO_SNDBUF}}}에 설정한 값은 커널 내에서 2배로 증폭됩니다 ({{{sock_setsockopt()}}} 내부). 이는 skb 구조체와 메타데이터 오버헤드를 고려한 것입니다. {{{getsockopt()}}}으로 읽으면 2배된 값이 반환됩니다. 시스템 전역 상한은 {{{/proc/sys/net/core/rmem_max}}}, {{{wmem_max}}}입니다. ==== Raw Socket과 sk_buff ==== Raw socket({{{SOCK_RAW}}})은 프로토콜 스택의 일부를 우회하여 직접 패킷을 구성하거나 수신합니다. 일반 {{{SOCK_STREAM}}}/{{{SOCK_DGRAM}}}과 달리 커널의 L4 프로토콜 처리를 거치지 않고 skb를 직접 다루므로, 네트워크 도구(ping, traceroute, tcpdump, nmap 등)와 프로토콜 구현의 핵심입니다. {{{#!plain [SVG 텍스트 변환: Raw Socket 계층별 접근 범위 다이어그램] 텍스트 요소: - Raw Socket 계층별 접근 범위 - 사용자 공간 (User Space) - Socket Layer — socket(), sendmsg(), recvmsg() - L4: TCP/UDP (SOCK_STREAM/DGRAM은 여기서 처리) - L3: IP Layer — ip_rcv(), ip_output() - L2: Ethernet / Device Driver — dev_queue_xmit(), netif_receive_skb() - AF_INET SOCK_RAW - AF_PACKET SOCK_RAW }}} ===== Raw Socket 타입 비교 ===== ||타입||생성||접근 계층||skb 관계|| ||IP raw socket||{{{socket(AF_INET, SOCK_RAW, IPPROTO_XXX)}}}||L3 (IP)||{{{IP_HDRINCL}}} 없으면 커널이 IP 헤더 생성. 수신 시 IP 헤더 포함|| ||IP raw + IP_HDRINCL||{{{setsockopt(IP_HDRINCL, 1)}}}||L3 (IP)||사용자가 IP 헤더까지 직접 작성. skb->data가 IP 헤더부터 시작|| ||Packet socket (L2 raw)||{{{socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))}}}||L2 (Ethernet)||Ethernet 헤더 포함 전체 프레임 접근. skb를 MAC 헤더부터 수신/전송|| ||Packet socket (L2 cooked)||{{{socket(AF_PACKET, SOCK_DGRAM, htons(ETH_P_ALL))}}}||L3 (IP)||L2 헤더 제거 후 전달. 송신 시 커널이 L2 헤더 생성|| ||AF_PACKET + TPACKET||{{{setsockopt(PACKET_VERSION, TPACKET_V3)}}}||L2 (Ethernet)||mmap 기반 ring buffer → skb 없이 직접 DMA 버퍼 접근 (고성능)|| ||Ping socket||{{{socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)}}}||L4 (ICMP)||CAP_NET_RAW 불필요. 커널이 ICMP 헤더 id/checksum 관리|| ===== Raw Socket 권한 모델 ===== Raw socket 생성에는 {{{CAP_NET_RAW}}} capability가 필요합니다. 커널은 {{{sock_create()}}} → {{{inet_create()}}} 경로에서 capability를 검사합니다: {{{#!plain [코드: C] /* net/ipv4/af_inet.c — inet_create() */ static int inet_create(struct net *net, struct socket *sock, int protocol, int kern) { struct inet_protosw *answer; struct sock *sk; /* SOCK_RAW 사용 시 CAP_NET_RAW 검사 */ if (sock->type == SOCK_RAW && !kern && !ns_capable(net->user_ns, CAP_NET_RAW)) return -EPERM; /* protocol 번호로 inetsw[] 해시 테이블에서 프로토콜 핸들러 검색 */ answer = inet_protosw_lookup(sock->type, protocol); /* SOCK_RAW → raw_prot (net/ipv4/raw.c) * SOCK_STREAM → tcp_prot * SOCK_DGRAM → udp_prot */ sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer->prot, kern); /* ... */ } /* net/packet/af_packet.c — AF_PACKET도 CAP_NET_RAW 필요 */ static int packet_create(struct net *net, struct socket *sock, int protocol, int kern) { if (!kern && !ns_capable(net->user_ns, CAP_NET_RAW)) return -EPERM; /* ... */ } }}} '''정보''' Ping socket 예외: Linux 3.0+에서 도입된 ping socket({{{socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)}}})은 {{{CAP_NET_RAW}}} 없이도 ICMP Echo Request를 보낼 수 있습니다. {{{/proc/sys/net/ipv4/ping_group_range}}}로 허용 GID 범위를 설정합니다. {{{setcap cap_net_raw+ep /usr/bin/ping}}} 대신 이 메커니즘을 사용합니다. ===== 커널 내부 자료구조 — struct raw_sock ===== {{{#!plain [코드: C] /* include/net/raw.h */ struct raw_sock { struct inet_sock inet; /* inet_sock 상속 (→ sock → sock_common) */ struct icmp_filter filter; /* ICMP 타입별 필터 비트맵 */ u32 ipmr_table; /* 멀티캐스트 라우팅 테이블 ID */ }; /* raw socket 프로토콜 해시 테이블 * protocol 번호로 해싱하여 수신 시 O(1) 조회 */ struct raw_hashinfo { spinlock_t lock; struct hlist_head ht[RAW_HTABLE_SIZE]; /* 256 버킷 */ }; /* 전역 raw 해시 테이블 — 모든 AF_INET SOCK_RAW 소켓 관리 */ struct raw_hashinfo raw_v4_hashinfo; /* IPv4 */ struct raw_hashinfo raw_v6_hashinfo; /* IPv6 */ /* 해시 함수: protocol 번호를 버킷 인덱스로 변환 */ static inline u32 raw_hashfunc(const struct net *net, u32 proto) { return proto & (RAW_HTABLE_SIZE - 1); /* 0~255 */ } /* raw socket의 프로토콜 연산 테이블 */ struct proto raw_prot = { .name = "RAW", .owner = THIS_MODULE, .close = raw_close, .connect = ip4_datagram_connect, .sendmsg = raw_sendmsg, .recvmsg = raw_recvmsg, .bind = raw_bind, .hash = raw_hash_sk, .unhash = raw_unhash_sk, .obj_size = sizeof(struct raw_sock), }; }}} ===== AF_INET SOCK_RAW 수신 경로 ===== IP 계층에서 패킷이 로컬로 배달될 때, TCP/UDP 디먹싱 이전에 raw socket으로의 복제가 먼저 수행됩니다. 즉, raw socket은 패킷의 사본을 받으며, 원본 skb는 정상 프로토콜 스택으로 계속 진행합니다: {{{#!plain [SVG 텍스트 변환: Raw Socket 수신 경로 (IPv4) 다이어그램] 텍스트 요소: - Raw Socket 수신 경로 (IPv4) - ip_local_deliver() - ip_local_deliver_finish() - 분기 - ① Raw Socket 경로 (먼저 실행) - raw_local_deliver() - raw_v4_hashinfo - [protocol] 조회 - raw_v4_input() - sk_for_each(): - 매칭 소켓 순회 - skb_clone(skb, GFP_ATOMIC) - 데이터 공유 (zero-copy clone) - 원본 skb 유지 - 사본 → raw sock - raw_rcv() - xfrm4_policy_check() - sock_queue_rcv_skb() - sk_rmem_alloc 체크 - Raw Socket 수신 큐 - recvfrom()으로 IP 헤더 포함 수신 - ② 프로토콜 핸들러 경로 (이후 실행) - ipprot->handler(skb) - inet_protos[protocol] - tcp_v4_rcv() - udp_rcv() - icmp_rcv() - 정상 소켓 수신 큐 - 핵심 포인트 - • raw socket은 항상 IP 헤더 포함 수신 - • skb_clone()은 데이터 공유 (zero-copy) - • 원본 skb는 프로토콜 핸들러로 정상 전달 - • CAP_NET_RAW 권한 필요 (비특권 차단) - 실행 순서: ① raw_local_deliver(skb, protocol) → ② ipprot->handler(skb) (순차 실행, 동시 아님) }}} {{{#!plain [코드: C] /* net/ipv4/ip_input.c — ip_local_deliver_finish() * 패킷이 로컬 배달될 때 raw socket에 먼저 전달 */ static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb) { __skb_pull(skb, skb_network_header_len(skb)); int protocol = ip_hdr(skb)->protocol; const struct net_protocol *ipprot; /* ① raw socket이 있으면 먼저 skb 사본 전달 */ raw_local_deliver(skb, protocol); /* ② 등록된 프로토콜 핸들러 호출 (tcp_v4_rcv, udp_rcv 등) */ ipprot = rcu_dereference(inet_protos[protocol]); if (ipprot) { ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb); } } /* net/ipv4/raw.c — raw_local_deliver() * 해당 프로토콜의 raw socket들을 해시 테이블에서 찾아 전달 */ int raw_local_deliver(struct sk_buff *skb, int protocol) { struct raw_hashinfo *h = &raw_v4_hashinfo; struct hlist_head *head; int hash; hash = raw_hashfunc(dev_net(skb_dst(skb)->dev), protocol); head = &h->ht[hash]; if (!hlist_empty(head)) { /* 매칭되는 모든 raw socket에 skb 복제본 전달 */ raw_v4_input(skb, ip_hdr(skb), hash); } return 0; } /* net/ipv4/raw.c — raw_v4_input() * 프로토콜 번호와 목적지 주소가 매칭되는 모든 raw socket에 전달 */ static int raw_v4_input(struct sk_buff *skb, const struct iphdr *iph, int hash) { struct sock *sk; struct hlist_head *head = &raw_v4_hashinfo.ht[hash]; int delivered = 0; rcu_read_lock(); sk_for_each_rcu(sk, head) { /* 프로토콜 번호, 목적지 IP, 소스 IP, 네트워크 네임스페이스 매칭 */ if (raw_v4_match(net, sk, iph->protocol, iph->saddr, iph->daddr, skb->dev->ifindex, sdif)) { /* skb를 clone하여 해당 raw socket에 전달 */ raw_rcv(sk, skb); delivered++; } } rcu_read_unlock(); return delivered; } /* net/ipv4/raw.c — raw_rcv() * skb clone → IP 헤더 포함한 상태로 수신 큐에 추가 */ int raw_rcv(struct sock *sk, struct sk_buff *skb) { struct raw_sock *rp = raw_sk(sk); /* ICMP 필터 적용: 관심 없는 ICMP 타입은 드롭 */ if (sk->sk_protocol == IPPROTO_ICMP) { struct icmphdr *icmph = icmp_hdr(skb); if (raw_icmp_type_filtered(rp, icmph->type)) return 0; /* 필터에 의해 드롭 */ } /* skb 복제: 원본은 프로토콜 스택이 계속 사용 */ struct sk_buff *clone = skb_clone(skb, GFP_ATOMIC); if (!clone) return 0; /* 핵심: data 포인터를 network_header (IP 헤더) 위치로 복원 * ip_local_deliver_finish()에서 __skb_pull로 L4까지 당겼으므로 * raw socket은 IP 헤더부터 보여줘야 함 */ skb_push(clone, clone->data - skb_network_header(clone)); /* 수신 큐에 추가 → recvmsg()로 사용자에게 전달 */ if (sock_queue_rcv_skb(sk, clone) < 0) kfree_skb(clone); return 0; } }}} '''팁''' 핵심 포인트: 동일 프로토콜 번호를 사용하는 여러 raw socket이 열려 있으면, 하나의 수신 패킷이 모든 매칭 소켓에 clone되어 전달됩니다. 예: 두 프로세스가 각각 {{{IPPROTO_ICMP}}} raw socket을 열면, ICMP 패킷 수신 시 두 프로세스 모두 사본을 받습니다. 이는 {{{raw_v4_input()}}}의 {{{sk_for_each_rcu()}}} 루프가 해시 버킷의 모든 소켓을 순회하기 때문입니다. ===== AF_INET SOCK_RAW 전송 경로 ===== Raw socket의 전송 경로는 {{{IP_HDRINCL}}} 옵션에 따라 두 가지로 분기됩니다: {{{#!plain [코드: C] /* net/ipv4/raw.c — raw_sendmsg() * 사용자 공간의 sendto()/sendmsg() → raw_sendmsg() */ static int raw_sendmsg(struct sock *sk, struct msghdr *msg, size_t len) { struct inet_sock *inet = inet_sk(sk); struct flowi4 fl4; struct rtable *rt; int err; /* 목적지 주소 결정 */ struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name; __be32 daddr; if (usin) { daddr = usin->sin_addr.s_addr; } else { /* connect()로 미리 바인딩된 주소 사용 */ daddr = inet->inet_daddr; if (!daddr) return -EDESTADDRREQ; } /* 라우팅 테이블 조회 */ flowi4_init_output(&fl4, ...); rt = ip_route_output_flow(net, &fl4, sk); if (inet->hdrincl) { /* ── IP_HDRINCL 모드 ── * 사용자가 IP 헤더를 직접 작성 * 커널은 최소한의 필드만 보정 */ err = raw_send_hdrinc(sk, &fl4, msg, len, &rt, msg->msg_flags); } else { /* ── 일반 raw 모드 ── * 커널이 IP 헤더를 자동 생성 * 사용자 데이터는 L4 페이로드로 취급 */ err = ip_append_data(sk, &fl4, raw_getfrag, msg, len, 0, &ipc, &rt, msg->msg_flags); if (!err) { err = ip_push_pending_frames(sk, &fl4); /* → ip_output() → dev_queue_xmit() */ } } ip_rt_put(rt); return err; } }}} ===== IP_HDRINCL 상세 — 커널의 보정 동작 ===== {{{IP_HDRINCL}}}을 설정하면 사용자가 IP 헤더를 직접 작성하지만, 커널은 안전성과 정확성을 위해 일부 필드를 자동 보정합니다: {{{#!plain [코드: C] /* net/ipv4/raw.c — raw_send_hdrinc() * IP_HDRINCL 모드의 실제 전송 처리 */ static int raw_send_hdrinc(struct sock *sk, struct flowi4 *fl4, struct msghdr *msg, unsigned int len, struct rtable **rtp, unsigned int flags) { struct iphdr *iph; struct sk_buff *skb; unsigned int iphlen; /* skb 할당: IP 헤더 + 페이로드 크기 */ skb = sock_alloc_send_skb(sk, len + LL_ALLOCATED_SPACE(rt->dst.dev), /* L2 headroom 확보 */ flags & MSG_DONTWAIT, &err); if (!skb) return err; /* L2 헤더 공간 예약 */ skb_reserve(skb, LL_RESERVED_SPACE(rt->dst.dev)); skb->protocol = htons(ETH_P_IP); /* 사용자 데이터를 skb에 복사 (IP 헤더 포함) */ skb_put(skb, len); skb->network_header = skb->data; skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, len); iph = ip_hdr(skb); /* ── 커널이 자동 보정하는 필드 ── */ /* (1) tot_len: 0이면 커널이 skb->len으로 설정 */ if (!iph->tot_len) iph->tot_len = htons(len); /* (2) saddr: 0이면 라우팅 결과의 소스 IP로 채움 */ if (!iph->saddr) iph->saddr = fl4->saddr; /* (3) id: 0이면 커널이 고유 ID 할당 */ if (!iph->id) ip_select_ident(net, skb, NULL); /* (4) check: 항상 커널이 재계산 (사용자 값 무시) */ iph->check = 0; iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl); /* Netfilter OUTPUT 체인 통과 후 전송 */ err = NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT, net, sk, skb, NULL, rt->dst.dev, dst_output); return err; } }}} ||IP 헤더 필드||사용자 제공 시||0 또는 미설정 시|| ||{{{version}}}||사용자 값 사용||사용자가 반드시 4로 설정해야 함|| ||{{{ihl}}}||사용자 값 사용||사용자가 설정 (보통 5)|| ||{{{tos}}}||사용자 값 사용||0 (기본 서비스)|| ||{{{tot_len}}}||사용자 값 사용||커널이 {{{skb->len}}}으로 설정|| ||{{{id}}}||사용자 값 사용||커널이 {{{ip_select_ident()}}}로 할당|| ||{{{frag_off}}}||사용자 값 사용||0 (단편화 없음)|| ||{{{ttl}}}||사용자 값 사용||사용자가 반드시 설정해야 함|| ||{{{protocol}}}||사용자 값 사용||사용자가 반드시 설정해야 함|| ||{{{saddr}}}||사용자 값 사용 (스푸핑 가능)||커널이 라우팅 테이블에서 결정|| ||{{{daddr}}}||사용자 값 사용||사용자가 반드시 설정해야 함|| ||{{{check}}}||무시 — 커널이 항상 재계산||커널이 {{{ip_fast_csum()}}}으로 계산|| '''주의''' IP_HDRINCL과 IP Spoofing: {{{IP_HDRINCL}}}을 사용하면 {{{saddr}}}(소스 IP)를 임의로 설정할 수 있어 IP 스푸핑이 가능합니다. 이것이 {{{CAP_NET_RAW}}}가 필요한 주요 보안 이유 중 하나입니다. 네트워크 장비의 BCP 38 (uRPF) 필터링이나 커널의 {{{rp_filter}}} 설정으로 발신 스푸핑 패킷을 차단할 수 있습니다. ===== raw_recvmsg() — 사용자 공간으로 전달 ===== {{{#!plain [코드: C] /* net/ipv4/raw.c — raw_recvmsg() * 사용자의 recvfrom()/recvmsg() 처리 */ static int raw_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int flags, int *addr_len) { struct sk_buff *skb; struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name; int err, copied; /* 수신 큐에서 skb 꺼내기 (대기 가능) */ skb = skb_recv_datagram(sk, flags, &err); if (!skb) return err; /* skb에서 사용자 버퍼로 데이터 복사 * → IP 헤더부터 전체 패킷이 사용자에게 전달됨 */ copied = skb->len; if (len < copied) { msg->msg_flags |= MSG_TRUNC; /* 잘림 알림 */ copied = len; } skb_copy_datagram_msg(skb, 0, msg, copied); /* 소스 주소 정보 채우기 */ if (sin) { sin->sin_family = AF_INET; sin->sin_addr.s_addr = ip_hdr(skb)->saddr; sin->sin_port = 0; /* raw socket은 포트 개념 없음 */ } /* IP_PKTINFO, IP_TTL 등 ancillary data (cmsg) 전달 */ if (inet_cmsg_flags(inet)) ip_cmsg_recv(msg, skb); skb_free_datagram(sk, skb); return copied; } }}} ===== ICMP 필터 (ICMP_FILTER) ===== {{{IPPROTO_ICMP}}} raw socket에서 관심 있는 ICMP 타입만 수신하도록 비트맵 필터를 설정할 수 있습니다: {{{#!plain [코드: C] /* include/uapi/linux/icmp.h */ struct icmp_filter { __u32 data; /* 비트맵: bit N이 1이면 ICMP type N을 필터링(드롭) */ }; /* 사용자 공간 예: Echo Reply(type 0)만 수신, 나머지 필터링 */ struct icmp_filter filt; filt.data = ~(1 << ICMP_ECHOREPLY); /* type 0만 통과 */ setsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt)); /* 커널 내부: raw_rcv()에서 필터 검사 */ static inline bool raw_icmp_type_filtered(const struct raw_sock *rp, u8 type) { /* type에 해당하는 비트가 1이면 필터링(드롭) */ return (rp->filter.data >> type) & 1; } }}} ===== IPv6 Raw Socket (AF_INET6 SOCK_RAW) ===== IPv6 raw socket은 IPv4와 유사하지만 중요한 차이점이 있습니다: ||특성||IPv4 (AF_INET)||IPv6 (AF_INET6)|| ||IP 헤더 접근||{{{IP_HDRINCL}}}로 IP 헤더 포함 가능||IPv6 헤더는 항상 커널이 생성 ({{{IPV6_HDRINCL}}} 미지원)|| ||확장 헤더||IP 옵션을 {{{IP_OPTIONS}}}로 설정||{{{IPV6_RTHDR}}}, {{{IPV6_HOPOPTS}}} 등 ancillary data(cmsg)로 설정|| ||ICMPv6 체크섬||사용자가 직접 계산||커널이 자동 계산 (RFC 3542 요구사항)|| ||체크섬 오프셋||해당 없음||{{{IPV6_CHECKSUM}}} — 페이로드 내 체크섬 위치 지정, 커널이 계산|| ||필터||{{{ICMP_FILTER}}}||{{{ICMPV6_FILTER}}} — 256비트 비트맵 ({{{struct icmp6_filter}}})|| ||커널 소스||{{{net/ipv4/raw.c}}}||{{{net/ipv6/raw.c}}}|| {{{#!plain [코드: C] /* IPv6 raw socket에서 ICMPv6 체크섬은 커널이 자동 계산 */ /* net/ipv6/raw.c — rawv6_send_hdrinc() 내부 */ /* IPV6_CHECKSUM 소켓 옵션: 체크섬 계산 위치 지정 */ int offset = 2; /* 페이로드 시작부터 체크섬 필드의 바이트 오프셋 */ setsockopt(fd, IPPROTO_IPV6, IPV6_CHECKSUM, &offset, sizeof(offset)); /* 커널이 IPv6 pseudo-header 포함 체크섬을 해당 오프셋에 기록 */ /* ICMPv6 raw socket (protocol = IPPROTO_ICMPV6)은 * IPV6_CHECKSUM이 자동으로 offset=2에 설정됨 * → ICMPv6 체크섬 필드 위치가 헤더 시작+2바이트 */ /* ICMPv6 필터 예: Neighbor Solicitation만 수신 */ struct icmp6_filter filt; ICMP6_FILTER_SETBLOCKALL(&filt); ICMP6_FILTER_SETPASS(ND_NEIGHBOR_SOLICIT, &filt); setsockopt(fd, IPPROTO_ICMPV6, ICMPV6_FILTER, &filt, sizeof(filt)); }}} ===== AF_PACKET 심화 — L2 프레임 접근 ===== {{{AF_PACKET}}} 소켓은 Ethernet 프레임 수준에서 패킷을 캡처/전송합니다. {{{tcpdump}}}, {{{wireshark}}}, {{{dhclient}}}, {{{arping}}} 등이 사용합니다. ||소켓 타입||수신 시 포함 헤더||전송 시 필요 헤더||사용 사례|| ||{{{AF_PACKET, SOCK_RAW}}}||Ethernet + IP + L4 + 페이로드||사용자가 Ethernet 헤더 포함 전체 작성||tcpdump, 패킷 injection|| ||{{{AF_PACKET, SOCK_DGRAM}}}||IP + L4 + 페이로드 (Ethernet 제거)||커널이 Ethernet 헤더 생성||dhclient, 프로토콜 분석|| {{{#!plain [코드: C] /* net/packet/af_packet.c — packet_type 등록 * AF_PACKET 소켓 생성 시 packet_type을 등록하여 * NIC 드라이버의 수신 경로에 후킹 */ static int packet_create(struct net *net, struct socket *sock, int protocol, int kern) { struct packet_sock *po; struct sock *sk; sk = sk_alloc(net, PF_PACKET, GFP_KERNEL, &packet_proto, kern); po = pkt_sk(sk); /* packet_type 구조체 설정 */ po->prot_hook.func = packet_rcv; /* 수신 콜백 */ po->prot_hook.af_packet_priv = sk; /* 소켓 포인터 */ if (protocol) { po->prot_hook.type = protocol; /* ETH_P_ALL, ETH_P_IP 등 */ __register_prot_hook(sk); /* → dev_add_pack() → ptype_all 또는 ptype_base[] 리스트에 등록 * → netif_receive_skb() 경로에서 모든 수신 패킷에 대해 콜백 */ } } /* 수신 콜백: netif_receive_skb() → deliver_skb() → packet_rcv() */ static int packet_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { struct sock *sk = pt->af_packet_priv; struct sk_buff *copy; unsigned int snaplen, res; /* BPF 필터 적용 (setsockopt SO_ATTACH_FILTER) */ res = run_filter(skb, sk, snaplen); if (!res) goto drop; /* BPF 필터에 의해 드롭 */ /* ETH_P_ALL인 경우 모든 패킷에 대해 호출됨 */ copy = skb_clone(skb, GFP_ATOMIC); if (!copy) goto drop; /* SOCK_RAW: MAC 헤더부터 전체 프레임 노출 */ if (sk->sk_type == SOCK_RAW) skb_push(copy, skb_mac_header_len(skb)); /* sockaddr_ll에 수신 메타데이터 기록 */ struct sockaddr_ll *sll = &PACKET_SKB_CB(copy)->sa.ll; sll->sll_ifindex = orig_dev->ifindex; sll->sll_hatype = dev->type; sll->sll_pkttype = skb->pkt_type; /* PACKET_HOST, PACKET_BROADCAST 등 */ sock_queue_rcv_skb(sk, copy); return 0; drop: kfree_skb(skb); return 0; } /* AF_PACKET 전송: 사용자 → dev_queue_xmit() 직접 전달 */ static int packet_sendmsg(struct socket *sock, struct msghdr *msg, size_t len) { /* SOCK_RAW: 사용자가 Ethernet 헤더 포함 전체 프레임 작성 */ /* SOCK_DGRAM: sockaddr_ll에서 목적지 MAC, 커널이 Ethernet 헤더 생성 */ struct sk_buff *skb = packet_alloc_skb(sk, ...); /* 사용자 데이터 복사 */ skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, len); /* IP 스택을 완전히 우회하여 직접 디바이스 큐로 전송 */ err = dev_queue_xmit(skb); /* → qdisc → NIC 드라이버 → 물리 전송 */ } }}} ===== TPACKET — mmap 기반 고성능 캡처 ===== {{{TPACKET}}}(PACKET_MMAP)은 커널-사용자 간 mmap된 공유 ring buffer를 사용하여 {{{recvmsg()}}}/{{{sendmsg()}}} 시스템콜 오버헤드 없이 패킷을 교환합니다. {{{tcpdump}}}, {{{libpcap}}}, {{{suricata}}} 등 고성능 캡처 도구의 핵심입니다. ||버전||커널||특징||제한/이슈|| ||{{{TPACKET_V1}}}||2.4+||기본 ring buffer, 고정 크기 프레임||32비트 타임스탬프, 큰 패킷 지원 불가|| ||{{{TPACKET_V2}}}||2.6.27+||VLAN 태그 보존, 64비트 타임스탬프||여전히 고정 크기 프레임|| ||{{{TPACKET_V3}}}||3.2+||가변 크기 블록, 타임아웃 기반 블록 해제, 배치 처리||TX ring 미지원 (V2 사용), 구현 복잡|| {{{#!plain [SVG 텍스트 변환: TPACKET_V3 Ring Buffer 구조 다이어그램] 텍스트 요소: - TPACKET_V3 Ring Buffer 구조 - 커널 공간 - Block 0 - TP_STATUS_KERNEL - pkt 1 - pkt 2 - ← 커널이 쓰는 중 - Block 1 - TP_STATUS_USER - pkt 3 - pkt 4 - → 사용자가 읽는 중 - Block 2 (빈 블록) - Block N-1 - 사용자 공간 (mmap) - mmap()으로 매핑된 동일 물리 메모리 - → 복사 없이 직접 접근 (zero-copy 수신) - poll()/ppoll()로 TP_STATUS_USER 블록 대기 - → 시스콜 없이 블록 순회하며 패킷 읽기 - 처리 완료 후 TP_STATUS_KERNEL로 반환 - → 커널이 다시 사용 가능 - mmap }}} {{{#!plain [코드: C] /* TPACKET_V3 ring buffer 설정 예 (사용자 공간) */ struct tpacket_req3 req = { .tp_block_size = 1 << 22, /* 4MB 블록 */ .tp_block_nr = 64, /* 64개 블록 = 256MB */ .tp_frame_size = TPACKET_ALIGNMENT << 7, /* 프레임 정렬 */ .tp_frame_nr = (1 << 22) * 64 / (TPACKET_ALIGNMENT << 7), .tp_retire_blk_tov = 60, /* 블록 타임아웃 60ms */ .tp_feature_req_word = TP_FT_REQ_FILL_RXHASH, }; /* TPACKET 버전 설정 */ int ver = TPACKET_V3; setsockopt(fd, SOL_PACKET, PACKET_VERSION, &ver, sizeof(ver)); /* RX ring buffer 설정 */ setsockopt(fd, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req)); /* 커널-사용자 공유 메모리 매핑 */ void *ring = mmap(NULL, req.tp_block_size * req.tp_block_nr, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, 0); /* 패킷 수신 루프 (V3 블록 기반) */ while (1) { struct tpacket_block_desc *pbd = block_descs[current_block]; /* 블록이 준비될 때까지 대기 */ while (!(pbd->hdr.bh1.block_status & TP_STATUS_USER)) poll(&pfd, 1, -1); /* 블록 내 모든 패킷 순회 */ int num_pkts = pbd->hdr.bh1.num_pkts; struct tpacket3_hdr *ppd = (struct tpacket3_hdr *) ((uint8_t *)pbd + pbd->hdr.bh1.offset_to_first_pkt); for (int i = 0; i < num_pkts; i++) { uint8_t *pkt_data = (uint8_t *)ppd + ppd->tp_mac; uint32_t pkt_len = ppd->tp_snaplen; process_packet(pkt_data, pkt_len); /* 패킷 처리 */ ppd = (struct tpacket3_hdr *) ((uint8_t *)ppd + ppd->tp_next_offset); } /* 블록을 커널에 반환 */ pbd->hdr.bh1.block_status = TP_STATUS_KERNEL; current_block = (current_block + 1) % req.tp_block_nr; } /* 커널 내부: TPACKET_V3 수신 처리 * net/packet/af_packet.c — tpacket_rcv() */ static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev) { /* V3: 현재 블록에 패킷 추가 (가변 크기) * → skb 데이터를 mmap 버퍼에 직접 복사 * → 블록이 가득 차거나 타임아웃 시 TP_STATUS_USER로 전환 * → 사용자는 poll()로 알림 받고 mmap 메모리에서 직접 읽음 * → recvmsg() 시스콜 불필요 = zero-copy 수신 */ /* BPF 필터 먼저 실행 */ res = run_filter(skb, sk, snaplen); if (!res) goto drop; /* ring buffer의 현재 블록에 패킷 데이터 복사 */ h.raw = packet_current_rx_frame(po, skb, ...); skb_copy_bits(skb, 0, h.raw + macoff, snaplen); /* 패킷 메타데이터 기록: 타임스탬프, 길이, VLAN 등 */ h.h3->tp_sec = ts.tv_sec; h.h3->tp_nsec = ts.tv_nsec; h.h3->tp_snaplen = snaplen; h.h3->tp_len = skb->len; } }}} ===== PACKET_FANOUT — 멀티코어 패킷 분산 ===== 여러 AF_PACKET 소켓이 동일 인터페이스에서 패킷을 분산 처리할 수 있습니다. {{{suricata}}}, {{{PF_RING}}} 대안으로 사용됩니다: {{{#!plain [코드: C] /* PACKET_FANOUT 모드 */ #define PACKET_FANOUT_HASH 0 /* 흐름 해시 기반 분배 (기본) */ #define PACKET_FANOUT_LB 1 /* 라운드 로빈 */ #define PACKET_FANOUT_CPU 2 /* CPU ID 기반 (RSS 활용) */ #define PACKET_FANOUT_ROLLOVER 3 /* 큐 가득 차면 다음 소켓으로 */ #define PACKET_FANOUT_RND 4 /* 랜덤 분배 */ #define PACKET_FANOUT_QM 5 /* skb 큐 매핑 기반 */ #define PACKET_FANOUT_CBPF 6 /* cBPF 프로그램으로 분배 결정 */ #define PACKET_FANOUT_EBPF 7 /* eBPF 프로그램으로 분배 결정 */ /* 사용 예: 4개 워커 스레드가 흐름 해시 기반으로 패킷 분산 */ int fanout_arg = (PACKET_FANOUT_HASH | PACKET_FANOUT_FLAG_DEFRAG) | (group_id << 16); setsockopt(fd, SOL_PACKET, PACKET_FANOUT, &fanout_arg, sizeof(fanout_arg)); /* 커널 내부: fanout_demux() — 소켓 선택 */ static struct sock *fanout_demux_hash( struct packet_fanout *f, struct sk_buff *skb, unsigned int num) { /* skb 흐름 해시를 소켓 수로 나눠 분배 */ return f->arr[reciprocal_scale( __skb_get_hash_symmetric(skb), num)]; } /* PACKET_FANOUT_FLAG 옵션 */ #define PACKET_FANOUT_FLAG_ROLLOVER 0x1000 /* 소켓 백로그 시 롤오버 */ #define PACKET_FANOUT_FLAG_UNIQUEID 0x2000 /* 고유 그룹 ID 자동 할당 */ #define PACKET_FANOUT_FLAG_DEFRAG 0x8000 /* IP 단편화 재조합 후 분배 */ }}} ===== Raw Socket과 Netfilter 관계 ===== Raw socket으로 전송하는 패킷도 Netfilter 체인을 통과합니다. 수신은 프로토콜 핸들러 이전(raw_local_deliver)에 처리되므로 INPUT 체인보다 먼저 clone이 발생합니다: ||방향||소켓 타입||Netfilter 통과 여부||설명|| ||TX||AF_INET SOCK_RAW||OUTPUT 체인 통과||{{{raw_send_hdrinc()}}} → {{{NF_HOOK(NF_INET_LOCAL_OUT)}}}|| ||TX||AF_PACKET SOCK_RAW||Netfilter 우회||{{{dev_queue_xmit()}}} 직접 호출 (L3 스택 미통과)|| ||RX||AF_INET SOCK_RAW||PREROUTING 이후, INPUT 이전||NF_INET_PRE_ROUTING 통과 후 {{{raw_local_deliver()}}}|| ||RX||AF_PACKET SOCK_RAW||Netfilter 이전에 수신||{{{netif_receive_skb()}}}에서 ptype 콜백 (L3 이전)|| '''팁''' tcpdump가 DROP된 패킷도 보이는 이유: {{{AF_PACKET}}} 소켓은 {{{netif_receive_skb()}}}의 {{{ptype_all}}} 리스트에 등록되어 Netfilter 이전에 skb 사본을 받습니다. 따라서 iptables/nftables에서 DROP된 패킷도 tcpdump에서 관찰됩니다. 전송 방향도 마찬가지로, {{{AF_PACKET}}} TX는 {{{dev_queue_xmit()}}}을 직접 호출하여 Netfilter OUTPUT 체인을 우회합니다. ===== Raw Socket의 bind()와 connect() ===== {{{#!plain [코드: C] /* AF_INET SOCK_RAW에서 bind()와 connect()의 역할 */ /* bind() — 수신 필터링: 특정 로컬 IP로의 패킷만 수신 */ struct sockaddr_in addr = { .sin_family = AF_INET, .sin_addr.s_addr = inet_addr("192.168.1.10"), }; bind(fd, (struct sockaddr *)&addr, sizeof(addr)); /* → raw_v4_match()에서 daddr 매칭에 사용 * → 해당 IP가 목적지인 패킷만 수신 큐에 전달 */ /* connect() — 기본 목적지 설정 + 수신 필터링 */ struct sockaddr_in dest = { .sin_family = AF_INET, .sin_addr.s_addr = inet_addr("10.0.0.1"), }; connect(fd, (struct sockaddr *)&dest, sizeof(dest)); /* → send()에서 목적지 주소 생략 가능 (sendto 대신 send 사용) * → 수신 시 해당 소스 IP에서 온 패킷만 수신 (소스 필터) */ /* AF_PACKET에서 bind() — 특정 인터페이스에 바인딩 */ struct sockaddr_ll sll = { .sll_family = AF_PACKET, .sll_protocol = htons(ETH_P_ALL), .sll_ifindex = if_nametoindex("eth0"), }; bind(fd, (struct sockaddr *)&sll, sizeof(sll)); /* → 해당 인터페이스의 패킷만 수신 * → 바인딩 없으면 모든 인터페이스의 패킷 수신 */ }}} ===== 실용 예제 ===== {{{#!plain [코드: C] /* 예제 1: ICMP Echo Request 전송 (ping 구현) */ int fd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); /* CAP_NET_RAW 필요 */ struct { struct icmphdr hdr; char data[56]; /* 페이로드 (타임스탬프 등) */ } pkt; pkt.hdr.type = ICMP_ECHO; pkt.hdr.code = 0; pkt.hdr.un.echo.id = htons(getpid()); pkt.hdr.un.echo.sequence = htons(seq++); pkt.hdr.checksum = 0; pkt.hdr.checksum = icmp_checksum(&pkt, sizeof(pkt)); struct sockaddr_in dest = { .sin_family = AF_INET, .sin_addr.s_addr = inet_addr("8.8.8.8") }; sendto(fd, &pkt, sizeof(pkt), 0, (struct sockaddr *)&dest, sizeof(dest)); /* 커널이 IP 헤더를 자동 생성 (IP_HDRINCL 미설정이므로) * → skb 할당 → ICMP 페이로드 복사 → IP 헤더 추가 * → raw_sendmsg() → ip_append_data() → ip_push_pending_frames() * → Netfilter OUTPUT → ip_output() → dev_queue_xmit() */ /* 수신: raw socket은 모든 ICMP 패킷을 받으므로 필터 설정 */ struct icmp_filter filt; filt.data = ~(1 << ICMP_ECHOREPLY); setsockopt(fd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt)); char buf[1500]; struct sockaddr_in from; socklen_t fromlen = sizeof(from); int n = recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&from, &fromlen); /* buf[0..19] = IP 헤더 (raw socket은 항상 IP 헤더 포함 수신) * buf[20..] = ICMP 헤더 + 페이로드 */ }}} {{{#!plain [코드: C] /* 예제 2: ARP Request 전송 (AF_PACKET SOCK_RAW) */ int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP)); /* 전체 Ethernet 프레임을 직접 구성 */ struct { struct ethhdr eth; /* Ethernet 헤더 (14 bytes) */ struct arphdr arp; /* ARP 헤더 */ uint8_t ar_sha[6]; /* 송신자 MAC */ uint8_t ar_sip[4]; /* 송신자 IP */ uint8_t ar_tha[6]; /* 대상 MAC (ARP Request에서는 0) */ uint8_t ar_tip[4]; /* 대상 IP */ } frame; /* Ethernet 헤더: 브로드캐스트 */ memset(frame.eth.h_dest, 0xff, ETH_ALEN); /* FF:FF:FF:FF:FF:FF */ memcpy(frame.eth.h_source, my_mac, ETH_ALEN); frame.eth.h_proto = htons(ETH_P_ARP); /* ARP 헤더: ARP Request */ frame.arp.ar_hrd = htons(ARPHRD_ETHER); frame.arp.ar_pro = htons(ETH_P_IP); frame.arp.ar_hln = 6; frame.arp.ar_pln = 4; frame.arp.ar_op = htons(ARPOP_REQUEST); /* sockaddr_ll로 출력 인터페이스 지정 */ struct sockaddr_ll sll = { .sll_ifindex = if_nametoindex("eth0"), .sll_halen = ETH_ALEN, }; memset(sll.sll_addr, 0xff, ETH_ALEN); sendto(fd, &frame, sizeof(frame), 0, (struct sockaddr *)&sll, sizeof(sll)); /* → packet_sendmsg() → dev_queue_xmit() * → IP 스택, Netfilter 완전 우회 * → 직접 NIC 드라이버로 전달 */ }}} {{{#!plain [코드: C] /* 예제 3: IP_HDRINCL로 커스텀 IP 패킷 전송 */ int fd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); /* IPPROTO_RAW (255)는 자동으로 IP_HDRINCL 활성화 */ struct { struct iphdr ip; struct udphdr udp; char payload[64]; } pkt; /* IP 헤더 구성 */ pkt.ip.version = 4; pkt.ip.ihl = 5; pkt.ip.tos = 0; pkt.ip.tot_len = htons(sizeof(pkt)); pkt.ip.id = 0; /* 커널이 자동 할당 */ pkt.ip.frag_off = htons(IP_DF); pkt.ip.ttl = 64; pkt.ip.protocol = IPPROTO_UDP; pkt.ip.check = 0; /* 커널이 자동 계산 */ pkt.ip.saddr = inet_addr("10.0.0.1"); pkt.ip.daddr = inet_addr("10.0.0.2"); /* UDP 헤더 구성 */ pkt.udp.source = htons(12345); pkt.udp.dest = htons(53); pkt.udp.len = htons(sizeof(pkt.udp) + sizeof(pkt.payload)); pkt.udp.check = 0; /* UDP 체크섬은 사용자가 계산해야 함 */ sendto(fd, &pkt, sizeof(pkt), 0, ...); /* → raw_sendmsg() → inet->hdrincl=1이므로 raw_send_hdrinc() * → 커널은 check, tot_len(0이면), id(0이면), saddr(0이면)만 보정 * → NF_HOOK(NF_INET_LOCAL_OUT) → dst_output() → dev_queue_xmit() */ }}} ===== 보안 고려사항 ===== ||위협||관련 소켓 타입||방어 메커니즘|| ||IP 스푸핑||AF_INET + IP_HDRINCL||{{{rp_filter}}} (Reverse Path Filtering), BCP 38 (uRPF)|| ||ARP 스푸핑||AF_PACKET SOCK_RAW||DAI (Dynamic ARP Inspection), 정적 ARP 엔트리|| ||패킷 스니핑||AF_PACKET (ETH_P_ALL)||CAP_NET_RAW 제한, 네트워크 네임스페이스 격리|| ||프로토콜 스택 DoS||SOCK_RAW 대량 전송||{{{net.core.rmem_max}}}, {{{sk->sk_sndbuf}}} 제한|| ||컨테이너 탈출||AF_PACKET TPACKET||CAP_NET_RAW 제거, seccomp 필터|| {{{#!plain [코드: bash] # CAP_NET_RAW 관련 보안 설정 # 특정 바이너리에만 CAP_NET_RAW 부여 (setuid 대체) setcap cap_net_raw+ep /usr/bin/ping # ping socket 허용 범위 설정 (CAP_NET_RAW 불필요) # GID 0~2147483647 범위의 사용자가 ICMP ping 가능 sysctl -w net.ipv4.ping_group_range="0 2147483647" # Reverse Path Filtering (IP 스푸핑 방지) sysctl -w net.ipv4.conf.all.rp_filter=1 # strict mode sysctl -w net.ipv4.conf.all.rp_filter=2 # loose mode # 컨테이너에서 CAP_NET_RAW 제거 (Docker) docker run --cap-drop=NET_RAW ... # seccomp으로 raw socket 시스콜 차단 # socket(AF_PACKET, ...) 또는 socket(AF_INET, SOCK_RAW, ...) 블록 # 열린 raw socket 확인 ss -w -a # RAW 소켓 목록 cat /proc/net/raw # IPv4 raw socket 상세 정보 cat /proc/net/raw6 # IPv6 raw socket 상세 정보 cat /proc/net/packet # AF_PACKET 소켓 목록 }}} '''주의''' IPPROTO_RAW (255) 특수 동작: {{{socket(AF_INET, SOCK_RAW, IPPROTO_RAW)}}}는 전송 전용 raw socket을 생성합니다. {{{IP_HDRINCL}}}이 자동 활성화되며, 이 소켓으로는 수신이 불가합니다 ({{{recvmsg()}}}가 영원히 블록). 수신하려면 {{{IPPROTO_RAW}}} 대신 구체적인 프로토콜 번호(예: {{{IPPROTO_UDP}}})를 지정하거나 별도의 수신용 raw socket을 생성해야 합니다. ==== 소켓 디먹싱과 skb 전달 ==== 수신된 skb가 올바른 소켓을 찾아가는 과정 (디먹싱): {{{#!plain [코드: C] /* TCP 수신 디먹싱: 4-tuple 해시 → 소켓 lookup */ /* tcp_v4_rcv() 내부 */ struct sock *sk = __inet_lookup_skb( &tcp_hashinfo, /* TCP 소켓 해시 테이블 */ skb, __tcp_hdrlen(th), th->source, /* 소스 포트 */ th->dest, /* 목적지 포트 */ iph->saddr, /* 소스 IP */ iph->daddr, /* 목적지 IP */ sdif); /* 반환: established 소켓 또는 listen 소켓 */ /* UDP 수신 디먹싱 */ struct sock *sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable); /* 소켓을 찾은 후 skb를 수신 큐에 전달 */ if (!sock_owned_by_user(sk)) { /* 소켓이 lock 상태가 아니면 직접 수신 큐에 추가 */ __skb_queue_tail(&sk->sk_receive_queue, skb); sk->sk_data_ready(sk); /* epoll/poll/select wakeup */ } else { /* 소켓이 lock 중이면 backlog에 임시 저장 */ __sk_add_backlog(sk, skb); /* → release_sock() 시 backlog 처리 */ } }}} '''팁''' 성능 팁: {{{SO_REUSEPORT}}}와 {{{BPF_PROG_TYPE_SK_REUSEPORT}}}를 조합하면, 동일 포트를 여러 소켓이 공유하면서 BPF 프로그램으로 skb를 특정 소켓에 스티어링할 수 있습니다. 이는 {{{nginx}}}, {{{envoy}}} 등의 고성능 프록시에서 활용됩니다. === Zero-copy 전송 === 대용량 데이터를 전송할 때, 사용자 공간 버퍼를 커널로 복사하지 않고 직접 NIC에 전달하여 CPU 사용량과 지연을 줄일 수 있습니다: ||메커니즘||시스템콜||동작 방식|| ||{{{sendfile()}}}||{{{sendfile(out_fd, in_fd, ...)}}}||파일 → 소켓 직접 전송 (페이지 캐시 → skb frag)|| ||{{{splice()}}}||{{{splice(fd_in, ..., fd_out, ...)}}}||파이프 기반 zero-copy, 파일 ↔ 소켓 모두 가능|| ||{{{MSG_ZEROCOPY}}}||{{{send(fd, buf, len, MSG_ZEROCOPY)}}}||사용자 버퍼 → skb frag (완료 통지 필요, 4.14+)|| {{{#!plain [코드: C] /* 커널 내부: sendfile의 skb 구성 */ /* 파일 페이지를 skb fragment로 직접 참조 */ skb_fill_page_desc(skb, frag_idx, page, offset, size); /* page refcount 증가, 복사 없음 */ /* MSG_ZEROCOPY: 사용자 버퍼 페이지를 pin */ /* skb->destructor = sock_zerocopy_callback; * 전송 완료 시 사용자 공간에 completion notification 전달 * (errqueue에서 SO_EE_ORIGIN_ZEROCOPY 메시지 수신) */ /* 드라이버: skb_page_frag_refill로 페이지 풀 활용 */ struct page_frag_cache *nc = &this_cpu_ptr(&nf_skb_cache)->pf_cache; if (!skb_page_frag_refill(size, nc, GFP_ATOMIC)) return -ENOMEM; /* nc->va + nc->offset 에서 size 바이트 사용 가능 */ }}} '''팁''' {{{MSG_ZEROCOPY}}}는 10Gbps 이상 고속 네트워크에서 효과적입니다. 그러나 작은 패킷(~수KB 이하)에서는 페이지 pinning과 completion 통지 오버헤드가 복사 비용보다 클 수 있습니다. Google의 벤치마크에 따르면 5~8% CPU 절감이 일반적입니다. === 수신/전송 경로에서의 skb 변형 === ==== sk_buff 생애주기 (Lifecycle) ==== sk_buff는 네트워크 패킷의 전체 생명주기 동안 커널 메모리를 차지하며, 할당부터 해제까지 다양한 변형을 거칩니다. 다음 다이어그램은 수신(RX)과 송신(TX) 경로에서 sk_buff의 생애주기를 보여줍니다. {{{#!plain [SVG 텍스트 변환: sk_buff 생애주기 개요] 캡션: 먼저 개요 흐름을 보고, 아래 상세 다이어그램에서 함수 단위로 추적하면 이해가 빠릅니다. 텍스트 요소: - 할당/초기화 - RX/TX 경로 처리 - 큐잉/해제 - `alloc_skb()`, 헤더 포인터 설정 - L2/L3/L4, Netfilter, qdisc - 소켓 전달 또는 NIC 전송 완료 - 상세 단계는 아래 상세 다이어그램 참고 }}} {{{#!plain [SVG 텍스트 변환: sk_buff 생애주기 상세 흐름] 캡션: sk_buff 생애주기: 할당 → 프로토콜 스택 통과 → 소켓 큐 → 유저스페이스 전달 → 해제 텍스트 요소: - sk_buff 생애주기 — 수신/송신 경로 - 수신 경로 (RX) - 1. skb 할당 - netdev_alloc_skb() / napi_alloc_skb() - 2. DMA 데이터 복사 - NIC → skb->data (ring buffer) - 3. L2 프로토콜 처리 - eth_type_trans(), mac_header 설정 - 4. L3 프로토콜 처리 - ip_rcv(), network_header 설정 - 5. Netfilter 훅 - 6. L4 프로토콜 처리 - tcp_v4_rcv(), transport_header 설정 - 7. 소켓 수신 큐 - skb_queue_tail(&sock->sk_receive_queue) - 8. 유저스페이스 복사 - recvmsg() → copy_to_user() - 9. skb 해제 - kfree_skb() / consume_skb() - 송신 경로 (TX) - 1. 유저 데이터 복사 - sendmsg() → copy_from_user() - 2. skb 할당 - sock_alloc_send_skb() / alloc_skb() - 3. L4 헤더 추가 - skb_push(TCP/UDP 헤더) - 4. L3 헤더 추가 - skb_push(IP 헤더), 라우팅 결정 - 6. L2 헤더 추가 - skb_push(Ethernet 헤더) - 7. TC/Qdisc (QoS) - 트래픽 제어, 우선순위 큐 - 8. 드라이버 전송 - dev_queue_xmit() → DMA 매핑 - 9. 전송 완료 & 해제 - TX 완료 인터럽트 → dev_kfree_skb() - skb 포인터 이동 - RX: data, tail 이동 (pull) - TX: data 이동 (push) - 각 계층에서 헤더 참조: - mac_header, network_header, - transport_header }}} ===== 참조 카운트와 메모리 관리 ===== {{{#!plain [코드: C] /* sk_buff의 참조 카운트는 users 필드로 관리 */ struct sk_buff { atomic_t users; /* 참조 카운트 (skb_get/skb_put으로 증감) */ /* ... */ }; /* 참조 카운트 증가 — 소유권 공유 */ static inline struct sk_buff *skb_get(struct sk_buff *skb) { refcount_inc(&skb->users); return skb; } /* 참조 카운트 감소 — 0이 되면 해제 */ static inline void kfree_skb(struct sk_buff *skb) { if (!skb) return; if (refcount_dec_and_test(&skb->users)) __kfree_skb(skb); /* 실제 해제 */ } /* 정상 소비 (드롭 아님) — 통계 구분 */ static inline void consume_skb(struct sk_buff *skb) { if (!skb) return; if (refcount_dec_and_test(&skb->users)) __kfree_skb(skb); } /* 사용 예: 소켓 큐에서 꺼낸 후 해제 */ struct sk_buff *skb = skb_dequeue(&sk->sk_receive_queue); if (skb) { process_packet(skb); consume_skb(skb); /* 정상 소비 */ } /* 드롭 시 kfree_skb 사용 (디버깅 추적 가능) */ if (!pskb_may_pull(skb, sizeof(struct iphdr))) { kfree_skb(skb); /* 드롭: perf/dropwatch로 추적됨 */ return -EINVAL; } }}} ===== 수신/송신 경로에서의 데이터 영역 변화 ===== 수신 경로: 각 계층에서 헤더를 제거하며 {{{data}}} 포인터가 앞으로 이동합니다. ||단계||대표 함수||버퍼 레이아웃||{{{data}}} 포인터 위치|| ||1. DMA 복사 직후||NIC RX||`headroom||ETH||IP||TCP||DATA||tail`||ETH 시작점|| ||2. L2 처리||{{{eth_type_trans()}}}||`headroom||ETH||IP||TCP||DATA||tail`||{{{skb_pull(ETH_HLEN)}}} 후 IP 시작점|| ||3. L3 처리||{{{ip_rcv()}}}||`headroom||ETH||IP||TCP||DATA||tail`||{{{skb_pull(ip_hdr_len)}}} 후 TCP 시작점|| ||4. L4 처리||{{{tcp_rcv()}}}||`headroom||ETH||IP||TCP||DATA||tail`||payload 시작점 (헤더 제거 완료)|| 송신 경로: 각 계층에서 헤더를 추가하며 {{{data}}} 포인터가 뒤로 이동합니다. ||단계||대표 함수||버퍼 레이아웃||{{{data}}} 포인터 이동|| ||1. payload 준비||소켓 송신 준비||`headroom||PAYLOAD||tail`||payload 시작점|| ||2. L4 헤더 추가||{{{tcp_transmit_skb()}}}||`headroom||TCP||PAYLOAD||tail`||{{{skb_push(tcp_hdr_len)}}}|| ||3. L3 헤더 추가||{{{ip_queue_xmit()}}}||`headroom||IP||TCP||PAYLOAD`||{{{skb_push(ip_hdr_len)}}}|| ||4. L2 헤더 추가||{{{dev_hard_start_xmit()}}}||`headroom||ETH||IP||TCP||PAYLOAD`||{{{skb_push(ETH_HLEN)}}} 후 NIC DMA 전송|| '''주의''' headroom 부족 문제: 송신 경로에서 헤더를 추가할 때 headroom이 부족하면 {{{skb_realloc_headroom()}}}이 호출되어 새로운 버퍼를 할당합니다. 이는 성능 저하를 유발하므로, 초기 할당 시 충분한 headroom을 확보하는 것이 중요합니다 ({{{NET_SKB_PAD}}} + {{{NET_IP_ALIGN}}} + 예상 헤더 크기). ===== eth_type_trans() 호출 전후 skb 필드 변화 ===== NIC 드라이버가 {{{eth_type_trans(skb, dev)}}}를 호출하면 다음 필드들이 갱신됩니다: ||필드||호출 전 (DMA 직후)||호출 후|| ||{{{skb->mac_header}}}||미설정||L2(Ethernet) 헤더 시작 오프셋으로 설정 ({{{skb_reset_mac_header()}}} 수행)|| ||{{{skb->protocol}}}||미설정||EtherType 값 (예: {{{ETH_P_IP}}}, {{{ETH_P_IPV6}}}) — {{{ntohs()}}} 변환 포함|| ||{{{skb->data}}}||L2(ETH) 헤더 시작||L3 헤더 시작 ({{{skb_pull(ETH_HLEN)}}} 또는 VLAN 포함 크기만큼 당겨짐)|| ||{{{skb->len}}}||L2 프레임 전체 크기||ETH 헤더 제거 후 L3 이상 크기|| ||{{{skb->network_header}}}||미설정||미설정 — {{{ip_rcv()}}} 진입 후 {{{skb_reset_network_header()}}}로 설정됨|| ||{{{skb->mac_len}}}||미설정||미설정 — L3 처리 진입 시 {{{skb->network_header - skb->mac_header}}}로 초기화됨|| '''팁''' 포인터 복원: {{{mac_header}}}가 설정된 뒤에는 {{{eth_hdr(skb)}}}로 Ethernet 헤더 포인터를 얻을 수 있고, {{{skb_mac_header(skb)}}}로 {{{skb->head + skb->mac_header}}}에 해당하는 포인터를 얻습니다. {{{skb->data}}}는 이미 L3 시작으로 옮겨졌으므로 혼동하지 않도록 주의하십시오. ==== 수신/전송 경로 요약 ==== 수신 경로 (NIC → 앱): 1. NIC 드라이버: {{{netdev_alloc_skb()}}}로 skb 할당, DMA 데이터 복사 2. L2 처리: {{{skb_pull(ETH_HLEN)}}}으로 Ethernet 헤더 제거 3. L3 처리: {{{skb_pull(ip_hdr_len)}}}으로 IP 헤더 제거, transport_header 설정 4. L4 처리: TCP/UDP 디먹싱, 소켓 수신 큐에 {{{skb_queue_tail()}}} 5. 앱: {{{recvmsg()}}}에서 데이터를 사용자 공간에 복사 전송 경로 (앱 → NIC): 1. 앱: {{{sendmsg()}}}에서 사용자 데이터를 skb에 복사 2. L4: {{{skb_push()}}}로 TCP/UDP 헤더 추가 3. L3: {{{skb_push()}}}로 IP 헤더 추가, 라우팅 4. L2: {{{skb_push()}}}로 Ethernet 헤더 추가 5. NIC 드라이버: {{{dev_queue_xmit()}}} → DMA 전송 === 커널 내 실제 사용 사례 === sk_buff는 네트워크 스택의 모든 서브시스템에서 핵심적으로 사용됩니다: ||서브시스템||주요 skb 활용||핵심 함수/패턴|| ||TCP||전송 큐, 재전송 큐, OOO 큐에 skb 관리||{{{tcp_write_xmit()}}}, {{{tcp_retransmit_skb()}}}, {{{TCP_SKB_CB()}}}로 cb[] 활용|| ||UDP||소켓 수신 큐에 skb 대기열||{{{udp_rcv()}}}, {{{skb_consume_udp()}}}, MSG_PEEK 처리|| ||Netfilter||패킷 필터링/수정/NAT||{{{skb_make_writable()}}} 후 헤더 수정, {{{nf_ct_get(skb)}}}로 conntrack|| ||Bridge||L2 포워딩, VLAN 처리||{{{skb_clone()}}}으로 멀티캐스트 복제, {{{skb_vlan_push/pop()}}}|| ||Tunnel (GRE, VXLAN)||encapsulation/decapsulation||{{{skb_cow_head()}}}로 headroom 확보, {{{skb_push()}}}로 외부 헤더 추가|| ||TC (Traffic Control)||QoS, 큐잉, 셰이핑||{{{skb->priority}}}, {{{skb->mark}}}, {{{skb_get_queue_mapping()}}}|| ||BPF/XDP||프로그래밍 가능 패킷 처리||TC-BPF: {{{__skb_buff}}} 컨텍스트, XDP: skb 이전 단계 ({{{xdp_buff}}} → {{{build_skb}}})|| ||SCTP||멀티스트리밍, 멀티호밍||{{{skb_queue_head_init()}}}으로 청크별 큐 관리|| {{{#!plain [코드: C] /* TCP: cb[]를 tcp_skb_cb로 활용하는 패턴 */ struct tcp_skb_cb { __u32 seq; /* 시작 시퀀스 번호 */ __u32 end_seq; /* 끝 시퀀스 번호 */ __u32 ack_seq; /* ACK 번호 */ __u8 tcp_flags; /* TCP 플래그 */ __u8 sacked; /* SACK 상태 */ /* ... */ }; #define TCP_SKB_CB(__skb) \ ((struct tcp_skb_cb *)&((__skb)->cb[0])) /* 사용 예: TCP 재전송 판단 */ if (after(TCP_SKB_CB(skb)->end_seq, tp->snd_una)) /* 아직 ACK되지 않은 데이터 — 재전송 대상 */ /* Netfilter: 패킷 수정 전 쓰기 가능 확보 */ static unsigned int my_nf_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { struct iphdr *iph; if (skb_ensure_writable(skb, sizeof(*iph))) return NF_DROP; iph = ip_hdr(skb); iph->ttl--; /* 안전하게 수정 가능 */ ip_send_check(iph); return NF_ACCEPT; } }}} === 성능 튜닝 경험적 팁 === 커널 네트워크 스택과 NIC 드라이버를 다루면서 얻는 경험적 성능 최적화 팁들입니다: ==== 할당 최적화 ==== * NAPI 컨텍스트에서는 {{{napi_alloc_skb}}} 사용: 일반 {{{alloc_skb}}}보다 per-CPU 캐시를 활용해Cache hit율 높임. IRQ 컨텍스트에서는 atomic GFP(Get Free Pages) 플래그 필수. * 페이지 프래그먼트 활용: 수신 시 데이터가 크면 linear 버퍼 대신 {{{skb_add_rx_frag}}}로 페이지을 DMA 버퍼에 직접 추가. memcpy를 피하면 대역폭 활용이 크게 향상됨. * headroom 충분하게 확보: 초기 할당 시 {{{NET_SKB_PAD}}}(보통 32바이트) + {{{NET_IP_ALIGN}}}(2 또는 0) + 최대 헤더 크기(예: 100바이트) 확보. 나중에 {{{skb_realloc_headroom}}} 호출은 심각한 성능 저하 유발. ==== Clone vs Copy 선택 ==== * 읽기 전용 경로: {{{skb_clone}}} 사용. 데이터 버퍼 공유하므로 memcpy 1회 절약. Netfilter의 MIRROR 타겟, tcpdump가 이 패턴. * 헤더만 수정: {{{pskb_copy}}} 사용. Linear 영역만 복사하고 프래그먼트는 refcount 공유. NAT, 라우팅 변경에서 주로 사용. * Payload 수정 필요: {{{skb_copy}}} 사용. 완전 복사이므로 가장 느리지만 안전. * 경험적 판단: "이 패킷을 두 곳에서 동시에 수정하는가?" → 아니면 clone, 그 외면 copy. ==== 큐 및 스케줄링 ==== * RSS(Receive Side Scaling) 활용: 멀티큐 NIC에서 {{{skb->queue_mapping}}}이 수신 큐 인덱스 저장. {{{irqbalance}}} 또는 수동 IRQ affinity 설정으로 각 큐를 다른 CPU에 분산. * softirq 튜닝: {{{/proc/net/softnet_stat}}}에서 각 CPU의 처리량 확인. {{{net.core.netdev_budget}}}(기본 300)으로 softirq time slice 조절. * NAPI 폴링 시간: {{{netif_napi_add}}} 시 {{{napi->weight}}}(기본 64) 값을 조절. 높은 대역폭 지연이 허용되면 값을 크게, 저지연이 중요하면 작게 설정. ==== Zero-Copy 경로 ==== * sendfile(): 파일 전송 시 가장 효율적. 페이지 캐시 → NIC 직접 경로로 복사 최소화. HTTP 서버 정적 파일 전송에 적합. * MSG_ZEROCOPY: 대용량 UDP 전송에서 효과적. 10Gbps 이상에서 CPU 절약이 크게 향상됨. 단, TX 완료 대기로 추가 지연이 발생함. * TPACKET (mbuf): 고성능 캡처에서 필수. mmap으로 커널-사용자 공간 복사를 완전히 제거함. {{{suricata}}}, {{{tcpdump -i any}}} 참조. ==== 하드웨어 offload 활용 ==== * 체크섬 offload 활성화: {{{ethtool -K eth0 rx-checksumming on tx-checksumming on}}}. 대부분의 modern NIC에서 기본값. * TSO/GSO 활성화: {{{ethtool -K eth0 tso on gso on}}}. 대용량 TCP 전송 시 극적인 성능 향상. 64KB super-packet이 NIC에서 자동 분할. * GRO 활성화: {{{ethtool -K eth0 gro on}}}. 수신측 병합으로 수천 PPS에서 CPU 사용량 크게 감소. {{{#!plain [코드: bash] # NIC 오프로드 상태 확인 $ ethtool -k eth0 | grep -E "checksum|gso|gro|rss" tcp-segmentation-offload: on udp-fragmentation-offload: [fixed] generic-segmentation-offload: on generic-receive-offload: on tcp6-segmentation-offload: on rx-checksumming: on tx-checksumming: on # RSS 설정 확인 및 변경 $ ethtool -l eth0 # 큐 개수 확인 $ ethtool -L eth0 combined 4 # 4개 combined 큐로 설정 # interrupt coalescing 조절 (지연 vs 처리량) $ ethtool -C eth0 rx-usecs 100 tx-usecs 100 # moderate coalescing $ ethtool -C eth0 rx-usecs 0 tx-usecs 0 # 낮은 지연 (latency) }}} '''팁''' 실제 서비스 경험: 제가 운영하는 10Gbps DDoS 완화 장비에서 {{{GRO off}}} → {{{GRO on}}}으로 변경 시 CPU 사용량이 약 40% 감소했습니다. 하지만 특정 레거시 애플리케이션에서는 GRO로 인한 packet reordering이 문제를 일으킬 수 있어, 프로덕션 변경 전 반드시 테스트 환경에서 검증하세요. === 주의사항과 함정 (Common Mistakes) === ==== 1. skb leak (메모리 누수) ==== {{{#!plain [코드: C] /* 잘못된 코드: 에러 경로에서 skb 해제 누락 */ static int my_rx(struct sk_buff *skb) { struct my_hdr *hdr = (struct my_hdr *)skb->data; if (hdr->version != MY_VERSION) return -EINVAL; /* BUG! skb가 해제되지 않음 */ /* ... */ } /* 올바른 코드 */ static int my_rx(struct sk_buff *skb) { struct my_hdr *hdr = (struct my_hdr *)skb->data; if (hdr->version != MY_VERSION) { kfree_skb(skb); /* 에러 경로 → kfree_skb (드롭) */ return -EINVAL; } /* ... 정상 처리 후 ... */ consume_skb(skb); /* 정상 경로 → consume_skb */ return 0; } }}} ==== 2. pskb_may_pull 후 포인터 미갱신 ==== {{{#!plain [코드: C] /* 잘못된 코드: pull 후 이전 포인터 사용 */ struct iphdr *iph = ip_hdr(skb); if (!pskb_may_pull(skb, iph->ihl * 4)) goto drop; /* BUG! pskb_may_pull이 버퍼를 재할당했을 수 있음 → iph는 dangling pointer */ pr_info("saddr: %pI4\\n", &iph->saddr); /* 올바른 코드: 포인터 재취득 */ if (!pskb_may_pull(skb, sizeof(struct iphdr))) goto drop; struct iphdr *iph = ip_hdr(skb); /* pull 후 재취득 */ if (!pskb_may_pull(skb, iph->ihl * 4)) goto drop; iph = ip_hdr(skb); /* 두 번째 pull 후에도 재취득! */ }}} ==== 3. 공유 skb 데이터 수정 ==== {{{#!plain [코드: C] /* 잘못된 코드: clone된 skb의 데이터를 바로 수정 */ struct iphdr *iph = ip_hdr(skb); iph->ttl = 64; /* BUG! skb가 clone 상태면 원본도 수정됨 */ /* 올바른 코드: 쓰기 전 독점 소유 확보 */ if (skb_ensure_writable(skb, skb_network_header_len(skb))) goto drop; struct iphdr *iph = ip_hdr(skb); /* 포인터 재취득 */ iph->ttl = 64; /* 이제 안전 */ }}} ==== 4. truesize 불일치 ==== {{{#!plain [코드: C] /* 잘못된 코드: skb에 페이지를 추가하면서 truesize 미갱신 */ skb_add_rx_frag(skb, idx, page, offset, size, size); /* 마지막 인자(truesize)가 실제 할당 크기보다 작으면 * → 소켓 메모리 추적(sk_rmem_alloc)이 실제보다 작게 계산됨 * → 소켓이 제한 없이 메모리를 소비 → OOM 가능 */ /* 올바른 코드: truesize는 실제 할당된 메모리 크기 */ skb_add_rx_frag(skb, idx, page, offset, size, PAGE_SIZE); /* PAGE_SIZE = 실제 할당 단위 (page order 0 기준) */ }}} ==== 5. refcount 이중 해제 ==== {{{#!plain [코드: C] /* 잘못된 코드: netif_rx 후 skb를 다시 해제 */ netif_rx(skb); /* 네트워크 스택에 소유권 이전 */ kfree_skb(skb); /* BUG! 이중 해제 → use-after-free */ /* 올바른 패턴: 전달 후 skb를 사용하지 않음 */ netif_rx(skb); /* 소유권 이전, skb는 더 이상 사용하지 않음 */ /* netif_rx(), netif_receive_skb(), napi_gro_receive() 등은 * skb의 소유권을 가져감 — 이후 skb 접근 금지 */ }}} ==== 6. headroom 부족으로 인한 skb_under_panic ==== {{{#!plain [코드: C] /* 잘못된 코드: headroom 확인 없이 헤더 추가 */ skb_push(skb, sizeof(struct my_encap_hdr)); /* headroom이 부족하면 skb_under_panic → 커널 panic */ /* 올바른 코드: headroom 확보 후 추가 */ int needed = sizeof(struct my_encap_hdr) + LL_RESERVED_SPACE(dev); if (skb_cow_head(skb, needed)) { kfree_skb(skb); return NETDEV_TX_OK; } skb_push(skb, sizeof(struct my_encap_hdr)); /* 이제 안전 */ }}} ==== 7. 레이어 경계 처리 실수 ==== RX → 포워딩 → TX 경로를 거치는 패킷은 {{{mac_header}}}, {{{network_header}}} 포인터의 유효성이 경계마다 달라집니다. 이를 무시하면 잘못된 헤더 접근이나 커널 BUG를 유발합니다. ===== RX → 포워딩: mac_header 갭 문제 ===== {{{#!plain [코드: C] /* 수신 후 포워딩 경로에서 mac_header와 network_header 사이에 * "갭(hole)"이 생길 수 있음: * eth_type_trans() → data를 L3 시작으로 당김 * ip_rcv() → network_header = data 위치로 재설정 * → mac_header는 그대로이므로 mac_header < network_header * (갭 = Ethernet 헤더 크기) * * 포워딩 후 재전송 시 L2 헤더 재구성이 필요하면: */ skb_mac_header_rebuild(skb); /* mac_header를 network_header 바로 앞으로 재정렬 */ }}} ===== 포워딩 → TX: mac_header 무효화 ===== {{{#!plain [코드: C] /* IPSec/GRE 등 캡슐화 후 원래 mac_header가 무효화될 수 있음. * 헤더 재구성 전에 반드시 유효성 확인: */ if (skb_mac_header_was_set(skb)) { /* mac_header가 유효할 때만 eth_hdr(skb) 접근 */ struct ethhdr *eth = eth_hdr(skb); /* ... */ } /* 캡슐화로 mac_header가 갱신되지 않은 경우 직접 재설정 */ skb_reset_mac_header(skb); /* mac_header = data - head */ }}} ===== IPSec: headroom 확보 및 network_header 재설정 ===== {{{#!plain [코드: C] /* IPSec 암호화(outbound) 전: ESP/AH 헤더를 위한 headroom 확보 */ int head_delta = skb_cow_head(skb, esp_hdr_len + LL_RESERVED_SPACE(dst->dev)); if (head_delta) goto error; skb_push(skb, esp_hdr_len); /* ESP 헤더 공간 확보 */ skb_reset_network_header(skb); /* IP 헤더 위치 재설정 */ /* IPSec 복호화(inbound) 후: network_header가 바뀌었으므로 재설정 필수 */ skb_pull(skb, esp_hdr_len); /* ESP 헤더 제거 */ skb_reset_network_header(skb); /* 복호화된 IP 헤더로 포인터 재설정 */ }}} '''주의''' 레이어 경계 핵심 원칙: 캡슐화/역캡슐화 후에는 {{{network_header}}}, {{{transport_header}}}, {{{mac_header}}}를 반드시 재설정하십시오. 포인터를 재설정하지 않으면 {{{ip_hdr(skb)}}}, {{{tcp_hdr(skb)}}} 등이 잘못된 주소를 반환하여 조용한 메모리 손상이 발생합니다. === 실제 트러블슈팅 사례 === 네트워크 문제를 분석하면서 자주 마주하는 skb 관련 실제 케이스들입니다: ==== 사례 1: 메모리 누수가 의심될 때 ==== 증상: {{{ss -s}}}나 {{{/proc/net/sockstat}}}에서 사용 중인 소켓 수가 비정상적으로 많거나, 시스템 메모리가 점진적으로 감소합니다. {{{#!plain [코드: bash] # 현재 소켓 상태 확인 $ ss -s $ cat /proc/net/sockstat # orphan(소멸된) 소켓 수 — TIME_WAIT 소켓이 정리되지 않으면 증가 $ cat /proc/net/sockstat | grep TCP # 드롭된 패킷 수 확인 $ nstat -az TcpExt.ListenOverflows $ cat /proc/net/netstat | grep -E "Tcp|Ext" | column -t }}} 원인: 에러 경로에서 {{{kfree_skb()}}} 호출 누락, 또는 {{{consume_skb()}}} 대신 {{{kfree_skb()}}}를 사용해서 메모리 참조가 해제되지 않음. ==== 사례 2: 체크섬 검증 실패 ==== 증상: 특정 NIC에서만 TCP/UDP 체크섬 오류가 발생하거나, 애플리케이션에서 "bad checksum" 로그가 반복됩니다. {{{#!plain [코드: bash] # NIC 드라이버와 체크섬 오프로드 상태 확인 $ ethtool -k eth0 | grep checksum rx-checksumming: on tx-checksumming: on # 드라이버 메시지 확인 (dmesg) $ dmesg | grep -i "eth0\|ixgbe\|mlx5" [12345.678] ixgbe 0000:01:00.0: ixgbe_check_bad_counter: Detected bad TCP checksum, but feature turned on — actual problem may exist # 테스트: 체크섬 offload 비활성화 $ ethtool -K eth0 rx-checksumming off tx-checksumming off }}} 원인: 일부 저가형 또는 legacy NIC에서 HW 체크섬 계산이 부정확한 경우 (false positive). 커널 버그로 인해 특정 드라이버에서만 발생. ==== 사례 3: GRO로 인한 TCP 재전송 증가 ==== 증상: GRO 활성화 후 TCP 재전송이 증가하거나, 특정 애플리케이션에서 패킷 순서 오류 발생. {{{#!plain [코드: bash] # GRO 상태 확인 $ ethtool -k eth0 | grep generic-receive-offload # TCP 재전송 통계 확인 $ nstat -az TcpRetransSegs $ cat /proc/net/snmp | grep -E "Retrans|OutSegs" # 문제 구간 확인 — 서버-클라이언트 양쪽에서 GRO 상태 맞춰야 함 $ ethtool -K eth0 gro off # 테스트를 위해 off $ iperf3 -c 10.0.0.1 -P 4 # 대역폭 재테스트 }}} 원인: GRO가 다른 흐름의 패킷을 잘못 병합하거나, NIC HW GRO의 구현 버그. 특히 가상화 환경(virtio, VM에서) 자주 발생. ==== 사례 4: 프래그먼트된 대용량 패킷 처리 지연 ==== 증상: 대용량 파일 전송 시 예상보다 낮은 throughput, 또는 특정 크기(예: 64KB 근처)에서 throughput 급격 감소. {{{#!plain [코드: bash] # skb_linearize 빈도 확인 — linearization은 비용이 큼 $ cat /proc/net/netstat | grep SkbConcatenate TcpSmbConcatenate: 12345 # GSO/TSO 상태 확인 $ ethtool -k eth0 | grep -E "segmentation|offload" # 수신측 gro_flush_timeout 확인 (지연 병합) $ sysctl net.core.gro_flush_timeout net.core.gro_flush_timeout = 2000 }}} 원인: NIC이 TSO를 지원하지 않으면 커널에서 software GSO가 linearize를 유발하거나, 수신 측 GRO가 타임아웃까지 대기를 위해 지연 발생. ==== 사례 5: NAPI 기아 상태 (starvation) ==== 증상: 고대역폭 트래픽에서 일부 CPU만 max softirq time에 도달하고, 다른 CPU는 유휴 상태. 드롭이 특정 CPU에서 집중됨. {{{#!plain [코드: bash] # CPU별 softirq 처리량 확인 $ cat /proc/net/softnet_stat | awk '{print $1, $2, $3, $4}' | head -20 # 컬럼: cpu_id, processed, dropped, time_squeeze # IRQ affinity 확인 $ cat /proc/interrupts | grep eth0 $ cat /proc/irq//smp_affinity # NAPI 가중치 확인 $ ls /sys/class/net/eth0/napi $ cat /sys/class/net/eth0/napi//poll_time }}} 원인: IRQ가 단일 CPU에 집중되거나, NAPI weight가 너무 작아서 time slice 내에 처리를 못 함. RSS 설정과 IRQ balancing 문제. '''주의''' 트러블슈팅 핵심 원칙: 네트워크 문제는 غالب히 상호작용하는 여러 요소(RSS, GRO, IRQ affinity, 드라이버 버그)가 복합적으로 작용합니다. 단일 변수만 바꾸고 측정하는 체계적인 접근이 필요합니다. 예를 들어 "GRO만 끄고 latency 측정" → "IRQ affinity만 바꾸고 측정" 식으로요. === 디버깅 기법 === ==== tracepoint 활용 ==== {{{#!plain [코드: bash] # skb 드롭 추적 (kfree_skb 호출 위치와 원인) $ perf trace -e skb:kfree_skb --call-graph dwarf -a sleep 5 # skb 드롭 실시간 모니터링 $ cat /sys/kernel/debug/tracing/events/skb/kfree_skb/format $ echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable $ cat /sys/kernel/debug/tracing/trace_pipe # dropwatch (커널 6.x+: kfree_skb_reason으로 드롭 원인 표시) $ dropwatch -l kas > start }}} ==== perf probe로 동적 추적 ==== {{{#!plain [코드: bash] # 특정 함수에서 skb->len 값 추적 $ perf probe --add 'tcp_v4_rcv skb->len skb->data_len' $ perf record -e probe:tcp_v4_rcv -a sleep 10 $ perf script # skb 할당 빈도 측정 $ perf stat -e 'skb:*' -a sleep 10 }}} ==== /proc/net 진단 ==== {{{#!plain [코드: bash] # 소켓 메모리 사용량 확인 (skb leak 의심 시) $ cat /proc/net/sockstat sockets: used 1234 TCP: inuse 56 orphan 2 tw 128 alloc 60 mem 1024 UDP: inuse 12 mem 256 # mem: 페이지 단위 (mem * PAGE_SIZE = 실제 바이트) # orphan: 소속 프로세스 없는 TCP 소켓 (skb leak 원인 가능) # tw: TIME_WAIT 상태 (정상적이지만 과다하면 문제) # 네트워크 스택 통계 (드롭/에러 확인) $ nstat -az | grep -i drop $ cat /proc/net/softnet_stat }}} ==== 디버깅 커널 옵션 ==== ||옵션||기능|| ||{{{CONFIG_DEBUG_KMEMLEAK}}}||skb를 포함한 커널 메모리 누수 탐지|| ||{{{CONFIG_KASAN}}}||use-after-free, out-of-bounds 접근 탐지|| ||{{{CONFIG_NET_DROP_MONITOR}}}||네트워크 패킷 드롭 위치 추적|| ||{{{CONFIG_DEBUG_NET}}}||네트워크 스택 디버깅 assertion 활성화|| ||{{{CONFIG_SKB_EXTENSIONS}}}||skb extension (conntrack, bridge 등) 디버깅|| === 커널 버전별 변경사항 === ||버전||변경 내용|| ||3.18||{{{skb_frag_off()}}} 접근자 도입 (직접 필드 접근 대체)|| ||4.14||{{{MSG_ZEROCOPY}}} 소켓 옵션 도입|| ||4.18||UDP GSO ({{{SKB_GSO_UDP_L4}}}) 지원|| ||5.0||XDP에서 skb 모드 ({{{XDP_FLAGS_SKB_MODE}}}) 공식 지원|| ||5.3||{{{skb_ensure_writable()}}} 도입 ({{{skb_make_writable}}} 대체)|| ||5.17||page_pool 기반 skb 할당 최적화|| ||6.0||{{{kfree_skb_reason()}}} 도입 — 드롭 원인 추적 개선|| ||6.2||skb->csum_level 필드로 중첩 체크섬 오프로드 지원|| ||6.8||{{{netmem}}} 기반 skb frag 관리 (page → netmem 추상화)|| '''팁''' 참고 자료: skbuff.h (Bootlin) (https://elixir.bootlin.com/linux/latest/source/include/linux/skbuff.h), O'Reilly "Understanding Linux Network Internals" Ch. 2, LWN의 skb lifecycle 시리즈 (https://lwn.net/Articles/615238/), {{{Documentation/networking/skbuff.rst}}} === skb 확장 (skb_ext) === 커널 5.x부터 {{{sk_buff}}}에 가변 메타데이터를 동적으로 연결하는 skb_ext 메커니즘이 도입되었습니다. 이전에는 conntrack 포인터, IPsec secpath 등이 skb 내부에 직접 포함되어 항상 메모리를 차지했으나, skb_ext를 통해 필요할 때만 할당되어 메모리 효율이 크게 개선되었습니다. {{{#!plain [SVG 텍스트 변환: skb_ext 확장 아키텍처] 캡션: skb_ext는 필요할 때만 할당되며, clone 시 refcount 공유 + COW(Copy-on-Write) 방식으로 동작 텍스트 요소: - struct sk_buff - extensions (skb_ext *) - active_extensions (u8) - len, data, protocol... - refcount, users - struct skb_ext - refcnt (refcount_t) - chunks (u8) — 할당 청크 수 - data[] — 가변 확장 데이터 - extensions - SKB_EXT_SEC_PATH - IPsec xfrm_state 참조 - SKB_EXT_BRIDGE_NF - br_netfilter 상태 - SKB_EXT_TC - TC cls_act 메타데이터 - SKB_EXT_MPTCP - MPTCP 옵션 (6.x+) - skb_clone 시 skb_ext 동작 - 원본 skb - clone skb - skb_ext 공유 (refcnt++) - COW: 수정 시 - skb_ext_cow() → - 독립 복사본 생성 }}} {{{#!plain [코드: C] /* include/linux/skbuff.h — skb_ext 구조체 */ struct skb_ext { refcount_t refcnt; /* 참조 카운트 (clone 시 공유) */ u8 offset[SKB_EXT_NUM]; /* 각 확장의 data[] 내 오프셋 */ u8 chunks; /* 할당된 청크 수 (64B 단위) */ char data[]; /* 가변 길이 확장 데이터 */ }; /* 확장 타입 열거형 */ enum skb_ext_id { SKB_EXT_BRIDGE_NF, /* br_netfilter 상태 (struct nf_bridge_info) */ SKB_EXT_SEC_PATH, /* IPsec 보안 경로 (struct sec_path) */ SKB_EXT_MPTCP, /* MPTCP 옵션 (struct mptcp_ext) */ TC_SKB_EXT, /* TC cls_act 메타데이터 (struct tc_skb_ext) */ SKB_EXT_NUM /* 총 확장 타입 수 */ }; /* skb_ext 추가 — 해당 타입의 확장 공간을 할당하고 포인터 반환 */ void *skb_ext_add(struct sk_buff *skb, enum skb_ext_id id) { struct skb_ext *new; /* active_extensions 비트맵에 id 설정 */ skb->active_extensions |= 1 << id; /* 확장 공간 할당 또는 기존 공간에서 오프셋 반환 */ return skb->extensions->data + skb->extensions->offset[id]; } /* skb_ext 조회 — 해당 확장이 있으면 포인터, 없으면 NULL */ static inline void *skb_ext_find(const struct sk_buff *skb, enum skb_ext_id id) { if (skb->active_extensions & (1 << id)) return skb->extensions->data + skb->extensions->offset[id]; return NULL; } /* Netfilter conntrack 연결: nf_ct_get()으로 conntrack 참조 */ static inline struct nf_conn *nf_ct_get( const struct sk_buff *skb, enum ip_conntrack_info *ctinfo) { /* skb->_nfct에서 conntrack 포인터와 상태 정보 추출 */ unsigned long nfct = skb->_nfct; *ctinfo = nfct & NFCT_INFOMASK; return (struct nf_conn *)(nfct & NFCT_PTRMASK); } /* TC 확장: cls_act에서 skb에 메타데이터 연결 */ struct tc_skb_ext { __u32 chain; /* TC 체인 번호 */ __u16 mru; /* TC Maximum Receive Unit */ __u16 zone; /* conntrack zone */ u8 post_ct:1; /* CT action 이후 여부 */ u8 post_ct_snat:1; u8 post_ct_dnat:1; }; /* TC에서 skb_ext 사용 예 */ struct tc_skb_ext *ext = skb_ext_add(skb, TC_SKB_EXT); if (ext) ext->chain = chain_index; }}} ||확장 타입||구조체||크기 (약)||사용 서브시스템|| ||{{{SKB_EXT_SEC_PATH}}}||{{{sec_path}}}||~40B||IPsec/xfrm — SA 참조 배열|| ||{{{SKB_EXT_BRIDGE_NF}}}||{{{nf_bridge_info}}}||~48B||br_netfilter — 원본 포트/MAC 보존|| ||{{{TC_SKB_EXT}}}||{{{tc_skb_ext}}}||~12B||TC cls_act — 체인/zone/CT 메타|| ||{{{SKB_EXT_MPTCP}}}||{{{mptcp_ext}}}||~24B||MPTCP — DSS/DSN 매핑|| '''팁''' 성능 영향: skb_ext 도입 전, {{{struct sec_path}}}와 {{{struct nf_bridge_info}}}는 skb 내에 항상 포인터를 차지했습니다 (각 8바이트). IPsec이나 bridge를 사용하지 않는 대다수 패킷에서 이 공간이 낭비되었습니다. skb_ext 전환 후 {{{sizeof(struct sk_buff)}}}가 약 16바이트 줄어들었고, 이는 수백만 동시 패킷을 처리하는 환경에서 상당한 메모리 절감입니다. === page_pool 기반 고성능 할당 === 커널 5.17+에서 도입된 page_pool은 네트워크 드라이버의 skb 데이터 버퍼 할당을 혁신적으로 개선합니다. DMA 매핑을 캐시하고, 해제된 페이지를 재활용하며, bulk 할당으로 lock contention을 최소화합니다. {{{mlx5}}}, {{{ice}}}, {{{i40e}}}, {{{bnxt}}} 등 주요 고성능 드라이버가 page_pool을 사용합니다. {{{#!plain [SVG 텍스트 변환: page_pool 재활용 아키텍처] 캡션: page_pool은 DMA 매핑을 캐시하고 해제된 페이지를 재활용하여 할당/해제 비용을 5배 이상 절감 텍스트 요소: - Buddy Allocator - (초기 할당/부족 시) - page_pool - alloc.cache[] (128) - ring.queue (1024) - DMA 매핑 캐시 + per-CPU 접근 - bulk - NIC 드라이버 RX - page → DMA → skb - alloc - struct sk_buff - frags[] → page_pool page - 네트워크 스택 처리 - L2 → L3 → L4 → 소켓 - skb 해제 - page_pool_put_page() - 재활용! - DMA unmap 생략 - Slow path 해제 - dma_unmap + put_page() - ring full - 성능 비교 (10Gbps NIC, 64B 패킷) - 기존: alloc_page + dma_map 매번 → ~150ns/pkt - page_pool: 캐시 히트 + DMA skip → ~30ns/pkt }}} {{{#!plain [코드: C] /* include/net/page_pool/types.h — page_pool 생성 파라미터 */ struct page_pool_params { unsigned int flags; /* PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV */ unsigned int order; /* page order (0 = 4KB, 1 = 8KB) */ unsigned int pool_size; /* ring 크기 (기본 1024) */ int nid; /* NUMA 노드 (-1 = 현재 노드) */ struct device *dev; /* DMA 매핑 대상 디바이스 */ enum dma_data_direction dma_dir; /* DMA_FROM_DEVICE (RX) */ unsigned int max_len; /* 최대 데이터 길이 */ unsigned int offset; /* 데이터 시작 오프셋 */ }; /* 드라이버 초기화: page_pool 생성 */ struct page_pool_params pp_params = { .flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV, .order = 0, /* 4KB 페이지 */ .pool_size = 1024, .nid = dev_to_node(dev), .dev = &pdev->dev, .dma_dir = DMA_FROM_DEVICE, .max_len = PAGE_SIZE, .offset = XDP_PACKET_HEADROOM, }; struct page_pool *pool = page_pool_create(&pp_params); /* 수신 경로: page_pool에서 페이지 할당 */ struct page *page = page_pool_dev_alloc_pages(pool); /* → alloc.cache[]에서 O(1) 반환 (캐시 히트) * → 캐시 비면 ring.queue에서 bulk refill * → ring도 비면 buddy allocator + DMA 매핑 */ dma_addr_t dma = page_pool_get_dma_addr(page); /* DMA 주소가 이미 캐시됨 — dma_map_page() 호출 불필요! */ /* skb 생성 후 page_pool 페이지를 frag로 추가 */ struct sk_buff *skb = napi_alloc_skb(napi, 256); skb_add_rx_frag(skb, 0, page, offset, len, PAGE_SIZE); skb_mark_for_recycle(skb); /* 해제 시 page_pool로 반환 */ /* skb 해제 시: page_pool_put_page()로 자동 재활용 */ /* consume_skb(skb) → skb_free_frag() → page_pool_put_page() * → alloc.cache[]에 반환 (fast path) * → 또는 ring.queue에 반환 (다른 CPU에서 해제 시) */ }}} ||비교 항목||기존 (alloc_page + dma_map)||page_pool|| ||페이지 할당||매번 buddy allocator 호출||per-CPU 캐시에서 O(1)|| ||DMA 매핑||매번 {{{dma_map_page()}}}||캐시된 DMA 주소 재사용|| ||해제||{{{dma_unmap}}} + {{{put_page()}}}||캐시에 반환 (unmap 생략)|| ||NUMA 인식||수동 관리 필요||{{{nid}}} 파라미터로 자동|| ||XDP 호환||직접 구현 필요||내장 XDP headroom 지원|| ||bulk 할당||지원 안 함||{{{page_pool_alloc_pages_batch()}}}|| '''정보''' page_pool 통계 확인: {{{/sys/kernel/debug/page_pool/}}}에서 각 풀의 할당/재활용/실패 통계를 확인할 수 있습니다. {{{ethtool -S eth0 | grep page_pool}}}로 드라이버별 통계도 확인 가능합니다. 재활용율이 90% 이하면 ring 크기 증가 또는 NUMA 문제를 점검하세요. === XDP와 sk_buff 인터페이스 === XDP(eXpress Data Path)는 sk_buff 할당 이전 단계에서 패킷을 처리하는 고성능 프레임워크입니다. NIC 드라이버 내부에서 {{{xdp_buff}}}라는 경량 구조체로 패킷을 표현하며, XDP 프로그램의 판정(verdict)에 따라 패킷을 드롭/포워딩하거나 일반 네트워크 스택으로 전달합니다. {{{#!plain [SVG 텍스트 변환: XDP ↔ sk_buff 변환 흐름] 캡션: XDP는 sk_buff 할당 이전에 패킷을 처리. XDP_PASS 시에만 build_skb()로 sk_buff 변환 텍스트 요소: - NIC RX (DMA) - ring buffer → page - struct xdp_buff - data, data_hard_start - data_end, data_meta - XDP BPF 프로그램 - bpf_xdp_adjust_head() - bpf_redirect_map() - XDP_DROP - XDP_TX - XDP_REDIRECT - XDP_PASS - xdp_buff → sk_buff 변환 - build_skb() 또는 __xdp_build_skb_from_frame() - struct sk_buff - 일반 네트워크 스택 진입 - netif_receive_skb() → L2/L3/L4 - XDP Generic (SKB 모드) - sk_buff가 이미 존재 - __skb_buff로 래핑 - 성능 이점 감소 }}} {{{#!plain [코드: C] /* include/net/xdp.h — xdp_buff 구조체 (sk_buff보다 훨씬 경량) */ struct xdp_buff { void *data; /* 패킷 데이터 시작 (L2) */ void *data_end; /* 패킷 데이터 끝 */ void *data_meta; /* 메타데이터 시작 (data 앞) */ void *data_hard_start;/* 버퍼 절대 시작 */ struct xdp_rxq_info *rxq; /* RX 큐 정보 */ struct xdp_txq_info *txq; /* TX 큐 정보 */ u32 frame_sz; /* 전체 프레임 크기 */ u32 flags; /* XDP_FLAGS_* */ }; /* xdp_buff → sk_buff 변환 (XDP_PASS 시) */ struct sk_buff *xdp_build_skb_from_buff(struct xdp_buff *xdp) { unsigned int headroom = xdp->data - xdp->data_hard_start; unsigned int data_len = xdp->data_end - xdp->data; struct sk_buff *skb; /* build_skb: 기존 버퍼에 sk_buff 메타데이터만 생성 */ skb = build_skb(xdp->data_hard_start, xdp->frame_sz); if (!skb) return NULL; skb_reserve(skb, headroom); __skb_put(skb, data_len); /* XDP 메타데이터가 있으면 skb에 전달 */ if (xdp->data_meta != xdp->data) { int metasize = xdp->data - xdp->data_meta; skb_metadata_set(skb, metasize); } return skb; } /* XDP BPF 프로그램 예: 특정 포트 패킷만 PASS, 나머지 DROP */ SEC("xdp") int xdp_filter(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; struct ethhdr *eth = data; if (data + sizeof(*eth) > data_end) return XDP_DROP; if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS; /* IP 아니면 일반 스택으로 */ struct iphdr *ip = data + sizeof(*eth); if ((void *)(ip + 1) > data_end) return XDP_DROP; if (ip->protocol == IPPROTO_TCP) { struct tcphdr *tcp = (void *)ip + (ip->ihl * 4); if ((void *)(tcp + 1) > data_end) return XDP_DROP; if (tcp->dest == bpf_htons(80)) return XDP_PASS; /* HTTP → sk_buff 생성 → 스택 진입 */ } return XDP_DROP; /* sk_buff 할당 없이 즉시 드롭 */ } /* XDP 메타데이터: xdp_buff → sk_buff 전달 * BPF 프로그램이 data_meta 영역에 커스텀 메타데이터 기록 가능 */ SEC("xdp") int xdp_with_meta(struct xdp_md *ctx) { /* 메타데이터 영역 확보 (data 앞으로 4바이트) */ if (bpf_xdp_adjust_meta(ctx, -4)) return XDP_PASS; __u32 *meta = (void *)(long)ctx->data_meta; if ((void *)(meta + 1) > (void *)(long)ctx->data) return XDP_PASS; *meta = 0xCAFE; /* 커스텀 마크 */ return XDP_PASS; /* → sk_buff 변환 후 skb_metadata_len(skb) == 4 * → TC BPF에서 __sk_buff->data_meta로 접근 가능 */ } }}} ||XDP 액션||sk_buff 할당||동작||성능|| ||{{{XDP_DROP}}}||안 함||패킷 즉시 드롭, 페이지 반환||~24Mpps (64B)|| ||{{{XDP_TX}}}||안 함||같은 NIC으로 즉시 재전송||~14Mpps|| ||{{{XDP_REDIRECT}}}||안 함||다른 NIC/CPU/AF_XDP로 전달||~12Mpps|| ||{{{XDP_PASS}}}||생성||{{{build_skb()}}} → 일반 스택||일반 스택 수준|| ||{{{XDP_ABORTED}}}||안 함||에러 발생, tracepoint 기록||—|| '''주의''' XDP Generic vs Native: {{{XDP_FLAGS_SKB_MODE}}}(Generic)는 sk_buff가 이미 할당된 후 XDP 프로그램을 실행합니다. 따라서 XDP_DROP해도 skb 할당 비용이 발생하며, 성능 이점이 크게 감소합니다. 진정한 고성능을 위해서는 드라이버가 네이티브 XDP를 지원해야 합니다 ({{{XDP_FLAGS_DRV_MODE}}}). {{{ethtool -i eth0}}}으로 드라이버를 확인하고, {{{ip link set dev eth0 xdp obj prog.o}}}으로 로드합니다. === 패킷 타임스탬핑 (SO_TIMESTAMPING) === 정밀한 네트워크 지연 측정, PTP(Precision Time Protocol) 동기화, 금융 거래 시스템 등에서 패킷의 정확한 송수신 시각이 필요합니다. Linux는 {{{SO_TIMESTAMPING}}} 소켓 옵션으로 하드웨어 타임스탬프(NIC PHY 수준)부터 소프트웨어 타임스탬프(커널 네트워크 스택)까지 다양한 수준의 타임스탬핑을 지원하며, 이 정보는 sk_buff를 통해 전달됩니다. {{{#!plain [SVG 텍스트 변환: 패킷 타임스탬프 삽입 지점] 캡션: HW 타임스탬프는 NIC PHY 수준(ns 정밀도), SW 타임스탬프는 커널 softirq 수준(μs 정밀도) 텍스트 요소: - 전송 경로 (TX) - sendmsg() - SW TX ① - SOF_TIMESTAMPING_TX_SOFTWARE - SCHED TX ② - SOF_TIMESTAMPING_TX_SCHED - dev_queue_xmit() - HW TX ③ - SOF_TIMESTAMPING_TX_HARDWARE - (NIC PHY, ns 정밀도) - 수신 경로 (RX) - HW RX ① - SOF_TIMESTAMPING_RX_HARDWARE - netif_receive_skb() - SW RX ② - SOF_TIMESTAMPING_RX_SOFTWARE - (ktime_get_real(), μs 정밀도) - recvmsg() + cmsg 전달 - skb 내부 타임스탬프 저장 - skb_hwtstamps(skb)->hwtstamp - HW 타임스탬프 (ktime_t, ns) - skb->tstamp (= skb_mstamp_ns) - SW 타임스탬프 (ktime_t, ns) }}} {{{#!plain [코드: C] /* include/linux/skbuff.h — 타임스탬프 관련 구조체 */ struct skb_shared_hwtstamps { union { ktime_t hwtstamp; /* HW 타임스탬프 (NIC PHY) */ void *netdev_data; /* 드라이버별 데이터 */ }; }; /* skb에서 HW 타임스탬프 접근 */ static inline struct skb_shared_hwtstamps *skb_hwtstamps( struct sk_buff *skb) { return &skb_shinfo(skb)->hwtstamps; } /* NIC 드라이버: 수신 시 HW 타임스탬프 기록 */ static void my_nic_rx_hwtstamp(struct sk_buff *skb, u64 hw_ns) { struct skb_shared_hwtstamps *hwts = skb_hwtstamps(skb); hwts->hwtstamp = ns_to_ktime(hw_ns); /* → recvmsg()에서 SOF_TIMESTAMPING_RAW_HARDWARE cmsg로 전달 */ } /* 사용자 공간: SO_TIMESTAMPING 설정 */ int flags = SOF_TIMESTAMPING_RX_HARDWARE /* 수신 HW 타임스탬프 */ | SOF_TIMESTAMPING_TX_HARDWARE /* 전송 HW 타임스탬프 */ | SOF_TIMESTAMPING_RAW_HARDWARE /* 원시 HW 시각 (PTP 클럭) */ | SOF_TIMESTAMPING_SOFTWARE /* SW 타임스탬프 */ | SOF_TIMESTAMPING_OPT_TSONLY; /* 타임스탬프만 (페이로드 생략) */ setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags)); /* recvmsg()로 타임스탬프 수신 */ struct msghdr msg = { .msg_control = cbuf, .msg_controllen = sizeof(cbuf) }; recvmsg(fd, &msg, 0); /* cmsg에서 타임스탬프 추출 */ struct cmsghdr *cm; for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) { if (cm->cmsg_level == SOL_SOCKET && cm->cmsg_type == SO_TIMESTAMPING) { struct timespec *ts = (struct timespec *)CMSG_DATA(cm); /* ts[0] = SW 타임스탬프 (SOF_TIMESTAMPING_SOFTWARE) * ts[1] = 예약 (사용 안 함) * ts[2] = HW 타임스탬프 (SOF_TIMESTAMPING_RAW_HARDWARE) */ printf("HW: %ld.%09ld\n", ts[2].tv_sec, ts[2].tv_nsec); } } /* TX 타임스탬프: 전송 완료 시 errqueue에서 수신 */ recvmsg(fd, &msg, MSG_ERRQUEUE); /* → SOF_TIMESTAMPING_TX_HARDWARE cmsg에 전송 시각 포함 * → NIC 드라이버가 TX 완료 인터럽트에서 HW 타임스탬프 기록 */ }}} ||타임스탬프 종류||정밀도||지연 소스||용도|| ||HW 타임스탬프 (PHY)||~1ns||NIC PTP 클럭||PTP 동기화, 금융 트레이딩|| ||SW 타임스탬프 (커널)||~1μs||{{{ktime_get_real()}}} (softirq)||일반 지연 측정, tcpdump|| ||TX SCHED||~1μs||qdisc 진입 시점||큐잉 지연 측정|| ||TX ACK (TCP)||~1μs||ACK 수신 시점||RTT 측정|| '''팁''' PTP 하드웨어 지원 확인: {{{ethtool -T eth0}}}으로 NIC의 HW 타임스탬핑 지원 여부를 확인합니다. {{{hardware-transmit}}}/{{{hardware-receive}}}/{{{hardware-raw-clock}}}이 표시되면 HW 타임스탬프를 사용할 수 있습니다. Intel i210/i225, Mellanox ConnectX-4+, Broadcom BCM57416 등이 대표적인 PTP 지원 NIC입니다. === NAPI와 GRO 상세 흐름 === NAPI(New API)는 인터럽트와 폴링을 결합하여 고속 패킷 수신을 효율적으로 처리합니다. GRO(Generic Receive Offload)는 NAPI poll 내부에서 동일 플로우의 패킷들을 하나의 큰 sk_buff로 병합하여 프로토콜 스택 처리 오버헤드를 줄입니다. 이 두 메커니즘은 현대 Linux 네트워크 성능의 핵심 축입니다. {{{#!plain [SVG 텍스트 변환: NAPI poll → GRO → 프로토콜 전달 흐름] 캡션: NAPI poll에서 GRO가 동일 플로우 패킷을 병합 → 하나의 super-skb로 프로토콜 스택 전달 텍스트 요소: - NIC RX IRQ - napi_schedule() - IRQ 비활성화 - softirq 스케줄 (NET_RX) - napi_poll() - 최대 weight(64)개 패킷 처리 - budget 소진 → 계속 poll - GRO 병합 엔진 - gro_list[] (해시 버킷) - napi_gro_receive(skb) - 각 패킷 - GRO_MERGED - 기존 skb에 병합 (frag 추가) - GRO_HELD - gro_list에 대기 (더 병합 기대) - GRO_NORMAL - 병합 불가 → 즉시 스택 전달 - gro_normal_list → netif_receive_skb_list() - 병합된 super-skb를 일반 스택에 배치 전달 - flush/timeout - L3/L4 프로토콜 스택 (ip_rcv → tcp_v4_rcv) - napi_complete_done() - IRQ 재활성화 - budget 남음 }}} {{{#!plain [코드: C] /* include/linux/netdevice.h — NAPI 구조체 */ struct napi_struct { struct list_head poll_list; /* softirq 폴링 리스트 */ unsigned long state; /* NAPI_STATE_SCHED 등 */ int weight; /* 한 번 poll에서 처리할 최대 패킷 수 (기본 64) */ int defer_hard_irqs_count; unsigned long gro_bitmask; /* GRO 활성 프로토콜 비트맵 */ int (*poll)(struct napi_struct *, int); /* 드라이버 poll 함수 */ struct list_head rx_list; /* GRO 병합 완료 skb 리스트 */ int rx_count; /* rx_list 내 skb 수 */ struct gro_list gro_hash[GRO_HASH_BUCKETS]; /* GRO 해시 테이블 */ }; /* 드라이버 poll 함수 패턴 */ static int my_driver_poll(struct napi_struct *napi, int budget) { int work_done = 0; while (work_done < budget) { struct sk_buff *skb = my_rx_one(napi); if (!skb) break; /* GRO에 전달: 동일 플로우 병합 시도 */ napi_gro_receive(napi, skb); work_done++; } if (work_done < budget) { /* budget을 다 쓰지 않음 → 패킷이 없음 → IRQ 재활성화 */ napi_complete_done(napi, work_done); /* → gro_list flush → IRQ unmask */ } /* budget 소진 → softirq가 다시 poll 호출 예정 */ return work_done; } /* GRO 내부: 병합 판단 로직 (net/core/gro.c) */ static enum gro_result dev_gro_receive(struct napi_struct *napi, struct sk_buff *skb) { /* 1. 프로토콜별 GRO 콜백 호출 (inet_gro_receive 등) */ /* 2. gro_hash[]에서 동일 플로우 검색 (5-tuple 매칭) */ /* 3. 매칭 결과에 따라: */ if (same_flow && !flush) { /* GRO_MERGED: 기존 skb에 새 패킷을 frag로 추가 * → skb_shinfo(p)->frag_list에 연결 * → p->len += skb->len (super-packet 크기 증가) * → NAPI_GRO_CB(p)->count++ */ return GRO_MERGED; } if (same_flow && flush) { /* 플로우는 같지만 병합 불가 (PSH 플래그, 순서 불일치 등) * → 기존 skb를 flush하고 새 skb를 gro_hash에 등록 */ napi_gro_complete(napi, pp); } /* GRO_HELD: 새 플로우 → gro_hash에 등록하고 대기 * → 같은 플로우의 후속 패킷이 올 때까지 보류 */ list_add(&skb->list, &napi->gro_hash[hash].list); return GRO_HELD; } /* GRO 병합 완료 → 일반 스택으로 전달 */ static void napi_gro_complete(struct napi_struct *napi, struct sk_buff *skb) { /* 프로토콜별 GRO complete 콜백 */ /* → TCP: tcp_gro_complete() — 헤더 보정, 체크섬 설정 * → skb->ip_summed = CHECKSUM_UNNECESSARY (병합된 패킷) */ /* gro_normal_one(): rx_list에 추가 */ gro_normal_one(napi, skb, NAPI_GRO_CB(skb)->count); /* → rx_count >= gro_normal_batch(8) 이면 배치 전달: * gro_normal_list() → netif_receive_skb_list() * → 한 번의 함수 호출로 여러 skb를 스택에 전달 */ } }}} ||GRO 파라미터||기본값||조절 방법||영향|| ||NAPI weight||64||{{{netif_napi_add(dev, napi, poll, weight)}}}||poll당 처리 패킷 수. 높으면 throughput↑, latency↑|| ||gro_flush_timeout||0 (즉시)||{{{sysctl net.core.gro_flush_timeout}}}||0이 아니면 타이머로 flush → 병합 기회 증가|| ||gro_normal_batch||8||{{{sysctl net.core.gro_normal_batch}}}||배치 전달 크기. 높으면 처리량↑, 지연↑|| ||netdev_budget||300||{{{sysctl net.core.netdev_budget}}}||softirq당 전체 NAPI 처리 패킷 상한|| ||busy_poll||0 (off)||{{{sysctl net.core.busy_poll}}}||소켓별 busy polling 시간 (μs)|| {{{#!plain [코드: bash] # GRO 병합 효과 확인 $ ethtool -S eth0 | grep gro rx_gro_packets: 1234567 # GRO 처리된 패킷 수 rx_gro_bytes: 987654321 # GRO 처리된 바이트 # NAPI 통계 확인 $ cat /proc/net/softnet_stat # 컬럼: processed, dropped, time_squeeze, ..., cpu_collision, received_rps, flow_limit_count # time_squeeze > 0: budget/time 부족으로 처리 중단 → netdev_budget 증가 고려 # Busy polling 활성화 (저지연 용도) $ sysctl -w net.core.busy_poll=50 # 50μs 폴링 $ sysctl -w net.core.busy_read=50 # 읽기 시 50μs 폴링 # GRO flush 타임아웃 설정 (병합 기회 증가) $ sysctl -w net.core.gro_flush_timeout=20000 # 20μs # per-NAPI 설정 (커널 6.x+) $ echo 100 > /sys/class/net/eth0/napi/0/gro_flush_timeout $ echo 16 > /sys/class/net/eth0/napi/0/defer_hard_irqs }}} '''주의''' GRO와 Netfilter 상호작용: GRO로 병합된 super-packet은 {{{skb->len}}}이 64KB에 달할 수 있습니다. 이 상태로 Netfilter를 통과하면 conntrack 등이 정상 동작하지만, {{{iptables -m length}}} 같은 패킷 길이 기반 규칙은 예상과 다르게 동작할 수 있습니다. 필요시 {{{ethtool -K eth0 gro off}}}로 비활성화하거나, nftables의 {{{@th}}} 표현식으로 개별 세그먼트 길이를 확인하세요. === GSO/TSO 분할 메커니즘 상세 === GSO(Generic Segmentation Offload)는 커널이 대용량 sk_buff를 NIC의 MTU에 맞는 작은 세그먼트로 분할하는 메커니즘입니다. NIC이 TSO(TCP Segmentation Offload)를 지원하면 하드웨어가 분할하고, 미지원 시 커널의 {{{skb_segment()}}}가 소프트웨어로 처리합니다. 이 과정에서 {{{skb_shared_info}}}의 GSO 필드가 핵심 역할을 합니다. {{{#!plain [SVG 텍스트 변환: GSO/TSO 분할 흐름과 skb_shared_info GSO 필드] 캡션: HW TSO 지원 NIC은 super-packet을 그대로 전송, 미지원 시 skb_segment()가 SW로 분할 텍스트 요소: - GSO/TSO 분할 흐름 - Super sk_buff (최대 64KB) - skb->len = 65536 - gso_size=1448, gso_segs=45 - gso_type=SKB_GSO_TCPV4 - 전송 - HW TSO? - Yes - NIC 하드웨어 분할 - Super-packet 그대로 DMA → NIC이 분할 - CPU 비용 0, 최고 성능 - No - skb_segment(skb, features) - 소프트웨어 GSO: gso_size 기준 분할 - 분할된 개별 sk_buff 체인 (frag_list 연결) - seg 1 (1448B) - IP+TCP+payload - seg 2 (1448B) - ... - seg 45 (나머지) - skb_segment() 내부 동작 - 1. gso_size 기준 페이로드 분할 - 2. 각 세그먼트에 IP+TCP 헤더 복사 - 3. IP.id 순차 증가, TCP.seq 순차 증가 - 4. 마지막 세그먼트에 PSH 플래그 설정 - 분할 후 각 세그먼트의 gso_size=0, gso_segs=0 (더 이상 GSO 아님) }}} {{{#!plain [코드: C] /* skb_shared_info의 GSO 관련 필드 */ struct skb_shared_info { unsigned short gso_size; /* 세그먼트 크기 (MSS) */ unsigned short gso_segs; /* 세그먼트 수 */ unsigned short gso_type; /* GSO 타입 비트맵 */ /* ... */ }; /* GSO 타입 상수 */ #define SKB_GSO_TCPV4 (1 << 0) /* TCP/IPv4 분할 */ #define SKB_GSO_TCPV6 (1 << 4) /* TCP/IPv6 분할 */ #define SKB_GSO_UDP_L4 (1 << 17) /* UDP L4 분할 (4.18+) */ #define SKB_GSO_GRE (1 << 6) /* GRE 터널 내부 분할 */ #define SKB_GSO_GRE_CSUM (1 << 7) /* GRE+체크섬 */ #define SKB_GSO_UDP_TUNNEL (1 << 9) /* VXLAN/Geneve */ #define SKB_GSO_UDP_TUNNEL_CSUM (1 << 10) /* 터널+체크섬 */ #define SKB_GSO_PARTIAL (1 << 13) /* 부분 GSO (외부 헤더만 HW) */ /* skb_segment(): SW GSO 분할 핵심 함수 (net/core/skbuff.c) */ struct sk_buff *skb_segment(struct sk_buff *head_skb, netdev_features_t features) { struct sk_buff *segs = NULL; unsigned int mss = skb_shinfo(head_skb)->gso_size; unsigned int doffset = head_skb->data - skb_mac_header(head_skb); unsigned int offset = doffset; unsigned int tnl_hlen, headroom; unsigned int len, nfrags; /* 각 세그먼트에 대해: */ do { struct sk_buff *nskb; int hsize, size; /* 1. 새 skb 할당 */ nskb = alloc_skb(hsize + doffset + headroom, GFP_ATOMIC); /* 2. L2+L3+L4 헤더 복사 (공통 헤더) */ skb_copy_from_linear_data(head_skb, skb_put(nskb, doffset), doffset); /* 3. 페이로드를 mss 크기만큼 복사/참조 */ if (!sg && !nskb->remcsum_offload) { /* linear 복사 */ skb_copy_from_linear_data_offset(head_skb, offset, skb_put(nskb, size), size); } else { /* SG: page fragment 참조 (zero-copy) */ skb_fill_page_desc(nskb, i, frag->bv_page, frag->bv_offset, frag_size); } /* 4. 각 세그먼트 고유 필드 설정 */ skb_shinfo(nskb)->gso_size = 0; /* 더 이상 GSO 아님 */ skb_shinfo(nskb)->gso_segs = 0; skb_shinfo(nskb)->gso_type = 0; /* 5. IP 헤더: tot_len 갱신, id 순차 증가 */ /* 6. TCP 헤더: seq 순차 증가, 마지막 seg에만 PSH */ offset += size; } while (offset < head_skb->len); return segs; /* 분할된 skb 체인 (next 포인터 연결) */ } /* GSO 분할 트리거 지점: dev_queue_xmit() → validate_xmit_skb() */ static struct sk_buff *validate_xmit_skb(struct sk_buff *skb, struct net_device *dev, bool *again) { netdev_features_t features = dev->features; if (skb_is_gso(skb)) { /* NIC이 해당 GSO 타입을 지원하는지 확인 */ if (skb_gso_ok(skb, features)) return skb; /* HW TSO: 그대로 전달 */ /* SW GSO: 커널에서 분할 */ struct sk_buff *segs = skb_gso_segment(skb, features); consume_skb(skb); /* 원본 super-packet 해제 */ return segs; } return skb; } /* GSO 관련 유틸리티 함수 */ static inline bool skb_is_gso(const struct sk_buff *skb) { return skb_shinfo(skb)->gso_size; } static inline unsigned int skb_gso_network_seglen(const struct sk_buff *skb) { /* gso_size + L4 헤더 + L3 헤더 = 실제 세그먼트의 IP 총 길이 */ return skb_shinfo(skb)->gso_size + skb_network_header_len(skb) + skb_transport_header_len(skb); } }}} ||GSO 타입||프로토콜||커널 버전||특이사항|| ||{{{SKB_GSO_TCPV4}}}||TCP/IPv4||2.6+||가장 기본적인 TSO. 대부분 NIC이 HW 지원|| ||{{{SKB_GSO_TCPV6}}}||TCP/IPv6||2.6+||IPv6 확장 헤더가 있으면 SW fallback 가능|| ||{{{SKB_GSO_UDP_L4}}}||UDP||4.18+||UDP GSO (USO). QUIC 등에서 활용. HW 지원 희소|| ||{{{SKB_GSO_GRE}}}||GRE 터널||3.10+||외부 GRE 헤더 + 내부 TCP 분할|| ||{{{SKB_GSO_UDP_TUNNEL}}}||VXLAN/Geneve||3.12+||외부 UDP + 내부 TCP 분할|| ||{{{SKB_GSO_PARTIAL}}}||다양||4.7+||외부 헤더만 HW, 내부는 SW. tunnel GSO 최적화|| ||{{{SKB_GSO_SCTP}}}||SCTP||4.15+||SCTP 청크 기반 분할|| '''팁''' GSO vs TSO 성능 비교: HW TSO는 CPU 비용이 0에 가깝습니다(DMA 한 번으로 64KB 전송). SW GSO는 {{{skb_segment()}}}에서 세그먼트 수만큼 메모리 할당+헤더 복사가 필요하지만, 그래도 사용자 공간에서 {{{sendmsg()}}}를 45번 호출하는 것보다 훨씬 효율적입니다. 시스콜 오버헤드를 한 번으로 줄이는 것이 GSO의 핵심 이점입니다. {{{ethtool -k eth0 | grep segmentation}}}으로 HW 지원 여부를 확인하세요. === skb_shared_info: frags[] vs frag_list 상세 === sk_buff의 비선형 데이터는 두 가지 방식으로 표현됩니다: frags[](page fragment 배열)과 frag_list(skb 체인). 이 두 구조는 목적과 사용 상황이 완전히 다르며, 혼동하면 심각한 버그가 발생합니다. {{{#!plain [SVG 텍스트 변환: frags[] vs frag_list 구조 비교] 캡션: frags[]는 page 배열로 SG DMA에 최적화, frag_list는 skb 체인으로 GRO/IP 재조합에 사용 텍스트 요소: - frags[] (Scatter-Gather) - sk_buff - head → linear data - len=4096, data_len=3072 - skb_shared_info - nr_frags = 3 - frag_list = NULL - frags[0]: page A, 1024B - frags[1]: page B, 1024B - frags[2]: page C, 1024B - 물리 페이지 (struct page) - page A - page B - page C - MAX_SKB_FRAGS = 17 (보통) - DMA SG 전송에 최적화 - NIC scatter-gather 직접 지원 - skb_add_rx_frag()로 추가 - frag_list (skb 체인) - sk_buff (head) - linear: IP+TCP 헤더 - len=전체, data_len=하위합 - nr_frags = 0 - frag_list → skb2 - sk_buff (skb2) - payload part 1 - sk_buff (skb3) - payload part 2 - next - sk_buff (skb4) - payload part 3 - 크기 제한 없음 (skb 체인) - GRO 병합, IP 재조합에 사용 - 각 skb가 독립적 메타데이터 - SG DMA에 직접 사용 불가 - 전송 전 linearize 필요할 수 있음 }}} ||특성||frags[] (page fragments)||frag_list (skb chain)|| ||저장 형태||{{{skb_frag_t}}} 배열 (page+offset+size)||{{{struct sk_buff}}} 연결 리스트|| ||최대 개수||{{{MAX_SKB_FRAGS}}} (보통 17)||제한 없음|| ||DMA SG||직접 SG 매핑 가능||불가 — linearize 또는 변환 필요|| ||오버헤드||frag당 16바이트 (page+offset+size)||skb당 ~240바이트 (전체 sk_buff)|| ||주요 사용처||NIC RX ({{{skb_add_rx_frag}}}), sendfile, splice||GRO 병합, IP defrag, GSO 분할 결과|| ||데이터 접근||{{{skb_frag_page()}}}, {{{skb_frag_off()}}}||{{{skb_walk_frags(skb, frag_skb)}}}|| ||len/data_len||{{{data_len}}} = frags 총합||{{{data_len}}} = frag_list skb들의 len 총합|| {{{#!plain [코드: C] /* frags[] 접근 패턴 */ struct skb_shared_info *si = skb_shinfo(skb); for (int i = 0; i < si->nr_frags; i++) { skb_frag_t *f = &si->frags[i]; struct page *page = skb_frag_page(f); unsigned int off = skb_frag_off(f); unsigned int sz = skb_frag_size(f); /* kmap_local_page(page) + off 로 데이터 접근 */ } /* frag_list 순회 패턴 */ struct sk_buff *frag_iter; skb_walk_frags(skb, frag_iter) { /* frag_iter는 frag_list의 각 skb */ process_fragment(frag_iter->data, frag_iter->len); } /* 전체 skb 데이터를 순차 복사하는 범용 함수 */ /* skb_copy_bits(): linear + frags[] + frag_list 모두 처리 */ int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len) { /* 1. linear 영역에서 복사 */ /* 2. frags[]에서 복사 */ /* 3. frag_list의 각 skb에서 재귀적으로 복사 */ } /* MAX_SKB_FRAGS 계산 */ #define MAX_SKB_FRAGS (65536 / PAGE_SIZE + 1) /* PAGE_SIZE=4096 → MAX_SKB_FRAGS=17 * 64KB 데이터를 frags로 표현하는 데 필요한 최대 페이지 수 * +1은 페이지 경계 걸침 고려 */ }}} '''주의''' frags[]와 frag_list 혼용 주의: 하나의 skb에 frags[]와 frag_list가 동시에 존재할 수 있습니다. {{{skb->data_len}}}은 두 영역의 합산입니다. {{{skb_linearize()}}}는 모든 비선형 데이터(frags[] + frag_list)를 linear 영역으로 합치므로, 대용량 패킷에서 호출하면 거대한 연속 메모리 할당이 필요해 실패할 수 있습니다. GRO로 병합된 64KB super-packet에 {{{skb_linearize()}}}를 호출하는 것은 안티패턴입니다. === VLAN 태그 처리와 sk_buff === Linux 커널은 VLAN 태그를 두 가지 방식으로 처리합니다: 하드웨어 가속(HW VLAN acceleration)과 소프트웨어 처리. NIC이 VLAN 태그를 추출/삽입하는 HW 가속 방식이 더 효율적이며, 대부분의 현대 NIC이 지원합니다. {{{#!plain [SVG 텍스트 변환: VLAN 태그 HW 가속 vs SW 처리] 캡션: HW 가속: NIC이 VLAN 태그를 RX descriptor로 추출하여 skb 메타데이터에 저장. 패킷 데이터에서 4바이트 절약 텍스트 요소: - VLAN 태그 RX 처리: HW 가속 vs SW - HW VLAN Acceleration (대부분의 NIC) - NIC: VLAN 태그 추출 - RX descriptor에 기록 - __vlan_hwaccel_put_tag(skb) - skb->vlan_tci = tag, vlan_present=1 - skb->data → IP 헤더 시작 - VLAN 태그는 skb 메타에만 존재 - 빠름! - SW VLAN 처리 (HW 미지원 또는 QinQ) - NIC: 원시 프레임 전달 - VLAN 태그 inline - __vlan_get_tag(skb, &tag) - Ethernet 프레임 내부에서 파싱 - skb_vlan_untag(skb) - 4B VLAN 태그 제거 + skb 메타 설정 - sk_buff VLAN 관련 필드 - skb->vlan_proto - ETH_P_8021Q (0x8100) - 또는 ETH_P_8021AD (QinQ) - skb->vlan_tci - PCP(3bit) | DEI(1bit) | VID(12bit) - skb_vlan_tag_get(skb) → VID 추출 }}} {{{#!plain [코드: C] /* sk_buff의 VLAN 필드 */ struct sk_buff { __be16 vlan_proto; /* VLAN 프로토콜 (0x8100 또는 0x88a8) */ __u16 vlan_tci; /* TCI: PCP(3) + DEI(1) + VID(12) */ /* vlan_present는 6.x에서 vlan_all로 통합 */ }; /* NIC 드라이버: HW VLAN 가속 — 수신 시 */ static void my_nic_rx(struct napi_struct *napi, u16 rx_vlan) { struct sk_buff *skb = napi_alloc_skb(napi, 256); /* ... DMA 데이터 복사 (VLAN 태그 없는 프레임) ... */ if (rx_vlan) { /* NIC이 추출한 VLAN 태그를 skb 메타에 저장 */ __vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), rx_vlan); /* → skb->vlan_proto = ETH_P_8021Q * → skb->vlan_tci = rx_vlan * → 패킷 데이터에는 VLAN 태그 없음 */ } napi_gro_receive(napi, skb); } /* VLAN 태그 확인/추출 */ if (skb_vlan_tag_present(skb)) { u16 vid = skb_vlan_tag_get_id(skb); /* VID (0~4095) */ u16 prio = skb_vlan_tag_get_prio(skb); /* PCP (0~7) */ } /* VLAN 태그 추가/제거 (소프트웨어) */ skb_vlan_push(skb, htons(ETH_P_8021Q), vid | (prio << 13)); /* → 패킷 데이터에 4B VLAN 태그 삽입, headroom 필요 */ skb_vlan_pop(skb); /* → 패킷에서 VLAN 태그 제거, skb 메타로 이동 */ /* QinQ (802.1ad): 이중 VLAN 태그 */ /* 외부 VLAN: skb->vlan_proto = ETH_P_8021AD, skb->vlan_tci = outer */ /* 내부 VLAN: 패킷 데이터 내 ETH_P_8021Q 태그로 존재 */ skb_vlan_push(skb, htons(ETH_P_8021AD), outer_vid); /* → 외부 S-tag + 내부 C-tag 이중 태그 구성 */ }}} === TCP의 sk_buff 분할/병합/재전송 === TCP는 sk_buff를 가장 정교하게 활용하는 프로토콜입니다. 전송 큐의 skb를 MSS 단위로 분할하고, 수신 경로에서 인접 세그먼트를 병합하며, 재전송 시 skb를 재활용합니다. 이 과정에서 {{{TCP_SKB_CB()}}}를 통한 cb[] 활용이 핵심입니다. {{{#!plain [코드: C] /* tcp_fragment(): 하나의 skb를 두 개로 분할 * 용도: MSS 변경, SACK 기반 부분 재전송, cwnd 축소 시 * net/ipv4/tcp_output.c */ int tcp_fragment(struct sock *sk, enum tcp_queue tcp_queue, struct sk_buff *skb, u32 len, unsigned int mss_now, gfp_t gfp) { struct sk_buff *buff; int old_factor; /* 새 skb 할당 (뒷부분 데이터용) */ buff = sk_stream_alloc_skb(sk, 0, gfp, 0); /* 페이로드 분할: skb의 len 이후 데이터를 buff로 이동 */ skb_split(skb, buff, len); /* TCP_SKB_CB 갱신: 시퀀스 번호 분할 */ TCP_SKB_CB(buff)->seq = TCP_SKB_CB(skb)->seq + len; TCP_SKB_CB(buff)->end_seq = TCP_SKB_CB(skb)->end_seq; TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(buff)->seq; /* GSO 세그먼트 수 재계산 */ tcp_set_skb_tso_segs(skb, mss_now); tcp_set_skb_tso_segs(buff, mss_now); /* 전송 큐에서 skb 뒤에 buff 삽입 */ skb_append(skb, buff, &sk->sk_write_queue); return 0; } /* tcp_try_coalesce(): 인접 수신 skb를 하나로 병합 * 용도: RX 경로에서 연속 세그먼트 병합 → 소켓 큐 skb 수 감소 * net/ipv4/tcp_input.c */ static bool tcp_try_coalesce(struct sock *sk, struct sk_buff *to, struct sk_buff *from, bool *fragstolen) { /* from의 데이터를 to에 병합 가능한지 확인 */ if (TCP_SKB_CB(from)->seq != TCP_SKB_CB(to)->end_seq) return false; /* 연속이 아님 */ if (!skb_try_coalesce(to, from, fragstolen, &delta)) return false; /* 메모리/frag 제한 초과 */ /* skb_try_coalesce: from의 frags를 to의 frags[]에 추가 * → from은 해제 가능, to->len 증가 */ TCP_SKB_CB(to)->end_seq = TCP_SKB_CB(from)->end_seq; TCP_SKB_CB(to)->ack_seq = TCP_SKB_CB(from)->ack_seq; return true; } /* tcp_collapse(): OOO(Out-of-Order) 큐에서 중복/겹침 제거 * 용도: OOO 큐의 skb가 과도하게 쌓일 때 메모리 절약 * net/ipv4/tcp_input.c */ static void tcp_collapse(struct sock *sk, struct sk_buff_head *list, struct rb_root *root, struct sk_buff *head, struct sk_buff *tail, u32 start, u32 end) { /* start~end 범위의 skb들을 하나의 skb로 합침 * → 겹치는 시퀀스 번호는 제거 * → OOO 큐의 메모리 사용량 감소 * → tcp_prune_ofo_queue()에서 메모리 압박 시 호출 */ } /* TCP 재전송: 기존 skb 재활용 */ int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs) { /* 1. skb가 clone 상태면 pskb_copy()로 헤더 독립화 */ if (skb_cloned(skb)) { struct sk_buff *nskb = pskb_copy(skb, GFP_ATOMIC); /* 원본을 큐에서 교체 */ } /* 2. TCP 헤더 재구성 (seq, ack, window, timestamp) */ tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC); /* clone_it=1: 재전송 큐에 남기면서 clone 전송 */ } }}} ||TCP skb 연산||함수||트리거 조건||skb 변화|| ||분할||{{{tcp_fragment()}}}||MSS 축소, SACK partial retx||1개 skb → 2개 (seq 분할)|| ||수신 병합||{{{tcp_try_coalesce()}}}||연속 세그먼트 수신||2개 skb → 1개 (frags 합체)|| ||OOO 압축||{{{tcp_collapse()}}}||OOO 큐 메모리 압박||N개 skb → 1개 (데이터 병합)|| ||재전송||{{{__tcp_retransmit_skb()}}}||RTO, SACK, TLP||기존 skb clone 후 재전송|| ||GSO 생성||{{{tcp_write_xmit()}}}||cwnd 허용, TSQ 미달||여러 MSS를 하나의 GSO skb로|| === BPF/TC의 __sk_buff 컨텍스트 === eBPF 프로그램(TC classifier, socket filter)은 커널의 {{{struct sk_buff}}}에 직접 접근하지 않고, 안전한 래퍼인 {{{struct __sk_buff}}}를 통해 접근합니다. BPF 검증기(verifier)가 이 구조체의 필드 접근을 커널 내부 sk_buff 필드로 변환합니다. {{{#!plain [코드: C] /* include/uapi/linux/bpf.h — BPF 프로그램이 보는 skb 뷰 */ struct __sk_buff { __u32 len; /* skb->len */ __u32 pkt_type; /* skb->pkt_type */ __u32 mark; /* skb->mark (읽기/쓰기) */ __u32 queue_mapping; /* skb->queue_mapping */ __u32 protocol; /* skb->protocol */ __u32 vlan_present; /* skb_vlan_tag_present(skb) */ __u32 vlan_tci; /* skb->vlan_tci */ __u32 vlan_proto; /* skb->vlan_proto */ __u32 priority; /* skb->priority (읽기/쓰기) */ __u32 ingress_ifindex; /* skb->skb_iif */ __u32 ifindex; /* skb->dev->ifindex */ __u32 tc_index; /* skb->tc_index */ __u32 cb[5]; /* skb->cb[] (TC에서 사용) */ __u32 hash; /* skb->hash */ __u32 tc_classid; /* skb->tc_classid (쓰기) */ __u32 data; /* skb->data 포인터 (패킷 시작) */ __u32 data_end; /* skb->data + skb_headlen(skb) */ __u32 napi_id; /* skb->napi_id */ __u32 family; /* sk->sk_family */ __u32 data_meta; /* skb->data - skb_metadata_len */ __u32 flow_keys; /* flow dissector 결과 */ __u64 tstamp; /* skb->tstamp (읽기/쓰기) */ __u32 wire_len; /* 원래 와이어 길이 (GSO 이전) */ __u32 gso_segs; /* skb_shinfo(skb)->gso_segs */ __u64 hwtstamp; /* skb_hwtstamps(skb)->hwtstamp */ }; /* BPF 검증기: __sk_buff 필드 접근 → 실제 skb 오프셋 변환 * net/core/filter.c — bpf_convert_ctx_access() */ static u32 bpf_convert_ctx_access(...) { switch (si->off) { case offsetof(struct __sk_buff, len): /* __sk_buff.len → skb->len 직접 매핑 */ *insn++ = BPF_LDX_MEM(BPF_W, si->dst_reg, si->src_reg, offsetof(struct sk_buff, len)); break; case offsetof(struct __sk_buff, data): /* __sk_buff.data → skb->data 포인터 로드 */ *insn++ = BPF_LDX_MEM(BPF_FIELD_SIZEOF(struct sk_buff, data), si->dst_reg, si->src_reg, offsetof(struct sk_buff, data)); break; } } /* TC-BPF 프로그램에서 skb 패킷 데이터 직접 접근 */ SEC("tc") int tc_filter(struct __sk_buff *skb) { void *data = (void *)(long)skb->data; void *data_end = (void *)(long)skb->data_end; /* 패킷 데이터 직접 접근 (bounds check 필수!) */ struct ethhdr *eth = data; if (data + sizeof(*eth) > data_end) return TC_ACT_OK; /* skb 필드 수정 */ skb->mark = 42; /* → skb->mark = 42 (netfilter/tc 마킹) */ skb->priority = 7; /* → skb->priority = 7 (QoS) */ /* 패킷 데이터 수정: bpf_skb_store_bytes() 헬퍼 사용 */ __u8 new_ttl = 64; bpf_skb_store_bytes(skb, ETH_HLEN + offsetof(struct iphdr, ttl), &new_ttl, sizeof(new_ttl), 0); /* 헤더 축소/확장: encap/decap */ bpf_skb_adjust_room(skb, -14, BPF_ADJ_ROOM_MAC, 0); /* → skb_pull(14) 효과: L2 헤더 제거 */ return TC_ACT_OK; } /* 주요 BPF skb 헬퍼 함수 */ /* bpf_skb_load_bytes() — 오프셋에서 N바이트 로드 (비선형 안전) */ /* bpf_skb_store_bytes() — 오프셋에 N바이트 저장 */ /* bpf_skb_pull_data() — pskb_may_pull() 래퍼 */ /* bpf_skb_change_head() — headroom 변경 (encap) */ /* bpf_skb_change_tail() — tailroom 변경 */ /* bpf_skb_adjust_room() — MAC/NET 레벨 크기 조정 */ /* bpf_skb_vlan_push/pop() — VLAN 태그 추가/제거 */ /* bpf_skb_change_proto() — L3 프로토콜 변경 (IPv4↔IPv6) */ /* bpf_skb_cgroup_id() — cgroup ID 조회 */ /* bpf_skb_get_tunnel_key() — 터널 메타데이터 조회 */ /* bpf_redirect() — 다른 인터페이스로 리다이렉트 */ /* bpf_clone_redirect() — clone 후 리다이렉트 */ }}} '''정보''' direct packet access vs 헬퍼: {{{data}}}/{{{data_end}}}를 통한 직접 접근은 linear 영역만 가능합니다. 비선형 데이터(frags/frag_list)에 접근하려면 {{{bpf_skb_pull_data(skb, offset)}}}로 먼저 linearize하거나, {{{bpf_skb_load_bytes()}}} 헬퍼를 사용해야 합니다. 직접 접근이 더 빠르지만, GRO로 병합된 대용량 패킷은 헤더만 linear이므로 페이로드 파싱 시 헬퍼가 필요합니다. === Flow Dissector와 RSS/RPS 해시 === Flow dissector는 skb에서 프로토콜 헤더를 파싱하여 플로우 키(5-tuple 등)를 추출하는 커널 프레임워크입니다. 추출된 키는 {{{skb->hash}}}에 저장되어 RSS(Receive Side Scaling), RPS(Receive Packet Steering), GRO 병합, 소켓 lookup 등 다양한 곳에서 플로우 기반 분배에 사용됩니다. {{{#!plain [SVG 텍스트 변환: Flow Dissector와 skb->hash 활용 경로] 캡션: Flow dissector가 패킷 헤더에서 5-tuple을 추출하고 해시를 계산 → RSS/RPS/GRO/소켓 분배에 사용 텍스트 요소: - Flow Dissector → skb->hash 활용 경로 - 수신 패킷 - ETH+IP+TCP/UDP - __skb_flow_dissect() - L3: saddr, daddr, protocol - L4: sport, dport - → flow_keys 구조체 생성 - __skb_get_hash() - flow_keys → jhash() - → skb->hash = result - RSS (NIC HW) - HW 해시 → RX 큐 선택 - RPS (SW) - hash → CPU 선택 - GRO 병합 - hash → gro_hash[] 버킷 - SO_REUSEPORT - hash → 소켓 선택 - skb->hash 해시 타입 (skb->l4_hash, skb->sw_hash) - HW hash (NIC RSS) - l4_hash=1, sw_hash=0 - NIC의 Toeplitz 해시 사용 - SW hash (커널 계산) - l4_hash=0/1, sw_hash=1 - flow dissector + jhash }}} {{{#!plain [코드: C] /* include/net/flow_dissector.h — 플로우 키 구조체 */ struct flow_keys { struct flow_dissector_key_control control; struct flow_dissector_key_basic basic; /* n_proto, ip_proto */ struct flow_dissector_key_addrs addrs; /* saddr, daddr */ struct flow_dissector_key_ports ports; /* sport, dport */ /* VLAN, GRE, MPLS 키도 포함 가능 */ }; /* skb->hash 계산 (lazy — 처음 접근 시 계산) */ static inline __u32 skb_get_hash(struct sk_buff *skb) { if (!skb->l4_hash && !skb->sw_hash) __skb_get_hash(skb); /* flow dissector 실행 */ return skb->hash; } /* RPS: SW 기반 CPU 분배 (net/core/dev.c) */ static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb, struct rps_dev_flow **rflowp) { u32 hash = skb_get_hash(skb); /* hash를 CPU 수로 나눠 대상 CPU 결정 */ u32 cpu = reciprocal_scale(hash, cpumask_weight(rps_mask)); return cpu; } }}} ||해시 소스||설정 방법||성능||커스터마이즈|| ||NIC RSS (HW)||{{{ethtool -X eth0 hkey/hfunc}}}||최고 (HW 처리)||해시 키, 해시 함수, indirection table|| ||RPS (SW)||{{{/sys/class/net/eth0/queues/rx-0/rps_cpus}}}||양호 (softirq)||CPU 비트맵|| ||RFS (Flow Steering)||{{{/proc/sys/net/core/rps_sock_flow_entries}}}||양호||앱이 실행 중인 CPU로 스티어링|| ||XPS (TX)||{{{/sys/class/net/eth0/queues/tx-0/xps_cpus}}}||TX 큐 선택||CPU→TX 큐 매핑|| === Encapsulation/Tunnel과 sk_buff === 터널 프로토콜(GRE, VXLAN, Geneve, WireGuard)은 sk_buff에 외부 헤더를 추가(encap)하거나 제거(decap)합니다. 이 과정에서 headroom 관리, 헤더 포인터 재설정, inner/outer 프로토콜 구분이 핵심입니다. {{{#!plain [SVG 텍스트 변환: 터널 Encapsulation 시 skb 헤더 변화] 캡션: VXLAN encap: 50바이트 외부 헤더 추가. headroom 부족 시 skb_cow_head()로 재할당 필요 텍스트 요소: - VXLAN Encapsulation 시 skb 변화 - Encapsulation 전 (원본 패킷) - headroom - Inner ETH - 14B - Inner IP - 20B - Inner TCP - Payload - data - Encapsulation 후 (VXLAN 캡슐화) - Outer - ETH 14B - IP 20B - UDP 8B - VXLAN - 8B - data (새 위치) - skb_push(50B) = Outer ETH(14) + Outer IP(20) + Outer UDP(8) + VXLAN(8) }}} {{{#!plain [코드: C] /* VXLAN encapsulation 흐름 (drivers/net/vxlan/vxlan_core.c) */ static void vxlan_xmit_one(struct sk_buff *skb, ...) { int headroom = sizeof(struct iphdr) /* 20B outer IP */ + sizeof(struct udphdr) /* 8B outer UDP */ + sizeof(struct vxlanhdr) /* 8B VXLAN */ + LL_RESERVED_SPACE(dst->dev); /* outer L2 */ /* 1. headroom 확보 (clone이면 독립화) */ if (skb_cow_head(skb, headroom)) { kfree_skb(skb); return; } /* 2. skb->inner_* 헤더 포인터 저장 (decap 시 복원용) */ skb_set_inner_protocol(skb, skb->protocol); skb_set_inner_network_header(skb, skb_network_offset(skb)); skb_set_inner_transport_header(skb, skb_transport_offset(skb)); /* 3. encapsulation 플래그 설정 */ skb->encapsulation = 1; /* 4. VXLAN 헤더 추가 */ struct vxlanhdr *vxh = (struct vxlanhdr *)__skb_push(skb, sizeof(*vxh)); vxh->vx_flags = htonl(VXLAN_HF_VNI); vxh->vx_vni = vxlan_vni_field(vni); /* 5. 외부 UDP 헤더 */ udp_set_csum(skb, ...); /* 6. 외부 IP 헤더 → ip_tunnel_xmit() */ iptunnel_xmit(..., skb, ...); /* → skb_push(IP 헤더) → skb_reset_network_header() * → ip_local_out() → Netfilter OUTPUT → dev_queue_xmit() */ } /* Decapsulation: 외부 헤더 제거 후 inner 헤더 복원 */ static int vxlan_rcv(struct sock *sk, struct sk_buff *skb) { /* 1. VXLAN 헤더 파싱 및 VNI 추출 */ struct vxlanhdr *vxh = vxlan_hdr(skb); /* 2. 외부 헤더 제거 */ __skb_pull(skb, sizeof(struct vxlanhdr)); skb_reset_network_header(skb); /* inner IP로 재설정 */ /* 3. inner 패킷으로 프로토콜 재설정 */ skb->protocol = eth_type_trans(skb, vxlan->dev); skb->encapsulation = 0; /* 4. 일반 스택으로 재진입 */ netif_rx(skb); } /* sk_buff의 inner 헤더 포인터 */ struct sk_buff { sk_buff_data_t inner_transport_header; /* 내부 L4 */ sk_buff_data_t inner_network_header; /* 내부 L3 */ sk_buff_data_t inner_mac_header; /* 내부 L2 */ __be16 inner_protocol; /* 내부 프로토콜 */ __u8 encapsulation:1; /* 캡슐화 여부 */ }; }}} '''주의''' 터널과 GSO 상호작용: {{{skb->encapsulation = 1}}}이면 GSO/체크섬 오프로드가 inner 패킷 기준으로 동작합니다. NIC이 {{{NETIF_F_GSO_UDP_TUNNEL}}}을 지원하면 HW가 외부 UDP + 내부 TCP를 한 번에 분할합니다. 미지원 NIC에서는 {{{SKB_GSO_PARTIAL}}}을 사용하여 외부 헤더만 SW로, 내부 분할은 HW로 처리하는 하이브리드 방식이 가능합니다(4.7+). === sk_buff 할당 내부 (kmem_cache) === sk_buff의 메모리 할당은 일반 {{{kmalloc()}}}이 아닌 전용 SLAB 캐시({{{skbuff_head_cache}}})를 사용합니다. 이는 빈번한 할당/해제에 최적화되어 있으며, fclone(fast clone) 메커니즘으로 clone 비용을 더 줄입니다. {{{#!plain [코드: C] /* net/core/skbuff.c — sk_buff SLAB 캐시 초기화 */ static struct kmem_cache *skbuff_head_cache; static struct kmem_cache *skbuff_fclone_cache; void __init skb_init(void) { /* 일반 sk_buff 캐시 */ skbuff_head_cache = kmem_cache_create( "skbuff_head_cache", sizeof(struct sk_buff), /* ~240바이트 */ 0, /* 정렬 */ SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL); /* fclone 캐시: sk_buff 2개 + fclone_ref를 하나의 슬랩 객체로 */ skbuff_fclone_cache = kmem_cache_create( "skbuff_fclone_cache", sizeof(struct sk_buff_fclones), /* sk_buff*2 + ref */ 0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL); } /* fclone 구조체: clone 전용 최적화 */ struct sk_buff_fclones { struct sk_buff skb1; /* 원본 sk_buff */ struct sk_buff skb2; /* 사전 할당된 clone sk_buff */ refcount_t fclone_ref; /* 공유 참조 카운트 */ }; /* __alloc_skb: 내부 할당 로직 */ struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, int flags, int node) { struct sk_buff *skb; u8 *data; if (flags & SKB_ALLOC_FCLONE) { /* fclone 모드: 2개의 sk_buff를 한 번에 할당 * TCP 전송 경로에서 사용: 재전송 시 clone 필요 예상 */ struct sk_buff_fclones *fclones; fclones = kmem_cache_alloc_node(skbuff_fclone_cache, gfp_mask, node); skb = &fclones->skb1; skb->fclone = SKB_FCLONE_ORIG; /* 원본 표시 */ fclones->skb2.fclone = SKB_FCLONE_CLONE; /* clone 슬롯 */ } else { /* 일반 모드: sk_buff 1개만 할당 */ skb = kmem_cache_alloc_node(skbuff_head_cache, gfp_mask, node); skb->fclone = SKB_FCLONE_UNAVAILABLE; } /* 데이터 버퍼 할당 (별도) */ size = SKB_DATA_ALIGN(size); data = kmalloc_reserve(size + sizeof(struct skb_shared_info), gfp_mask, node, &pfmemalloc); skb->head = data; skb->data = data; skb->truesize = SKB_TRUESIZE(size); refcount_set(&skb->users, 1); return skb; } /* fclone으로 빠른 clone (별도 할당 불필요) */ struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask) { struct sk_buff *n; if (skb->fclone == SKB_FCLONE_ORIG) { /* fclone 슬롯이 사용 가능하면 할당 없이 즉시 clone */ struct sk_buff_fclones *fclones = container_of(skb, struct sk_buff_fclones, skb1); n = &fclones->skb2; if (refcount_inc_not_zero(&fclones->fclone_ref)) { /* 할당 없이 clone 완료! → kmem_cache_alloc 비용 절약 */ goto do_clone; } } /* fclone 불가: 일반 할당 */ n = kmem_cache_alloc(skbuff_head_cache, gfp_mask); do_clone: /* sk_buff 메타데이터 복사 (데이터 버퍼 공유) */ __copy_skb_header(n, skb); n->cloned = 1; skb->cloned = 1; atomic_inc(&skb_shinfo(skb)->dataref); return n; } /* NAPI per-CPU 캐시 (napi_alloc_skb 최적화) */ /* NAPI 수신 경로에서는 skbuff_head_cache 대신 * per-CPU page fragment cache를 사용하여 allocation lock 경합을 회피 * → napi_alloc_cache (struct page_frag_cache) * → 같은 page에서 연속 skb의 data 버퍼를 할당 * → TLB miss, cache miss 최소화 */ }}} ||할당 방식||캐시||크기||사용처|| ||일반 skb||{{{skbuff_head_cache}}}||~240B||대부분의 skb 할당|| ||fclone skb||{{{skbuff_fclone_cache}}}||~490B||TCP TX (clone 예상 시)|| ||데이터 버퍼||{{{kmalloc}}} slab||가변||linear 데이터 영역|| ||NAPI 수신||per-CPU page frag||PAGE_SIZE||NAPI poll 내 고속 할당|| ||page_pool||per-pool 캐시||PAGE_SIZE||고성능 NIC 드라이버 (6.x+)|| '''팁''' fclone 효과: TCP 전송 경로에서 {{{sk_stream_alloc_skb()}}}는 {{{SKB_ALLOC_FCLONE}}} 플래그로 skb를 할당합니다. 이는 재전송 시 {{{skb_clone()}}}이 별도 메모리 할당 없이 사전 할당된 슬롯을 사용하게 합니다. 고부하 TCP 서버에서 재전송율이 높을 때 {{{kmem_cache_alloc()}}} 호출 수를 크게 줄여 성능을 개선합니다. {{{slabinfo -s | grep skbuff}}}로 캐시 사용 통계를 확인할 수 있습니다. === 네트워크 네임스페이스와 sk_buff === Linux 네트워크 네임스페이스는 독립된 네트워크 스택(인터페이스, 라우팅, iptables, 소켓)을 제공합니다. sk_buff는 {{{skb->dev}}}를 통해 네임스페이스에 소속되며, veth, bridge 등을 통해 네임스페이스를 넘나들 때 sk_buff의 처리가 변화합니다. {{{#!plain [코드: C] /* sk_buff가 속한 네트워크 네임스페이스 확인 */ static inline struct net *dev_net(const struct net_device *dev) { return read_pnet(&dev->nd_net); } /* skb->dev를 통해 네임스페이스 참조 */ struct net *net = dev_net(skb->dev); /* → net->ipv4.ip_forward (포워딩 설정) * → net->ct.nf_conntrack_hash (conntrack 해시) * → net->loopback_dev (lo 인터페이스) * → 모두 네임스페이스별 독립 */ /* veth: 네임스페이스 간 패킷 전달 */ /* drivers/net/veth.c — veth_xmit() */ static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev) { struct veth_priv *priv = netdev_priv(dev); struct net_device *rcv = rcu_dereference(priv->peer); /* rcv는 다른 네임스페이스의 veth peer 디바이스 */ /* skb->dev를 peer 디바이스로 교체 → 네임스페이스 전환 */ skb->dev = rcv; /* L2 헤더 재처리 */ skb->protocol = eth_type_trans(skb, rcv); /* → 이제 skb는 rcv가 속한 네임스페이스에서 처리됨 * → rcv 네임스페이스의 Netfilter, 라우팅, 소켓 lookup 적용 */ if (likely(veth_forward_skb(rcv, skb, priv, rq, rcv_xdp) == NET_RX_SUCCESS)) return NETDEV_TX_OK; /* veth_forward_skb → netif_rx() 또는 napi_gro_receive() * → rcv 네임스페이스의 네트워크 스택에 진입 */ } /* 네임스페이스 경계에서 주의할 skb 처리 */ /* 1. conntrack: 네임스페이스별 독립 → skb->_nfct 초기화 필요할 수 있음 */ /* 2. skb->mark: 네임스페이스 간 보존됨 → 의도하지 않은 정책 적용 주의 */ /* 3. skb->sk: NULL이 아니면 소켓 네임스페이스와 dev 네임스페이스 불일치 가능 */ }}} '''정보''' 컨테이너 네트워킹과 skb: Docker/Kubernetes의 Pod 네트워킹은 veth 쌍을 통해 구현됩니다. 호스트 네임스페이스의 veth에서 {{{dev_queue_xmit(skb)}}}를 호출하면 skb->dev가 컨테이너 네임스페이스의 peer veth로 교체되어 {{{netif_rx()}}}로 재진입합니다. 이 과정에서 XDP는 veth 드라이버에서 실행되어 컨테이너로 진입하기 전에 패킷을 필터링/리다이렉트할 수 있습니다 (Cilium의 veth XDP 모드). === 관련 문서 === sk_buff와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요. * GSO/GRO와 네트워크 오프로드 (https://minzkn.com/linuxkernel/pages/gso-gro.html) — 체크섬 오프로드, GSO/TSO, GRO 병합 * 네트워크 스택 (https://minzkn.com/linuxkernel/pages/networking-overview.html) — 네트워크 스택 개요 * TCP (https://minzkn.com/linuxkernel/pages/tcp.html) — TCP에서의 sk_buff 사용 * Netfilter (https://minzkn.com/linuxkernel/pages/netfilter.html) — sk_buff 조작 * DMA (https://minzkn.com/linuxkernel/pages/dma.html) — sk_buff의 DMA 매핑 * 디바이스 드라이버 (https://minzkn.com/linuxkernel/pages/device-drivers.html) — 네트워크 드라이버와 sk_buff * AF_XDP (https://minzkn.com/linuxkernel/pages/af-xdp.html) — XDP 소켓과 UMEM, zero-copy 수신 * eBPF (https://minzkn.com/linuxkernel/pages/bpf-xdp.html) — BPF 프로그램의 skb 접근과 헬퍼 함수 * 네트워크 심화 (https://minzkn.com/linuxkernel/pages/networking-advanced.html) — 고급 네트워크 최적화와 디버깅