sk_buff 자료구조 (Socket Buffer)

Linux 커널 네트워크 스택의 핵심 자료구조 sk_buff: 메모리 레이아웃, 데이터 조작 함수, clone/copy, 소켓(struct sock) 연동, 소켓 옵션, raw socket, 체크섬 오프로드, GSO/GRO, zero-copy 전송까지 종합 분석합니다.

관련 표준: 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를 사용하면 다음 레이어에서 덮어쓰므로, 레이어 간 데이터 전달에는 사용하지 마세요.

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

데이터 조작 함수

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_local_deliver() raw_v4_input() raw_rcv() sock_queue_rcv_skb() skb clone → raw socket 수신 큐 ipprot->handler() (tcp_v4_rcv / udp_rcv) ① raw 먼저 → ② 프로토콜 핸들러
/* 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 등의 고성능 프록시에서 활용됩니다.

체크섬 오프로드 (Checksum Offload)

ip_summed 필드는 sk_buff의 체크섬 처리 상태를 나타냅니다. NIC 하드웨어 체크섬 오프로드를 제어하는 핵심 필드입니다:

모드수신 (RX)전송 (TX)
CHECKSUM_NONE HW 체크섬 미지원. 소프트웨어가 검증해야 함 소프트웨어가 체크섬을 이미 계산 완료
CHECKSUM_UNNECESSARY HW가 체크섬 검증 완료, 유효함 체크섬 불필요 (loopback 등)
CHECKSUM_COMPLETE HW가 전체 패킷 체크섬을 skb->csum에 제공 사용하지 않음
CHECKSUM_PARTIAL 사용하지 않음 HW에게 체크섬 계산 위임. csum_start/csum_offset 설정 필요
/* 수신 경로: 드라이버에서 체크섬 상태 설정 */
static void my_rx_handler(struct sk_buff *skb, bool hw_csum_ok)
{
    if (hw_csum_ok) {
        /* NIC가 체크섬 검증 완료 → 소프트웨어 재검증 생략 */
        skb->ip_summed = CHECKSUM_UNNECESSARY;
    } else {
        /* HW 미지원 → 프로토콜 스택이 직접 검증 */
        skb->ip_summed = CHECKSUM_NONE;
    }
}

/* 전송 경로: 소프트웨어 체크섬 fallback */
if (skb->ip_summed == CHECKSUM_PARTIAL) {
    /* NIC가 NETIF_F_HW_CSUM을 지원하지 않으면 */
    if (skb_checksum_help(skb))  /* SW로 체크섬 계산 */
        goto drop;
}

/* 체크섬 관련 유틸리티 */
skb_checksum(skb, offset, len, 0);       /* skb 데이터의 체크섬 계산 */
csum_tcpudp_magic(saddr, daddr, len, proto, csum); /* pseudo-header 포함 */
skb_postpull_rcsum(skb, hdr, hdr_len);   /* pull 후 csum 보정 */

GSO/GRO와 sk_buff

GSO (Generic Segmentation Offload)GRO (Generic Receive Offload)는 대량 데이터 전송/수신 시 성능을 극대화하는 핵심 메커니즘입니다. 기본 원리는 단순합니다: 네트워크 스택을 통과하는 패킷 수를 줄여 per-packet 오버헤드(헤더 파싱, 룩업, lock 경합, cache miss)를 최소화합니다.

핵심 개념: GSO는 전송(TX) 방향에서 대형 skb를 마지막 순간에 분할하고, GRO는 수신(RX) 방향에서 작은 패킷들을 하나의 대형 skb로 병합합니다. 둘 다 네트워크 스택 통과를 한 번으로 줄여 성능을 극대화합니다. MTU=1500 기준 64KB 데이터 처리 시, ~43개 패킷을 개별 처리하는 대신 1개의 대형 skb로 스택을 한 번만 통과합니다.

TX 경로 — GSO (Generic Segmentation Offload) tcp_sendmsg() tcp_write_xmit() 64KB super-skb 생성 __dev_queue_xmit() qdisc enqueue validate_xmit_skb() GSO 분할 결정 NIC HW TSO 하드웨어가 분할 → 1개 skb 전달 SW GSO skb_gso_segment() → N개 skb 리스트 dev→features NETIF_F_TSO ✓ NETIF_F_TSO ✗ RX 경로 — GRO (Generic Receive Offload) NIC → NAPI poll() napi_gro_receive() GRO 병합 시도 dev_gro_receive() 프로토콜별 GRO 콜백 inet_gro_receive() → tcp4_gro_receive() GRO_MERGED 기존 skb에 병합 → 대형 super-skb GRO_NORMAL 병합 불가 → 일반 경로

GSO (전송 오프로드) 심화

GSO(Generic Segmentation Offload)는 TSO(TCP Segmentation Offload)의 소프트웨어 일반화입니다. 핵심 아이디어: 가능한 한 늦게(late) 세그먼트를 분할하여 네트워크 스택의 중간 계층(Netfilter, TC, qdisc)에서 처리하는 패킷 수를 최소화합니다.

TSO에서 GSO로의 발전

/* === 일반 전송 (세그먼트 오프로드 없음) ===
 *
 * tcp_sendmsg()가 MSS 단위로 skb를 생성:
 *   write(fd, buf, 64000)
 *   → 44개 skb 생성 (각 1460 바이트)
 *   → 44번 TCP 헤더 생성 + 체크섬 계산
 *   → 44번 IP 계층 통과 + Netfilter/TC 처리
 *   → 44번 qdisc enqueue/dequeue
 *   → 44번 NIC DMA → 매우 높은 CPU 부하
 */

/* === TSO (하드웨어 오프로드) ===
 *
 * 커널이 최대 64KB의 대형 skb를 생성하여 NIC에 직접 전달:
 *   → 1개 skb (64KB) → NIC 하드웨어가 MSS 단위 분할
 *   → NIC가 각 세그먼트에 TCP/IP 헤더 복사 + 시퀀스 번호 증가 + 체크섬 계산
 *   → 장점: CPU 부하 최소, 10Gbps+ 환경 필수
 *   → 단점: NIC가 TSO를 지원해야 함, 터널/암호화 등에선 미지원 가능
 *
 * 확인: ethtool -k eth0 | grep tcp-segmentation
 */

