sk_buff 자료구조

Linux 커널 네트워크 스택의 핵심 자료구조 sk_buff: 메모리 레이아웃, 데이터 조작 함수, clone/copy, 소켓(struct sock) 연동, 소켓 옵션, raw socket, 체크섬 오프로드, GSO/GRO, zero-copy 전송까지 종합 분석합니다. 커널 내부 데이터 경로, 핵심 자료구조/API, 운영 환경 튜닝 포인트와 장애 디버깅 절차까지 실무 관점으로 다룹니다.

전제 조건: 네트워크 스택네트워크 디바이스 드라이버 문서를 먼저 읽으세요. 고성능 패킷 경로는 큐 구조, 메모리 배치, 드롭 위치가 성능을 좌우하므로 드라이버 경계를 먼저 이해하는 것이 중요합니다.
일상 비유: 이 주제는 고속 톨게이트 차선 분리와 비슷합니다. 일반 차선(커널 스택)과 하이패스 차선(XDP/DPDK)을 구분해 보면 왜 지연과 처리량이 달라지는지 명확해집니다.

핵심 요약

  • 메모리 레이아웃head, data, tail, end 4개 포인터로 버퍼 관리. skb_push/pull/put로 데이터 영역 조작.
  • 참조 모델clone은 메타데이터만 복사하고 버퍼 공유, copy는 완전 복사. 참조 카운트 관리가 핵심.
  • 소켓 메모리sk_rmem_alloc/sk_wmem_alloctruesize 기반 소켓 버퍼 제한 구현.
  • 헤더 포인터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는 이 표준들이 정의하는 패킷 구조를 커널 내부에서 표현합니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

개요

struct sk_buff(흔히 skb라 부름)는 Linux 네트워크 스택에서 모든 패킷을 표현하는 자료구조입니다. 수신 패킷이든 전송 패킷이든, L2 프레임이든 L4 세그먼트든, 커널 내에서 네트워크 데이터가 이동할 때는 항상 sk_buff가 사용됩니다.

O'Reilly의 "Understanding Linux Network Internals"에서 Christian Benvenuti는 sk_buff를 "네트워크 스택의 왕(king of networking)"이라 표현했습니다. 이 자료구조를 이해하지 않고는 Linux 네트워크 코드를 읽을 수 없습니다.

struct sk_buff 주요 필드

/* 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에는 프로토콜 처리와 성능 최적화에 쓰이는 중요한 필드들이 있습니다:

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)
hash32비트수신 패킷의 flow hash (RSS, RPS에 활용)skb_get_hash(skb)
pkt_type3비트패킷 수신 유형: HOST, BROADCAST, MULTICAST, OTHERHOST직접 접근
ip_summed2비트체크섬 오프로드 모드 (NONE/UNNECESSARY/COMPLETE/PARTIAL)직접 접근
mark32비트netfilter, tc, 라우팅 결정에 사용되는 패킷 마크직접 접근
queue_mapping16비트멀티큐 NIC에서 TX/RX 큐 선택skb_get_queue_mapping(skb)
napi_id32비트NAPI 인스턴스 식별 (SO_BUSY_POLL 연동)직접 접근
ℹ️

cb[48]은 각 프로토콜 레이어가 자신만의 구조체로 캐스팅하여 사용합니다. 예를 들어 TCP는 TCP_SKB_CB(skb)struct tcp_skb_cb에 시퀀스 번호, 타임스탬프 등을 저장합니다. 한 레이어가 cb를 사용하면 다음 레이어에서 덮어쓰므로, 레이어 간 데이터 전달에는 사용하지 마세요.

체크섬 오프로드와 ip_summed

ip_summed 필드는 NIC 하드웨어의 체크섬 오프로드 상태를 나타내는 핵심 플래그입니다:

의미 (RX)의미 (TX)
CHECKSUM_NONEHW 미지원, SW 검증 필요SW가 체크섬 계산 완료
CHECKSUM_UNNECESSARYHW 검증 완료, 유효함체크섬 불필요 (loopback 등)
CHECKSUM_COMPLETEHW가 전체 체크섬 제공사용 안 함
CHECKSUM_PARTIAL사용 안 함HW에 체크섬 계산 위임
/* 수신: 드라이버에서 체크섬 상태 설정 */
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)내부 APISKB_ALLOC_FCLONE, SKB_ALLOC_RX 등 플래그 지정
/* 일반적인 전송 경로 할당 */
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개의 핵심 포인터로 관리됩니다:

sk_buff 메모리 레이아웃 headroom 데이터 영역 (len - data_len) tailroom head data tail end skb_ shared_ info (frags[])
head~end: 할당된 버퍼 전체, data~tail: 현재 유효 데이터, end 뒤에 skb_shared_info

이 레이아웃은 불변식 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)
/* 전형적인 전송 경로에서의 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), 세 가지 복사 전략을 선택할 수 있습니다:

skb_clone vs pskb_copy vs skb_copy skb_clone sk_buff (원본) sk_buff (clone) 공유 데이터 버퍼 dataref = 2 pskb_copy sk_buff (원본) sk_buff (copy) linear (원본) linear (복사) 공유 paged frags (refcount++) skb_copy sk_buff (원본) sk_buff (copy) 전체 버퍼 (원본) 전체 버퍼 (복사) 비교 요약 skb_clone 메타만 복사 버퍼 100% 공유 가장 빠름 데이터 수정 불가 pskb_copy linear 헤더 복사 paged data 공유 중간 비용 헤더 수정 가능 skb_copy 전체 완전 복사 독립적 버퍼 가장 느림 자유로운 수정
clone은 메타데이터만 복사하고 버퍼를 공유, pskb_copy는 linear 데이터만 복사, skb_copy는 전체 독립 복사
/* 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를 사용합니다:

/* 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에서 패킷 내용 수정 전
/* 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 리스트 관리

/* 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단계 계층으로 구성됩니다:

소켓 구조체 계층과 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
struct socket → struct sock → 프로토콜별 sock. sk_buff는 sk->sk_receive_queue 등에 연결되며 skb->sk로 소켓을 역참조
/* 소켓 구조체 계층 (간략) */
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 제한을 실현합니다:

/* 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_allocRX수신 큐에 쌓인 skb의 총 truesize (SO_RCVBUF와 비교)
sk_wmem_allocTX전송 중인 skb의 총 truesize (SO_SNDBUF와 비교)
sock_rfreeRXskb 해제 시 sk_rmem_alloc 차감
sock_wfreeTXskb 해제 시 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_RCVBUFSOL_SOCKETsk->sk_rcvbuf 설정 → 수신 큐 skb 총량 제한
SO_SNDBUFSOL_SOCKETsk->sk_sndbuf 설정 → 전송 큐 skb 총량 제한
SO_MARKSOL_SOCKETsk->sk_markskb->mark로 복사 (netfilter/tc/라우팅)
SO_PRIORITYSOL_SOCKETsk->sk_priorityskb->priority로 복사 (QoS)
SO_BINDTODEVICESOL_SOCKETsk->sk_bound_dev_if → skb의 dev 제한
SO_TIMESTAMPSOL_SOCKET수신 skb에 타임스탬프 기록, recvmsg() cmsg로 전달
SO_BUSY_POLLSOL_SOCKETsk->sk_napi_id + skb->napi_id로 busy polling
IP_TOSSOL_IPinet->tos → 전송 skb IP 헤더 TOS 필드
IP_TTLSOL_IPinet->uc_ttl → 전송 skb IP 헤더 TTL 필드
IP_HDRINCLSOL_IPraw socket: 사용자가 IP 헤더를 직접 제공
TCP_NODELAYSOL_TCPNagle 알고리즘 해제 → 소량 데이터 즉시 skb 전송
TCP_CORKSOL_TCPskb 전송 지연 (cork), uncork 시 한번에 전송
UDP_CORKSOL_UDP여러 sendmsg를 하나의 skb로 합쳐 전송
UDP_GROSOL_UDP수신 UDP GRO 활성화 → 여러 패킷이 하나의 큰 skb로
/* 전송 경로에서 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 등)와 프로토콜 구현의 핵심입니다.

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를 검사합니다:

/* 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

/* 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는 정상 프로토콜 스택으로 계속 진행합니다:

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) (순차 실행, 동시 아님)
/* 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 옵션에 따라 두 가지로 분기됩니다:

/* 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 헤더를 직접 작성하지만, 커널은 안전성과 정확성을 위해 일부 필드를 자동 보정합니다:

/* 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() — 사용자 공간으로 전달

/* 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 타입만 수신하도록 비트맵 필터를 설정할 수 있습니다:

/* 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
/* 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, 프로토콜 분석
/* 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 사용), 구현 복잡
TPACKET_V3 Ring Buffer 구조 커널 공간 Block 0 TP_STATUS_KERNEL pkt 1 pkt 2 ← 커널이 쓰는 중 Block 1 TP_STATUS_USER pkt 3 pkt 4 → 사용자가 읽는 중 Block 2 (빈 블록) TP_STATUS_KERNEL Block N-1 TP_STATUS_KERNEL 사용자 공간 (mmap) mmap()으로 매핑된 동일 물리 메모리 → 복사 없이 직접 접근 (zero-copy 수신) poll()/ppoll()로 TP_STATUS_USER 블록 대기 → 시스콜 없이 블록 순회하며 패킷 읽기 처리 완료 후 TP_STATUS_KERNEL로 반환 → 커널이 다시 사용 가능 mmap
/* 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 대안으로 사용됩니다:

/* 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()

/* 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));
/* → 해당 인터페이스의 패킷만 수신
 * → 바인딩 없으면 모든 인터페이스의 패킷 수신 */