/* === GSO (소프트웨어 일반화, 커널 2.6.18+) ===
 *
 * TSO와 동일한 "대형 skb" 전략을 소프트웨어로 구현:
 *   → 커널이 대형 skb를 유지하며 네트워크 스택을 통과
 *   → validate_xmit_skb()에서 NIC feature 확인:
 *     - NIC가 TSO 지원 → 그대로 NIC에 전달 (HW offload)
 *     - NIC가 TSO 미지원 → skb_gso_segment()로 소프트웨어 분할
 *   → 중간 계층(Netfilter, TC, qdisc)은 1개 skb만 처리 → 효율 극대화
 *
 * GSO의 핵심 이점:
 *   1. TSO 미지원 NIC에서도 성능 향상 (SW 분할이라도 늦은 분할이 유리)
 *   2. 터널, 가상화 등 복잡한 경로에서도 동작
 *   3. UDP, SCTP 등 TCP 외 프로토콜도 지원
 */

GSO 타입 전체 목록

skb_shared_info→gso_type 필드에 설정되는 비트마스크입니다. 여러 타입이 OR로 조합될 수 있습니다:

GSO 타입 플래그대상 프로토콜설명
SKB_GSO_TCPV41 << 0IPv4 TCP가장 일반적인 TSO/GSO. MSS 단위로 TCP 세그먼트 분할
SKB_GSO_TCPV61 << 5IPv6 TCPIPv6 환경의 TSO/GSO
SKB_GSO_UDP1 << 1UDP (IP frag)IP 단편화(fragmentation) 기반 UDP GSO. UFO(UDP Fragmentation Offload)
SKB_GSO_UDP_L41 << 11UDP (L4 분할)UDP 세그먼트 단위 분할 (커널 4.18+). QUIC, WireGuard 등에 사용
SKB_GSO_DODGY1 << 2모두신뢰할 수 없는 GSO skb (VM에서 전달 등). 재검증 필요
SKB_GSO_TCP_ECN1 << 3TCP + ECNECN(Explicit Congestion Notification) 플래그 있는 TCP GSO
SKB_GSO_TCP_FIXEDID1 << 9TCP모든 세그먼트가 동일 IP ID 사용 (드문 경우)
SKB_GSO_GRE1 << 6GRE 터널GRE 캡슐화 안의 내부 패킷 GSO
SKB_GSO_GRE_CSUM1 << 7GRE + 체크섬GRE 체크섬이 활성화된 터널 GSO
SKB_GSO_UDP_TUNNEL1 << 8VXLAN/GeneveUDP 기반 터널 내부 패킷 GSO
SKB_GSO_UDP_TUNNEL_CSUM1 << 10VXLAN + csum외부 UDP 체크섬이 활성화된 터널 GSO
SKB_GSO_PARTIAL1 << 13터널/복합부분 GSO — 외부 헤더만 NIC가 처리, 내부는 소프트웨어 분할
SKB_GSO_TUNNEL_REMCSUM1 << 12터널터널 원격 체크섬 오프로드
SKB_GSO_SCTP1 << 14SCTPSCTP 청크 단위 GSO
SKB_GSO_ESP1 << 15IPsec ESPESP(Encapsulating Security Payload) GSO
SKB_GSO_FRAGLIST1 << 17UDP/IPfrag_list 기반 GSO (GRO에서 병합된 skb 재전송 시)

GSO 관련 skb_shared_info 필드

/* include/linux/skbuff.h — skb_shared_info의 GSO 관련 필드 */
struct skb_shared_info {
    ...
    unsigned short gso_size;   /* 분할 단위 크기 (MSS 또는 세그먼트 크기)
                                   * TCP: MSS (예: 1460)
                                   * UDP GSO: 각 데이터그램 크기 (예: 1472)
                                   * 0이면 GSO 미사용 */
    unsigned short gso_segs;   /* 예상 분할 세그먼트 수 (힌트 값)
                                   * DIV_ROUND_UP(skb->len - hdr_len, gso_size)
                                   * NIC의 BQL(Byte Queue Limit) 계산에 활용 */
    unsigned int   gso_type;   /* SKB_GSO_* 비트마스크 (위 표 참조)
                                   * 여러 플래그 OR 조합 가능
                                   * 예: SKB_GSO_TCPV4 | SKB_GSO_TCP_ECN */
    ...
};

/* GSO skb인지 확인하는 헬퍼 함수들 */
static inline bool skb_is_gso(const struct sk_buff *skb)
{
    return skb_shinfo(skb)->gso_size;  /* gso_size != 0이면 GSO skb */
}

static inline bool skb_is_gso_v6(const struct sk_buff *skb)
{
    return skb_shinfo(skb)->gso_type & SKB_GSO_TCPV6;
}

/* GSO 세그먼트의 실제 와이어(wire) 크기 계산 */
static inline unsigned int skb_gso_network_seglen(const struct sk_buff *skb)
{
    unsigned int hdr_len = skb_transport_header(skb) - skb_network_header(skb);
    return hdr_len + skb_shinfo(skb)->gso_size;
}

GSO 전송 경로 상세

TCP 전송을 예로 들어 GSO skb가 생성되고 분할되는 전체 경로를 추적합니다:

/* 1단계: tcp_sendmsg() — 사용자 데이터를 skb에 적재 */
/*
 * tcp_sendmsg_locked()에서 sk_stream_alloc_skb()로 skb 할당
 * → tcp_send_mss()로 현재 MSS 결정 (경로 MTU - 헤더)
 * → size_goal: GSO 활성 시 MSS * max_segs (최대 64KB)
 * → 하나의 skb에 size_goal만큼 데이터를 적재
 */
int mss_now = tcp_send_mss(sk, &size_goal, flags);
/* size_goal 예시:
 *   MSS=1460, sk->sk_gso_max_segs=44 → size_goal = 1460 * 44 = 64240
 *   → 하나의 skb에 최대 64KB 데이터 적재
 */

/* 2단계: tcp_write_xmit() — skb에 TCP 헤더 부착 및 GSO 설정 */
/*
 * tcp_init_tso_segs()에서 GSO 관련 필드 설정:
 */
static void tcp_set_skb_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
    struct skb_shared_info *shinfo = skb_shinfo(skb);

    if (skb->len <= mss_now) {
        /* MSS 이하 → 분할 불필요, GSO 미사용 */
        shinfo->gso_segs = 1;
        shinfo->gso_size = 0;
        shinfo->gso_type = 0;
    } else {
        /* MSS 초과 → GSO skb로 설정 */
        shinfo->gso_segs = DIV_ROUND_UP(skb->len, mss_now);
        shinfo->gso_size = mss_now;
        shinfo->gso_type = sk->sk_gso_type;  /* SKB_GSO_TCPV4 등 */
    }
}

/* 3단계: ip_queue_xmit() → __dev_queue_xmit() → qdisc */
/*
 * IP 계층에서 라우팅 조회, TTL 설정 등을 한 번만 수행
 * qdisc에서도 1개의 대형 skb만 큐잉
 * → 여기까지 N개 패킷이 아닌 1개의 대형 skb로 처리
 */

/* 4단계: validate_xmit_skb() — GSO 분할 결정의 핵심 */
static struct sk_buff *validate_xmit_skb(struct sk_buff *skb,
                                           struct net_device *dev, bool *again)
{
    netdev_features_t features;

    /* NIC가 지원하는 feature 확인 */
    features = netif_skb_features(skb);

    if (skb_is_gso(skb)) {
        /* GSO skb이고 NIC가 해당 오프로드를 지원하면 → HW 처리 (분할 안 함) */
        /* NIC가 미지원이면 → __skb_gso_segment()로 소프트웨어 분할 */
        struct sk_buff *segs = __skb_gso_segment(skb, features, true);
        if (IS_ERR_OR_NULL(segs)) {
            if (!segs)
                return skb;  /* NIC가 HW 처리 가능 → 원본 skb 그대로 */
            kfree_skb(skb);
            return NULL;
        }
        /* SW 분할 완료: segs는 분할된 skb 리스트 */
        consume_skb(skb);
        return segs;
    }
    ...
}

skb_gso_segment() 내부 동작

/* net/core/skbuff.c — 소프트웨어 GSO 분할의 핵심 함수 */
struct sk_buff *skb_gso_segment(struct sk_buff *skb, netdev_features_t features)
{
    /* 프로토콜별 GSO 콜백 호출 */
    /* TCP: tcp4_gso_segment() 또는 tcp6_gso_segment()
     * UDP: udp4_ufo_fragment() 또는 __udp_gso_segment()
     * 터널: skb_udp_tunnel_segment() 등 */

    return skb_mac_gso_segment(skb, features);
}

/* net/ipv4/tcp_offload.c — TCP GSO 분할 */
struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
                                 netdev_features_t features)
{
    /* tcp_gso_segment()가 실제 분할 수행:
     *
     * 1. 원본 skb의 데이터를 gso_size(MSS) 단위로 분할
     * 2. 각 세그먼트에 새 TCP 헤더 복사:
     *    - seq 번호 증가 (prev_seq += gso_size)
     *    - PSH 플래그는 마지막 세그먼트에만 설정
     *    - FIN 플래그는 마지막 세그먼트에만 설정
     *    - CWR 플래그는 첫 세그먼트에만 설정
     * 3. 각 세그먼트의 IP 헤더 업데이트:
     *    - total_length 재계산
     *    - IP ID 증가 (SKB_GSO_TCP_FIXEDID 아닌 경우)
     * 4. 체크섬 재계산 (CHECKSUM_PARTIAL이면 pseudo-header만)
     * 5. 분할된 skb들을 linked list로 반환 (skb->next)
     */

    if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
        return ERR_PTR(-EINVAL);

    return tcp_gso_segment(skb, features);
}

/* 분할 결과 사용 예시 */
struct sk_buff *segs = skb_gso_segment(skb, features);
struct sk_buff *seg, *tmp;

skb_list_walk_safe(segs, seg, tmp) {
    skb_mark_not_on_list(seg);
    /* 각 세그먼트는 독립적인 skb:
     * - 자체 TCP/IP 헤더 보유
     * - 정확한 시퀀스 번호
     * - 올바른 체크섬
     * - skb->len == gso_size + hdr_len (마지막은 작을 수 있음)
     */
    dev_queue_xmit(seg);
}

GSO_PARTIAL — 터널 환경의 부분 오프로드

SKB_GSO_PARTIAL은 NIC가 외부(outer) 헤더는 처리할 수 있지만 내부(inner) 패킷의 GSO는 지원하지 않는 경우를 위한 메커니즘입니다:

/* VXLAN 터널에서의 GSO_PARTIAL 동작:
 *
 * [Outer Eth][Outer IP][Outer UDP][VXLAN Hdr][Inner Eth][Inner IP][Inner TCP][Payload]
 *
 * NIC가 NETIF_F_GSO_UDP_TUNNEL은 지원하지만
 * NETIF_F_GSO_UDP_TUNNEL_CSUM은 미지원하는 경우:
 *
 * 1. 커널이 내부(inner) TCP 세그먼트를 소프트웨어로 분할
 * 2. 각 분할된 세그먼트에 외부(outer) 헤더를 붙임
 * 3. 외부 헤더의 처리(체크섬 등)는 NIC에 위임
 *
 * 관련 NIC feature 확인:
 */
/* # ethtool -k eth0 | grep tunnel
 *   tx-udp_tnl-segmentation: on        (외부 UDP 터널 GSO)
 *   tx-udp_tnl-csum-segmentation: off   (외부 csum 미지원 → PARTIAL 필요)
 */