실용 예제

/* 예제 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 헤더 + 페이로드 */
/* 예제 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 드라이버로 전달 */
/* 예제 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 필터
# 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가 올바른 소켓을 찾아가는 과정 (디먹싱):

/* 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_REUSEPORTBPF_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_ZEROCOPYsend(fd, buf, len, MSG_ZEROCOPY)사용자 버퍼 → skb frag (완료 통지 필요, 4.14+)
/* 커널 내부: 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의 생애주기를 보여줍니다.

sk_buff 생애주기 개요 할당/초기화 RX/TX 경로 처리 큐잉/해제 `alloc_skb()`, 헤더 포인터 설정 L2/L3/L4, Netfilter, qdisc 소켓 전달 또는 NIC 전송 완료 상세 단계는 아래 상세 다이어그램 참고
먼저 개요 흐름을 보고, 아래 상세 다이어그램에서 함수 단위로 추적하면 이해가 빠릅니다.
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 헤더), 라우팅 결정 5. Netfilter 훅 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
sk_buff 생애주기: 할당 → 프로토콜 스택 통과 → 소켓 큐 → 유저스페이스 전달 → 해제

참조 카운트와 메모리 관리

/* 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_buffbuild_skb)
SCTP 멀티스트리밍, 멀티호밍 skb_queue_head_init()으로 청크별 큐 관리
/* 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 드라이버를 다루면서 얻는 경험적 성능 최적화 팁들입니다:

할당 최적화

Clone vs Copy 선택

큐 및 스케줄링

Zero-Copy 경로

하드웨어 offload 활용

# 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 offGRO on으로 변경 시 CPU 사용량이 약 40% 감소했습니다. 하지만 특정 레거시 애플리케이션에서는 GRO로 인한 packet reordering이 문제를 일으킬 수 있어, 프로덕션 변경 전 반드시 테스트 환경에서 검증하세요.

주의사항과 함정 (Common Mistakes)

1. skb leak (메모리 누수)

/* 잘못된 코드: 에러 경로에서 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 후 포인터 미갱신

/* 잘못된 코드: 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 데이터 수정

/* 잘못된 코드: 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 불일치

/* 잘못된 코드: 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 이중 해제

/* 잘못된 코드: 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

/* 잘못된 코드: 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 갭 문제

/* 수신 후 포워딩 경로에서 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 무효화

/* 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 재설정

/* 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에서 사용 중인 소켓 수가 비정상적으로 많거나, 시스템 메모리가 점진적으로 감소합니다.

# 현재 소켓 상태 확인
$ 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" 로그가 반복됩니다.

# 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 재전송이 증가하거나, 특정 애플리케이션에서 패킷 순서 오류 발생.

# 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 급격 감소.

# 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에서 집중됨.

# 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/<irq_num>/smp_affinity

# NAPI 가중치 확인
$ ls /sys/class/net/eth0/napi
$ cat /sys/class/net/eth0/napi/<napi_id>/poll_time

원인: IRQ가 단일 CPU에 집중되거나, NAPI weight가 너무 작아서 time slice 내에 처리를 못 함. RSS 설정과 IRQ balancing 문제.

트러블슈팅 핵심 원칙: 네트워크 문제는 غالب히 상호작용하는 여러 요소(RSS, GRO, IRQ affinity, 드라이버 버그)가 복합적으로 작용합니다. 단일 변수만 바꾸고 측정하는 체계적인 접근이 필요합니다. 예를 들어 "GRO만 끄고 latency 측정" → "IRQ affinity만 바꾸고 측정" 식으로요.

디버깅 기법

tracepoint 활용

# 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로 동적 추적

# 특정 함수에서 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 진단

# 소켓 메모리 사용량 확인 (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_KMEMLEAKskb를 포함한 커널 메모리 누수 탐지
CONFIG_KASANuse-after-free, out-of-bounds 접근 탐지
CONFIG_NET_DROP_MONITOR네트워크 패킷 드롭 위치 추적
CONFIG_DEBUG_NET네트워크 스택 디버깅 assertion 활성화
CONFIG_SKB_EXTENSIONSskb extension (conntrack, bridge 등) 디버깅

커널 버전별 변경사항

버전변경 내용
3.18skb_frag_off() 접근자 도입 (직접 필드 접근 대체)
4.14MSG_ZEROCOPY 소켓 옵션 도입
4.18UDP GSO (SKB_GSO_UDP_L4) 지원
5.0XDP에서 skb 모드 (XDP_FLAGS_SKB_MODE) 공식 지원
5.3skb_ensure_writable() 도입 (skb_make_writable 대체)
5.17page_pool 기반 skb 할당 최적화
6.0kfree_skb_reason() 도입 — 드롭 원인 추적 개선
6.2skb->csum_level 필드로 중첩 체크섬 오프로드 지원
6.8netmem 기반 skb frag 관리 (page → netmem 추상화)
💡

참고 자료: skbuff.h (Bootlin), O'Reilly "Understanding Linux Network Internals" Ch. 2, LWN의 skb lifecycle 시리즈, Documentation/networking/skbuff.rst

skb 확장 (skb_ext)

커널 5.x부터 sk_buff에 가변 메타데이터를 동적으로 연결하는 skb_ext 메커니즘이 도입되었습니다. 이전에는 conntrack 포인터, IPsec secpath 등이 skb 내부에 직접 포함되어 항상 메모리를 차지했으나, skb_ext를 통해 필요할 때만 할당되어 메모리 효율이 크게 개선되었습니다.

skb_ext 확장 아키텍처 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() → 독립 복사본 생성
skb_ext는 필요할 때만 할당되며, clone 시 refcount 공유 + COW(Copy-on-Write) 방식으로 동작
/* 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_PATHsec_path~40BIPsec/xfrm — SA 참조 배열
SKB_EXT_BRIDGE_NFnf_bridge_info~48Bbr_netfilter — 원본 포트/MAC 보존
TC_SKB_EXTtc_skb_ext~12BTC cls_act — 체인/zone/CT 메타
SKB_EXT_MPTCPmptcp_ext~24BMPTCP — DSS/DSN 매핑
💡

성능 영향: skb_ext 도입 전, struct sec_pathstruct 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을 사용합니다.

page_pool 재활용 아키텍처 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
page_pool은 DMA 매핑을 캐시하고 해제된 페이지를 재활용하여 할당/해제 비용을 5배 이상 절감
/* 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)에 따라 패킷을 드롭/포워딩하거나 일반 네트워크 스택으로 전달합니다.

XDP ↔ 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로 래핑 성능 이점 감소
XDP는 sk_buff 할당 이전에 패킷을 처리. XDP_PASS 시에만 build_skb()로 sk_buff 변환
/* 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를 통해 전달됩니다.

패킷 타임스탬프 삽입 지점 전송 경로 (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 (NIC PHY, ns 정밀도) 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)
HW 타임스탬프는 NIC PHY 수준(ns 정밀도), SW 타임스탬프는 커널 softirq 수준(μs 정밀도)
/* 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)~1nsNIC PTP 클럭PTP 동기화, 금융 트레이딩
SW 타임스탬프 (커널)~1μsktime_get_real() (softirq)일반 지연 측정, tcpdump
TX SCHED~1μsqdisc 진입 시점큐잉 지연 측정
TX ACK (TCP)~1μsACK 수신 시점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 네트워크 성능의 핵심 축입니다.

NAPI poll → GRO → 프로토콜 전달 흐름 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 남음
NAPI poll에서 GRO가 동일 플로우 패킷을 병합 → 하나의 super-skb로 프로토콜 스택 전달
/* 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 weight64netif_napi_add(dev, napi, poll, weight)poll당 처리 패킷 수. 높으면 throughput↑, latency↑
gro_flush_timeout0 (즉시)sysctl net.core.gro_flush_timeout0이 아니면 타이머로 flush → 병합 기회 증가
gro_normal_batch8sysctl net.core.gro_normal_batch배치 전달 크기. 높으면 처리량↑, 지연↑
netdev_budget300sysctl net.core.netdev_budgetsoftirq당 전체 NAPI 처리 패킷 상한
busy_poll0 (off)sysctl net.core.busy_poll소켓별 busy polling 시간 (μs)
# 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 필드가 핵심 역할을 합니다.

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) IP+TCP+payload ... seg 45 (나머지) IP+TCP+payload skb_segment() 내부 동작 1. gso_size 기준 페이로드 분할 2. 각 세그먼트에 IP+TCP 헤더 복사 3. IP.id 순차 증가, TCP.seq 순차 증가 4. 마지막 세그먼트에 PSH 플래그 설정 분할 후 각 세그먼트의 gso_size=0, gso_segs=0 (더 이상 GSO 아님)
HW TSO 지원 NIC은 super-packet을 그대로 전송, 미지원 시 skb_segment()가 SW로 분할
/* 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_TCPV4TCP/IPv42.6+가장 기본적인 TSO. 대부분 NIC이 HW 지원
SKB_GSO_TCPV6TCP/IPv62.6+IPv6 확장 헤더가 있으면 SW fallback 가능
SKB_GSO_UDP_L4UDP4.18+UDP GSO (USO). QUIC 등에서 활용. HW 지원 희소
SKB_GSO_GREGRE 터널3.10+외부 GRE 헤더 + 내부 TCP 분할
SKB_GSO_UDP_TUNNELVXLAN/Geneve3.12+외부 UDP + 내부 TCP 분할
SKB_GSO_PARTIAL다양4.7+외부 헤더만 HW, 내부는 SW. tunnel GSO 최적화
SKB_GSO_SCTPSCTP4.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 체인). 이 두 구조는 목적과 사용 상황이 완전히 다르며, 혼동하면 심각한 버그가 발생합니다.

frags[] vs frag_list 구조 비교 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=하위합 skb_shared_info 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 배열로 SG DMA에 최적화, frag_list는 skb 체인으로 GRO/IP 재조합에 사용
특성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, spliceGRO 병합, IP defrag, GSO 분할 결과
데이터 접근skb_frag_page(), skb_frag_off()skb_walk_frags(skb, frag_skb)
len/data_lendata_len = frags 총합data_len = frag_list skb들의 len 총합
/* 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이 지원합니다.

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 추출
HW 가속: NIC이 VLAN 태그를 RX descriptor로 추출하여 skb 메타데이터에 저장. 패킷 데이터에서 4바이트 절약
/* 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[] 활용이 핵심입니다.

/* 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 retx1개 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 필드로 변환합니다.

/* 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 등 다양한 곳에서 플로우 기반 분배에 사용됩니다.

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
Flow dissector가 패킷 헤더에서 5-tuple을 추출하고 해시를 계산 → RSS/RPS/GRO/소켓 분배에 사용
/* 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_cpusTX 큐 선택CPU→TX 큐 매핑

Encapsulation/Tunnel과 sk_buff

터널 프로토콜(GRE, VXLAN, Geneve, WireGuard)은 sk_buff에 외부 헤더를 추가(encap)하거나 제거(decap)합니다. 이 과정에서 headroom 관리, 헤더 포인터 재설정, inner/outer 프로토콜 구분이 핵심입니다.

VXLAN Encapsulation 시 skb 변화 Encapsulation 전 (원본 패킷) headroom Inner ETH 14B Inner IP 20B Inner TCP 20B Payload data Encapsulation 후 (VXLAN 캡슐화) Outer ETH 14B Outer IP 20B Outer UDP 8B VXLAN 8B Inner ETH Inner IP Inner TCP Payload data (새 위치) skb_push(50B) = Outer ETH(14) + Outer IP(20) + Outer UDP(8) + VXLAN(8)
VXLAN encap: 50바이트 외부 헤더 추가. headroom 부족 시 skb_cow_head()로 재할당 필요
/* 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 비용을 더 줄입니다.

/* 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 최소화 */
할당 방식캐시크기사용처
일반 skbskbuff_head_cache~240B대부분의 skb 할당
fclone skbskbuff_fclone_cache~490BTCP TX (clone 예상 시)
데이터 버퍼kmalloc slab가변linear 데이터 영역
NAPI 수신per-CPU page fragPAGE_SIZENAPI poll 내 고속 할당
page_poolper-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의 처리가 변화합니다.

/* 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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.