/* net_device features 비트 매핑 */
NETIF_F_TSO                  /* TCP Segmentation Offload (IPv4) */
NETIF_F_TSO6                 /* TCP Segmentation Offload (IPv6) */
NETIF_F_GSO_GRE              /* GRE 터널 내부 GSO */
NETIF_F_GSO_GRE_CSUM         /* GRE 체크섬 + 내부 GSO */
NETIF_F_GSO_UDP_TUNNEL       /* UDP 터널 (VXLAN/Geneve) 내부 GSO */
NETIF_F_GSO_UDP_TUNNEL_CSUM  /* UDP 터널 + 외부 UDP 체크섬 GSO */
NETIF_F_GSO_PARTIAL          /* 부분 GSO: 외부는 HW, 내부는 SW */
NETIF_F_GSO_UDP_L4           /* UDP L4 세그먼트 오프로드 (4.18+) */
NETIF_F_GSO_ESP              /* IPsec ESP GSO */
NETIF_F_GSO_SCTP             /* SCTP GSO */

GSO 최대 크기 제어

/* net_device의 GSO 크기 제한 */
struct net_device {
    unsigned int gso_max_size;    /* GSO skb 최대 크기 (기본 65536)
                                    * ip link set dev eth0 gso_max_size 32768
                                    * 으로 조절 가능 */
    u16          gso_max_segs;    /* 최대 세그먼트 수 (기본 65535)
                                    * NIC 하드웨어 제한에 따라 설정 */
    unsigned int gso_ipv4_max_size; /* IPv4 전용 GSO 최대 크기 (6.3+) */
};

/* 소켓 레벨에서도 GSO 크기 제어 가능 */
struct sock {
    int          sk_gso_max_size;  /* 소켓별 GSO 최대 크기 */
    u16          sk_gso_max_segs;  /* 소켓별 최대 세그먼트 수 */
    int          sk_gso_type;      /* 지원하는 GSO 타입 비트마스크 */
};

/* BIG TCP (커널 6.3+): IPv6에서 64KB 넘는 GSO 허용 */
/* ip link set dev eth0 gso_max_size 185000
 * → IPv6 jumbogram 활용, 단일 skb에 ~185KB 적재 가능
 * → GRO에서도 gro_max_size로 대응
 *
 * 주의: IPv4는 IP 헤더의 total_length가 16비트이므로 64KB가 한계
 *       IPv6 jumbogram 확장 헤더로 이 제한을 우회
 */

GRO (수신 오프로드) 심화

GRO(Generic Receive Offload)는 수신된 작은 패킷들을 하나의 큰 skb로 합치는 메커니즘입니다. LRO(Large Receive Offload)의 소프트웨어 일반화로, LRO와 달리 원본 헤더 정보를 보존하여 라우팅/포워딩 환경에서도 안전하게 동작합니다.

LRO vs GRO 비교

특성LRO (Large Receive Offload)GRO (Generic Receive Offload)
구현 위치NIC 드라이버 또는 하드웨어커널 네트워크 스택 (NAPI 레벨)
헤더 보존병합 시 원본 헤더 정보 손실 가능원본 헤더 정보 완전 보존
라우팅/포워딩패킷 재분할 시 헤더 복구 불가 → 부적합안전하게 재분할 가능 → 적합
프로토콜 지원TCP만TCP, UDP, GRE, VXLAN 등 확장 가능
병합 기준느슨함 (IP/TCP 4-tuple만)엄격함 (헤더 완전 일치 검증)
현재 상태deprecated (대부분 GRO로 대체)표준 수신 오프로드

GRO 수신 파이프라인

/* === GRO 수신 경로 전체 흐름 ===
 *
 * NIC IRQ
 *  └→ napi_schedule()
 *      └→ NAPI poll 함수 (드라이버)
 *          └→ napi_gro_receive(napi, skb)           ← 진입점
 *              └→ dev_gro_receive(napi, skb)
 *                  ├→ gro_list에서 동일 flow 검색
 *                  └→ 프로토콜별 GRO 콜백 체인:
 *                      └→ inet_gro_receive()         (L3: IP)
 *                          └→ tcp4_gro_receive()     (L4: TCP)
 *                              └→ GRO 결과 반환
 *                                  ├→ GRO_MERGED:      기존 skb에 병합 완료
 *                                  ├→ GRO_MERGED_FREE: 병합 + 현재 skb 해제
 *                                  ├→ GRO_HELD:        gro_list에 보관 (다음 패킷 대기)
 *                                  ├→ GRO_NORMAL:      병합 불가 → 일반 경로
 *                                  └→ GRO_CONSUMED:    콜백이 직접 처리 완료
 */

/* net/core/gro.c — GRO 핵심 진입점 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
                              struct sk_buff *skb)
{
    /* skb 전처리: VLAN 태그 처리, 해시 설정 등 */
    skb_gro_reset_offset(skb);

    return napi_skb_finish(napi, skb, dev_gro_receive(napi, skb));
}

static enum gro_result dev_gro_receive(struct napi_struct *napi,
                                        struct sk_buff *skb)
{
    struct list_head *gro_head = &napi->gro_hash[bucket].list;
    int count = napi->gro_hash[bucket].count;

    /* gro_list에서 병합 가능한 기존 skb 검색 */
    list_for_each_entry(pp, gro_head, list) {
        /* same_flow: 동일 flow인지 빠른 비교 (rxhash 기반) */
        NAPI_GRO_CB(pp)->same_flow = 1;
    }

    /* 프로토콜별 GRO 콜백 호출 (inet_gro_receive 등) */
    pp = call_gro_receive(ptype->callbacks.gro_receive, gro_head, skb);

    if (pp == skb) return GRO_NORMAL;     /* 병합 불가 */
    if (pp)        return GRO_MERGED;     /* 병합 성공 → flush 대상 */
    if (NAPI_GRO_CB(skb)->flush)
        return GRO_NORMAL;                /* 병합 불가 플래그 */

    /* 새 flow → gro_list에 추가 (다음 패킷 대기) */
    if (count < MAX_GRO_SKBS)            /* MAX_GRO_SKBS = 8 */
        return GRO_HELD;
    else
        return GRO_NORMAL;                /* 버킷 가득 참 → 일반 경로 */
}

GRO 병합 기준

패킷이 기존 flow에 병합되려면 다음 조건을 모두 만족해야 합니다:

/* === TCP GRO 병합 조건 (tcp4_gro_receive) ===
 *
 * 1. L2 레벨: 동일 수신 해시 (rxhash) — 빠른 사전 필터링
 *
 * 2. L3 레벨 (inet_gro_receive):
 *    - 동일 IP 프로토콜 (IPPROTO_TCP)
 *    - 동일 src/dst IP 주소
 *    - 동일 TOS (Type of Service)
 *    - 동일 TTL (합치면 안 되는 경우 방지)
 *    - IP 옵션 없음 (옵션이 있으면 병합 거부)
 *    - IP ID가 순차적으로 증가 (또는 DF 비트 설정 시 무관)
 *
 * 3. L4 레벨 (tcp_gro_receive):
 *    - 동일 src/dst 포트
 *    - TCP 시퀀스 번호가 연속 (이전 패킷의 끝 + 1)
 *    - TCP 윈도우 크기가 동일
 *    - ACK 플래그만 설정 (SYN, FIN, RST, URG, ECE, CWR → 병합 거부)
 *    - TCP 타임스탬프 옵션이 있으면 값이 동일하거나 증가
 *    - 이전에 병합된 패킷과 옵션 길이가 동일
 *
 * 4. 크기 제한:
 *    - 병합된 skb 크기가 gro_max_size를 초과하지 않아야 함
 *    - 기본 gro_max_size = 65536 (BIG TCP 시 더 큼)
 */

/* net/ipv4/tcp_offload.c — TCP GRO 병합 핵심 검증 */
struct sk_buff *tcp_gro_receive(struct list_head *head,
                                struct sk_buff *skb)
{
    struct tcphdr *th = tcp_hdr(skb);

    list_for_each_entry(p, head, list) {
        if (!NAPI_GRO_CB(p)->same_flow)
            continue;

        th2 = tcp_hdr(p);

        /* 포트 비교 */
        if (*((u32 *)&th->source) ^ *((u32 *)&th2->source)) {
            NAPI_GRO_CB(p)->same_flow = 0;
            continue;
        }

        /* 시퀀스 번호 연속 확인 */
        if (ntohl(th2->seq) + skb_gro_len(p) != ntohl(th->seq)) {
            NAPI_GRO_CB(p)->flush = 1;  /* 비연속 → flush */
            continue;
        }

        /* ACK 외 플래그 확인: SYN/FIN/RST → flush */
        if (th->fin || th->syn || th->rst || th->urg)
            flush = 1;

        /* 윈도우 크기 일치 확인 */
        if (th->window ^ th2->window)
            flush = 1;

        /* 타임스탬프 옵션 검증 */
        if (pcount > 1 || tcp_flag_word(th2) & TCP_FLAG_CWR)
            flush = 1;

        /* 병합 수행: skb 데이터를 기존 p에 append */
        if (!flush)
            skb_gro_receive(p, skb);  /* → 아래 설명 */
    }
    ...
}

GRO 데이터 병합 방식

GRO는 두 가지 방식으로 수신 데이터를 병합합니다:

병합 방식조건데이터 구조장단점
frag 기반 skb가 선형(linear) 데이터일 때 skb_shared_info→frags[] 배열에 페이지 추가 메모리 효율적, scatter-gather DMA에 적합. MAX_SKB_FRAGS(17) 제한
frag_list 기반 frag 공간 부족 또는 skb 자체가 이미 비선형일 때 skb_shared_info→frag_list에 skb 체인 연결 구조 단순, 제한 없음. 하지만 GSO 재분할 시 오버헤드
/* net/core/gro.c — skb_gro_receive() 핵심 로직 */
int skb_gro_receive(struct sk_buff *p, struct sk_buff *skb)
{
    unsigned int new_len = p->len + skb->len;

    /* gro_max_size 초과 검사 */
    if (new_len > gro_max_size(p))
        return -E2BIG;

    if (skb_headlen(skb) <= offset) {
        /* === frag 기반 병합 ===
         * 새 skb의 페이지를 기존 skb의 frags[]에 추가 */
        struct skb_shared_info *pinfo = skb_shinfo(p);

        if (pinfo->nr_frags + skb_shinfo(skb)->nr_frags > MAX_SKB_FRAGS)
            goto merge_frag_list;

        /* frags[] 배열에 새 페이지들을 복사 */
        for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
            pinfo->frags[pinfo->nr_frags++] = skb_shinfo(skb)->frags[i];
        }
    } else {
merge_frag_list:
        /* === frag_list 기반 병합 ===
         * 새 skb를 기존 skb의 frag_list 체인에 연결 */
        if (!skb_shinfo(p)->frag_list)
            skb_shinfo(p)->frag_list = skb;
        else
            NAPI_GRO_CB(p)->last->next = skb;

        NAPI_GRO_CB(p)->last = skb;
    }

    /* 병합된 skb의 총 길이 업데이트 */
    p->len += skb->len;
    p->data_len += skb->len;
    p->truesize += skb->truesize;
    NAPI_GRO_CB(p)->count++;      /* 병합된 패킷 수 */

    return 0;
}

GRO Flush 메커니즘

gro_list에 보관 중인 병합된 skb는 다음 조건에서 상위 스택으로 전달(flush)됩니다:

/* GRO flush 발생 조건과 동작 */

/* 1. napi_complete_done() 호출 시 — 가장 일반적
 *    NAPI poll에서 budget 미만 처리 → 인터럽트 재활성화 전 flush */
bool napi_complete_done(struct napi_struct *napi, int work_done)
{
    /* gro_list의 모든 보류 skb를 flush */
    gro_normal_list(napi);  /* → netif_receive_skb_list_internal() */
    ...
}

/* 2. MAX_GRO_SKBS(8) 초과 시 — 버킷당 보관 제한
 *    동일 해시 버킷에 8개 이상 skb가 쌓이면 가장 오래된 것 flush */

/* 3. 비연속 패킷 수신 시 (flush 플래그)
 *    시퀀스 번호 불연속, 다른 플래그 등 → 현재 보관 중인 skb flush */

/* 4. gro_flush_timeout 타이머 만료 (busy polling 관련) */
/* sysctl: net.core.gro_flush_timeout
 *   기본값: 0 (즉시 flush, 타이머 미사용)
 *   설정 시: napi_complete에서 바로 flush하지 않고 타이머 대기
 *   → 더 많은 패킷을 병합할 기회를 제공하지만 지연 증가
 *
 * 관련 sysctl:
 *   net.core.napi_defer_hard_irqs = N
 *   → N번의 빈 poll 후에야 IRQ 재활성화
 *   → gro_flush_timeout과 함께 사용하면 GRO 효율 극대화
 */

/* 5. 명시적 flush 호출 */
napi_gro_flush(napi, false);  /* 모든 보류 skb를 즉시 flush */

/* flush된 skb는 netif_receive_skb_list_internal()로 전달되어
 * 일반 수신 경로(IP → TCP → 소켓)를 탐니다.
 * 이때 skb->len은 원래 여러 패킷의 합이므로
 * TCP 수신 측에서 효율적으로 처리됩니다. */

GRO API 변형

/* 드라이버에서 사용하는 GRO 진입점들 */

/* 1. napi_gro_receive() — 가장 일반적
 *    완전한 skb를 GRO 처리 */
gro_result_t napi_gro_receive(struct napi_struct *napi,
                              struct sk_buff *skb);

/* 2. napi_gro_frags() — 헤더/데이터 분리 수신 시
 *    NIC가 헤더를 선형 영역에, 페이로드를 페이지에 배치한 경우
 *    napi->skb에 미리 설정된 skb 사용 */
gro_result_t napi_gro_frags(struct napi_struct *napi);
/*
 * 사용 패턴 (고성능 드라이버):
 *   napi->skb = netdev_alloc_skb(...);
 *   skb_put(napi->skb, hdr_len);        // 헤더를 선형 영역에
 *   skb_fill_page_desc(napi->skb, ...);  // 페이로드를 frag으로
 *   napi_gro_frags(napi);
 *   // napi->skb는 내부에서 소비/해제됨
 */

/* 3. napi_gro_complete() — 내부 함수
 *    GRO 완료 시 호출, 프로토콜별 gro_complete 콜백 실행 후 상위 전달 */

/* GRO 결과 타입 */
enum gro_result {
    GRO_MERGED,       /* 기존 skb에 병합 성공 — flush 대상으로 표시 */
    GRO_MERGED_FREE,  /* 병합 성공 + 현재 skb 해제 가능 */
    GRO_HELD,         /* gro_list에 보관 (새 flow, 다음 패킷 대기) */
    GRO_NORMAL,       /* 병합 불가 — 일반 수신 경로로 */
    GRO_CONSUMED,     /* 콜백이 직접 처리 완료 */
};

하드웨어 GRO (HW-GRO)

커널 5.19+에서 도입된 HW-GRO는 NIC 하드웨어가 GRO를 수행하되, LRO와 달리 헤더 정보를 보존합니다:

/* HW-GRO vs SW-GRO vs LRO 비교
 *
 * LRO (deprecated):
 *   - NIC가 패킷 병합, 헤더 정보 손실
 *   - 포워딩 시 문제 → ip_summed 등 불일치
 *   - ethtool -K eth0 lro on/off
 *
 * SW-GRO (기본):
 *   - 커널 NAPI 레벨에서 병합
 *   - 헤더 완전 보존, 모든 프로토콜 지원
 *   - CPU 오버헤드 있음
 *   - ethtool -K eth0 gro on/off
 *
 * HW-GRO (5.19+):
 *   - NIC 하드웨어가 병합하되 개별 헤더를 보존
 *   - NIC가 "header split" 또는 RSC(Receive Side Coalescing) 활용
 *   - CPU 오버헤드 최소화 + 헤더 보존의 장점
 *   - ethtool -K eth0 rx-gro-hw on/off
 *
 * 확인:
 *   # ethtool -k eth0 | grep gro
 *   generic-receive-offload: on
 *   rx-gro-hw: on [requested on]
 */

/* NIC feature 플래그 */
NETIF_F_GRO     /* 소프트웨어 GRO 지원 (기본 on) */
NETIF_F_GRO_HW  /* 하드웨어 GRO 지원 (5.19+) */

/* 드라이버에서 HW-GRO 결과를 커널에 전달하는 방법 */
/* NIC가 병합한 패킷을 수신하면, 드라이버는:
 * 1. skb->len에 병합된 총 크기 설정
 * 2. skb_shinfo(skb)->gso_size에 원래 MSS 설정
 * 3. skb_shinfo(skb)->gso_type에 SKB_GSO_TCPV4 등 설정
 * 4. napi_gro_receive()가 아닌 일반 경로로 전달 가능
 *    (이미 병합됨, 커널 GRO 불필요)
 */

프로토콜별 GRO 콜백

/* GRO 콜백 등록 구조 */
struct net_offload {
    struct offload_callbacks callbacks;
};

struct offload_callbacks {
    struct sk_buff *(*gro_receive)(struct list_head *head,
                                    struct sk_buff *skb);
    int            (*gro_complete)(struct sk_buff *skb, int nhoff);
};

/* 프로토콜별 GRO 콜백 체인 (L2 → L3 → L4):
 *
 * Ethernet:
 *   → eth_gro_receive()
 *     → inet_gro_receive() (IPv4) 또는 ipv6_gro_receive()
 *       → tcp4_gro_receive() 또는 udp4_gro_receive()
 *
 * VXLAN 터널:
 *   → eth_gro_receive()
 *     → inet_gro_receive()
 *       → udp4_gro_receive()
 *         → vxlan_gro_receive()      ← 터널 디캡슐화
 *           → eth_gro_receive()      ← 내부 패킷 재귀 처리
 *             → inet_gro_receive()
 *               → tcp4_gro_receive()
 */

/* UDP GRO 콜백 (4.18+ UDP GSO와 짝) */
static struct sk_buff *udp4_gro_receive(struct list_head *head,
                                         struct sk_buff *skb)
{
    /* UDP는 TCP와 달리 시퀀스 번호가 없으므로
     * 병합 기준이 다름:
     *   - 동일 src/dst IP + port
     *   - 동일 데이터그램 크기 (마지막 제외)
     *   - 소켓이 UDP_GRO 옵션을 설정했어야 함
     */

    /* 터널 프로토콜 콜백이 등록되어 있으면 터널 GRO */
    struct udp_sock *up = udp_sk(sk);
    if (up->encap_type && up->gro_receive)
        return call_gro_receive(up->gro_receive, head, skb);

    /* 일반 UDP GRO */
    return udp_gro_receive(head, skb);
}

드라이버에서의 GRO 사용 패턴

/* 전형적인 NAPI poll 함수에서의 GRO 사용 */
static int my_napi_poll(struct napi_struct *napi, int budget)
{
    struct my_adapter *adapter = container_of(napi, struct my_adapter, napi);
    int work_done = 0;

    while (work_done < budget) {
        struct my_rx_desc *desc = my_get_rx_desc(adapter);
        struct sk_buff *skb;

        if (!desc)
            break;

        skb = my_build_skb(adapter, desc);
        if (!skb) {
            adapter->stats.alloc_fail++;
            break;
        }

        /* 필수 skb 메타데이터 설정 — GRO 정확도에 영향 */
        skb->protocol = eth_type_trans(skb, adapter->netdev);

        /* 체크섬 오프로드 상태: GRO 효율에 직접 영향
         * CHECKSUM_UNNECESSARY → GRO가 체크섬 재검증 생략 → 빠른 병합 */
        if (desc->rx_status & RX_CSUM_OK)
            skb->ip_summed = CHECKSUM_UNNECESSARY;

        /* RX 해시: GRO가 동일 flow를 빠르게 찾는 데 사용 */
        skb_set_hash(skb, desc->rss_hash, PKT_HASH_TYPE_L4);

        /* VLAN 태그 (있으면) */
        if (desc->rx_status & RX_VLAN_STRIPPED)
            __vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), desc->vlan_tci);

        /* GRO 진입 — 여기서 패킷 병합 시도 */
        napi_gro_receive(napi, skb);
        work_done++;
    }

    if (work_done < budget) {
        /* 모든 수신 패킷 처리 완료 → IRQ 재활성화 */
        if (napi_complete_done(napi, work_done))
            my_enable_irq(adapter);
        /* napi_complete_done() 내부에서 gro_list flush 수행 */
    }

    return work_done;
}

GSO ↔ GRO 상호작용

포워딩 환경에서 GRO로 병합된 skb가 다시 GSO로 분할되는 경우가 빈번합니다. 이 "GRO → forward → GSO" 파이프라인이 네트워크 장비(라우터, 브리지, 로드밸런서)의 성능을 결정합니다:

NIC RX 43 × 1500B pkts GRO → 1 × 64KB skb IP Forward / Netfilter / TC 1개 skb만 처리 (per-pkt 비용 1/43) conntrack, NAT 등 1회만 수행 GSO → 43개 세그먼트 NIC TX 43 × 1500B wire 포워딩 경로: 스택 처리 비용 1/43로 감소 conntrack 1회, routing lookup 1회, Netfilter 규칙 매칭 1회 → 10Gbps+ 라우터/방화벽에서 필수 최적화
/* 포워딩 시 GRO → GSO 동작 */
/*
 * 1. 수신 NIC에서 GRO가 43개 패킷을 1개 대형 skb로 병합
 * 2. ip_forward()에서 라우팅 결정 (1회)
 * 3. Netfilter (conntrack, NAT 등) 통과 (1회)
 * 4. 송신 NIC로 전달:
 *    - 송신 NIC가 TSO 지원 → 대형 skb 그대로 전달
 *    - 송신 NIC가 TSO 미지원 → GSO가 소프트웨어 분할
 *
 * 포워딩 효율:
 *   GRO/GSO OFF: 43 × {routing + conntrack + NAT + filter} = 43 × CPU cycles
 *   GRO/GSO ON:  1 × {routing + conntrack + NAT + filter} = 1 × CPU cycles
 *   → 약 43배 효율 향상 (64KB / 1500 MTU 기준)
 */

/* GRO로 병합된 skb가 다시 분할될 때의 GSO 타입 */
/* GRO 병합 시 gso_size와 gso_type이 자동 설정되므로
 * 포워딩 경로의 GSO가 원본 패킷과 동일하게 분할 가능 */
if (NAPI_GRO_CB(p)->count > 1) {
    skb_shinfo(p)->gso_size = skb_gro_len(skb);  /* 원본 세그먼트 크기 */
    skb_shinfo(p)->gso_type |= SKB_GSO_TCPV4;
}

성능 튜닝과 모니터링

ethtool 오프로드 제어

/* GSO/GRO 관련 ethtool 명령어 */

/* 현재 오프로드 상태 확인 */
# ethtool -k eth0 | grep -E 'offload|gso|gro|tso'
tcp-segmentation-offload: on          # TSO (HW)
generic-segmentation-offload: on      # GSO (SW fallback)
generic-receive-offload: on           # GRO (SW)
rx-gro-hw: on [requested on]          # HW-GRO (5.19+)
udp-segmentation-offload: off         # USO (4.18+)
tx-udp_tnl-segmentation: on           # 터널 TSO
tx-udp_tnl-csum-segmentation: on      # 터널 TSO + csum
large-receive-offload: off [requested off]  # LRO (deprecated)

/* 개별 오프로드 제어 */
# ethtool -K eth0 gso on|off        # GSO 전환
# ethtool -K eth0 gro on|off        # GRO 전환
# ethtool -K eth0 tso on|off        # TSO 전환
# ethtool -K eth0 rx-gro-hw on|off  # HW-GRO 전환
# ethtool -K eth0 lro on|off        # LRO 전환 (비권장)

/* GSO/GRO 최대 크기 확인 및 설정 */
# ip -d link show eth0 | grep gso
#   gso_max_size 65536 gso_max_segs 65535
#   gro_max_size 65536
#
# BIG TCP 활성화 (IPv6, 6.3+):
# ip link set dev eth0 gso_max_size 185000
# ip link set dev eth0 gro_max_size 185000

GRO 관련 sysctl 튜닝

/* GRO 성능에 영향을 주는 sysctl 파라미터 */

/* 1. gro_flush_timeout — GRO 패킷 보관 타임아웃 (나노초)
 *    기본값: 0 (napi_complete 시 즉시 flush)
 *    설정 시: 타이머 만료까지 더 많은 패킷 병합 시도
 *    → 처리량 증가, 하지만 지연 시간도 증가 */
# sysctl -w net.core.gro_flush_timeout=20000     # 20μs

/* 2. napi_defer_hard_irqs — 빈 poll 허용 횟수
 *    기본값: 0 (즉시 IRQ 재활성화)
 *    설정 시: N번 빈 poll 후에야 IRQ 재활성화
 *    → gro_flush_timeout과 함께 사용하면 busy-poll 모드 */
# sysctl -w net.core.napi_defer_hard_irqs=2       # 2번 빈 poll 허용

/* 3. 권장 조합 (10Gbps+ 고처리량 환경):
 *    gro_flush_timeout=20000 + napi_defer_hard_irqs=2
 *    → IRQ 없이 busy-poll로 패킷 수신 → GRO 병합 극대화
 *    → 단, CPU 사용률 약간 증가 (IRQ coalescence와 트레이드오프)
 *
 * 4. 지연 민감 환경 (금융, 게임):
 *    gro_flush_timeout=0 + napi_defer_hard_irqs=0
 *    → 최소 지연, 하지만 GRO 병합 기회 감소
 *    → 극단적 경우 GRO 자체를 비활성화 고려 */

/* 5. busy_poll / busy_read — 소켓 레벨 busy polling
 *    → epoll/poll에서 커널이 먼저 NAPI poll 시도 (IRQ 대기 없이)
 *    → GRO와 결합하면 ultra-low latency + 높은 처리량 */
# sysctl -w net.core.busy_poll=50           # 50μs busy poll
# sysctl -w net.core.busy_read=50           # 50μs busy read

모니터링과 통계

/* GSO/GRO 동작 상태 확인 */

/* 1. NIC 통계 — GRO 병합 횟수 */
# ethtool -S eth0 | grep -i gro
#   rx_gro_packets: 1234567        # GRO로 병합된 패킷 수
#   rx_gro_bytes: 98765432000      # GRO로 병합된 바이트 수
#   rx_gro_dropped: 0              # GRO 중 드롭

/* 2. /proc/net/dev — 일반 인터페이스 통계 */
# cat /proc/net/dev
# → GRO 활성 시 RX packets가 줄고 bytes는 동일
# → 패킷당 평균 크기가 크면 GRO가 잘 동작하는 것

/* 3. nstat — 프로토콜별 통계 */
# nstat -az | grep -i gro
#   TcpExtTCPAutoCorking    123456  # TCP autocorking (GSO 관련)

/* 4. perf로 GSO/GRO 함수 프로파일링 */
# perf top -g -e cycles -- -K
#   → skb_gso_segment, tcp_gso_segment, dev_gro_receive 등의 CPU 비중 확인
#   → GRO/GSO 오버헤드가 높으면 HW 오프로드 확인

/* 5. ftrace로 GSO 분할 추적 */
# echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_xmit/enable
# cat /sys/kernel/debug/tracing/trace_pipe
#   → skb->len 변화로 GSO 분할 여부 확인

/* 6. GRO 효율 지표 계산 */
/* GRO ratio = (NIC rx_packets) / (netif_receive_skb 호출 수)
 * → 비율이 높을수록 GRO가 효과적으로 동작
 * → TCP 워크로드에서 일반적으로 40~60:1 (64KB / 1500 MTU)
 */

GSO/GRO 주의사항

GSO/GRO가 문제를 일으키는 경우:
  1. 패킷 캡처(tcpdump) — GRO가 병합한 대형 패킷이 캡처되어 MTU 초과로 보일 수 있음. 디버깅 시 ethtool -K eth0 gro off로 비활성화하거나 tcpdump가 자동 처리
  2. Netfilter conntrack — GRO skb가 conntrack에서 분할 없이 하나로 추적되므로, 패킷 카운터가 실제보다 적게 표시됨
  3. TC(Traffic Control) — GSO skb는 분할 전이므로 큐 깊이가 실제보다 적게 보임. tc -s qdisc 출력 해석 시 주의
  4. MTU 변경 — GSO/GRO 활성 상태에서 MTU 변경 시 기존 skb의 gso_size와 불일치 가능. 트래픽 중단 후 변경 권장
  5. IPsec — ESP 암호화 후 GSO 분할 필요. NETIF_F_GSO_ESP 미지원 NIC에서 성능 저하. xfrm offload 확인 필요
  6. Bridge/OVS — 포워딩 경로에서 GRO로 병합된 skb가 재분할 없이 브리지되면, 수신 측에서 예상치 못한 대형 프레임을 받을 수 있음
GSO/GRO 디버깅 팁:
  • 성능 문제 발생 시 ethtool -K eth0 gro off; ethtool -K eth0 tso off로 오프로드를 순차 비활성화하여 원인 분리
  • ss -ti로 TCP 소켓별 MSS, cwnd를 확인하여 GSO 세그먼트 크기 추정
  • ip -s link show eth0에서 TX/RX 패킷 수 대비 바이트 수 비율로 GSO/GRO 효과 확인
  • 가상화 환경에서는 virtio-net의 VIRTIO_NET_F_MRG_RXBUF 플래그가 GRO에 직접 영향

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 변형

수신 경로 (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;
}

주의사항과 함정 (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));  /* 이제 안전 */

디버깅 기법

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