네트워크 스택 (Network Stack)

Linux 커널 네트워크 스택: sk_buff, TCP/IP, netfilter, socket, NAPI 종합 가이드.

관련 표준: RFC 791 (IPv4), RFC 8200 (IPv6), RFC 793 (TCP), RFC 768 (UDP), IEEE 802.3 (Ethernet) — 커널 네트워크 스택이 구현하는 핵심 프로토콜 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

네트워크 스택 개요

Linux 네트워크 스택은 OSI 7계층 모델에 대응하는 계층적 구조로 설계되어 있습니다. 패킷은 sk_buff(소켓 버퍼) 구조체로 표현되며, 각 계층을 통과하면서 헤더가 추가/제거됩니다.

Linux 네트워크 스택 계층 User Space: socket(), send(), recv() Socket Layer (AF_INET, AF_PACKET, ...) Transport Layer (TCP, UDP, SCTP) Network Layer (IPv4, IPv6, routing) Netfilter (iptables/nftables) Traffic Control (tc/qdisc) Device Driver (NIC) → Hardware
Linux 네트워크 스택: 소켓에서 NIC 드라이버까지의 패킷 경로

sk_buff 구조체

struct sk_buff는 네트워크 패킷을 표현하는 핵심 자료구조입니다. 패킷 데이터, 헤더 포인터, 메타데이터를 포함합니다.

struct sk_buff {
    union {
        struct {
            struct sk_buff *next, *prev;
        };
        struct rb_node rbnode;
    };
    struct sock     *sk;           /* 소속 소켓 */
    struct net_device *dev;       /* 네트워크 디바이스 */

    unsigned char   *head;         /* 버퍼 시작 */
    unsigned char   *data;         /* 현재 계층 데이터 시작 */
    unsigned char   *tail;         /* 데이터 끝 */
    unsigned char   *end;          /* 버퍼 끝 */

    unsigned int    len;           /* 총 데이터 길이 */
    __u16           protocol;      /* ETH_P_IP 등 */
    __u16           transport_header;
    __u16           network_header;
    __u16           mac_header;
    /* ... */
};

Netfilter

Netfilter는 커널의 패킷 필터링 프레임워크입니다. 네트워크 스택의 여러 지점(hook)에 콜백 함수를 등록하여 패킷을 검사, 수정, 차단합니다. iptables/nftables의 백엔드입니다.

#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>

static unsigned int my_hook(void *priv,
    struct sk_buff *skb,
    const struct nf_hook_state *state)
{
    struct iphdr *iph = ip_hdr(skb);
    pr_info("packet from %pI4\n", &iph->saddr);
    return NF_ACCEPT;  /* or NF_DROP, NF_QUEUE */
}

static struct nf_hook_ops my_nf_ops = {
    .hook     = my_hook,
    .pf       = NFPROTO_IPV4,
    .hooknum  = NF_INET_PRE_ROUTING,
    .priority = NF_IP_PRI_FIRST,
};

nf_register_net_hook(&init_net, &my_nf_ops);

네트워크 디바이스

struct net_device는 네트워크 인터페이스(eth0, wlan0 등)를 나타냅니다. NAPI(New API)를 통한 고속 패킷 처리를 지원합니다.

💡

NAPI는 인터럽트와 폴링을 혼합하여 고속 네트워크에서의 인터럽트 폭풍을 방지합니다. 패킷이 도착하면 인터럽트로 시작하여 폴링 모드로 전환, 큐가 비면 다시 인터럽트 모드로 복귀합니다.

소켓 계층 (Socket Layer)

소켓은 유저스페이스 프로세스와 커널 네트워크 스택을 연결하는 인터페이스입니다. struct socketstruct sock의 이중 구조로 되어 있습니다.

struct socket {                    /* VFS/유저 인터페이스 */
    socket_state        state;       /* SS_CONNECTED 등 */
    short               type;        /* SOCK_STREAM, SOCK_DGRAM */
    struct file         *file;       /* VFS file 연결 */
    struct sock         *sk;         /* 프로토콜 소켓 */
    const struct proto_ops *ops;   /* sendmsg/recvmsg 등 */
};

struct sock {                      /* 프로토콜 계층 (TCP/UDP) */
    struct sock_common  __sk_common;
    struct sk_buff_head sk_receive_queue;  /* 수신 큐 */
    struct sk_buff_head sk_write_queue;    /* 송신 큐 */
    atomic_t            sk_wmem_alloc;     /* 송신 버퍼 사용량 */
    atomic_t            sk_rmem_alloc;     /* 수신 버퍼 사용량 */
    int                 sk_sndbuf;         /* SO_SNDBUF */
    int                 sk_rcvbuf;         /* SO_RCVBUF */
    /* ... */
};

TCP 내부 구현

TCP 연결 상태 머신

커널의 TCP 구현은 RFC 793의 상태 머신을 정밀하게 따릅니다:

/* include/net/tcp_states.h */
enum {
    TCP_ESTABLISHED = 1,
    TCP_SYN_SENT    = 2,
    TCP_SYN_RECV    = 3,
    TCP_FIN_WAIT1   = 4,
    TCP_FIN_WAIT2   = 5,
    TCP_TIME_WAIT   = 6,
    TCP_CLOSE       = 7,
    TCP_CLOSE_WAIT  = 8,
    TCP_LAST_ACK    = 9,
    TCP_LISTEN      = 10,
    TCP_CLOSING     = 11,
    TCP_NEW_SYN_RECV = 12,
};
TCP 상태 전이 다이어그램 CLOSED LISTEN listen() SYN_SENT connect() SYN_RECV rcv SYN ESTABLISHED rcv ACK rcv SYN+ACK FIN_WAIT1 close() CLOSE_WAIT rcv FIN FIN_WAIT2 rcv ACK LAST_ACK close() TIME_WAIT rcv FIN rcv ACK → CLOSED 2MSL(60s) → CLOSED
TCP 상태 전이: 연결 수립(위)에서 종료(아래)까지의 흐름. 굵은 테두리는 데이터 전송 상태(ESTABLISHED)

TCP_NEW_SYN_RECV(12)는 커널 4.4+에서 추가된 최적화 상태입니다. 기존 SYN_RECV가 전체 struct sock을 할당하던 것과 달리, 경량 request_sock만 사용하여 SYN Flood 방어 시 메모리 효율을 높입니다. 상세 구현은 TCP 심화 섹션을 참고하세요.

혼잡 제어 (Congestion Control)

Linux는 플러그인 방식의 혼잡 제어 알고리즘을 지원합니다. 기본값은 CUBIC이며, BBR, Reno 등을 선택할 수 있습니다. 상세한 알고리즘 내부 동작은 혼잡 제어 심화 섹션에서 다룹니다.

/* 혼잡 제어 알고리즘 등록 */
struct tcp_congestion_ops tcp_reno = {
    .name        = "reno",
    .ssthresh    = tcp_reno_ssthresh,
    .cong_avoid  = tcp_reno_cong_avoid,
    .undo_cwnd   = tcp_reno_undo_cwnd,
    .owner       = THIS_MODULE,
};

/* sysctl로 알고리즘 선택 */
/* net.ipv4.tcp_congestion_control = bbr */
/* net.ipv4.tcp_available_congestion_control 로 목록 확인 */
/* net.ipv4.tcp_allowed_congestion_control = cubic reno bbr */
알고리즘감지 방식특징사용 시나리오
CUBIC손실 기반3차 함수 cwnd 증가, Hystart++ 슬로 스타트기본값, 일반적 환경
BBR대역폭/RTT 모델BtlBw × RTprop 기반, pacing 필수고지연, 패킷 손실 환경
Reno손실 기반클래식 AIMD (additive increase, multiplicative decrease)레퍼런스, 교육용
DCTCPECN 기반ECN 비율에 비례하여 cwnd 조절데이터센터 내부
Vegas지연 기반RTT 증가량으로 혼잡 감지안정적 지연 요구 환경
Westwood+대역폭 추정ACK 간격으로 가용 대역폭 추정무선 네트워크

NAPI 상세 구현

/* 드라이버: NAPI 초기화 */
netif_napi_add(netdev, &priv->napi, my_poll);
napi_enable(&priv->napi);

/* 인터럽트 핸들러: 폴링 모드로 전환 */
static irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_priv *priv = data;
    disable_hw_irq(priv);           /* HW 인터럽트 비활성화 */
    napi_schedule(&priv->napi);     /* 폴링 스케줄 */
    return IRQ_HANDLED;
}

/* 폴링 함수: softirq 컨텍스트에서 실행 */
static int my_poll(struct napi_struct *napi, int budget)
{
    int work_done = 0;
    while (work_done < budget) {
        struct sk_buff *skb = read_packet_from_hw(priv);
        if (!skb) break;
        napi_gro_receive(napi, skb);  /* GRO 처리 후 상위 전달 */
        work_done++;
    }
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        enable_hw_irq(priv);          /* 인터럽트 재활성화 */
    }
    return work_done;
}

라우팅 서브시스템

Linux 라우팅은 FIB(Forwarding Information Base)를 기반으로 Longest Prefix Match(LPM) 조회를 수행합니다. Policy Routing, ECMP, VRF, SRv6 등 고급 기능을 포함한 상세한 내용은 별도 페이지에서 다룹니다.

💡

라우팅 심화: FIB 내부 구조(LC-trie), Policy Routing, ECMP/Multipath, VRF, IPv6 라우팅, Netfilter 상호작용, SRv6 등의 상세 내용은 라우팅 (Routing Subsystem) 페이지를 참고하세요.

TC (Traffic Control)와 qdisc

패킷 스케줄링과 트래픽 셰이핑을 담당합니다. 각 네트워크 디바이스에 qdisc(큐잉 규칙)가 연결됩니다.

qdisc유형설명
pfifo_fastclassless기본 3-band 우선순위 큐
fq_codelclasslessFair Queuing + CoDel (권장)
htbclassfulHierarchical Token Bucket
tbfclasslessToken Bucket Filter (대역폭 제한)
netemclassless지연, 손실, 중복 시뮬레이션
ingressspecial수신 경로에서 필터링

net.core.netdev_budget (기본 300)은 NAPI 폴링 한 사이클에서 처리할 최대 패킷 수입니다. 10Gbps+ 환경에서는 이 값을 증가시켜 처리량을 높일 수 있습니다.

NAPI 심화 — 성능 튜닝과 주의사항

NAPI 상태 전이와 Budget 관리

DISABLED SCHED (대기) POLLING COMPLETE enable softirq done<budget IRQ → napi_schedule() done==budget (계속 폴링)
매개변수기본값설명튜닝 가이드
budget (per-NAPI) 64 poll() 한 번 호출에서 처리할 최대 패킷 수 증가 시 처리량↑, 지연↑. NIC 드라이버에서 설정
netdev_budget 300 softirq 한 사이클에서 모든 NAPI의 총 처리량 10G+ 환경에서 600~1200으로 증가 고려
netdev_budget_usecs 2000 (2ms) softirq 한 사이클의 시간 제한 지연 민감 환경에서 감소, 처리량 중시에서 증가
busy_poll 0 (비활성) 소켓 busy polling 시간 (μs) 50~100μs 설정 시 지연 감소 (CPU 사용률 증가)
busy_read 0 (비활성) 소켓 읽기 busy polling 시간 (μs) busy_poll과 함께 설정

GRO (Generic Receive Offload)

GRO는 NAPI poll 내에서 수신된 여러 패킷을 하나의 대형 skb로 병합하여 상위 스택 호출 횟수를 줄입니다. LRO(Large Receive Offload)의 소프트웨어 대체로, 원본 헤더 정보를 보존하여 포워딩 환경에서도 안전합니다.

/* === GRO 수신 경로 ===
 *
 * NIC IRQ → napi_schedule()
 *  └→ NAPI poll()
 *      └→ napi_gro_receive(napi, skb)
 *          └→ dev_gro_receive()
 *              └→ inet_gro_receive()        (L3: IP)
 *                  └→ tcp4_gro_receive()    (L4: TCP)
 *                      ├→ 동일 flow 검색 (rxhash → gro_hash 버킷)
 *                      ├→ 병합 기준 검증:
 *                      │   - 동일 src/dst IP + port
 *                      │   - TCP seq 연속 (이전 끝 + 1)
 *                      │   - ACK만 설정 (SYN/FIN/RST → 거부)
 *                      │   - 윈도우 크기 동일
 *                      │   - TCP 타임스탬프 일관성
 *                      └→ 결과:
 *                          GRO_MERGED     : 기존 skb에 병합
 *                          GRO_HELD       : gro_list에 보관 (새 flow)
 *                          GRO_NORMAL     : 병합 불가 → 일반 경로
 */

/* NAPI poll 함수에서 GRO 사용 패턴 */
napi_gro_receive(napi, skb);   /* 일반적: 완전한 skb를 GRO 처리 */
napi_gro_frags(napi);          /* 페이지 기반 수신 시 (헤더/데이터 분리)
                                 * 고성능 NIC 드라이버에서 선호:
                                 *   napi->skb에 헤더(선형) + 페이로드(frag)
                                 *   → 메모리 복사 최소화 */

/* GRO 데이터 병합 방식 */
/* 1. frag 기반: skb_shinfo→frags[]에 페이지 추가 (MAX_SKB_FRAGS=17 제한)
 * 2. frag_list 기반: skb_shinfo→frag_list에 skb 체인 (제한 없음)
 *    → frag 공간 부족 시 자동 전환 */

/* GRO flush 조건:
 * 1. napi_complete_done() 호출 시 (budget 미만 처리)
 * 2. gro_hash 버킷에 MAX_GRO_SKBS(8)개 초과 시
 * 3. 비연속 패킷 수신 시 (seq 불연속, 다른 플래그)
 * 4. gro_flush_timeout 타이머 만료 시
 *    → sysctl net.core.gro_flush_timeout (기본 0 = 즉시 flush)
 *    → net.core.napi_defer_hard_irqs와 함께 사용하면 GRO 효율↑ */

/* GRO 성능 효과 예시 (1500 MTU, TCP):
 *   GRO OFF: 1M pps → 1M번 netif_receive_skb() 호출
 *   GRO ON:  1M pps → ~15K번 호출 (64KB super-packet 생성)
 *   → CPU 사용률 대폭 감소, 처리량 증가
 *
 * 포워딩 환경 (라우터, 브리지):
 *   GRO OFF: 43개 패킷 × routing/conntrack/NAT
 *   GRO ON:  1개 대형 skb × routing/conntrack/NAT → ~43배 효율 */

/* HW-GRO (커널 5.19+) — NIC가 GRO 수행하되 헤더 보존 */
/* # ethtool -K eth0 rx-gro-hw on
 * → LRO와 달리 원본 헤더 정보 유지 → 포워딩에도 안전
 * → NIC의 RSC(Receive Side Coalescing) 기능 활용 */

/* GRO 제어 및 확인 */
/* # ethtool -K eth0 gro on|off           # SW GRO 전환 */
/* # ethtool -K eth0 rx-gro-hw on|off     # HW GRO 전환 (5.19+) */
/* # ethtool -k eth0 | grep gro           # 상태 확인 */
/* # ethtool -S eth0 | grep gro           # GRO 통계 확인 */
GRO 상세 분석: 병합 기준, 프로토콜별 콜백 체인, flush 메커니즘, HW-GRO 등 심화 내용은 sk_buff — GSO/GRO 심화 섹션을 참고하세요.

NAPI 드라이버 구현 주의사항

NAPI 구현 시 흔한 실수:
  1. budget 미준수 — poll 함수가 budget 이상 처리하면 안 됨. 정확히 budget만큼 처리했으면 budget 반환, 적게 처리하면 실제 수를 반환
  2. napi_complete_done 누락 — work_done < budget일 때 반드시 호출해야 다음 IRQ에서 재스케줄 가능
  3. IRQ 재활성화 순서napi_complete_done() 이후에 HW 인터럽트를 재활성화해야 함. 순서가 반대면 race condition
  4. 멀티큐 미고려 — RSS/멀티큐 NIC에서는 큐마다 별도의 NAPI 인스턴스 필요. CPU affinity 설정 중요
  5. RX 링 고갈 — poll에서 버퍼 refill을 하지 않으면 RX 링이 비어서 패킷 드롭 발생

RSS, RPS, RFS — 멀티코어 네트워크 분산

기법계층설명설정
RSS Hardware NIC가 flow hash로 큐 분배 (H/W 인터럽트 분산) ethtool -L eth0 combined 8
RPS Software 커널에서 패킷을 CPU로 분배 (RSS 미지원 NIC용) echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
RFS Software 패킷을 해당 소켓을 처리하는 CPU로 전달 (캐시 친화) echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
XPS Software TX 큐를 CPU에 매핑 (TX 측 분산) echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
aRFS HW+SW NIC가 flow를 올바른 RX 큐로 직접 스티어링 NIC ntuple filter + RFS 조합

Toeplitz Hash — RSS 해시 알고리즘

RSS의 핵심은 NIC 하드웨어가 패킷 헤더로부터 해시 값을 계산하여 수신 큐를 결정하는 것이다. 대부분의 NIC는 Microsoft가 정의한 Toeplitz 해시를 사용한다. Toeplitz 해시는 XOR 기반의 비트 연산으로, 하드웨어 구현이 매우 단순하면서도 트래픽 분산 특성이 우수하다.

해시 입력 (Hash Input)

NIC는 패킷 유형에 따라 해시 입력 필드를 선택한다:

해시 타입입력 필드적용 대상
4-tuple src IP, dst IP, src port, dst port TCP, UDP, SCTP
2-tuple src IP, dst IP non-TCP/UDP IPv4, IPv6 (포트 없는 프로토콜)
확장 src IP, dst IP, SPI (Security Parameter Index) IPsec (ESP/AH)
참고: UDP의 경우, 단편화(fragmentation) 시 첫 번째 단편만 포트 정보를 포함하므로 후속 단편은 2-tuple로 해싱된다. 이로 인해 동일 플로우의 단편이 다른 큐로 분배될 수 있다. ethtool -N eth0 rx-flow-hash udp4 sd로 UDP를 2-tuple로 고정하면 이 문제를 완화할 수 있다.

Toeplitz 해시 알고리즘

Toeplitz 해시는 해시 키(Key)입력 데이터를 비트 단위로 XOR 누적하여 32비트 해시를 생성한다:

/*
 * Toeplitz Hash 의사코드
 *
 * input[]:  해시 입력 (예: src_ip + dst_ip + src_port + dst_port)
 *           IPv4 4-tuple = 12바이트 (96비트)
 * key[]:    해시 키 (40바이트 = 320비트, 네트워크 바이트 순서)
 * 결과:     32비트 해시 값
 */
uint32_t toeplitz_hash(uint8_t *input, int input_len, uint8_t *key)
{
    uint32_t result = 0;
    int i, j;

    for (i = 0; i < input_len; i++) {
        for (j = 0; j < 8; j++) {
            if (input[i] & (1 << (7 - j))) {
                /* key의 (i*8+j) 위치에서 시작하는 32비트를 XOR */
                result ^= get_unaligned_be32(key + i) << j
                        | (uint32_t)get_unaligned_be32(key + i + 4) >> (32 - j);
            }
        }
    }
    return result;
}
핵심 원리: 입력 데이터의 각 비트가 1이면, 해시 키의 해당 위치에서 시작하는 32비트 윈도우를 결과에 XOR한다. 입력 비트가 0이면 건너뛴다. 즉, 입력 비트가 키 윈도우를 선택(select)하는 구조로, 하드웨어에서 시프트 레지스터와 XOR 게이트만으로 구현 가능하다.

커널 내부의 소프트웨어 구현은 include/linux/netdevice.h의 인라인과 lib/toeplitz.c에 위치한다. RPS가 사용하는 소프트웨어 해시도 동일한 Toeplitz를 사용하며, net/core/flow_dissector.c__skb_get_hash()에서 호출된다:

/* include/linux/netdevice.h — 커널 소프트웨어 Toeplitz */
static inline __u32
__toeplitz_hash(const __u32 *key_cache, int nkeys,
                const __u32 *data, int ndata)
{
    __u32 hash = 0;
    int i;

    for (i = 0; i < ndata; i++)
        hash ^= toeplitz_byte(data[i], key_cache + i);
    return hash;
}

해시 키 (Hash Key)

Toeplitz 해시 키는 일반적으로 40바이트 (320비트)이다. NIC 드라이버가 초기화 시 기본 키를 설정하며, ethtool로 조회·변경할 수 있다:

# 현재 RSS 해시 키 조회
$ ethtool -x eth0
RX flow hash indirection table for eth0 with 4 RX ring(s):
    0:      0     1     2     3     0     1     2     3
    8:      0     1     2     3     0     1     2     3
  ...
RSS hash key:
6d:5a:56:da:25:5b:0e:c2:41:67:25:3d:43:a3:8f:b0:d0:ca:2b:cb:...

# 커스텀 해시 키 설정 (대칭 키 예시 — src/dst 교환 시 동일 해시)
$ ethtool -X eth0 hkey \
  6d:5a:56:da:25:5b:0e:c2:41:67:25:3d:43:a3:8f:b0:d0:ca:2b:cb:...

# 해시 함수 종류 확인 (toeplitz / xor / crc32)
$ ethtool -x eth0 | grep "RSS hash function"
RSS hash function:
    toeplitz: on
    xor: off
    crc32: off

# 해시 함수 변경 (NIC 지원 시)
$ ethtool -X eth0 hfunc toeplitz
대칭 해시 (Symmetric Hash): 기본 Toeplitz 키에서는 (A→B)(B→A) 트래픽이 다른 해시 값을 가져 서로 다른 큐로 분배될 수 있다. 연결 추적이나 양방향 플로우 모니터링이 필요한 경우, 대칭 키를 사용하면 src/dst를 교환해도 동일한 해시가 생성된다. 일부 NIC(Intel ixgbe 등)는 symmetric-xor 해시 함수를 별도로 지원한다.

해시 필드 설정 (ethtool -N)

프로토콜별로 해시에 사용할 필드를 세밀하게 제어할 수 있다:

# TCP4: 4-tuple 해시 (기본값)
$ ethtool -N eth0 rx-flow-hash tcp4 sdfn
# s=src IP, d=dst IP, f=src port, n=dst port

# UDP4: 2-tuple로 변경 (단편화 이슈 방지)
$ ethtool -N eth0 rx-flow-hash udp4 sd

# 현재 설정 확인
$ ethtool -n eth0 rx-flow-hash tcp4
TCP over IPV4 flows use these fields for computing Hash flow key:
IP SA
IP DA
L4 bytes 0 & 1 [TCP/UDP src port]
L4 bytes 2 & 3 [TCP/UDP dst port]

RETA — Redirection Table (인다이렉션 테이블)

Toeplitz 해시가 32비트 해시 값을 생성하면, NIC는 이 값의 하위 N비트를 인덱스로 사용하여 RETA(Redirection Table)를 참조한다. RETA의 각 엔트리는 실제 수신 큐 번호를 가리킨다.

수신 패킷 헤더 src/dst IP + port Toeplitz Hash + 40-byte Key 32-bit Hash 0x7A3B...F2 하위 N비트 추출 index = hash & (size-1) RETA (128 entries) [0]=Q0 [1]=Q1 [2]=Q2 [3]=Q3 [4]=Q0 [5]=Q1 [6]=Q2 [7]=Q3 ... ... ... ... [124]=Q0 [125]=Q1 [126]=Q2 [127]=Q3 RX Queue 0 RX Queue 1 RX Queue 2 → CPU 0 → CPU 1 → CPU 2

RETA 구조와 크기

NIC 계열RETA 크기인덱스 비트비고
Intel i350, 82576 128 엔트리 하위 7비트 GbE 서버용
Intel 82599 (ixgbe) 128 엔트리 하위 7비트 10GbE, SR-IOV 지원
Intel X710 (i40e) 512 엔트리 하위 9비트 더 세밀한 분배 가능
Intel E810 (ice) 2048 엔트리 하위 11비트 100GbE, ADQ 지원
Mellanox ConnectX-5/6 가변 (최대 4096) 가변 TIR (Transport Interface Receive) 기반
Broadcom BCM57xx 128 엔트리 하위 7비트 bnxt 드라이버

RETA의 각 엔트리는 0부터 시작하는 수신 큐 번호를 저장한다. 기본적으로 라운드 로빈(RETA[i] = i % num_queues)으로 초기화되며, 이렇게 하면 트래픽이 모든 큐에 균등하게 분배된다.

RETA 조회 및 설정

# RETA 인다이렉션 테이블 조회
$ ethtool -x eth0
RX flow hash indirection table for eth0 with 4 RX ring(s):
    0:      0     1     2     3     0     1     2     3
    8:      0     1     2     3     0     1     2     3
   16:      0     1     2     3     0     1     2     3
   24:      0     1     2     3     0     1     2     3
   ...

# 균등 분배 (기본값) — N개 큐에 라운드 로빈
$ ethtool -X eth0 equal 4
# RETA = [0,1,2,3,0,1,2,3,...] → 4개 큐 균등 분배

# 가중치 분배 — 큐별 비율 지정
$ ethtool -X eth0 weight 3 1 1 1
# Queue 0에 50%, Queue 1~3에 각 16.7%
# RETA = [0,0,0,1,0,0,0,2,...] 등으로 채워짐

# 특정 큐만 사용 (큐 0, 1만 활성)
$ ethtool -X eth0 weight 1 1 0 0
# Queue 2, 3은 RSS 트래픽 수신 안 함
NUMA 최적화: 큐를 특정 NUMA 노드의 CPU에만 매핑하면 캐시 효율이 향상된다. 예를 들어 8큐 NIC에서 NUMA 노드 0의 CPU 0~3만 사용하려면: ethtool -X eth0 weight 1 1 1 1 0 0 0 0으로 큐 0~3만 활성화하고, /proc/irq/<IRQ>/smp_affinity로 해당 큐의 IRQ를 같은 CPU에 고정한다.

커널 내부: RETA 설정 경로

RETA 설정은 ethtool_ops 콜백을 통해 드라이버로 전달된다:

/* include/linux/ethtool.h — 드라이버가 구현하는 콜백 */
struct ethtool_ops {
    /* RETA 인다이렉션 테이블 조회/설정 */
    int (*get_rxfh_indir_size)(struct net_device *);
    int (*get_rxfh)(struct net_device *,
                    struct ethtool_rxfh_param *);
    int (*set_rxfh)(struct net_device *,
                    struct ethtool_rxfh_param *,
                    struct netlink_ext_ack *);
    /* ... */
};

/* ethtool_rxfh_param — RETA + 해시 키 + 해시 함수를 한 번에 전달 */
struct ethtool_rxfh_param {
    u32 *indir;          /* RETA 테이블 (큐 번호 배열) */
    u8  *key;            /* Toeplitz 해시 키 */
    u8   hfunc;          /* 해시 함수 (ETH_RSS_HASH_*) */
    u32  indir_size;     /* RETA 엔트리 수 */
    u32  key_size;       /* 해시 키 바이트 수 */
};

예를 들어 Intel ixgbe 드라이버에서의 RETA 프로그래밍:

/* drivers/net/ethernet/intel/ixgbe/ixgbe_main.c
 * RETA를 하드웨어 레지스터에 기록 */
static void ixgbe_store_reta(struct ixgbe_adapter *adapter)
{
    u32 reta_entries = ixgbe_rss_indir_tbl_entries(adapter);  /* 128 */
    u32 i, reta = 0;

    for (i = 0; i < reta_entries; i++) {
        reta |= (u32)adapter->rss_indir_tbl[i] <<
                (i & 0x3) * 8;        /* 4개 엔트리를 32비트에 패킹 */
        if ((i & 3) == 3) {
            IXGBE_WRITE_REG(hw, IXGBE_RETA(i >> 2), reta);
            reta = 0;
        }
    }
}

RSS 전체 흐름 요약

단계위치동작
1. 패킷 수신 NIC H/W 패킷 헤더에서 해시 입력 필드 추출 (IP, port)
2. 해시 계산 NIC H/W Toeplitz(input, key) → 32비트 해시 값
3. RETA 참조 NIC H/W queue = RETA[hash & (reta_size - 1)]
4. DMA 전송 NIC → Memory 패킷을 해당 큐의 RX 링 버퍼에 DMA
5. 인터럽트 NIC → CPU 해당 큐에 바인딩된 CPU로 MSI-X 인터럽트 발생
6. NAPI poll 커널 해당 CPU에서 큐의 패킷 처리 (softirq)

RSS 디버깅 및 모니터링

# 큐별 패킷 수 확인 — 분배가 균등한지 검증
$ ethtool -S eth0 | grep rx_queue
     rx_queue_0_packets: 1523847
     rx_queue_1_packets: 1518293
     rx_queue_2_packets: 1521056
     rx_queue_3_packets: 1519834

# 큐별 IRQ 확인
$ grep eth0 /proc/interrupts
 128:   152384    0    0    0  IR-PCI-MSI eth0-TxRx-0
 129:        0  151829    0    0  IR-PCI-MSI eth0-TxRx-1
 130:        0    0  152105    0  IR-PCI-MSI eth0-TxRx-2
 131:        0    0    0  151983  IR-PCI-MSI eth0-TxRx-3

# IRQ affinity 설정 (큐 0 → CPU 0)
$ echo 1 > /proc/irq/128/smp_affinity

# 활성 큐 수 변경
$ ethtool -L eth0 combined 8     # combined RX+TX 8큐로
$ ethtool -l eth0                 # 현재 설정 확인
Channel parameters for eth0:
Pre-set maximums:
RX:     0
TX:     0
Other:  1
Combined:    63
Current hardware settings:
RX:     0
TX:     0
Other:  1
Combined:    8

# sk_buff의 해시 값 확인 (BPF로)
$ bpftrace -e 'kprobe:netif_receive_skb {
    printf("hash=0x%x queue=%d\n",
           ((struct sk_buff *)arg0)->hash,
           ((struct sk_buff *)arg0)->queue_mapping);
}'
큐 불균형 진단: ethtool -S에서 특정 큐에 트래픽이 집중되면: (1) 소수의 플로우가 대부분의 트래픽을 차지하는지 확인 (elephant flow), (2) 해시 키를 변경하여 분포 개선 시도, (3) Flow Director(ntuple filter)로 특정 플로우를 지정된 큐로 스티어링하는 것을 고려한다.

Flow Director — 정밀 플로우 스티어링

RSS의 해시 기반 분배로 충분하지 않을 때, Flow Director (Intel의 fdir / ntuple filter)로 특정 플로우를 원하는 큐에 직접 매핑할 수 있다. Flow Director 규칙은 RSS보다 높은 우선순위를 가진다:

# 특정 목적지 포트의 트래픽을 큐 3으로 스티어링
$ ethtool -N eth0 flow-type tcp4 dst-port 80 action 3

# 특정 5-tuple 매칭
$ ethtool -N eth0 flow-type tcp4 \
    src-ip 10.0.0.1 dst-ip 10.0.0.2 \
    src-port 12345 dst-port 443 action 2

# 현재 규칙 목록
$ ethtool -n eth0
4 RX rings available
Total 2 rules
Filter: 1023
    Rule Type: TCP over IPv4
    Src IP addr: 0.0.0.0 mask: 255.255.255.255
    Dest IP addr: 0.0.0.0 mask: 255.255.255.255
    TOS: 0x0 mask: 0xff
    Src port: 0 mask: 0xffff
    Dest port: 80 mask: 0x0
    Action: Direct to queue 3

# 규칙 삭제
$ ethtool -N eth0 delete 1023

RPS (Receive Packet Steering) — 소프트웨어 RSS

RPS는 커널 소프트웨어에서 수신 패킷을 여러 CPU로 분배하는 메커니즘이다. RSS를 지원하지 않는 NIC나, 큐 수가 CPU 수보다 적은 환경에서 유용하다. NIC의 하드웨어 큐에서 패킷을 받은 CPU가 해시를 계산하고, 그 결과에 따라 다른 CPU의 backlog 큐에 패킷을 넣어 처리를 분산시킨다.

RPS 아키텍처

NIC (single queue) CPU 0 (IRQ 처리) netif_receive_skb() → get_rps_cpu() Hash 계산 skb_get_hash() CPU 1 backlog process_backlog() CPU 2 backlog process_backlog() CPU 3 backlog process_backlog() IPI (Inter-Processor Interrupt) IP → TCP 프로토콜 처리 IP → TCP 프로토콜 처리 IP → TCP 프로토콜 처리

RPS 커널 구현

RPS의 핵심 로직은 net/core/dev.cget_rps_cpu() 함수에 있다. 이 함수는 패킷의 해시 값을 계산하고, rps_map을 참조하여 대상 CPU를 결정한다:

/* net/core/dev.c — RPS CPU 선택 핵심 로직 */
static int get_rps_cpu(struct net_device *dev,
                       struct sk_buff *skb,
                       struct rps_dev_flow **rflowp)
{
    struct netdev_rx_queue *rxqueue;
    struct rps_map *map;
    struct rps_sock_flow_table *sock_flow_table;
    int cpu = -1;
    u32 hash;

    rxqueue = dev->_rx + skb_get_rx_queue(skb);
    map = rcu_dereference(rxqueue->rps_map);
    if (!map)
        return -1;

    /* 패킷의 flow hash 계산 (Toeplitz 기반) */
    hash = skb_get_hash(skb);
    if (!hash)
        return -1;

    /* RFS (sock_flow_table)가 설정된 경우, 소켓을 처리하는 CPU 우선 */
    sock_flow_table = rcu_dereference(rps_sock_flow_table);
    if (sock_flow_table) {
        /* RFS 로직: 소켓의 마지막 처리 CPU를 참조 */
        /* ... (아래 RFS 섹션 참조) */
    }

    /* 해시 기반 CPU 선택: hash를 rps_map의 CPU 배열 인덱스로 변환 */
    cpu = map->cpus[reciprocal_scale(hash, map->len)];

    return cpu;
}

대상 CPU가 결정되면, enqueue_to_backlog()를 통해 해당 CPU의 per-CPU backlog 큐(softnet_data.input_pkt_queue)에 패킷을 삽입하고, IPI(Inter-Processor Interrupt)로 대상 CPU를 깨운다:

/* net/core/dev.c — 대상 CPU의 backlog에 패킷 삽입 */
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
                              unsigned int *qtail)
{
    struct softnet_data *sd = &per_cpu(softnet_data, cpu);

    rps_lock_irqsave(sd, &flags);
    if (skb_queue_len(&sd->input_pkt_queue) <= netdev_max_backlog) {
        __skb_queue_tail(&sd->input_pkt_queue, skb);
        rps_unlock_irq_restore(sd, &flags);

        /* 대상 CPU에 NET_RX_SOFTIRQ 스케줄링 (IPI 발생) */
        ____napi_schedule(sd, &sd->backlog);
        return NET_RX_SUCCESS;
    }

    /* backlog 큐 초과 → 패킷 드롭 */
    sd->dropped++;
    rps_unlock_irq_restore(sd, &flags);
    kfree_skb(skb);
    return NET_RX_DROP;
}

RPS 핵심 자료구조

/* include/linux/netdevice.h — rps_map: 큐별 대상 CPU 목록 */
struct rps_map {
    unsigned int   len;          /* 활성 CPU 수 */
    struct rcu_head rcu;
    u16            cpus[];      /* CPU 번호 배열 */
};

/* softnet_data: per-CPU 네트워크 처리 구조체 */
struct softnet_data {
    struct list_head     poll_list;       /* NAPI poll 리스트 */
    struct sk_buff_head  input_pkt_queue; /* RPS backlog 큐 */
    struct sk_buff_head  process_queue;   /* 처리 중인 큐 */
    struct napi_struct   backlog;         /* backlog NAPI */
    unsigned int         dropped;         /* 드롭 카운터 */
    /* ... */
};

RPS 설정 방법

# RPS 설정: 특정 RX 큐에서 어떤 CPU로 패킷을 분산할지 결정
# rps_cpus: CPU 비트맵 (16진수)

# 8-core 시스템에서 모든 CPU 활성화
$ echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
# ff = 11111111(2) → CPU 0~7 모두 사용

# NUMA 노드 0의 CPU(0~3)만 사용
$ echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus
# f = 00001111(2) → CPU 0~3만

# 32-core 시스템: 모든 CPU
$ echo ffffffff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# backlog 큐 크기 조절 (기본 1000)
$ echo 5000 > /proc/sys/net/core/netdev_budget
$ echo 10000 > /proc/sys/net/core/netdev_max_backlog

# RPS flow hash 엔트리 수 (전역, RFS와 함께 사용)
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 큐별 flow 엔트리 수
$ echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

# 현재 설정 확인
$ cat /sys/class/net/eth0/queues/rx-0/rps_cpus
000000ff
$ cat /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
0
RPS 사용 시 주의사항:
  1. RSS가 가능하면 RSS를 먼저 사용 — RPS는 소프트웨어 처리이므로 IRQ 처리 CPU에 추가 부하가 발생한다. NIC가 RSS를 지원하면 하드웨어 분산이 더 효율적이다.
  2. IPI 오버헤드 — 패킷마다 IPI를 발생시키므로 cache line bouncing이 생길 수 있다. 대량 트래픽에서는 RSS 대비 성능이 낮다.
  3. IRQ CPU 제외 — IRQ를 처리하는 CPU를 rps_cpus 비트맵에서 제외하면, 해당 CPU의 부하를 줄이고 다른 CPU로만 분산시킬 수 있다.
  4. NUMA 경계 고려 — 원격 NUMA 노드의 CPU로 패킷을 보내면 메모리 접근 지연이 증가한다. NIC가 연결된 NUMA 노드의 CPU로 제한하는 것이 좋다.

RSS vs RPS 비교

특성RSS (Hardware)RPS (Software)
분산 시점 NIC 하드웨어에서 DMA 전 드라이버의 NAPI poll 후, 프로토콜 스택 진입 전
해시 계산 NIC 하드웨어 (Toeplitz) 커널 소프트웨어 (skb_get_hash)
CPU 오버헤드 없음 (H/W) 해시 계산 + IPI + backlog 큐잉
NIC 요구사항 멀티큐 + RSS 지원 필수 싱글큐 NIC도 가능
동적 재설정 ethtool (드라이버 리셋 가능) sysfs 즉시 반영 (무중단)
캐시 효율 높음 (DMA부터 같은 CPU) 보통 (IRQ CPU → 대상 CPU 이동)
주요 사용 사례 고성능 서버, 10G+ NIC 가상머신 (virtio), 싱글큐 NIC, 큐 < CPU 수

RFS (Receive Flow Steering) — 캐시 친화적 분배

RFS는 RPS를 확장하여 패킷을 해당 소켓을 마지막으로 처리한 CPU로 전달한다. RPS가 해시 기반으로 아무 CPU나 선택하는 것과 달리, RFS는 애플리케이션의 CPU 위치를 추적하여 L1/L2 캐시 히트율을 극대화한다.

RFS 동작 원리

RFS는 두 개의 해시 테이블을 사용한다:

테이블위치내용갱신 시점
rps_sock_flow_table 전역 (per-net) flow hash → 소켓을 마지막으로 처리한 CPU (desired CPU) recvmsg(), sendmsg() 등 소켓 시스템 콜 시
rps_dev_flow_table per-queue flow hash → 패킷이 마지막으로 전달된 CPU (current CPU) get_rps_cpu()에서 패킷 처리 시
/* include/linux/netdevice.h — RFS 자료구조 */
struct rps_sock_flow_table {
    u32   mask;             /* 엔트리 수 - 1 (power of 2) */
    u32   ents[];           /* flow hash → desired CPU */
};

struct rps_dev_flow {
    u16   cpu;              /* 패킷이 마지막으로 전달된 CPU */
    u16   filter;           /* aRFS에서 사용하는 필터 ID */
    unsigned int last_qtail; /* 마지막 삽입 시 큐 tail 위치 */
};

struct rps_dev_flow_table {
    unsigned int          mask;  /* 엔트리 수 - 1 */
    struct rcu_head       rcu;
    struct rps_dev_flow   flows[];
};

RFS CPU 선택 로직

RFS의 CPU 선택은 get_rps_cpu() 내부에서 다음 우선순위로 진행된다:

/* get_rps_cpu() 내부 RFS 로직 (단순화) */

/* 1. 전역 sock_flow_table에서 desired CPU 조회 */
desired_cpu = sock_flow_table->ents[hash & sock_flow_table->mask];

/* 2. per-queue dev_flow_table에서 current CPU 조회 */
rflow = &flow_table->flows[hash & flow_table->mask];
current_cpu = rflow->cpu;

/* 3. CPU 선택 결정 */
if (desired_cpu == current_cpu) {
    /* 동일 CPU → 그대로 사용 (최적) */
    cpu = desired_cpu;
} else if (current_cpu_unset || current_cpu_offline ||
         unlikely(qtail - rflow->last_qtail >= backlog_len)) {
    /* current CPU가 미설정/오프라인/backlog 소진됨
     * → desired CPU로 전환 (out-of-order 방지 후) */
    cpu = desired_cpu;
    rflow->cpu = cpu;
} else {
    /* current CPU의 backlog에 아직 이전 패킷이 있음
     * → 순서 보장을 위해 current CPU 유지 */
    cpu = current_cpu;
}
순서 보장 (Out-of-Order 방지): RFS가 CPU를 변경할 때, 이전 CPU의 backlog에 아직 처리되지 않은 패킷이 있으면 동일 플로우의 패킷 순서가 뒤바뀔 수 있다. 이를 방지하기 위해 last_qtail을 추적하여, 이전 CPU의 backlog가 해당 지점을 넘어서 처리될 때까지 CPU 전환을 지연시킨다.

RFS 설정 방법

# 1. 전역 sock_flow_table 크기 설정 (power of 2 권장)
# 활성 연결 수의 2배 이상으로 설정
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries

# 2. 큐별 dev_flow_table 크기 설정
# rps_sock_flow_entries / N (N = RX 큐 수)
$ echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
$ echo 2048 > /sys/class/net/eth0/queues/rx-1/rps_flow_cnt
# ... 모든 RX 큐에 대해 반복

# 3. RPS도 함께 활성화해야 동작함 (RFS는 RPS 위에서 동작)
$ echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# 한 번에 모든 큐 설정 (스크립트)
for rxq in /sys/class/net/eth0/queues/rx-*; do
    echo ff > $rxq/rps_cpus
    echo 2048 > $rxq/rps_flow_cnt
done

소켓 측 CPU 갱신

소켓의 desired CPU는 sock_rps_record_flow()를 통해 갱신된다. 이 함수는 recvmsg(), sendmsg(), tcp_v4_rcv() 등 소켓 처리 경로에서 호출된다:

/* include/net/sock.h — 소켓 처리 시 CPU 기록 */
static inline void sock_rps_record_flow(const struct sock *sk)
{
    struct rps_sock_flow_table *table;

    table = rcu_dereference(rps_sock_flow_table);
    if (table) {
        u32 hash = sk->sk_rxhash;
        if (hash) {
            u32 index = hash & table->mask;
            /* 현재 CPU를 desired CPU로 기록 */
            if (table->ents[index] != raw_smp_processor_id())
                table->ents[index] = raw_smp_processor_id();
        }
    }
}

XPS (Transmit Packet Steering) — 송신 측 CPU-큐 매핑

XPS는 송신(TX) 패킷을 보내는 CPU에 최적화된 TX 큐를 선택하는 메커니즘이다. 멀티큐 NIC에서 TX 큐를 CPU에 적절히 매핑하면 lock contention 감소캐시 효율 향상을 얻을 수 있다.

XPS가 해결하는 문제

XPS 없이 멀티큐 NIC에서 패킷을 전송하면, 커널은 skb_tx_hash()를 사용해 해시 기반으로 TX 큐를 선택한다. 이 경우 여러 CPU가 같은 TX 큐를 사용하여 TX 큐 락 경합이 발생할 수 있다:

/* XPS 미설정 시: 해시 기반 TX 큐 선택 */
static u16 skb_tx_hash(const struct net_device *dev,
                       const struct sk_buff *skb)
{
    /* 여러 CPU가 같은 큐를 선택할 수 있음 → lock contention */
    return reciprocal_scale(skb_get_hash(skb),
                           dev->real_num_tx_queues);
}

XPS 동작 원리

XPS를 설정하면, 각 TX 큐에 대해 어떤 CPU가 사용할 수 있는지를 매핑한다. 패킷 전송 시 현재 CPU에 매핑된 TX 큐 중 하나를 선택하여 lock contention을 최소화한다:

/* include/linux/netdevice.h — XPS 매핑 구조체 */
struct xps_map {
    unsigned int   len;          /* 큐 수 */
    unsigned int   alloc_len;
    struct rcu_head rcu;
    u16            queues[];    /* 이 CPU가 사용할 TX 큐 번호 배열 */
};

struct xps_dev_maps {
    struct rcu_head rcu;
    unsigned int   nr_ids;      /* CPU 수 또는 RX 큐 수 */
    s16            num_tc;      /* Traffic Class 수 */
    struct xps_map __rcu *attr_map[];  /* per-CPU 또는 per-RX-queue 매핑 */
};
/* net/core/dev.c — XPS가 활성화된 경우의 TX 큐 선택 */
static int __netdev_pick_tx(struct net_device *dev,
                           struct sk_buff *skb,
                           struct net_device *sb_dev)
{
    struct xps_dev_maps *dev_maps;
    struct xps_map *map;
    int queue_index = -1;

    /* 1. XPS RX-queue 매핑 시도 (수신 큐 → 송신 큐) */
    dev_maps = rcu_dereference(dev->xps_maps[XPS_RXQS]);
    if (dev_maps) {
        map = rcu_dereference(dev_maps->attr_map[skb_get_rx_queue(skb)]);
        if (map)
            queue_index = map->queues[reciprocal_scale(
                skb_get_hash(skb), map->len)];
    }

    /* 2. XPS CPU 매핑 시도 (현재 CPU → 송신 큐) */
    if (queue_index < 0) {
        dev_maps = rcu_dereference(dev->xps_maps[XPS_CPUS]);
        if (dev_maps) {
            map = rcu_dereference(
                dev_maps->attr_map[raw_smp_processor_id()]);
            if (map)
                queue_index = map->queues[
                    reciprocal_scale(skb_get_hash(skb), map->len)];
        }
    }

    /* 3. XPS 미설정 시 fallback: skb_tx_hash() */
    if (queue_index < 0)
        queue_index = skb_tx_hash(dev, skb);

    return queue_index;
}

XPS 두 가지 모드

모드매핑 기준설정 파일사용 사례
XPS (CPU) CPU → TX 큐 /sys/class/net/<dev>/queues/tx-<N>/xps_cpus CPU별 전용 TX 큐 할당으로 lock contention 제거
XPS (RXQ) RX 큐 → TX 큐 /sys/class/net/<dev>/queues/tx-<N>/xps_rxqs 수신-송신 큐 페어링, 같은 CPU에서 처리하여 캐시 효율 극대화

XPS 설정 방법

# === XPS CPU 모드: CPU → TX 큐 1:1 매핑 ===

# TX Queue 0 → CPU 0 전용
$ echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
# 1 = 00000001(2) → CPU 0만

# TX Queue 1 → CPU 1 전용
$ echo 2 > /sys/class/net/eth0/queues/tx-1/xps_cpus
# 2 = 00000010(2) → CPU 1만

# TX Queue 2 → CPU 2 전용
$ echo 4 > /sys/class/net/eth0/queues/tx-2/xps_cpus

# TX Queue 3 → CPU 3 전용
$ echo 8 > /sys/class/net/eth0/queues/tx-3/xps_cpus

# 8-queue NIC에서 CPU 1:1 매핑 스크립트
for i in $(seq 0 7); do
    printf '%x' $((1 << i)) > /sys/class/net/eth0/queues/tx-$i/xps_cpus
done

# NUMA 인식 매핑: NUMA 0 CPU(0~3) → TX Queue 0~3
#                 NUMA 1 CPU(4~7) → TX Queue 4~7
$ echo 0f > /sys/class/net/eth0/queues/tx-0/xps_cpus  # CPU 0~3
$ echo 0f > /sys/class/net/eth0/queues/tx-1/xps_cpus
$ echo 0f > /sys/class/net/eth0/queues/tx-2/xps_cpus
$ echo 0f > /sys/class/net/eth0/queues/tx-3/xps_cpus
$ echo f0 > /sys/class/net/eth0/queues/tx-4/xps_cpus  # CPU 4~7
$ echo f0 > /sys/class/net/eth0/queues/tx-5/xps_cpus
$ echo f0 > /sys/class/net/eth0/queues/tx-6/xps_cpus
$ echo f0 > /sys/class/net/eth0/queues/tx-7/xps_cpus

# === XPS RXQ 모드: RX 큐 → TX 큐 매핑 ===
# 커널 4.18+ 필요, RSS/RPS로 수신한 큐와 동일 TX 큐 사용

# TX Queue 0 → RX Queue 0에서 수신한 패킷의 응답 전송
$ echo 1 > /sys/class/net/eth0/queues/tx-0/xps_rxqs
# TX Queue 1 → RX Queue 1
$ echo 2 > /sys/class/net/eth0/queues/tx-1/xps_rxqs

# 현재 설정 확인
$ cat /sys/class/net/eth0/queues/tx-0/xps_cpus
00000001
$ cat /sys/class/net/eth0/queues/tx-0/xps_rxqs
0
XPS 최적 구성: CPU 1:1 매핑이 가장 효과적이다. 각 CPU가 전용 TX 큐를 가지면 qdisc 락 경합이 완전히 제거된다. CPU 수 > TX 큐 수인 경우, 같은 NUMA 노드의 CPU 그룹을 하나의 TX 큐에 매핑한다. xps_rxqs 모드는 TCP처럼 요청-응답 패턴에서 수신과 송신이 같은 CPU에서 처리되도록 하여 캐시 효율을 극대화한다.

XPS 모니터링

# TX 큐별 전송 통계
$ ethtool -S eth0 | grep tx_queue
     tx_queue_0_packets: 982341
     tx_queue_0_bytes: 587204160
     tx_queue_1_packets: 978892
     tx_queue_1_bytes: 585023408
     tx_queue_2_packets: 981204
     tx_queue_2_bytes: 586921600
     tx_queue_3_packets: 979563
     tx_queue_3_bytes: 585425376

# TX 큐 락 경합 확인 (perf로)
$ perf stat -e 'lock:contention_begin' -a -- sleep 5

# BPF로 TX 큐 선택 과정 추적
$ bpftrace -e 'kretprobe:__netdev_pick_tx {
    printf("cpu=%d txq=%d\n", cpu, retval);
}'

aRFS (Accelerated RFS) — 하드웨어 가속 RFS

aRFS는 RFS의 결정을 NIC 하드웨어에 반영하여, 패킷이 DMA 단계에서부터 올바른 CPU의 RX 큐로 전달되도록 한다. RFS가 소프트웨어로 패킷을 재분배하는 것과 달리, aRFS는 NIC의 ntuple filter (Flow Director)를 동적으로 프로그래밍하여 하드웨어 수준에서 스티어링한다.

aRFS 동작 흐름

Application recvmsg() on CPU 2 RFS 테이블 갱신 desired_cpu = 2 get_rps_cpu() desired(CPU2) != current(CPU0) → ndo_rx_flow_steer() 호출 NIC 드라이버 ndo_rx_flow_steer() ntuple filter 추가/갱신 NIC H/W Flow Director 규칙: flow X → RX Queue 2 (CPU 2에 바인딩된 큐) 이후 패킷 H/W가 직접 CPU 2의 큐로 DMA

aRFS 커널 API

aRFS를 지원하려면 NIC 드라이버가 ndo_rx_flow_steer 콜백을 구현해야 한다:

/* include/linux/netdevice.h — aRFS 드라이버 콜백 */
struct net_device_ops {
    /* ... */
    int (*ndo_rx_flow_steer)(struct net_device *dev,
                             const struct sk_buff *skb,
                             u16 rxq_index,
                             u32 flow_id);
    /* rxq_index: 대상 RX 큐 번호 */
    /* flow_id:   고유 플로우 식별자 */
    /* 반환값:     NIC에 설정된 필터 ID */
};

/* 예: Intel ixgbe 드라이버의 aRFS 구현 */
static int ixgbe_rx_flow_steer(struct net_device *dev,
                               const struct sk_buff *skb,
                               u16 rxq_index, u32 flow_id)
{
    struct ixgbe_adapter *adapter = netdev_priv(dev);
    struct ixgbe_fdir_filter *input;

    /* 패킷의 5-tuple로 Flow Director 필터 생성 */
    input = kzalloc(sizeof(*input), GFP_ATOMIC);
    /* skb에서 src/dst IP, port 추출 → ATR 필터 설정 */
    ixgbe_fdir_write_perfect_filter(adapter, input, rxq_index);

    return filter_id;
}

aRFS 설정

# aRFS 요구사항:
# 1. NIC가 ntuple filter (Flow Director) 지원
# 2. NIC 드라이버가 ndo_rx_flow_steer 구현
# 3. RFS가 활성화되어 있어야 함

# ntuple filter 활성화
$ ethtool -K eth0 ntuple on

# RFS 설정 (aRFS의 전제 조건)
$ echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
for rxq in /sys/class/net/eth0/queues/rx-*; do
    echo 2048 > $rxq/rps_flow_cnt
done

# aRFS 지원 여부 확인
$ ethtool -k eth0 | grep ntuple
ntuple-filters: on

# 현재 aRFS/Flow Director 규칙 수 확인
$ ethtool -S eth0 | grep fdir
     fdir_match: 28745
     fdir_miss: 312
     fdir_overflow: 0
aRFS 지원 NIC: Intel ixgbe (82599, X540), i40e (X710, XL710), ice (E810), Mellanox mlx4/mlx5 (ConnectX-3/4/5/6), Broadcom bnxt (BCM57xxx), Chelsio cxgb4 (T5/T6) 등이 aRFS를 지원한다. 확인 방법: grep ndo_rx_flow_steer drivers/net/ethernet/로 드라이버 소스에서 구현 여부를 검색한다.

멀티코어 네트워크 분산 전체 비교

기법계층방향분배 기준장점단점
RSS H/W RX 해시 → RETA → 큐 CPU 부하 없음, DMA부터 분산 NIC 지원 필요, 정적 매핑
RPS S/W RX 해시 → CPU backlog 어떤 NIC든 사용 가능 IPI 오버헤드, 캐시 비효율
RFS S/W RX 소켓의 CPU 추적 캐시 친화적, 앱-패킷 같은 CPU RPS 위에서만 동작, 테이블 메모리
aRFS H/W+S/W RX RFS 결정을 H/W 필터에 반영 RFS + H/W 가속, 최적 성능 ntuple 지원 NIC 필요, 필터 수 제한
XPS S/W TX CPU → TX 큐 매핑 TX 락 경합 제거, 설정 간단 송신 전용, 멀티큐 NIC 필요
Flow Dir. H/W RX 관리자가 수동 규칙 설정 정밀 제어, RSS보다 높은 우선순위 수동 관리, 규칙 수 제한
권장 구성 가이드:
  • 고성능 서버 (RSS NIC): RSS + IRQ affinity + XPS (CPU 1:1) + aRFS
  • 가상머신 (virtio): RPS + RFS (virtio는 RSS 미지원이 많으므로)
  • 큐 수 < CPU 수: RSS + RPS (S/W로 추가 분산) + RFS
  • 단순 구성: RSS + XPS만으로도 대부분의 경우 충분

네트워크 패킷 흐름 (Packet Flow) 심화

Netfilter 체인과 패킷 경로

NIC (RX) PREROUTING 라우팅 INPUT Local Process FORWARD POSTROUTING OUTPUT NIC (TX) local forward output routing DROP ✕ DROP ✕ DROP ✕ DROP ✕ DROP ✕

패킷 경로별 상세 흐름

경로Netfilter 훅 순서설명주요 처리
LOCAL_IN PREROUTING → INPUT 외부 → 로컬 프로세스 DNAT(PREROUTING), conntrack, 방화벽(INPUT), 소켓 전달
FORWARD PREROUTING → FORWARD → POSTROUTING 외부 → 라우팅 → 외부 DNAT, 포워딩 정책, SNAT(POSTROUTING), TTL 감소
LOCAL_OUT OUTPUT → POSTROUTING 로컬 프로세스 → 외부 출력 필터링(OUTPUT), SNAT/MASQUERADE(POSTROUTING)
DROP 어느 훅에서든 패킷 폐기 NF_DROP 반환, kfree_skb(), 드롭 카운터 증가

Connection Tracking (conntrack) 심화

/* conntrack은 PREROUTING/OUTPUT 훅에서 패킷의 연결 상태를 추적 */
/* 모든 netfilter 기반 NAT, stateful 방화벽의 기초 */

/* conntrack 엔트리 상태 */
IP_CT_NEW           /* 첫 번째 패킷 (SYN) */
IP_CT_ESTABLISHED   /* 양방향 트래픽 확인됨 */
IP_CT_RELATED       /* 기존 연결과 관련 (FTP data, ICMP error) */
IP_CT_INVALID       /* 상태 추적 실패 */

/* conntrack 해시 테이블 크기 — 성능에 직접 영향 */
/* /proc/sys/net/netfilter/nf_conntrack_max = 262144 */
/* /proc/sys/net/netfilter/nf_conntrack_buckets (readonly) */
/* 최적: max = buckets × 4 (체인 길이 ~4 유지) */
conntrack 테이블 포화 문제: 고트래픽 환경에서 conntrack 테이블이 가득 차면 nf_conntrack: table full, dropping packet 에러와 함께 패킷이 무작위로 드롭됩니다.
  • nf_conntrack_max 증가 (메모리 비용: 엔트리당 ~300바이트)
  • 타임아웃 조정: nf_conntrack_tcp_timeout_established (기본 432000초 = 5일)
  • conntrack 불필요한 트래픽은 NOTRACK (raw 테이블)으로 바이패스
  • 초고성능 라우터에서는 conntrack 자체를 비활성화 고려

패킷 드롭 디버깅

# 드롭 모니터 — 패킷이 어디서 드롭되는지 추적
perf record -e skb:kfree_skb -a sleep 10
perf script

# dropwatch 도구 활용
dropwatch -l kas
> start
# 출력: drop at: tcp_v4_rcv+0x1a (sobjects hit: 15)

# nftables/iptables 카운터로 규칙별 드롭 확인
iptables -L -v -n | grep DROP
nft list ruleset | grep drop

# 인터페이스 통계로 드롭 위치 파악
ethtool -S eth0 | grep -i drop
cat /proc/net/softnet_stat  # 컬럼: processed, dropped, time_squeeze

# netstat 프로토콜별 에러 통계
netstat -s | grep -i -E "drop|error|overflow|pruned"

# BPF 기반 패킷 추적 (bcc/bpftrace)
bpftrace -e 'tracepoint:skb:kfree_skb { @[kstack] = count(); }'

패킷 흐름 설계 시 고려사항

고성능 패킷 처리 설계 포인트:
  • PREROUTING에서 조기 드롭 — 불필요한 패킷은 가능한 일찍 드롭하여 후속 처리 비용 절감
  • conntrack 바이패스 — 상태 추적 불필요한 트래픽(DNS 캐시, CDN)은 raw 테이블에서 NOTRACK
  • FORWARD 최적화 — IP forwarding 시 bridge vs routing 성능 차이 고려. nf_conntrack 비활성화 검토
  • LOCAL_OUT 경로 — 로컬 소켓의 출력 경로도 netfilter를 거침. 컨테이너 환경에서 iptables 규칙 수 관리 중요
  • XDP 조기 처리 — netfilter보다 앞단(드라이버 레벨)에서 XDP로 패킷 필터링/리다이렉트 가능
  • nftables 선호 — iptables 대비 nftables는 단일 패스 처리로 체인이 많을 때 성능 우위

SmartNIC / DPU 기반 네트워크 가속

DPU(Data Processing Unit)는 단순 NIC를 넘어 자체 프로세서(ARM SoC), 메모리, 스토리지 인터페이스, 하드웨어 가속기를 탑재한 독립적인 컴퓨팅 플랫폼입니다. 데이터센터의 네트워킹, 보안, 스토리지 처리를 호스트 CPU에서 DPU로 오프로드하여 인프라 오버헤드를 제거하고, 호스트 CPU를 애플리케이션 워크로드에 전용할 수 있습니다.

NIC vs SmartNIC vs DPU 비교

구분전통 NICSmartNICDPU
프로세서 없음 (고정 ASIC) FPGA 또는 NP ARM SoC + HW 가속
메모리 소량 SRAM (버퍼용) 수백 MB DRAM 수 GB~16GB+ DRAM
OS 실행 불가 제한적 (마이크로컨트롤러) 완전한 Linux 커널 구동
오프로드 범위 체크섬, TSO/GRO 패킷 분류, 필터링 네트워킹 + 스토리지 + 보안 전체
프로그래밍 펌웨어 업데이트만 P4, BPF offload 풀 SDK (DOCA, DPDK, BPF 등)
사용 사례 일반 서버 패킷 처리 가속 클라우드 인프라, bare-metal-as-a-service
대표 제품 Intel X710, Mellanox CX-5 Netronome NFP, Xilinx SN1000 BlueField-3, Intel IPU, AMD Pensando

DPU 하드웨어 아키텍처

DPU (Data Processing Unit) ARM SoC ARM Cortex-A78 Linux Kernel (독립 OS) DRAM 8~16GB eMMC / NVMe OOB Management (BMC/IPMI/NC-SI) Hardware Accelerators eSwitch (TC flower HW) Crypto Engine (IPsec/TLS) RegEx Engine (DPI/IDS) Compress/Decomp (zlib/LZ4) Programmable Pipeline (P4/RTC) Network Ports Port 0 100/200/400GbE Port 1 100/200/400GbE RoCE/RDMA Engine (GPUDirect, NVMe-oF) SR-IOV VF / VDPA / Virtio PCIe Gen4/5 x16 Interface (호스트 연결) PF0 (네트워크) | PF1 (RDMA) | VF0~VFn (SR-IOV) | SF0~SFn (Scalable Functions) Host CPU (x86/ARM) mlx5_core / ice 드라이버 VF Representor Ports OVS / TC flower VM / Container

DPU의 핵심 특징은 완전한 Linux 커널을 자체 ARM SoC에서 구동한다는 점입니다. 호스트와 독립적인 OS를 실행하면서 네트워크, 스토리지, 보안 서비스를 제공하고, 호스트에는 SR-IOV VF나 Virtio 디바이스만 노출합니다. 이로써 호스트 OS가 침해되더라도 인프라 서비스(방화벽, 암호화, 스토리지 가상화)의 무결성이 유지됩니다.

주요 DPU/IPU 제품군

제조사제품SoC네트워크커널 드라이버핵심 특징
NVIDIA BlueField-3 16x ARM A78 + HW 가속 2x400GbE mlx5_core DOCA SDK, OVS-DOCA offload, GPUDirect, Crypto, RegEx
Intel IPU E2000 (Mount Evans) 16x ARM Neoverse N1 + FPGA 2x200GbE idpf IPDK (Infrastructure PDK), P4 프로그래밍, QAT 연동
AMD Pensando DSC-200 ARM A72 + P4 파이프라인 2x200GbE ionic P4 기반 프로그래밍, flow-aware 가속, 하드웨어 방화벽
Marvell OCTEON 10 DPU 36x ARM N2 + 가속기 2x400GbE octeontx2 인라인 IPsec, MACsec, OVS offload, 저전력
Broadcom Stingray PS1100R 8x ARM A72 2x100GbE bnxt_en TruFlow, 하드웨어 OVS, 인라인 crypto

DPU 오프로드 기능 상세

오프로드 기능설명성능 효과커널 인터페이스
OVS TC flower offload 가상 스위치 플로우 규칙을 NIC eSwitch로 이동 호스트 CPU ~90% 감소, 지연 ~5x 개선 tc flower + switchdev
IPsec inline crypto ESP 암호화/복호화를 NIC에서 수행 라인 레이트 IPsec (100Gbps+) xfrm_dev_offload
TLS/kTLS offload TLS 레코드 암호화를 NIC으로 오프로드 웹서버 TLS CPU 오버헤드 제거 TLS_TX_ZEROCOPY_RO
VXLAN/Geneve encap 터널 캡슐화/해제를 HW에서 수행 오버레이 네트워크 성능 향상 tc tunnel_key
Connection tracking conntrack을 NIC에서 처리 stateful 방화벽 CPU 오프로드 tc ct action
NVMe-oF / virtio-blk 원격 스토리지 접근을 DPU에서 처리 스토리지 가상화 오버헤드 제거 nvme-rdma / vhost
RegEx / DPI 정규식 매칭으로 딥 패킷 인스펙션 IDS/IPS 처리 가속 (400Gbps+) DOCA RegEx API
Compression 데이터 압축/해제 하드웨어 가속 스토리지/네트워크 데이터 압축 오프로드 DOCA Compress API

커널의 DPU 지원 서브시스템

리눅스 커널은 DPU를 지원하기 위해 여러 서브시스템을 활용합니다. 핵심은 devlink(디바이스 구성), switchdev(eSwitch 제어), representor port(VF/SF를 호스트에서 관리), auxiliary bus(다기능 디바이스 분할)입니다.

User Space devlink (iproute2) tc / OVS ip xfrm ethtool DOCA SDK Kernel Subsystems devlink switchdev (eSwitch 모드) TC subsystem (flower + ct) auxiliary_bus (SF, RDMA, vDPA) xfrm offload (IPsec HW) Representor Ports (PF rep, VF rep, SF rep) vDPA / virtio / vhost 서브시스템 DPU 드라이버 mlx5_core (NVIDIA) idpf (Intel IPU) ionic (AMD Pensando) octeontx2 (Marvell)

devlink은 DPU/SmartNIC를 관리하는 핵심 커널 인터페이스입니다. eSwitch 모드 전환, 포트 기능 설정, 리소스 할당, 펌웨어 관리, 헬스 리포터 등을 통합적으로 제어합니다.

# ━━━ devlink 기본 관리 ━━━

# DPU 디바이스 목록 조회
devlink dev show
# pci/0000:03:00.0: fw.mgmt 24.39.1002 fw.app 24.39.1002

# 펌웨어 버전 상세 조회
devlink dev info pci/0000:03:00.0
# driver: mlx5_core
# fw.mgmt: 24.39.1002
# fw.undi: 14.32.17
# fw.psid: MT_0000000835

# ━━━ eSwitch 모드 전환 ━━━

# legacy 모드 → switchdev 모드 (eSwitch 활성화)
# ⚠️ 모드 전환 시 네트워크 순간 단절 발생
devlink dev eswitch set pci/0000:03:00.0 mode switchdev

# eSwitch 인라인 모드 설정 (매칭 깊이)
devlink dev eswitch set pci/0000:03:00.0 inline-mode transport

# 현재 eSwitch 모드 확인
devlink dev eswitch show pci/0000:03:00.0
# mode switchdev inline-mode transport encap-mode basic

# ━━━ SR-IOV VF 관리 ━━━

# VF 생성 (PCIe SR-IOV)
echo 8 > /sys/class/net/enp3s0f0np0/device/sriov_numvfs

# VF의 MAC, VLAN, 대역폭 설정
ip link set enp3s0f0np0 vf 0 mac 00:11:22:33:44:55
ip link set enp3s0f0np0 vf 0 vlan 100
ip link set enp3s0f0np0 vf 0 max_tx_rate 10000  # Mbps

# VF representor 포트 확인 (switchdev 모드에서 자동 생성)
ip link show | grep "enp3s0f0np0_"
# enp3s0f0np0_0  ← VF0 representor
# enp3s0f0np0_1  ← VF1 representor

# ━━━ Scalable Functions (SF) ━━━

# SF 생성 (SR-IOV VF의 경량 대안, BlueField/ConnectX-7+)
devlink port add pci/0000:03:00.0 flavour pcisf pfnum 0 sfnum 88
devlink port function set pci/0000:03:00.0/32768 hw_addr 00:00:00:00:88:88 state active

# SF 포트 기능 설정
devlink port function set pci/0000:03:00.0/32768 \
    roce true \
    migratable true \
    ipsec_crypto true \
    ipsec_packet true

# SF의 auxiliary 디바이스 활성화
devlink port function set pci/0000:03:00.0/32768 state active

# 생성된 SF 확인
devlink port show pci/0000:03:00.0/32768
# pci/0000:03:00.0/32768: type eth netdev en3f0pf0sf88 flavour pcisf
#   controller 0 pfnum 0 sfnum 88 splittable false
#   function: hw_addr 00:00:00:00:88:88 state active opstate attached
SF vs VF:
  • VF(Virtual Function)는 PCIe SR-IOV 하드웨어 기반으로 생성 수가 제한적(보통 128~256개)이며, PCI 구성 공간을 소비합니다
  • SF(Scalable Function)는 소프트웨어 정의 방식으로 수천 개 생성 가능하며, 각 SF가 독립적인 네트워크 디바이스 + RDMA + crypto 기능을 가집니다
  • SF는 auxiliary_bus를 통해 커널에 등록되며, mlx5_core.sf.X 형태의 auxiliary 디바이스로 관리됩니다
  • 컨테이너 환경에서는 SF가 VF보다 유연하며, 마이그레이션 지원이 용이합니다

Representor Port 아키텍처

Representor port는 switchdev 모드에서 DPU가 호스트 측에 노출하는 가상 netdev입니다. 각 VF/SF에 대응하는 representor가 생성되어, 호스트에서 TC flower 규칙을 통해 VF/SF 트래픽을 제어할 수 있습니다. 이는 OVS offload의 핵심 메커니즘입니다.

/* drivers/net/ethernet/mellanox/mlx5/core/eswitch.h */
struct mlx5_eswitch_rep {
    struct mlx5_eswitch *esw;
    u16 vport;                      /* 연결된 VF/SF의 vport 번호 */
    u16 vlan;                       /* 기본 VLAN */
    struct net_device *netdev;       /* representor netdev */
    struct mlx5_flow_handle *send_to_vport_rule;
    struct mlx5e_rep_priv *rep_data;
};

/* Representor의 역할:
 * 1. VF/SF로 향하는 slow-path 패킷의 수신/송신 경로
 * 2. TC flower 규칙의 연결점 (representor에 규칙 설치 → eSwitch HW로 오프로드)
 * 3. OVS 브릿지의 포트로 연결 (ovs-vsctl add-port br0 enp3s0f0np0_0)
 * 4. conntrack offload의 앵커 포인트
 */

/* drivers/net/ethernet/mellanox/mlx5/core/en_rep.c */
static const struct net_device_ops mlx5e_netdev_ops_rep = {
    .ndo_open         = mlx5e_rep_open,
    .ndo_stop         = mlx5e_rep_close,
    .ndo_start_xmit   = mlx5e_xmit,           /* slow-path 송신 */
    .ndo_setup_tc     = mlx5e_rep_setup_tc,   /* TC flower 오프로드 진입점 */
    .ndo_get_stats64  = mlx5e_rep_get_stats,  /* HW 카운터 기반 통계 */
};

OVS TC Flower Offload 동작 원리

Open vSwitch(OVS)는 가상 스위칭의 표준이지만, 소프트웨어 처리로 인해 CPU 오버헤드가 큽니다. DPU의 eSwitch를 활용하면 OVS 플로우를 하드웨어로 오프로드하여 호스트 CPU 사용을 90% 이상 줄일 수 있습니다.

# ━━━ OVS-DPDK + TC Flower HW Offload 설정 ━━━

# 1. eSwitch를 switchdev 모드로 전환
devlink dev eswitch set pci/0000:03:00.0 mode switchdev

# 2. OVS에서 하드웨어 오프로드 활성화
ovs-vsctl set Open_vSwitch . other_config:hw-offload=true
ovs-vsctl set Open_vSwitch . other_config:tc-policy=skip_sw

# 3. OVS 브릿지에 PF와 VF representor 연결
ovs-vsctl add-br br-int
ovs-vsctl add-port br-int enp3s0f0np0       # PF (uplink)
ovs-vsctl add-port br-int enp3s0f0np0_0     # VF0 representor
ovs-vsctl add-port br-int enp3s0f0np0_1     # VF1 representor

# 4. 오프로드 동작 흐름:
#   a) 첫 패킷: eSwitch → representor → OVS userspace → flow 결정
#   b) OVS가 TC flower 규칙을 representor에 설치
#   c) 드라이버가 TC flower → eSwitch HW 규칙으로 변환
#   d) 이후 패킷: eSwitch HW에서 직접 포워딩 (CPU 바이패스)

# 오프로드된 플로우 확인
tc -s filter show dev enp3s0f0np0_0 ingress
# filter protocol ip pref 2 flower chain 0
#   eth_type ipv4
#   src_ip 10.0.0.5
#   dst_ip 10.0.0.10
#   in_hw in_hw_count 1    ← HW 오프로드 확인
#     action order 1: mirred (Egress Redirect to device enp3s0f0np0_1)
#     hw_stats immediate

# OVS 오프로드 통계 확인
ovs-appctl dpctl/dump-flows type=offloaded
# recirc_id(0),in_port(2),eth(...),ipv4(src=10.0.0.5,dst=10.0.0.10,...)
# packets:1523400, bytes:97497600, used:0.001s, flags:SFPR
OVS Offload 제한사항:
  • 모든 OVS 액션이 HW 오프로드 가능한 것은 아닙니다. ct()(conntrack), output, set() 등 기본 액션은 지원되지만, learn(), clone() 등 복잡한 액션은 소프트웨어 폴백됩니다
  • eSwitch의 HW 플로우 테이블 크기가 유한합니다 (보통 수백만 엔트리). 초과 시 자동 소프트웨어 폴백
  • conntrack offload는 지원 연결 수와 타임아웃 동작이 소프트웨어 conntrack과 다를 수 있습니다
  • VXLAN/Geneve 등 오버레이 터널의 캡슐화/해제도 HW 오프로드 가능하지만, 드라이버와 펌웨어 버전에 따라 지원 범위가 다릅니다

auxiliary_bus: DPU 다기능 디바이스 분할

auxiliary_bus는 하나의 PCIe 디바이스가 여러 기능(네트워크, RDMA, crypto, vDPA 등)을 독립적인 커널 드라이버에 분배하기 위한 메커니즘입니다. DPU처럼 다기능 디바이스에서 핵심적인 역할을 합니다.

/* include/linux/auxiliary_bus.h */
struct auxiliary_device {
    struct device dev;
    const char *name;   /* "mlx5_core.eth.0", "mlx5_core.rdma.0" 등 */
    u32 id;
};

struct auxiliary_driver {
    int (*probe)(struct auxiliary_device *adev,
                const struct auxiliary_device_id *id);
    void (*remove)(struct auxiliary_device *adev);
    const char *name;
    struct device_driver driver;
    const struct auxiliary_device_id *id_table;
};

/* mlx5 DPU에서 auxiliary_bus 활용 예시:
 *
 * mlx5_core (PCI 드라이버)
 *   ├── mlx5_core.eth.0     → mlx5e (이더넷 netdev)
 *   ├── mlx5_core.rdma.0    → mlx5_ib (RDMA/RoCE)
 *   ├── mlx5_core.vnet.0    → mlx5_vnet (vDPA - virtio 에뮬레이션)
 *   ├── mlx5_core.sf.88     → Scalable Function #88
 *   └── mlx5_core.crypto.0  → Crypto offload
 *
 * 각 기능이 독립 드라이버로 동작하며, 개별 로드/언로드 가능
 */

/* Scalable Function 등록 (drivers/net/ethernet/mellanox/mlx5/core/sf/) */
static int mlx5_sf_dev_probe(struct auxiliary_device *adev,
                             const struct auxiliary_device_id *id)
{
    struct mlx5_sf_dev *sf_dev = container_of(adev, struct mlx5_sf_dev, adev);
    struct mlx5_core_dev *mdev;

    mdev = mlx5_sf_dev_to_mdev(sf_dev);
    /* SF용 mlx5_core_dev 초기화 → 독립 netdev + RDMA + crypto 생성 */
    return mlx5_init_one(mdev);
}

IPsec / TLS 인라인 암호화 오프로드

DPU는 IPsec ESP와 TLS(kTLS)의 암호화/복호화를 하드웨어에서 수행하여 라인 레이트 암호화를 제공합니다. 커널의 xfrm_dev_offload 인터페이스를 통해 SA를 NIC에 직접 설치합니다.

/* include/net/xfrm.h — 하드웨어 오프로드 구조체 */
struct xfrm_dev_offload {
    struct net_device *dev;
    struct net_device *real_dev;  /* bond/vlan의 실제 HW 디바이스 */
    unsigned long offload_handle; /* 드라이버의 HW 오브젝트 핸들 */
    u8 dir : 2;                  /* XFRM_DEV_OFFLOAD_IN/OUT */
    u8 type : 2;                 /* CRYPTO (암호만) / PACKET (전체) */
    u8 flags : 2;
};

/* IPsec 오프로드 타입:
 * XFRM_DEV_OFFLOAD_CRYPTO — 암호화/복호화만 HW, 헤더 처리는 SW
 *   → ESP trailer/header는 커널이 추가, 암호 연산만 NIC 가속
 *   → 대부분의 NIC에서 지원 (ConnectX-6+, E810)
 *
 * XFRM_DEV_OFFLOAD_PACKET — 전체 IPsec 처리를 HW에서 수행
 *   → ESP 헤더 추가, SPI 매칭, anti-replay, 암호화 모두 NIC
 *   → 호스트 CPU 관여 0%. 최대 성능
 *   → BlueField-2/3에서 지원 (full offload)
 */

/* mlx5 드라이버의 IPsec offload 등록 */
/* drivers/net/ethernet/mellanox/mlx5/core/en_accel/ipsec.c */
static const struct xfrmdev_ops mlx5e_ipsec_xfrmdev_ops = {
    .xdo_dev_state_add    = mlx5e_xfrm_add_state,    /* SA를 HW에 설치 */
    .xdo_dev_state_delete = mlx5e_xfrm_del_state,    /* HW SA 삭제 */
    .xdo_dev_state_free   = mlx5e_xfrm_free_state,   /* 리소스 해제 */
    .xdo_dev_offload_ok   = mlx5e_ipsec_offload_ok,  /* 오프로드 가능 여부 확인 */
    .xdo_dev_policy_add   = mlx5e_xfrm_add_policy,   /* SP를 HW에 설치 */
    .xdo_dev_policy_delete= mlx5e_xfrm_del_policy,
};
# ━━━ IPsec HW Offload 설정 예시 ━━━

# 1. 디바이스의 IPsec offload 지원 확인
ethtool -k enp3s0f0np0 | grep esp
# esp-hw-offload: on
# esp-tx-csum-hw-offload: on

# 2. IPsec SA 추가 시 offload 지정
ip xfrm state add \
    src 10.0.0.1 dst 10.0.0.2 \
    proto esp spi 0x1000 mode transport \
    aead "rfc4106(gcm(aes))" 0x$(openssl rand -hex 20) 128 \
    offload dev enp3s0f0np0 dir out   # ← HW offload 지정

ip xfrm state add \
    src 10.0.0.2 dst 10.0.0.1 \
    proto esp spi 0x2000 mode transport \
    aead "rfc4106(gcm(aes))" 0x$(openssl rand -hex 20) 128 \
    offload dev enp3s0f0np0 dir in

# 3. packet offload (full offload — BlueField-2/3)
ip xfrm state add \
    src 10.0.0.1 dst 10.0.0.2 \
    proto esp spi 0x3000 mode tunnel \
    aead "rfc4106(gcm(aes))" 0x$(openssl rand -hex 20) 128 \
    offload packet dev enp3s0f0np0 dir out  # ← packet 전체 오프로드

# 4. HW offload 상태 확인
ip xfrm state list
# ... offload type packet dev enp3s0f0np0 dir out ...

# 5. kTLS offload (TLS 1.3)
ethtool -k enp3s0f0np0 | grep tls
# tls-hw-tx-offload: on
# tls-hw-rx-offload: on
# tls-hw-record: on

# nginx에서 kTLS + HW offload 활용 (커널 5.2+)
# setsockopt(fd, SOL_TLS, TLS_TX, ...) → 커널이 자동 HW 오프로드

DPU 스토리지 오프로드

DPU는 네트워크뿐만 아니라 스토리지 가상화도 오프로드합니다. NVMe-oF(NVMe over Fabrics) 타겟을 DPU에서 실행하거나, virtio-blk 백엔드를 DPU의 ARM에서 처리하여 호스트 CPU를 완전히 해방시킵니다.

스토리지 오프로드동작 방식효과
NVMe-oF SNAP DPU가 NVMe-oF initiator를 에뮬레이트하여 호스트에 로컬 NVMe 디스크처럼 노출 원격 스토리지를 로컬처럼 사용, 호스트 드라이버 불필요
virtio-blk SNAP DPU ARM에서 virtio-blk 백엔드를 실행, 호스트 VM에 virtio 디스크 제공 QEMU vhost-user 불필요, 스토리지 I/O CPU 오버헤드 제거
RDMA/RoCE 가속 NVMe-oF RDMA 트랜스포트를 DPU RNIC에서 처리 제로카피 원격 스토리지 접근, μs 단위 지연
GPUDirect Storage GPU ↔ DPU 간 직접 DMA로 스토리지 데이터 전달 CPU/시스템 메모리 바이패스, AI/HPC 워크로드 최적화
# ━━━ NVMe-oF SNAP (BlueField DPU) ━━━

# DPU ARM 측에서 NVMe-oF SNAP 컨트롤러 생성
# → 호스트에 가상 NVMe 디바이스가 나타남

# 1. SNAP 서비스 시작 (DPU ARM에서)
snap_rpc.py controller_nvme_create \
    --pf_id 0 \
    --nqn nqn.2022-01.com.nvidia:subsys1

# 2. 원격 NVMe-oF 타겟 연결
snap_rpc.py subsystem_nvme_create \
    --nqn nqn.2022-01.com.nvidia:subsys1 \
    --trtype rdma \
    --traddr 192.168.100.10 \
    --trsvcid 4420

# 호스트에서 확인 (별도 드라이버 불필요)
nvme list
# /dev/nvme0n1  SNAP Virtual NVMe  1.95 TB

# ━━━ vDPA (virtio DataPath Acceleration) ━━━

# DPU에서 vDPA 디바이스 생성 → VM에 virtio-net HW 가속 제공
# 호스트 커널의 vDPA bus + vhost-vdpa로 QEMU에 연결

# 1. vDPA management 디바이스 확인
vdpa mgmtdev show
# auxiliary/mlx5_core.sf.4:
#   supported_classes net

# 2. vDPA 디바이스 생성
vdpa dev add name vdpa0 mgmtdev auxiliary/mlx5_core.sf.4 \
    mac 00:11:22:33:44:55 max_vqp 8

# 3. QEMU에서 vhost-vdpa 디바이스로 VM에 연결
# -netdev vhost-vdpa,vhostdev=/dev/vhost-vdpa-0,id=vdpa0
# -device virtio-net-pci,netdev=vdpa0

DPU 보안 아키텍처

DPU의 가장 중요한 가치 중 하나는 인프라 보안의 격리입니다. 호스트 OS와 독립된 신뢰 도메인에서 보안 서비스를 실행하여, 호스트가 침해되더라도 인프라 무결성을 유지합니다.

보안 기능설명구현
Hardware Root of Trust DPU 부팅 시 ROM → 부트로더 → OS까지 서명 체인 검증 Secure Boot + TPM 2.0 on ARM
호스트 격리 호스트 OS가 DPU의 ARM OS를 변조 불가 PCIe 기반 분리, restricted 모드
인라인 방화벽 모든 네트워크 트래픽이 DPU eSwitch를 통과 CT offload, TC flower, stateful FW
인라인 암호화 IPsec/TLS를 와이어 스피드로 처리 AES-GCM, ChaCha20 HW 엔진
DPI/IDS RegEx 엔진으로 패턴 매칭 가속 Hyperscan 호환 HW RegEx
감사 및 로깅 DPU에서 독립적으로 트래픽 미러링/로깅 sFlow, IPFIX HW export
# ━━━ BlueField DPU 보안 모드 설정 ━━━

# DPU 동작 모드 확인 (mlxconfig)
mlxconfig -d /dev/mst/mt41692_pciconf0 query | grep -i "internal_cpu"
# INTERNAL_CPU_MODEL = EMBEDDED_CPU(1)

# 호스트 권한 모드 설정
# Privileged:    호스트가 DPU PF를 직접 제어 (개발/테스트용)
# Restricted:    DPU ARM이 모든 제어권 보유, 호스트는 VF만 사용
# Restricted+:   호스트가 DPU 리셋/재부팅도 불가
mlxconfig -d /dev/mst/mt41692_pciconf0 set INTERNAL_CPU_PAGE_SUPPLIER=ECPF
mlxconfig -d /dev/mst/mt41692_pciconf0 set INTERNAL_CPU_ESWITCH_MANAGER=ECPF
mlxconfig -d /dev/mst/mt41692_pciconf0 set INTERNAL_CPU_OFFLOAD_ENGINE=ENABLED

# ━━━ DPU 기반 격리된 방화벽 ━━━

# DPU ARM에서 nftables 방화벽 실행 (호스트 독립)
# → eSwitch를 통과하는 모든 호스트 트래픽에 적용
# → 호스트 root 권한으로도 우회 불가
nft add table inet host_fw
nft add chain inet host_fw input { type filter hook ingress device enp3s0f0np0 priority 0 \; }
nft add rule inet host_fw input ip saddr != 10.0.0.0/8 drop

# DPU의 독립 로깅 (syslog → 중앙 서버)
# 호스트 침해 시에도 DPU 로그는 보존
DPU 보안 아키텍처의 핵심: Restricted 모드에서 호스트 OS는 DPU의 PF(Physical Function)를 직접 제어할 수 없으며, DPU ARM에서 할당한 VF/SF만 사용할 수 있습니다. 이는 클라우드 환경에서 bare-metal-as-a-service를 구현하는 핵심 메커니즘으로, 테넌트에게 bare metal 성능을 제공하면서도 인프라 보안(방화벽, 암호화, 네트워크 격리)은 DPU에서 강제 적용됩니다.

DPU 프로그래밍 모델

프로그래밍 방식대상장점단점
TC flower + switchdev eSwitch (패킷 분류/포워딩) 표준 커널 API, OVS 연동 매칭/액션 범위 제한적
XDP/BPF offload NIC의 BPF JIT 엔진 유연한 패킷 처리 로직 BPF 명령어 서브셋만 지원
P4 프로그래머블 파이프라인 파싱/매칭/액션 완전 커스텀 제조사별 P4 컴파일러 필요
DOCA SDK (NVIDIA) BlueField ARM + 가속기 고수준 API, RegEx/Compress/Crypto 통합 NVIDIA 전용, 벤더 종속
IPDK (Intel) Intel IPU P4 + DPDK 기반 오픈소스 Intel IPU 전용
ARM Linux 직접 프로그래밍 DPU ARM SoC 완전한 자유도 (임의 데몬/서비스) 직접 개발/유지보수 부담
/* P4 프로그래밍 예시 — DPU의 프로그래머블 파이프라인에서 실행
 * Intel IPU (IPDK) 또는 AMD Pensando에서 P4 컴파일러로 HW 테이블 생성
 */

/* 커스텀 헤더 파싱 및 매칭 */
header custom_header_t {
    bit<16> type_id;
    bit<32> tenant_id;
    bit<16> service_tag;
}

parser CustomParser(packet_in pkt, out headers_t hdr) {
    state start {
        pkt.extract(hdr.ethernet);
        transition select(hdr.ethernet.etherType) {
            0x8100: parse_vlan;
            0x0800: parse_ipv4;
            0xFE01: parse_custom;  /* 커스텀 프로토콜 */
            default: accept;
        }
    }
    state parse_custom {
        pkt.extract(hdr.custom);
        transition parse_ipv4;
    }
}

/* 매칭 테이블: tenant_id 기반 라우팅 */
table tenant_routing {
    key = {
        hdr.custom.tenant_id : exact;
        hdr.ipv4.dstAddr     : lpm;
    }
    actions = {
        forward_to_port;
        apply_encryption;
        drop;
    }
    size = 1048576;  /* 100만 엔트리 */
}

DPU 모니터링 및 디버깅

# ━━━ devlink 헬스 리포터 ━━━
# DPU 하드웨어 상태 모니터링

devlink health show pci/0000:03:00.0
# reporter fw_fatal:
#   state healthy error 0 recover 0
# reporter fw:
#   state healthy error 0 recover 0
# reporter vnic:
#   state healthy error 0 recover 0

# 헬스 리포터 상세 덤프
devlink health dump show pci/0000:03:00.0 reporter fw

# ━━━ eSwitch 플로우 카운터 ━━━
# HW 오프로드된 플로우의 패킷/바이트 통계

tc -s filter show dev enp3s0f0np0_0 ingress
# filter ... flower ...
#   Sent 98234567 bytes 1523400 pkt (hardware)
#   action ... mirred ... (hardware)
#     Sent 98234567 bytes 1523400 pkt

# ━━━ DPU 리소스 사용량 ━━━

# 사용 가능한 HW 리소스 조회
devlink resource show pci/0000:03:00.0
# name flow_table size 4194304 occ 152340
# name flow_counter size 16777216 occ 304680
# name encap_entries size 65536 occ 1024

# ━━━ ethtool 확장 통계 ━━━

ethtool -S enp3s0f0np0 | grep -E "offload|hw_"
# rx_vport_rdma_unicast_packets: 4523100
# tx_vport_rdma_unicast_packets: 3891200
# rx_hw_timestamp: 15234001

# ━━━ SF/VF 개별 통계 ━━━

# Scalable Function 상태 확인
devlink port function show pci/0000:03:00.0/32768
# function:
#   hw_addr 00:00:00:00:88:88 state active opstate attached
#   roce true migratable true ipsec_crypto true

# ━━━ 디버깅 팁 ━━━
# HW offload 문제 시: skip_hw → skip_sw 순서로 테스트
# 1단계: skip_hw로 SW 경로에서 규칙이 올바른지 확인
tc filter add dev rep0 ingress flower ... action ... skip_hw
# 2단계: skip_sw로 HW 전용으로 전환, in_hw 확인
tc filter add dev rep0 ingress flower ... action ... skip_sw

# 오프로드 실패 원인 확인 (커널 로그)
dmesg | grep -i "offload\|eswitch\|flower"
# mlx5_core: TC flower offload failed: -EOPNOTSUPP (지원 안 되는 액션)
# mlx5_core: flow table full, falling back to software
DPU 운영 시 주요 주의사항:
  • 펌웨어-드라이버 호환성 — DPU 펌웨어와 호스트 커널 드라이버 버전 불일치는 오프로드 실패의 주요 원인입니다. NVIDIA의 경우 MLNX_OFED/DOCA 버전 매트릭스를 확인하세요
  • eSwitch 모드 전환 — legacy ↔ switchdev 전환 시 수 초간 네트워크 중단 발생. 운영 중 전환은 유지보수 윈도우에서만 수행
  • DPU ARM OS 업데이트 — DPU의 ARM Linux 커널/rootfs 업데이트 시 DPU 재부팅 필요. 호스트와 독립적이지만 네트워크 경로 단절 발생
  • HW 플로우 테이블 한계 — eSwitch의 HW 플로우 테이블은 유한합니다(수백만 엔트리). microflow가 많은 환경에서는 aging/eviction 정책 조율 필요
  • 열 관리 — 400GbE DPU는 75W+ 전력을 소비합니다. 적절한 서버 냉각 확보 필수
  • CT offload 차이 — 하드웨어 conntrack은 소프트웨어 conntrack과 타임아웃, 최대 연결 수 등이 다를 수 있습니다. 사전 검증 필요
SmartNIC/DPU 선택 가이드:
  • 클라우드/가상화 환경 — NVIDIA BlueField DPU가 주류. OVS offload + DOCA SDK + GPUDirect 생태계가 가장 성숙
  • 통신사/네트워크 기능 — Intel IPU(P4 프로그래밍) 또는 AMD Pensando(하드웨어 P4 파이프라인)가 적합
  • 순수 패킷 처리 가속 — Netronome NFP(XDP HW offload)가 BPF 오프로드에 최적화
  • Intel 서버 통합 — Intel E810(ADQ + QAT 연동) 또는 IPU E2000
  • 저전력/고밀도 — Marvell OCTEON 10이 전력 효율 우수

DPDK (Data Plane Development Kit) 심화

DPDK는 Intel이 주도하여 개발한 고성능 패킷 처리 프레임워크로, 커널 네트워크 스택을 완전히 바이패스하여 유저 공간에서 NIC를 직접 제어합니다. 표준 커널 드라이버가 인터럽트 기반으로 패킷을 처리하는 반면, DPDK는 폴링(polling) 기반 PMD(Poll Mode Driver)를 사용하여 컨텍스트 스위칭과 인터럽트 오버헤드를 제거합니다. 10~400GbE 환경에서 수십~수백 Mpps(백만 패킷/초)의 처리량을 단일 서버에서 달성할 수 있으며, 통신사(NFV), 클라우드(가상 스위칭), 금융(초저지연 트레이딩), CDN, 보안 장비 등에서 핵심 기술로 사용됩니다.

DPDK와 커널의 관계: DPDK는 커널 모듈이 아니라 유저 공간 라이브러리입니다. 그러나 커널의 VFIO, UIO, hugepages, IOMMU 등 여러 서브시스템에 의존하며, 최근에는 커널의 AF_XDP 소켓을 PMD 백엔드로 사용하는 하이브리드 접근도 지원합니다. 커널 개발자가 DPDK의 동작 원리를 이해하면 드라이버 최적화, VFIO/IOMMU 튜닝, AF_XDP 개발 등에서 큰 도움이 됩니다.

DPDK 아키텍처 개요

User Space (DPDK Application) DPDK Application (l2fwd, OVS-DPDK, VPP ...) mbuf 패킷 버퍼 Ring Lock-free 큐 Mempool 메모리 풀 Hash / LPM 탐색 라이브러리 Cryptodev / Eventdev 가속 프레임워크 EAL (Environment Abstraction Layer) — hugepage, CPU 코어, PCI, 로깅, 타이머 PMD (VFIO) PMD (UIO) PMD (AF_XDP) PMD (virtio) Kernel Space VFIO / IOMMU UIO (igb_uio) AF_XDP socket Hugepages (hugetlbfs) vfio-pci / uio_pci_generic Hardware NIC (10/25/100/400 GbE) DMA Engine PCIe BAR / MMIO 레지스터
계층구성 요소역할
Applicationl2fwd, l3fwd, OVS-DPDK, VPP, Pktgen패킷 처리 로직 구현
Core Librariesmbuf, Ring, Mempool, Hash, LPM, ACL고성능 데이터 구조 및 알고리즘
EALEnvironment Abstraction Layerhugepage, CPU 코어, PCI 디바이스, 타이머, 로깅 추상화
PMDPoll Mode Driver (ixgbe, i40e, mlx5, virtio, af_xdp...)NIC별 RX/TX 드라이버 (유저 공간)
KernelVFIO, UIO, hugepages, IOMMU디바이스 접근 및 메모리 관리 지원
HardwareNIC, DMA, PCIe BAR물리적 패킷 송수신, DMA 전송

커널 네트워크 스택 vs DPDK 비교

항목커널 네트워크 스택DPDK
실행 공간커널 공간유저 공간
패킷 수신 모델인터럽트 → NAPI 폴링 (하이브리드)100% 폴링 (busy-wait)
패킷 버퍼sk_buff (동적 할당)rte_mbuf (사전 할당 Mempool)
프로토콜 스택완전한 L2~L7 (TCP/IP, Netfilter, conntrack...)없음 (앱이 직접 구현 또는 별도 라이브러리)
CPU 사용필요 시만 사용 (이벤트 기반)전용 코어 상시 100% 사용 (busy-poll)
지연 시간수~수십 μs수백 ns ~ 수 μs
처리량 (64B pkt)~1-5 Mpps/core~14.88 Mpps/core (10GbE wire rate)
메모리 관리SLAB/SLUB + per-CPU 캐시Hugepage 기반 Mempool
보안/격리네임스페이스, Netfilter, SELinux 등IOMMU/VFIO로 DMA 격리만
도구/디버깅tcpdump, ss, ip, nftables, eBPFdpdk-proc-info, dpdk-pdump, DPDK telemetry
적용 분야범용 서버/데스크탑/IoTNFV, SDN 스위칭, 패킷 브로커, DPI, 로드밸런서
DPDK 사용 시 트레이드오프: DPDK는 커널 네트워크 스택을 바이패스하므로 iptables, tc, conntrack, TCP/IP 프로토콜 처리 등 커널이 제공하는 모든 기능을 사용할 수 없습니다. 이런 기능이 필요하면 앱에서 직접 구현하거나 VPP/FD.io 같은 유저 공간 네트워크 스택을 사용해야 합니다. 또한 전용 CPU 코어를 상시 폴링에 사용하므로 코어 수가 제한된 환경에서는 비효율적입니다.

EAL (Environment Abstraction Layer)

EAL은 DPDK의 초기화 계층으로, 애플리케이션이 하드웨어와 OS 세부 사항에 독립적으로 동작하도록 추상화합니다. rte_eal_init() 호출 시 수행되는 핵심 작업:

/* rte_eal_init() 초기화 순서 (Linux) */

1. 커맨드라인 파싱 (-l, -n, --socket-mem, --file-prefix, --proc-type ...)
2. Hugepage 매핑
   ├── /sys/kernel/mm/hugepages/ 에서 hugepage 정보 수집
   ├── hugetlbfs 마운트 확인 (/dev/hugepages 또는 --huge-dir)
   ├── hugepage 파일 생성 → mmap() → 물리 주소 역변환 (pagemap)
   └── NUMA 노드별 메모리 할당 (--socket-mem 0,1024 → 노드1에 1GB)
3. 메모리 초기화
   ├── malloc_heap 설정 (NUMA-aware)
   ├── memzone 등록 (이름→주소 매핑)
   └── mempool 사전 할당 준비
4. CPU 코어 관리
   ├── -l 0-3 또는 --lcores 옵션에서 논리 코어 목록 파싱
   ├── pthread 생성 + CPU affinity 설정 (pthread_setaffinity_np)
   └── main lcore 결정 (첫 번째 코어)
5. PCI 버스 스캔
   ├── /sys/bus/pci/devices/ 순회
   ├── VFIO 또는 UIO 바인딩 상태 확인
   └── 지원되는 PMD와 디바이스 매칭 (vendor:device ID)
6. 서비스 코어 (선택)
   └── --service-lcore로 비데이터 경로 작업 전담 코어 지정
7. 로깅, 타이머, 인터럽트 핸들러 초기화
/* DPDK 애플리케이션 기본 구조 */
#include <rte_eal.h>
#include <rte_ethdev.h>
#include <rte_mbuf.h>

int main(int argc, char *argv[])
{
    /* EAL 초기화: hugepage 매핑, 코어 설정, PCI 스캔 */
    int ret = rte_eal_init(argc, argv);
    if (ret < 0)
        rte_exit(EXIT_FAILURE, "EAL init failed\n");

    /* Mempool 생성 (패킷 버퍼 풀) */
    struct rte_mempool *mbuf_pool = rte_pktmbuf_pool_create(
        "MBUF_POOL",
        8192,              /* 풀 크기 (mbuf 개수) */
        256,               /* per-core 캐시 크기 */
        0,                 /* priv_size */
        RTE_MBUF_DEFAULT_BUF_SIZE,  /* 데이터룸 크기 */
        rte_socket_id()    /* NUMA 소켓 */
    );

    /* 포트 설정 및 시작 */
    struct rte_eth_conf port_conf = { 0 };
    rte_eth_dev_configure(port_id, nb_rx_q, nb_tx_q, &port_conf);
    rte_eth_rx_queue_setup(port_id, 0, 1024, rte_eth_dev_socket_id(port_id),
                          NULL, mbuf_pool);
    rte_eth_tx_queue_setup(port_id, 0, 1024, rte_eth_dev_socket_id(port_id),
                          NULL);
    rte_eth_dev_start(port_id);

    /* 메인 패킷 처리 루프 (busy-poll) */
    while (!force_quit) {
        struct rte_mbuf *bufs[32];
        uint16_t nb_rx = rte_eth_rx_burst(port_id, 0, bufs, 32);
        if (nb_rx == 0)
            continue;

        /* 패킷 처리 ... */
        process_packets(bufs, nb_rx);

        uint16_t nb_tx = rte_eth_tx_burst(port_id, 0, bufs, nb_rx);
        for (uint16_t i = nb_tx; i < nb_rx; i++)
            rte_pktmbuf_free(bufs[i]);  /* 전송 실패 mbuf 반환 */
    }

    rte_eal_cleanup();
    return 0;
}
EAL 핵심 옵션:
  • -l 0-3 — 사용할 논리 코어 목록. DPDK 워커 스레드가 이 코어에 1:1 매핑
  • -n 4 — 메모리 채널 수 (NUMA interleaving 최적화)
  • --socket-mem 1024,1024 — NUMA 노드별 hugepage 할당량 (MB)
  • -a 0000:03:00.0 — 허용할 PCI 디바이스 (allowlist)
  • --file-prefix dpdk1 — 여러 DPDK 프로세스 공존 시 hugepage 파일 구분
  • --proc-type secondary — 멀티프로세스 모드 (primary/secondary)
  • --no-huge — hugepage 없이 실행 (개발/테스트용, 성능 저하)

PMD (Poll Mode Driver)

PMD는 DPDK의 핵심으로, 커널 드라이버 없이 유저 공간에서 NIC를 직접 제어합니다. NIC의 PCIe BAR를 mmap()하여 하드웨어 레지스터에 직접 접근하고, DMA 디스크립터 링(descriptor ring)을 유저 공간 메모리에 배치합니다.

/* PMD RX 동작 원리 (ixgbe PMD 기준) */

  NIC Hardware                        User Space (PMD)
  ┌─────────────┐                    ┌─────────────────────┐
  │   RX Queue  │                    │   RX Descriptor Ring│
  │  ┌────────┐ │  DMA               │  ┌───────────────┐  │
  │  │ 패킷   │─┼──────────────────→│  │ DD bit = 1    │  │  ← NIC가 DMA 완료 후 DD 설정
  │  │ 수신   │ │                    │  │ mbuf addr     │  │
  │  └────────┘ │                    │  │ pkt_len, etc. │  │
  │             │                    │  └───────────────┘  │
  │  Tail Reg ←─┼────────────────────┤  PMD가 tail 갱신   │
  └─────────────┘                    │  (새 mbuf 주소 제공) │
                                     └─────────────────────┘

  1. PMD가 RX descriptor의 DD(Descriptor Done) 비트를 폴링
  2. DD=1이면 → DMA 완료 → mbuf에 패킷 데이터가 있음
  3. descriptor에서 패킷 메타데이터(길이, RSS hash, VLAN 등) 추출
  4. 사용된 descriptor에 새 mbuf 주소를 채워 NIC에 반환 (tail 레지스터 갱신)
  5. rte_eth_rx_burst()가 최대 nb_pkts개의 mbuf 포인터 배열을 반환
/* PMD의 RX burst 내부 (간략화) — ixgbe_recv_pkts() 기반 */
static uint16_t
pmd_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts, uint16_t nb_pkts)
{
    struct pmd_rx_queue *rxq = rx_queue;
    volatile union rx_desc *rxdp;  /* 하드웨어 descriptor 포인터 */
    uint16_t nb_rx = 0;

    while (nb_rx < nb_pkts) {
        rxdp = &rxq->rx_ring[rxq->rx_tail];

        /* DD 비트 확인: NIC가 DMA를 완료했는가? */
        if (!(rxdp->wb.status & RX_DESC_DD))
            break;  /* 더 이상 수신된 패킷 없음 */

        /* mbuf 회수 및 메타데이터 설정 */
        struct rte_mbuf *mb = rxq->sw_ring[rxq->rx_tail];
        mb->pkt_len = rxdp->wb.pkt_len;
        mb->data_len = rxdp->wb.pkt_len;
        mb->hash.rss = rxdp->wb.rss_hash;
        mb->vlan_tci = rxdp->wb.vlan_tag;

        rx_pkts[nb_rx++] = mb;

        /* 새 mbuf를 할당하여 descriptor에 채움 (NIC에 반환) */
        struct rte_mbuf *nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
        rxdp->read.pkt_addr = rte_mbuf_data_iova(nmb);
        rxq->sw_ring[rxq->rx_tail] = nmb;

        rxq->rx_tail = (rxq->rx_tail + 1) & rxq->rx_tail_mask;
    }

    /* tail 레지스터 갱신 → NIC에 새 descriptor 사용 가능 알림 */
    if (nb_rx)
        rte_write32(rxq->rx_tail, rxq->tail_ptr);

    return nb_rx;
}

/* PMD의 TX burst 내부 (간략화) */
static uint16_t
pmd_xmit_pkts(void *tx_queue, struct rte_mbuf **tx_pkts, uint16_t nb_pkts)
{
    struct pmd_tx_queue *txq = tx_queue;
    uint16_t nb_tx = 0;

    /* 이전에 전송 완료된 descriptor 정리 (mbuf 반환) */
    tx_free_bufs(txq);

    while (nb_tx < nb_pkts) {
        volatile union tx_desc *txdp = &txq->tx_ring[txq->tx_tail];
        struct rte_mbuf *mb = tx_pkts[nb_tx];

        /* TX descriptor에 mbuf의 DMA 주소와 길이 설정 */
        txdp->read.buffer_addr = rte_mbuf_data_iova(mb);
        txdp->read.cmd_type_len = mb->data_len | TX_DESC_EOP | TX_DESC_RS;

        txq->sw_ring[txq->tx_tail] = mb;
        txq->tx_tail = (txq->tx_tail + 1) & txq->tx_tail_mask;
        nb_tx++;
    }

    /* tail 레지스터 갱신 → NIC가 전송 시작 */
    rte_wmb();  /* 메모리 배리어: descriptor 쓰기 완료 보장 */
    rte_write32(txq->tx_tail, txq->tail_ptr);

    return nb_tx;
}
PMD 종류백엔드대표 드라이버특징
Physical PMDVFIO / UIOixgbe, i40e, ice, mlx5, bnxt, ena실제 NIC 직접 제어, 최고 성능
Virtual PMDvirtiovirtio-net, vmxnet3, avfVM 내부에서 가상 NIC 접근
AF_XDP PMD커널 AF_XDP 소켓af_xdp커널 기능 유지하면서 고성능, VFIO 불필요
SW PMDlibpcap / TAPpcap, net_tap개발/테스트용, 커널 NIC에 연결
Crypto PMDQAT / AESNI / SWqat, aesni_mb, openssl암호화 가속기 접근
Compress PMDQAT / zlibqat_comp, zlib압축 가속기
Event PMD하드웨어 이벤트 스케줄러dlb2, sw_event이벤트 기반 패킷 분배

rte_mbuf 구조체

rte_mbuf는 DPDK의 패킷 버퍼로, 커널의 sk_buff에 대응하지만 설계 철학이 다릅니다. sk_buff는 동적으로 할당되며 메타데이터가 풍부한 반면, rte_mbuf는 Mempool에서 사전 할당되어 고정 크기의 경량 구조체입니다.

/* rte_mbuf 핵심 필드 (lib/mbuf/rte_mbuf_core.h) */
struct rte_mbuf {
    /* ── 캐시라인 0 (핫 필드) ── */
    void         *buf_addr;       /* 가상 주소: headroom + 데이터 시작 */
    rte_iova_t    buf_iova;       /* IO 가상 주소 (DMA용, IOVA) */

    union {
        struct {
            uint32_t pkt_len;       /* 전체 패킷 길이 (체인 포함) */
            uint16_t data_len;      /* 이 세그먼트의 데이터 길이 */
            uint16_t vlan_tci;      /* VLAN Tag */
        };
    };
    uint64_t      ol_flags;       /* Offload 플래그 (checksum, TSO, VLAN...) */

    union {
        uint32_t rss;            /* RSS 해시값 */
        struct {
            uint16_t hash;
            uint16_t id;
        } fdir;                     /* Flow Director 필터 ID */
    } hash;

    uint16_t      data_off;       /* buf_addr부터 데이터 시작까지 오프셋 (headroom) */
    uint16_t      refcnt;         /* 참조 카운트 (공유 mbuf) */
    uint16_t      nb_segs;        /* 체인된 세그먼트 수 */
    uint16_t      port;           /* 입력 포트 ID */

    /* ── 캐시라인 1 (TX offload) ── */
    uint64_t      tx_offload;     /* L2/L3/L4 len, TSO segsz 등 (비트필드) */
    struct rte_mempool *pool;    /* 소속 Mempool (반환 시 사용) */
    struct rte_mbuf    *next;    /* 다음 세그먼트 (체인) */

    /* buf_addr 이전에 headroom(128B), 뒤에 데이터+tailroom이 위치 */
    /*                                                            */
    /* ┌──────────┬──────────┬──────────────────────┬──────────┐ */
    /* │ rte_mbuf │ headroom │     packet data      │ tailroom │ */
    /* │ (struct) │ (128B)   │     (variable)       │          │ */
    /* └──────────┴──────────┴──────────────────────┴──────────┘ */
    /* ^buf_addr  ^data_off                                     */
};
비교 항목sk_buff (커널)rte_mbuf (DPDK)
크기~240 바이트 (가변)2 캐시라인 (128 바이트) 고정
할당__alloc_skb() → SLABMempool에서 사전 할당 (rte_pktmbuf_alloc)
해제kfree_skb() / consume_skb()rte_pktmbuf_free() → Mempool 반환
DMA 주소dma_map_single() 필요buf_iova 필드에 사전 매핑
메타데이터풍부 (sk, dst, nf_bridge, tc...)최소한 (앱이 필요 시 priv_size로 확장)
체인frag_list, skb_shinfonext 포인터로 세그먼트 체인
복제skb_clone() (데이터 공유)rte_pktmbuf_clone() (refcnt 증가)

Ring 라이브러리

DPDK rte_ring은 고정 크기, FIFO, lock-free 큐로, 코어 간 패킷 전달, 멀티프로세스 IPC, 태스크 분배 등에 사용됩니다. CAS(Compare-And-Swap) 연산 기반으로 락 없이 다수 생산자/소비자를 지원합니다.

/* Ring 기본 사용 */
struct rte_ring *ring = rte_ring_create(
    "MY_RING",
    1024,                /* 크기 (2의 거듭제곱이어야 함) */
    rte_socket_id(),     /* NUMA 소켓 */
    RING_F_SP_ENQ | RING_F_SC_DEQ  /* 단일 생산자/소비자 (더 빠름) */
);

/* 생산자: 패킷을 Ring에 삽입 */
struct rte_mbuf *pkts[32];
unsigned nb = rte_eth_rx_burst(port, 0, pkts, 32);
unsigned sent = rte_ring_enqueue_burst(ring, (void **)pkts, nb, NULL);

/* 소비자: Ring에서 패킷 추출 (다른 코어) */
struct rte_mbuf *dequeued[32];
unsigned got = rte_ring_dequeue_burst(ring, (void **)dequeued, 32, NULL);
/* rte_ring 내부 구조 — Lock-free CAS 기반 */

  ┌───────────────────────────────────────────────────┐
  │ rte_ring (크기 = 2^n)                               │
  │                                                     │
  │  prod.head ──→ prod.tail ──→ ... ──→ cons.head ──→ cons.tail
  │                                                     │
  │  [slot0][slot1][slot2][...][slotN-1]                │
  │    ↑                   ↑                            │
  │  cons.tail          prod.head                       │
  └───────────────────────────────────────────────────┘

  Enqueue (MP — 다중 생산자):
  1. prod.head를 CAS로 원자적 증가 (여러 생산자가 동시에 슬롯 예약)
  2. 예약된 슬롯에 데이터 복사
  3. prod.tail을 순서대로 갱신 (모든 이전 생산자가 완료될 때까지 대기)

  Dequeue (MC — 다중 소비자):
  1. cons.head를 CAS로 원자적 증가 (여러 소비자가 동시에 슬롯 예약)
  2. 예약된 슬롯에서 데이터 읽기
  3. cons.tail을 순서대로 갱신

  SP/SC (단일 생산자/소비자):
  - CAS 대신 단순 증가. 메모리 배리어만 사용. 30~50% 더 빠름
Ring 모드 선택:
  • RING_F_SP_ENQ | RING_F_SC_DEQ — 1:1 코어 매핑 시 최적 (파이프라인 모델)
  • RING_F_MP_HTS_ENQ | RING_F_MC_HTS_DEQ — head/tail sync 방식. 기존 MP/MC보다 공정하고 순서 보장 강화
  • Ring 크기는 반드시 2의 거듭제곱이어야 합니다 (마스크 연산 최적화)

Mempool과 메모리 관리

rte_mempool은 고정 크기 객체의 풀로, DPDK에서 mbuf 할당/해제의 핵심입니다. Hugepage 위에 구축되며, per-core 캐시로 코어 간 경합을 최소화합니다.

/* Mempool 메모리 레이아웃 */

  Hugepage (2MB 또는 1GB)
  ┌──────────────────────────────────────────────┐
  │  rte_mempool header                           │
  │  ┌──────────────────────────────────────────┐ │
  │  │ Per-Core Cache (core 0): [mbuf ptr] x 256│ │  ← 락 없이 할당/반환
  │  │ Per-Core Cache (core 1): [mbuf ptr] x 256│ │
  │  │ Per-Core Cache (core 2): [mbuf ptr] x 256│ │
  │  │ ...                                      │ │
  │  └──────────────────────────────────────────┘ │
  │  Ring (공용 풀): [mbuf ptr] x N               │  ← per-core 캐시 부족 시 사용
  │  ┌──────────────────────────────────────────┐ │
  │  │ mbuf[0] | headroom | data area           │ │  ← 물리적으로 연속
  │  │ mbuf[1] | headroom | data area           │ │
  │  │ ...                                      │ │
  │  │ mbuf[N] | headroom | data area           │ │
  │  └──────────────────────────────────────────┘ │
  └──────────────────────────────────────────────┘

  할당 경로:
  1. per-core 캐시에서 mbuf 포인터 가져옴 (O(1), 락 없음)
  2. 캐시 비어있으면 → 공용 Ring에서 배치로 가져옴 (CAS 1회)

  반환 경로:
  1. per-core 캐시에 mbuf 포인터 반환 (O(1), 락 없음)
  2. 캐시 가득 차면 → 공용 Ring에 배치로 반환
/* Mempool 생성과 NUMA 인식 */
struct rte_mempool *pool;

/* 기본: 패킷 mbuf 풀 */
pool = rte_pktmbuf_pool_create(
    "PKT_POOL",
    65535,                   /* 총 mbuf 개수 (2^n - 1 권장) */
    512,                     /* per-core 캐시 크기 (0이면 캐시 비활성) */
    0,                       /* priv_size: mbuf당 추가 메타데이터 크기 */
    RTE_MBUF_DEFAULT_BUF_SIZE,  /* 2048 + 128(headroom) = 2176 */
    rte_socket_id()          /* NUMA 노드 */
);

/* 고급: 외부 메모리로 Mempool 생성 (huge 1GB) */
struct rte_pktmbuf_extmem ext_mem = {
    .buf_ptr  = mmap_addr,      /* 사전 매핑된 hugepage 주소 */
    .buf_iova = iova_addr,      /* IOMMU 매핑된 DMA 주소 */
    .buf_len  = 1ULL << 30,    /* 1GB */
    .elt_size = 2176,           /* mbuf + data 크기 */
};
pool = rte_pktmbuf_pool_create_extbuf("EXT_POOL", 65535, 512,
                                        0, 2176, rte_socket_id(),
                                        &ext_mem, 1);
Hugepage가 필수인 이유:
  • TLB 효율 — 64K개 mbuf × 2KB = 128MB. 4KB 페이지면 32,768 TLB 엔트리 필요하지만, 2MB hugepage면 64개, 1GB hugepage면 1개
  • 연속 물리 메모리 — NIC DMA가 물리적으로 연속된 영역을 필요로 함. Hugepage는 2MB/1GB 단위로 물리 연속 보장
  • IOVA 변환 단순화 — 큰 페이지 단위로 IOMMU 매핑하므로 IOTLB 미스 최소화
  • 페이지 폴트 제거 — mmap 시 MAP_POPULATE로 즉시 물리 페이지 할당. 런타임 페이지 폴트 없음

DPDK 커널 인터페이스: VFIO vs UIO

DPDK가 유저 공간에서 NIC를 제어하려면 커널의 도움이 필요합니다. PCIe BAR 메모리를 유저 공간에 매핑하고, DMA를 위한 물리/IO 주소 변환을 제공하는 두 가지 메커니즘:

항목VFIO (vfio-pci)UIO (igb_uio / uio_pci_generic)
IOMMU필수 (DMA 격리)불필요 (DMA 격리 없음)
보안안전 (IOMMU가 DMA 범위 제한)위험 (디바이스가 모든 물리 메모리에 접근 가능)
인터럽트MSI/MSI-X eventfd (전체 지원)제한적 (INTx만 또는 커널 패치 필요)
비특권 실행가능 (/dev/vfio 권한 설정)root 필수
IOMMU 그룹그룹 내 모든 디바이스를 vfio에 바인딩해야 함개별 디바이스만 바인딩
VM 패스스루KVM/QEMU와 통합 지원미지원
커널 모듈vfio-pci (in-tree)igb_uio (DPDK 동봉), uio_pci_generic (in-tree)
권장 여부권장 (기본)레거시 (IOMMU 없는 환경에서만)
# ━━━ VFIO 기반 DPDK 디바이스 바인딩 ━━━

# 1. IOMMU 활성화 확인
dmesg | grep -i iommu
# "DMAR: IOMMU enabled" 또는 "AMD-Vi: IOMMU performance counters supported"

# 2. vfio-pci 모듈 로드
modprobe vfio-pci

# 3. NIC 상태 확인
dpdk-devbind.py --status

# Network devices using kernel driver
# 0000:03:00.0 'Ethernet Controller X710' if=ens3f0 drv=i40e
# 0000:03:00.1 'Ethernet Controller X710' if=ens3f1 drv=i40e

# 4. 커널 드라이버에서 언바인딩 → VFIO에 바인딩
ip link set ens3f0 down
dpdk-devbind.py --bind=vfio-pci 0000:03:00.0

# 5. 바인딩 확인
dpdk-devbind.py --status
# Network devices using DPDK-compatible driver
# 0000:03:00.0 'Ethernet Controller X710' drv=vfio-pci

# ━━━ Hugepage 설정 ━━━

# 2MB hugepage 1024개 = 2GB
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

# NUMA 노드별 설정 (듀얼 소켓)
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

# 1GB hugepage (부팅 커맨드라인에서만 예약 가능)
# GRUB: default_hugepagesz=1G hugepagesz=1G hugepages=4

# hugetlbfs 마운트
mount -t hugetlbfs nodev /dev/hugepages

# ━━━ UIO 기반 (레거시, IOMMU 없는 환경) ━━━
modprobe uio_pci_generic
dpdk-devbind.py --bind=uio_pci_generic 0000:03:00.0
# ⚠ DMA 격리 없음 — 프로덕션에서 비권장
/* 커널 VFIO-PCI 드라이버가 DPDK에 제공하는 인터페이스 */

/* 1. Container FD — IOMMU 도메인 (DMA 매핑 관리) */
container_fd = open("/dev/vfio/vfio", O_RDWR);
ioctl(container_fd, VFIO_SET_IOMMU, VFIO_TYPE1v2_IOMMU);

/* 2. Group FD — IOMMU 그룹 */
group_fd = open("/dev/vfio/15", O_RDWR);
ioctl(group_fd, VFIO_GROUP_SET_CONTAINER, &container_fd);

/* 3. Device FD — 특정 PCI 디바이스 */
device_fd = ioctl(group_fd, VFIO_GROUP_GET_DEVICE_FD, "0000:03:00.0");

/* 4. BAR 매핑 — NIC 레지스터에 직접 접근 */
struct vfio_region_info reg = { .argsz = sizeof(reg), .index = 0 };
ioctl(device_fd, VFIO_DEVICE_GET_REGION_INFO, &reg);
bar0 = mmap(NULL, reg.size, PROT_READ | PROT_WRITE, MAP_SHARED,
            device_fd, reg.offset);
/* 이제 bar0[offset]으로 NIC 레지스터 직접 read/write 가능 */

/* 5. DMA 매핑 — hugepage를 IOMMU에 등록 */
struct vfio_iommu_type1_dma_map dma = {
    .argsz = sizeof(dma),
    .flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
    .vaddr = (uint64_t)hugepage_vaddr,
    .iova  = (uint64_t)hugepage_vaddr,  /* VA=IOVA 모드 */
    .size  = hugepage_size,
};
ioctl(container_fd, VFIO_IOMMU_MAP_DMA, &dma);
/* NIC DMA가 이 hugepage 영역에 접근 가능 */

패킷 처리 파이프라인 모델

DPDK 애플리케이션은 두 가지 기본 패킷 처리 모델을 사용합니다:

/* Run-to-Completion 모델 — 단순, 코어당 독립 처리 */

  Core 0                    Core 1                    Core 2
  ┌──────────────┐          ┌──────────────┐          ┌──────────────┐
  │ RX (Port 0)  │          │ RX (Port 0)  │          │ RX (Port 1)  │
  │     ↓        │          │     ↓        │          │     ↓        │
  │ Process pkt  │          │ Process pkt  │          │ Process pkt  │
  │     ↓        │          │     ↓        │          │     ↓        │
  │ TX (Port 1)  │          │ TX (Port 1)  │          │ TX (Port 0)  │
  └──────────────┘          └──────────────┘          └──────────────┘
  Queue 0                   Queue 1                   Queue 0

  - 각 코어가 RX → 처리 → TX를 완전히 독립 수행
  - 코어 간 통신 불필요 (Ring 없음)
  - RSS로 트래픽을 RX 큐별로 분산
  - 단순하고 확장성 좋음. 대부분의 L2/L3 포워딩에 적합
/* Pipeline 모델 — 복잡한 처리를 단계별로 분리 */

  Core 0 (RX)          Core 1 (Worker)       Core 2 (Worker)       Core 3 (TX)
  ┌────────────┐       ┌────────────┐       ┌────────────┐       ┌────────────┐
  │ RX burst   │──Ring→│ DPI/파싱   │──Ring→│ NAT/암호화 │──Ring→│ TX burst   │
  │ (Port 0,1) │       │ L3~L7 분류 │       │ 정책 적용  │       │ (Port 0,1) │
  └────────────┘       └────────────┘       └────────────┘       └────────────┘

  - 처리 단계를 여러 코어에 분배
  - 코어 간 rte_ring으로 패킷 전달
  - 각 단계의 부하를 독립적으로 스케일링 가능
  - DPI, 방화벽, 암호화 등 복잡한 처리에 적합
  - 단점: Ring 통과 시 지연 추가, NUMA 크로싱 주의
/* Run-to-Completion 모델 구현 예시 (RSS 기반 L2 포워딩) */

/* 포트 설정: 코어 수만큼 RX/TX 큐 생성 */
struct rte_eth_conf port_conf = {
    .rxmode = {
        .mq_mode = RTE_ETH_MQ_RX_RSS,    /* RSS 활성화 */
    },
    .rx_adv_conf = {
        .rss_conf = {
            .rss_hf = RTE_ETH_RSS_IP | RTE_ETH_RSS_TCP | RTE_ETH_RSS_UDP,
        },
    },
};
rte_eth_dev_configure(port_id, nb_cores, nb_cores, &port_conf);

/* 각 코어에 RX/TX 큐 1개씩 할당 */
for (int q = 0; q < nb_cores; q++) {
    rte_eth_rx_queue_setup(port_id, q, 1024, socket_id, NULL, mbuf_pool);
    rte_eth_tx_queue_setup(port_id, q, 1024, socket_id, NULL);
}

/* 워커 함수: 각 코어가 독립적으로 실행 */
static int
worker_main(void *arg)
{
    unsigned core_id = rte_lcore_id();
    unsigned queue_id = core_to_queue[core_id];

    while (!force_quit) {
        struct rte_mbuf *pkts[32];
        uint16_t nb_rx = rte_eth_rx_burst(port_id, queue_id, pkts, 32);

        for (uint16_t i = 0; i < nb_rx; i++) {
            struct rte_ether_hdr *eth = rte_pktmbuf_mtod(pkts[i],
                                        struct rte_ether_hdr *);
            /* MAC 주소 스왑 (L2 포워딩) */
            struct rte_ether_addr tmp = eth->dst_addr;
            eth->dst_addr = eth->src_addr;
            eth->src_addr = tmp;
        }

        uint16_t nb_tx = rte_eth_tx_burst(port_id, queue_id, pkts, nb_rx);
        for (uint16_t i = nb_tx; i < nb_rx; i++)
            rte_pktmbuf_free(pkts[i]);
    }
    return 0;
}

/* 모든 워커 코어에서 실행 시작 */
rte_eal_mp_remote_launch(worker_main, NULL, CALL_MAIN);

AF_XDP PMD — 커널 통합 고성능 경로

AF_XDP는 DPDK의 완전한 커널 바이패스와 커널 네트워크 스택 사이의 중간 지점입니다. 커널의 XDP 훅에서 패킷을 유저 공간으로 제로카피 전달하므로, VFIO 바인딩 없이 커널 드라이버를 유지하면서 고성능 패킷 처리가 가능합니다.

비교 항목DPDK (VFIO PMD)DPDK (AF_XDP PMD)커널 XDP
커널 바이패스완전 바이패스부분 바이패스 (XDP 훅까지 커널)커널 내 처리
NIC 드라이버DPDK PMD (유저 공간)커널 NIC 드라이버 유지커널 NIC 드라이버
커널 기능사용 불가 (tc, iptables, tcpdump...)부분 사용 가능 (같은 NIC의 다른 큐)모두 사용 가능
성능최고 (~14.88 Mpps/core @10G)높음 (~10 Mpps/core @10G)높음 (~24 Mpps @XDP_TX)
IOMMU필요 (VFIO)불필요불필요
특권CAP_SYS_RAWIOCAP_NET_RAW / CAP_BPFCAP_BPF
NIC 공유불가 (DPDK 전용)가능 (큐별 분리)가능
설정 복잡도VFIO 바인딩, hugepageXDP 프로그램 로드, UMEM 설정XDP 프로그램만
# ━━━ AF_XDP PMD로 DPDK 실행 ━━━

# NIC은 커널 드라이버에 그대로 유지 (VFIO 바인딩 불필요!)
ip link show ens3f0
# ens3f0: ... state UP ... driver: i40e

# AF_XDP PMD로 DPDK 앱 실행
dpdk-l2fwd -l 0-3 -n 4 \
  --vdev net_af_xdp0,iface=ens3f0,start_queue=0,queue_count=4 \
  -- -p 0x1

# 제로카피 모드 (NIC 드라이버가 지원하는 경우)
# i40e, ice, mlx5 등이 AF_XDP 제로카피 지원
dpdk-l2fwd -l 0-3 -n 4 \
  --vdev net_af_xdp0,iface=ens3f0,start_queue=0,queue_count=4 \
  -- -p 0x1
# 제로카피는 NIC 드라이버가 자동 감지하여 활성화
/* AF_XDP 제로카피 패킷 흐름 */

  NIC RX Queue ─── DMA ───→ UMEM (hugepage 공유 메모리)
                                    │
                              XDP 프로그램
                              (XDP_REDIRECT)
                                    │
                                    ↓
                            AF_XDP Socket
                            (Fill/Comp/RX/TX Ring)
                                    │
                                    ↓
                          DPDK af_xdp PMD
                          rte_eth_rx_burst()
                                    │
                                    ↓
                          DPDK Application

  - UMEM: 유저 공간과 커널이 공유하는 메모리 영역 (hugepage 기반)
  - Fill Ring: 앱 → 커널. 빈 버퍼 주소 전달 (NIC DMA 타겟)
  - Completion Ring: 커널 → 앱. TX 완료된 버퍼 반환
  - RX Ring: 커널 → 앱. 수신 패킷의 UMEM 오프셋 전달
  - TX Ring: 앱 → 커널. 전송할 패킷의 UMEM 오프셋 전달
  - 제로카피: NIC가 UMEM에 직접 DMA → 복사 없이 앱에 전달

OVS-DPDK (Open vSwitch + DPDK)

OVS-DPDK는 Open vSwitch의 데이터플레인을 DPDK로 교체하여 가상 스위칭 성능을 극대화한 구성입니다. 클라우드/NFV 환경에서 VM/컨테이너 간 네트워크 트래픽을 유저 공간에서 처리하여, 기존 커널 OVS 대비 5~10배의 처리량을 달성합니다.

/* OVS-DPDK 아키텍처 */

  VM1 (virtio)    VM2 (virtio)    VM3 (vhost-user)     물리 NIC
  ┌──────────┐   ┌──────────┐   ┌──────────────┐   ┌──────────────┐
  │  Guest   │   │  Guest   │   │   Guest      │   │   25GbE      │
  │  virtio  │   │  virtio  │   │   virtio     │   │   (VFIO)     │
  └────┬─────┘   └────┬─────┘   └──────┬───────┘   └──────┬───────┘
       │              │                │                   │
  vhost-user     vhost-user       vhost-user          DPDK PMD
  socket         socket           socket
       │              │                │                   │
  ┌────┴──────────────┴────────────────┴───────────────────┴───────┐
  │                    OVS-DPDK (vswitchd)                         │
  │  ┌──────────────────────────────────────────────────────────┐  │
  │  │  DPDK Datapath (PMD threads)                             │  │
  │  │  - EMC (Exact Match Cache): 가장 빈번한 플로우 캐시       │  │
  │  │  - dpcls (Datapath Classifier): 튜플 기반 플로우 매칭     │  │
  │  │  - upcall → ofproto: 미스 시 OpenFlow 테이블 참조         │  │
  │  └──────────────────────────────────────────────────────────┘  │
  │  ┌──────────────────────────────────────────────────────────┐  │
  │  │  OpenFlow Pipeline (ofproto)                             │  │
  │  │  Table 0 → Table 1 → ... → Actions                      │  │
  │  └──────────────────────────────────────────────────────────┘  │
  └────────────────────────────────────────────────────────────────┘

  - PMD threads: 전용 CPU 코어에서 busy-poll로 모든 포트 폴링
  - vhost-user: UNIX 소켓으로 QEMU와 공유 메모리 설정, 게스트 virtio 큐에 직접 접근
  - EMC hit rate가 높을수록 성능 극대화 (해시 기반 O(1) 조회)
# ━━━ OVS-DPDK 설정 예시 ━━━

# 1. OVS에 DPDK 초기화 파라미터 설정
ovs-vsctl --no-wait set Open_vSwitch . \
  other_config:dpdk-init=true \
  other_config:dpdk-socket-mem="1024,1024" \
  other_config:dpdk-lcore-mask=0x3 \
  other_config:dpdk-hugepage-dir="/dev/hugepages"

# 2. OVS 데몬 재시작
systemctl restart openvswitch-switch

# 3. DPDK 브릿지 생성
ovs-vsctl add-br br0 -- set bridge br0 datapath_type=netdev

# 4. DPDK 물리 포트 추가 (VFIO 바인딩된 NIC)
ovs-vsctl add-port br0 dpdk-p0 -- set Interface dpdk-p0 \
  type=dpdk options:dpdk-devargs=0000:03:00.0

# 5. vhost-user 포트 추가 (VM 연결용)
ovs-vsctl add-port br0 vhost-user0 -- set Interface vhost-user0 \
  type=dpdkvhostuserclient \
  options:vhost-server-path="/tmp/vhost-user0"

# 6. PMD 스레드 CPU 할당 (NUMA 인식)
ovs-vsctl set Open_vSwitch . other_config:pmd-cpu-mask=0x3C

# 7. 플로우 설정 (OpenFlow)
ovs-ofctl add-flow br0 "in_port=dpdk-p0,actions=output:vhost-user0"
ovs-ofctl add-flow br0 "in_port=vhost-user0,actions=output:dpdk-p0"

# ━━━ 성능 확인 ━━━
ovs-appctl dpif-netdev/pmd-stats-show   # PMD 스레드 통계
ovs-appctl dpif-netdev/pmd-rxq-show     # RX 큐 매핑
ovs-appctl dpctl/dump-flows             # 데이터패스 플로우
ovs-appctl coverage/show                # 내부 이벤트 카운터

DPDK 성능 튜닝

튜닝 항목설정효과
CPU 격리 isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15 DPDK 코어에서 커널 스케줄러/RCU/타이머 인터럽트 제거. jitter 최소화
1GB Hugepage hugepagesz=1G hugepages=8 2MB 대비 TLB 미스 99% 감소. IOTLB 효율도 향상
NUMA 정렬 NIC과 같은 NUMA 노드의 코어/메모리 사용 크로스-NUMA 접근 시 40~100ns 추가 지연 방지
RX/TX burst 크기 32~64 (2의 거듭제곱) 벡터화 PMD 경로 활성화. prefetch 효율 극대화
Descriptor ring 크기 2048~4096 마이크로버스트 흡수. 너무 크면 캐시 효율 저하
Mempool 캐시 per-core 캐시 256~512 공용 Ring 접근 빈도 감소. 캐시 > burst_size × 1.5 권장
하이퍼스레딩 비활성화 또는 sibling 회피 HT sibling이 같은 L1/L2 캐시를 공유하여 캐시 오염
전력 관리 intel_pstate=disable processor.max_cstate=1 C-state 전환 지연 제거 (C6→C0 복귀에 ~100μs)
IRQ 밸런싱 irqbalance 중지, DPDK 코어에서 IRQ 배제 폴링 코어에 인터럽트 간섭 제거
PCIe MMIO 최적화 Write-Combining 활성화 (RTE_ETH_TX_OFFLOAD_WC) TX tail 레지스터 쓰기를 배치하여 PCIe 트랜잭션 감소
# ━━━ 시스템 레벨 DPDK 성능 튜닝 ━━━

# CPU 격리 (GRUB 커맨드라인)
# GRUB_CMDLINE_LINUX="isolcpus=4-15 nohz_full=4-15 rcu_nocbs=4-15 \
#   intel_pstate=disable processor.max_cstate=1 intel_idle.max_cstate=0 \
#   default_hugepagesz=1G hugepagesz=1G hugepages=8 \
#   iommu=pt intel_iommu=on"

# NUMA 토폴로지 확인 — NIC이 어느 NUMA 노드에 있는지
cat /sys/bus/pci/devices/0000:03:00.0/numa_node
# 0  → NUMA 노드 0의 코어와 메모리를 사용해야 함

# 해당 NUMA 노드의 코어 확인
lscpu | grep "NUMA node0"
# NUMA node0 CPU(s): 0-7,16-23

# irqbalance 비활성화 + DPDK 코어에서 IRQ 배제
systemctl stop irqbalance
# /proc/irq/default_smp_affinity에서 DPDK 코어 제외
echo 000F > /proc/irq/default_smp_affinity  # 코어 0-3만 IRQ 허용

# CPU 주파수 고정 (Turbo Boost는 유지, P-state 전환 최소화)
cpupower -c 4-15 frequency-set -g performance

# 코어별 C-state 확인
turbostat --quiet --show Core,CPU,Busy%,Bzy_MHz,IRQ,C1%,C6% sleep 1

Eventdev — 이벤트 기반 패킷 스케줄링

rte_eventdev는 DPDK의 이벤트 기반 프로그래밍 모델로, 하드웨어 이벤트 스케줄러(Intel DLB2 등)나 소프트웨어 구현을 통해 패킷을 워커 코어에 동적으로 분배합니다. Run-to-Completion과 Pipeline 모델의 장점을 결합합니다.

/* Eventdev 동작 모델 */

  RX Adapter              Event Scheduler              TX Adapter
  (NIC RX → 이벤트)       (이벤트 → 워커 분배)          (이벤트 → NIC TX)
  ┌──────────┐           ┌──────────────┐              ┌──────────┐
  │ Port 0   │──event──→│  Atomic Queue │──event──→   │ Port 0   │
  │ Port 1   │          │  Ordered Queue│──event──→   │ Port 1   │
  └──────────┘          │  Parallel Que.│──event──→   └──────────┘
                        └──────────────┘
                              │
                   ┌──────────┼──────────┐
                   ↓          ↓          ↓
              Worker 0   Worker 1   Worker 2
              (Core 4)   (Core 5)   (Core 6)

  큐 타입:
  - Atomic: 같은 플로우는 항상 같은 워커에 전달 (순서 보장, 락 불필요)
  - Ordered: 순서 보장하면서 병렬 처리. 출력 시 원래 순서로 재정렬
  - Parallel: 순서 무관, 최대 병렬성

  장점:
  - 트래픽 부하에 따라 워커 수 동적 조절 가능
  - Atomic 큐로 플로우별 락 없는 처리 보장
  - 하드웨어 스케줄러(DLB2)는 CPU 오버헤드 없이 이벤트 분배

DPDK 멀티프로세스 모드

DPDK는 primary-secondary 프로세스 모델을 지원합니다. Primary 프로세스가 EAL 초기화, 포트 설정, Mempool 생성을 수행하고, Secondary 프로세스가 공유 메모리(hugepage)를 통해 동일한 자원에 접근합니다.

/* DPDK 멀티프로세스 아키텍처 */

  Primary Process                  Secondary Process(es)
  ┌──────────────────────┐        ┌──────────────────────┐
  │ rte_eal_init()       │        │ rte_eal_init(         │
  │  --proc-type primary │        │  --proc-type secondary│
  │                      │        │  --file-prefix dpdk0) │
  │ - Hugepage 매핑 생성 │        │ - 같은 Hugepage 매핑  │
  │ - Mempool 생성       │        │ - Mempool 룩업        │
  │ - 포트 설정/시작     │        │ - 포트 룩업           │
  │ - Ring 생성          │        │ - Ring 룩업           │
  └──────────┬───────────┘        └──────────┬───────────┘
             │                               │
             └───────────┬───────────────────┘
                         ↓
              Shared Hugepage Memory
              ┌──────────────────────────────┐
              │ Mempool, Ring, mbuf           │
              │ (물리 주소 동일, VA도 동일)    │
              │ rte_config 공유 메모리         │
              └──────────────────────────────┘

  사용 사례:
  - 패킷 캡처: primary가 포워딩, secondary가 pdump로 미러링
  - 무중단 업그레이드: 새 secondary가 시작된 후 기존 교체
  - 모니터링: secondary가 통계/텔레메트리 수집
# 멀티프로세스 실행 예시

# Primary 프로세스 시작
dpdk-l2fwd -l 0-3 -n 4 --proc-type primary -- -p 0x3

# Secondary 프로세스 (패킷 덤프)
dpdk-pdump -l 4 --proc-type secondary -- \
  --pdump 'port=0,queue=*,rx-dev=/tmp/rx.pcap'

# Secondary 프로세스 (통계 모니터링)
dpdk-proc-info --proc-type secondary -- --stats

DPDK 디버깅과 모니터링

도구용도사용법
dpdk-proc-info 포트 통계, 큐 통계, 메모리 사용량 dpdk-proc-info -- --stats --xstats
dpdk-pdump 패킷 캡처 (pcap 형식) dpdk-pdump -- --pdump 'port=0,...'
dpdk-telemetry.py JSON 기반 텔레메트리 API UNIX 소켓으로 런타임 통계 조회
dpdk-devbind.py PCI 디바이스 바인딩 관리 dpdk-devbind.py --status
dpdk-testpmd 포트 기능 테스트, 성능 벤치마크 dpdk-testpmd -- -i --forward-mode=io
rte_eth_stats_get() 프로그래밍 방식 통계 수집 RX/TX 패킷/바이트/에러 카운터
# ━━━ DPDK 디버깅/모니터링 명령 모음 ━━━

# 포트 통계 (기본 + 확장)
dpdk-proc-info -- --stats
dpdk-proc-info -- --xstats  # NIC별 상세 카운터 (rx_good_bytes, tx_errors, ...)

# 실시간 패킷 캡처 (secondary 프로세스)
dpdk-pdump -l 8 --proc-type secondary -- \
  --pdump 'port=0,queue=*,rx-dev=/tmp/capture.pcap'
# 캡처 파일을 tcpdump/Wireshark로 분석
tcpdump -r /tmp/capture.pcap

# 텔레메트리 API (DPDK 20.05+)
# UNIX 소켓 경로: /var/run/dpdk/rte/dpdk_telemetry.vX
dpdk-telemetry.py
# 대화형 쿼리:
# --> /ethdev/stats,0
# --> /ethdev/xstats,0
# --> /eal/params
# --> /mempool/list

# testpmd로 성능 측정
dpdk-testpmd -l 0-3 -n 4 -a 0000:03:00.0 -- \
  -i \
  --forward-mode=io \
  --rxq=4 --txq=4 \
  --nb-cores=3 \
  --burst=32

# testpmd 내부 명령:
# testpmd> start
# testpmd> show port stats all
# testpmd> show fwd stats all
# testpmd> show port xstats 0
# testpmd> stop

# Mempool 상태 확인
dpdk-proc-info -- --mempool=MBUF_POOL

# EAL 로그 레벨 (런타임 조정)
# --log-level=lib.eal:8   (DEBUG)
# --log-level=pmd.net.mlx5:7  (INFO)
/* 프로그래밍 방식 통계 수집 */
struct rte_eth_stats stats;
rte_eth_stats_get(port_id, &stats);

printf("Port %u: RX %lu pkts (%lu bytes) TX %lu pkts (%lu bytes)\n",
       port_id,
       stats.ipackets, stats.ibytes,
       stats.opackets, stats.obytes);
printf("  RX errors: %lu  TX errors: %lu  RX no-mbuf: %lu\n",
       stats.ierrors, stats.oerrors, stats.rx_nombuf);

/* 확장 통계 (xstats): NIC별 상세 카운터 */
int len = rte_eth_xstats_get(port_id, NULL, 0);
struct rte_eth_xstat *xstats = malloc(len * sizeof(*xstats));
struct rte_eth_xstat_name *names = malloc(len * sizeof(*names));
rte_eth_xstats_get(port_id, xstats, len);
rte_eth_xstats_get_names(port_id, names, len);

for (int i = 0; i < len; i++)
    printf("  %s: %lu\n", names[i].name, xstats[i].value);

VPP (FD.io) — 유저 공간 네트워크 스택

VPP(Vector Packet Processing)는 Cisco가 개발한 고성능 유저 공간 네트워크 스택으로, DPDK PMD 위에서 동작하며 L2~L4 스위칭/라우팅, NAT, IPSec, ACL 등 커널 네트워크 스택의 기능을 유저 공간에서 구현합니다.

비교 항목OVS-DPDKVPP/FD.io
주요 용도L2 가상 스위칭 (OpenFlow)L2~L4 라우팅/NAT/IPSec
패킷 처리플로우 테이블 매칭벡터 그래프 (노드 체인)
처리 모델플로우 캐시 + upcall벡터화: 같은 노드를 256패킷 배치로 처리
I-cache 효율보통 (플로우별 분기)높음 (동일 코드를 벡터 크기만큼 반복)
기능L2 스위칭 특화L2~L4, NAT44/NAT64, SRv6, IPSec, MPLS, VXLAN 등
설정OpenFlow / OVNCLI / API (VPP API, NETCONF/YANG)
성능 (64B)~10 Mpps/core~15 Mpps/core (벡터화 효과)

DPDK 관련 커널 소스 구조

DPDK 자체는 유저 공간 라이브러리이지만, 커널 측에서 DPDK 동작을 지원하는 핵심 구성 요소:

커널 경로역할DPDK 관련성
drivers/vfio/VFIO 프레임워크PCIe 디바이스를 유저 공간에 안전하게 노출
drivers/uio/UIO 프레임워크레거시 디바이스 접근 (IOMMU 없는 환경)
net/xdp/AF_XDP 소켓커널 기반 제로카피 패킷 전달
mm/hugetlb.cHugepage 관리DPDK 메모리 관리의 기반
drivers/iommu/IOMMU (VT-d/AMD-Vi)VFIO DMA 격리, IOVA 매핑
kernel/irq/IRQ 관리MSI/MSI-X eventfd (VFIO 인터럽트)
drivers/net/NIC 커널 드라이버AF_XDP PMD가 커널 드라이버의 XDP 지원에 의존
# DPDK 관련 커널 설정 (CONFIG_*)

# VFIO (권장)
CONFIG_VFIO=m
CONFIG_VFIO_PCI=m
CONFIG_VFIO_IOMMU_TYPE1=m
CONFIG_VFIO_NOIOMMU=y          # no-IOMMU 모드 (테스트용)

# UIO (레거시)
CONFIG_UIO=m
CONFIG_UIO_PCI_GENERIC=m

# IOMMU
CONFIG_IOMMU_SUPPORT=y
CONFIG_INTEL_IOMMU=y            # Intel VT-d
CONFIG_AMD_IOMMU=y              # AMD-Vi
CONFIG_IOMMU_DEFAULT_DMA_LAZY=y # IOVA 지연 해제 (성능)

# Hugepage
CONFIG_HUGETLBFS=y
CONFIG_HUGETLB_PAGE=y
CONFIG_TRANSPARENT_HUGEPAGE=y   # THP (DPDK는 명시적 hugetlbfs 선호)

# AF_XDP
CONFIG_XDP_SOCKETS=y
CONFIG_BPF_SYSCALL=y
CONFIG_NET_CLS_BPF=m

# NUMA
CONFIG_NUMA=y
CONFIG_NUMA_BALANCING=y
DPDK 학습 순서 권장:
  • 1단계dpdk-testpmd로 기본 포트 동작 이해 (io/mac/macswap 모드)
  • 2단계examples/l2fwd 소스 분석 (가장 단순한 DPDK 앱, ~300줄)
  • 3단계examples/l3fwd 분석 (LPM/EM 라우팅, RSS 활용)
  • 4단계rte_ring, rte_mempool 내부 구현 분석
  • 5단계 — PMD 소스 분석 (drivers/net/ixgbe/ 또는 drivers/net/i40e/)
  • 6단계 — Eventdev, Cryptodev 등 고급 프레임워크
관련 문서:

xfrm 프레임워크와 IPSec 심화

xfrm(transform)은 리눅스 커널의 IPSec 구현 프레임워크입니다. 패킷의 암호화, 인증, 압축, 캡슐화를 처리하며, SA(Security Association)와 SP(Security Policy) 데이터베이스로 관리됩니다.

xfrm 아키텍처

User Space IKE Daemon (strongSwan/Libreswan) XFRM Netlink (AF_KEY/XFRM) SPD SAD xfrm_state (SA: 키, 알고리즘, SPI) xfrm_policy (SP: 셀렉터, 방향) ESP / AH / IPCOMP Security Policy DB Security Association DB Netfilter (PREROUTING / POSTROUTING) → xfrm lookup → encrypt/decrypt → route net/xfrm/xfrm_input.c | net/xfrm/xfrm_output.c | net/ipv4/esp4.c | net/ipv6/esp6.c

IPSec 프로토콜 비교

프로토콜IP 번호기능보호 범위주의사항
ESP 50 암호화 + 인증 (선택적) 페이로드 전체 (터널: 원본 IP 헤더 포함) NAT 환경에서 UDP 캡슐화(port 4500) 필요. 가장 보편적
AH 51 인증만 (암호화 없음) IP 헤더 포함 전체 패킷 (변경 가능 필드 제외) NAT와 호환 불가 (IP 헤더가 인증 범위). 현대 환경에서 거의 미사용
IPCOMP 108 페이로드 압축 ESP/AH 전에 페이로드 압축 압축 효과 없으면 원본 전송. ESP와 조합 사용

터널 모드 vs 트랜스포트 모드

# 트랜스포트 모드: 호스트-to-호스트, 원본 IP 헤더 유지
# [IP Header][ESP Header][Payload (encrypted)][ESP Trailer][ESP Auth]
ip xfrm state add src 10.0.0.1 dst 10.0.0.2     proto esp spi 0x100 mode transport     enc "aes" 0x$(openssl rand -hex 16)     auth "hmac(sha256)" 0x$(openssl rand -hex 32)

# 터널 모드: 게이트웨이-to-게이트웨이, 원본 패킷 전체 캡슐화
# [New IP][ESP Header][Original IP][Payload (encrypted)][ESP Trailer][Auth]
ip xfrm state add src 203.0.113.1 dst 198.51.100.1     proto esp spi 0x200 mode tunnel     enc "aes" 0x$(openssl rand -hex 16)     auth "hmac(sha256)" 0x$(openssl rand -hex 32)

# Security Policy (어떤 트래픽에 IPSec 적용할지)
ip xfrm policy add src 192.168.1.0/24 dst 192.168.2.0/24 dir out     tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode tunnel

# 현재 SA/SP 확인
ip xfrm state list    # SA 목록 (키, SPI, 알고리즘)
ip xfrm policy list   # SP 목록 (셀렉터, 방향)
ip xfrm monitor       # 실시간 xfrm 이벤트 모니터링

커널 xfrm 내부 구조

/* net/xfrm/xfrm_state.c — Security Association */
struct xfrm_state {
    struct xfrm_id id;            /* (daddr, spi, proto) */
    xfrm_address_t saddr;        /* 소스 주소 */
    struct xfrm_lifetime_cfg lft; /* 수명: 바이트, 패킷, 시간 */

    struct xfrm_algo_auth *aalg;  /* 인증 알고리즘 (HMAC-SHA256 등) */
    struct xfrm_algo *ealg;       /* 암호 알고리즘 (AES-CBC 등) */
    struct xfrm_algo *calg;       /* 압축 알고리즘 (deflate 등) */
    struct xfrm_algo_aead *aead;  /* AEAD (AES-GCM 등) */

    u8 mode;                      /* XFRM_MODE_TRANSPORT / TUNNEL */
    u32 replay_maxdiff;           /* Anti-replay 윈도우 */
    struct xfrm_replay_state_esn *replay_esn;

    /* H/W offload (SmartNIC inline crypto) */
    struct xfrm_dev_offload xso;
};

/* net/ipv4/esp4.c — ESP 패킷 처리 */
/* esp_output(): 송신 패킷 암호화 (POSTROUTING 단계) */
/* esp_input():  수신 패킷 복호화 (PREROUTING 단계) */

권장 알고리즘 조합

용도암호화인증비고
권장 (AEAD) AES-GCM-128/256 (내장) 단일 패스 암호화+인증. H/W 가속 최적. 현대 표준
레거시 호환 AES-CBC-256 HMAC-SHA-256 구형 장비 호환. CBC는 패딩 오라클 취약점 주의
고성능 (ChaCha20) ChaCha20-Poly1305 (내장) AES-NI 없는 환경에서 고성능. ARM 모바일에 적합
금지 DES, 3DES, NULL MD5, SHA-1 보안 취약. 커널에서 지원하지만 사용 금지

IPSec/xfrm 주의사항

IPSec 운영 시 핵심 고려사항:
  • MTU/PMTUD — ESP 캡슐화로 패킷 크기 증가 (ESP: +36~73바이트, 터널 모드: +20 추가). PMTUD 실패 시 블랙홀 발생. ip link set dev ipsec0 mtu 1400 또는 MSS clamping 필요
  • NAT Traversal — ESP는 IP 프로토콜이라 NAT 통과 불가. NAT-T(UDP 4500 캡슐화)를 IKE에서 자동 감지/활성화해야 함
  • Anti-replay 윈도우 — 기본 32패킷. 고대역 환경에서 패킷 재정렬로 정상 패킷이 드롭될 수 있음. replay-window 1024 이상 권장
  • SA 수명 관리 — 키 재생성(rekey) 시 트래픽 순간 단절 가능. IKEv2의 CHILD_SA rekey가 seamless 하지만 구현 의존
  • CPU 오버헤드 — 소프트웨어 ESP 암호화는 CPU 집약적. 10Gbps 환경에서 CPU 포화 가능. QAT/SmartNIC 오프로드 활용
  • conntrack 상호작용 — ESP 패킷의 conntrack 처리. 터널 모드에서는 외부/내부 패킷 각각 conntrack 엔트리 생성
  • Policy routing 충돌 — xfrm policy와 ip rule/route의 우선순위 상호작용 주의. ip xfrm policy list로 정책 순서 확인
  • VTI vs xfrm interface — VTI(가상 터널 인터페이스)는 레거시. 커널 4.19+의 xfrm interface(if_id 기반)가 더 유연하고 netns 지원

IPSec 디버깅

# xfrm 통계 — 오류 원인 파악
ip -s xfrm state   # SA별 패킷/바이트 카운터
ip -s xfrm policy  # SP별 매칭 카운터
cat /proc/net/xfrm_stat
# XfrmInError: 복호화/인증 실패
# XfrmInNoStates: 매칭 SA 없음
# XfrmOutPolBlock: SP에 의해 차단
# XfrmOutBundleGenError: SA 번들 생성 실패

# 패킷 캡처 (ESP 헤더 확인)
tcpdump -i eth0 esp
tcpdump -i eth0 'ip proto 50'  # ESP
tcpdump -i eth0 'ip proto 51'  # AH

# IKE 데몬 로그 (strongSwan)
swanctl --log --level 2

ESP 패킷 형식 상세

ESP(Encapsulating Security Payload, IP 프로토콜 50)는 현대 IPSec의 핵심 프로토콜입니다. 트랜스포트 모드와 터널 모드에서 패킷 구조가 다르며, AEAD 알고리즘 사용 여부에 따라 내부 처리도 달라집니다.

ESP 패킷 구조 — 터널 모드 New IP Hdr (20B) ESP Header SPI(4B) + Seq#(4B) IV (8~16B) Orig IP Hdr (20B) Payload (가변) ESP Trailer Pad + PadLen + NH ICV (12~16B) ← 암호화 범위 (Encrypted) → ← 인증 범위 (Authenticated) → ESP 패킷 구조 — 트랜스포트 모드 Orig IP Hdr (수정됨) ESP Header SPI(4B) + Seq#(4B) IV (8~16B) TCP/UDP + Payload (가변) ESP Trailer Pad + PadLen + NH ICV (12~16B) 트랜스포트 모드: 원본 IP 헤더 유지 (protocol 필드만 50으로 변경) 터널 모드: 새 IP 헤더 추가, 원본 패킷 전체 암호화 AEAD(AES-GCM): IV = Salt(4B) + Nonce(8B), ICV = 16B, Pad/PadLen/NH도 인증 범위에 포함
/* ESP 헤더 (RFC 4303) — include/uapi/linux/ip.h */
struct ip_esp_hdr {
    __be32 spi;        /* Security Parameters Index — SA 식별 */
    __be32 seq_no;     /* 시퀀스 번호 (Anti-replay용, 단조 증가) */
    __u8   enc_data[]; /* 가변 길이: IV + 암호화된 페이로드 */
};

/* ESP Trailer (암호화 영역 끝에 위치) */
/*   [Padding (0~255 bytes)]     — 블록 정렬용 */
/*   [Pad Length (1 byte)]       — 패딩 바이트 수 */
/*   [Next Header (1 byte)]      — 원본 프로토콜 (TCP=6, UDP=17 등) */
/*   [ICV (12~16 bytes)]         — Integrity Check Value (MAC) */

/* ESN (Extended Sequence Number, RFC 4304) */
/* 32비트 시퀀스 번호는 10Gbps에서 ~7분 만에 소진 */
/* ESN은 64비트로 확장: 상위 32비트는 패킷에 미포함, ICV 계산에만 사용 */
struct xfrm_replay_state_esn {
    __u32 bmp_len;     /* 비트맵 길이 (워드 수) */
    __u32 oseq;        /* 송신 시퀀스 (하위 32비트) */
    __u32 seq;         /* 수신 시퀀스 (하위 32비트) */
    __u32 oseq_hi;     /* 송신 시퀀스 (상위 32비트) */
    __u32 seq_hi;      /* 수신 시퀀스 (상위 32비트) */
    __u32 replay_window; /* Anti-replay 윈도우 크기 */
    __u32 bmp[];       /* 수신 비트맵 (가변 길이) */
};
AEAD vs 개별 암호화+인증: AEAD(AES-GCM, ChaCha20-Poly1305)는 단일 패스로 암호화와 인증을 동시 수행하여 성능 우수. 개별 모드(AES-CBC + HMAC-SHA256)는 encrypt-then-MAC 순서로 두 번 처리. 커널에서 AEAD는 crypto_aead API를, 개별 모드는 crypto_skcipher + crypto_ahash를 사용합니다. AES-GCM의 IV는 Salt(4B, SA 생성 시 고정) + Nonce(8B, 패킷마다 증가)로 구성되며, ICV는 항상 16바이트(128비트)입니다.

AH 패킷 형식과 한계

AH(Authentication Header, IP 프로토콜 51)는 패킷의 무결성과 인증을 제공하지만 암호화는 하지 않습니다. IP 헤더를 포함한 전체 패킷이 인증 범위에 포함되는 것이 ESP와의 핵심 차이점이며, 이것이 NAT 환경과 호환되지 않는 근본 원인입니다.

/* AH 헤더 (RFC 4302) — include/uapi/linux/ip_auth.h */
struct ip_auth_hdr {
    __u8   nexthdr;     /* 다음 헤더 (TCP=6, ESP=50 등) */
    __u8   hdrlen;      /* 헤더 길이 (32비트 워드 단위 - 2) */
    __be16 reserved;    /* 예약 (0) */
    __be32 spi;         /* Security Parameters Index */
    __be32 seq_no;      /* 시퀀스 번호 */
    __u8   auth_data[]; /* ICV — 가변 길이 (알고리즘에 따라) */
};

/* AH 인증 범위: IP 헤더 전체 + AH 헤더 + 페이로드 */
/* 단, 변경 가능(mutable) 필드는 0으로 치환 후 MAC 계산: */
/*   - TTL (hop마다 감소)          */
/*   - Header Checksum (TTL 변경 시 재계산) */
/*   - TOS/DSCP (라우터가 변경 가능) */
/*   - Flags (Fragment offset)     */
AH가 현대 환경에서 폐기된 이유:
  • NAT 비호환 — NAT는 IP 헤더의 src/dst 주소를 변경하는데, AH는 IP 헤더를 인증 범위에 포함. NAT 통과 시 ICV 검증 실패. NAT-T(UDP 캡슐화)도 AH에는 적용 불가
  • 암호화 부재 — AH는 인증만 제공. ESP는 인증+암호화 모두 가능하므로 AH가 할 수 있는 것을 ESP가 모두 포함(ESP의 NULL 암호화 + 인증 = AH 동등)
  • 성능 패널티 — mutable 필드를 0으로 치환하는 추가 처리. ESP 대비 실질적 이점 없이 복잡도만 증가
  • RFC 7321 — IPSec 알고리즘 요구사항에서 AH를 MAY(선택)로 격하. IKEv2 구현에서 AH 지원은 필수가 아님

IKE 프로토콜과 SA 협상

IKE(Internet Key Exchange)는 IPSec SA의 자동 협상 프로토콜입니다. 커널의 xfrm 프레임워크는 데이터 평면(패킷 암호화/복호화)만 처리하며, SA 생성/삭제/갱신의 제어 평면은 유저스페이스 IKE 데몬(strongSwan, Libreswan)이 Netlink를 통해 커널에 주입합니다.

특성IKEv1 (RFC 2409)IKEv2 (RFC 7296)
교환 횟수 Phase 1: 6~9 메시지 (Main/Aggressive)
Phase 2: 3 메시지 (Quick Mode)
IKE_SA_INIT: 2 메시지
IKE_AUTH: 2 메시지
총 4 메시지로 완료
NAT-T 지원 확장(RFC 3947)으로 추가, 복잡 프로토콜에 내장 (NAT Detection payload)
인증 방식 PSK, RSA Signature, XAUTH(확장) PSK, RSA/ECDSA Signature, EAP (내장)
DPD (Dead Peer) 확장(RFC 3706), 선택적 구현 내장 (Informational Exchange)
MOBIKE 미지원 RFC 4555: IP 변경 시 SA 유지 (로밍)
CHILD_SA rekey Phase 2 재협상 (일시 중단 가능) CREATE_CHILD_SA로 무중단 rekey
상태 레거시, 신규 배포 권장하지 않음 현행 표준, 모든 신규 배포 권장
IKEv2 교환 흐름 Initiator Responder IKE_SA _INIT SAi1, KEi (DH), Ni (nonce) SAr1, KEr (DH), Nr, [CERTREQ] → SKEYSEED = PRF(Ni|Nr, DH_shared_secret) → SK_d, SK_ai, SK_ar, SK_ei, SK_er 파생 IKE_ AUTH [encrypted] IDi, [CERT], AUTH, SAi2, TSi, TSr IDr, [CERT], AUTH, SAr2, TSi, TSr → IKE SA + 첫 번째 CHILD SA (IPSec SA) 동시 생성 완료 CREATE_ CHILD_SA SA, Ni, [KEi], TSi, TSr (rekey/추가 SA) SA, Nr, [KEr], TSi, TSr PFS: KEi/KEr 포함 시 새 DH 교환 → CHILD SA마다 독립 키 (Forward Secrecy)

Diffie-Hellman 그룹과 PFS: IKE_SA_INIT에서 DH 교환으로 공유 비밀 생성. PFS(Perfect Forward Secrecy)를 활성화하면 CREATE_CHILD_SA에서도 새로운 DH 교환을 수행하여, IKE SA 키가 노출되더라도 개별 CHILD SA(IPSec SA)의 트래픽 키는 보호됩니다. 주요 DH 그룹:

그룹알고리즘강도권장 여부
14MODP 2048-bit~112비트최소 권장
19ECP 256-bit (NIST P-256)~128비트권장
20ECP 384-bit (NIST P-384)~192비트고보안
21ECP 521-bit (NIST P-521)~256비트고보안
31Curve25519~128비트권장 (고성능)

커널과 IKE 데몬 상호작용: IKE 데몬은 AF_NETLINK/NETLINK_XFRM 소켓을 통해 커널 xfrm 서브시스템과 통신합니다. 주요 Netlink 메시지:

/* include/uapi/linux/xfrm.h — 주요 XFRM Netlink 메시지 타입 */
/* SA (Security Association) 관리 */
XFRM_MSG_NEWSA      /* IKE → 커널: SA 생성 (키, 알고리즘, SPI, 모드) */
XFRM_MSG_DELSA      /* IKE → 커널: SA 삭제 */
XFRM_MSG_GETSA      /* IKE → 커널: SA 조회 */
XFRM_MSG_UPDSA      /* IKE → 커널: SA 갱신 (rekey) */

/* SP (Security Policy) 관리 */
XFRM_MSG_NEWPOLICY  /* IKE → 커널: 정책 생성 (셀렉터, 방향, 액션) */
XFRM_MSG_DELPOLICY  /* IKE → 커널: 정책 삭제 */

/* 커널 → IKE 이벤트 (비동기 알림) */
XFRM_MSG_ACQUIRE    /* 커널 → IKE: 매칭 SA 없음, 새 SA 생성 요청 */
XFRM_MSG_EXPIRE     /* 커널 → IKE: SA 수명 만료 (soft/hard) */
XFRM_MSG_MIGRATE    /* MOBIKE: SA를 새 주소로 마이그레이션 */
XFRM_MSG_MAPPING    /* NAT-T: NAT 매핑 변경 알림 */

/* 워크플로 예시:
 * 1. 패킷 도착 → xfrm_policy 매칭 → 해당 SA 없음
 * 2. 커널이 XFRM_MSG_ACQUIRE 전송 → IKE 데몬 수신
 * 3. IKE 데몬이 피어와 IKEv2 교환 수행
 * 4. IKE 데몬이 XFRM_MSG_NEWSA + XFRM_MSG_NEWPOLICY로 SA/SP 커널에 주입
 * 5. 대기 중이던 패킷 처리 재개
 */
DPD와 MOBIKE: DPD(Dead Peer Detection)는 주기적으로 Informational Exchange를 보내 피어 생존을 확인합니다. 응답 없으면 SA를 정리하고 재협상합니다. MOBIKE(RFC 4555)는 모바일 클라이언트가 네트워크를 변경(Wi-Fi → LTE)할 때 IKE SA와 CHILD SA의 주소를 갱신하여 터널을 유지합니다. 커널에서는 XFRM_MSG_MIGRATE로 SA의 주소를 동적으로 변경합니다.

xfrm 패킷 처리 경로

xfrm의 패킷 처리는 Netfilter 훅과 밀접하게 통합되어 있습니다. 송신 경로에서는 라우팅 후 xfrm 정책 검색을 수행하고, 수신 경로에서는 ESP 복호화 후 정책 검증을 거칩니다.

xfrm 송신/수신 패킷 처리 경로 송신 (TX) ip_output() xfrm_lookup() xfrm_output() bundle 적용 esp_output() 암호화+MAC ip_finish_output NIC TX SPD 검색: 셀렉터 매칭 → SA 조회 수신 (RX) NIC RX ip_input() esp_input() 복호화+검증 xfrm_input() SA 매칭 xfrm_policy_check 정책 검증 ip_local_deliver (daddr, spi, proto) → SAD 해시 검색
/* net/xfrm/xfrm_output.c — 송신 경로 핵심 */
static int xfrm_output_one(struct sk_buff *skb, int err)
{
    struct xfrm_state *x = skb_dst(skb)->xfrm;
    /* 1. 시퀀스 번호 할당 (ESN 지원) */
    err = x->outer_mode.output(x, skb);    /* 터널: 외부 IP 헤더 추가 */
    err = x->type->output(x, skb);         /* ESP: esp_output() 호출 */
    /* 2. skb→dst를 외부 라우팅 엔트리로 교체 */
    /* 3. 중첩 SA가 있으면 다음 xfrm_state에 대해 반복 (bundle) */
}

/* net/ipv4/esp4.c — ESP 암호화 처리 */
static int esp_output(struct xfrm_state *x, struct sk_buff *skb)
{
    struct crypto_aead *aead = x->data;
    /* 1. ESP 헤더 (SPI + Seq#) 삽입 */
    /* 2. IV 생성 (AEAD: salt + seq_no) */
    /* 3. 패딩 추가 (블록 크기 정렬) */
    /* 4. aead_request 생성 → crypto_aead_encrypt() */
    /*    → 비동기 완료: esp_output_done() 콜백 */
    /* 5. ICV 첨부 */
}

/* net/xfrm/xfrm_input.c — 수신 경로 핵심 */
int xfrm_input(struct sk_buff *skb, int nexthdr,
              __be32 spi, int encap_type)
{
    /* 1. (daddr, spi, proto)로 SAD 해시 테이블 검색 */
    x = xfrm_state_lookup(net, &daddr, spi, nexthdr, family);
    /* 2. anti-replay 검사 */
    xfrm_replay_check(x, skb, seq);
    /* 3. ESP 복호화: x→type→input() → esp_input() */
    /* 4. anti-replay 윈도우 업데이트 */
    xfrm_replay_advance(x, seq);
    /* 5. 정책 검증: 복호화된 패킷이 SP와 일치하는지 확인 */
    /*    (수신 정책 없으면 드롭 — XfrmInNoPols) */
}

/* xfrm_state 해시 테이블 검색 — O(1) 평균 */
/* 키: (daddr, spi, proto) → 해시 버킷 → 체인 순회 */
/* 대규모 SA 환경에서도 검색 성능 보장 */
xfrm_bundle과 캐싱: xfrm_lookup()에서 정책에 매칭되면 xfrm_bundle_create()가 호출되어 SA 체인(여러 SA를 순서대로 적용: 예컨대 IPCOMP → ESP)을 생성합니다. 이 번들은 dst_entry에 캐싱되어 동일 흐름의 후속 패킷은 정책 검색 없이 바로 SA를 적용합니다. 라우팅 테이블 변경이나 SA 만료 시 캐시가 무효화됩니다.

xfrm_policy 내부 구조

/* include/net/xfrm.h — Security Policy 핵심 구조체 */
struct xfrm_policy {
    struct hlist_node bydst;       /* dst 주소별 해시 체인 */
    struct hlist_node byidx;       /* 인덱스별 해시 체인 */

    struct xfrm_selector selector;  /* 트래픽 셀렉터 (아래 상세) */
    struct xfrm_lifetime_cfg lft;   /* 수명: 바이트/패킷/시간 */
    struct xfrm_lifetime_cur curlft; /* 현재 사용량 카운터 */

    u8 type;           /* XFRM_POLICY_TYPE_MAIN / SUB */
    u8 action;         /* XFRM_POLICY_ALLOW / BLOCK */
    u8 flags;          /* XFRM_POLICY_LOCALOK, ICMP 등 */
    u8 xfrm_nr;        /* tmpl 배열 크기 (최대 6) */
    u16 family;        /* AF_INET / AF_INET6 */
    u32 priority;      /* 정책 우선순위 (낮을수록 높음) */
    u32 if_id;         /* xfrm interface ID (4.19+) */

    struct xfrm_tmpl xfrm_vec[XFRM_MAX_DEPTH]; /* SA 템플릿 */
    /* tmpl: 요구하는 SA의 속성 (proto, mode, reqid, level) */
};

/* 트래픽 셀렉터 — 어떤 패킷에 정책을 적용할지 결정 */
struct xfrm_selector {
    xfrm_address_t daddr;    /* 목적지 주소 */
    xfrm_address_t saddr;    /* 소스 주소 */
    __be16 dport;             /* 목적지 포트 */
    __be16 dport_mask;        /* 포트 마스크 (0xFFFF = exact) */
    __be16 sport;             /* 소스 포트 */
    __be16 sport_mask;
    __u16 family;             /* AF_INET / AF_INET6 */
    __u8 prefixlen_d;         /* 목적지 서브넷 길이 */
    __u8 prefixlen_s;         /* 소스 서브넷 길이 */
    __u8 proto;               /* 프로토콜 (6=TCP, 17=UDP, 0=all) */
    int ifindex;              /* 인터페이스 바인딩 */
    __kernel_uid32_t user;   /* UID 기반 정책 (Android) */
};

SPD 검색 알고리즘: 정책 검색은 3개의 방향(in/out/fwd)별로 독립된 해시 테이블에서 수행됩니다. 패킷의 (src, dst, proto, sport, dport)를 셀렉터와 매칭하며, 여러 정책이 매칭되면 priority가 가장 낮은(= 우선순위 높은) 정책이 선택됩니다.

/* net/xfrm/xfrm_policy.c — SPD 검색 핵심 */
static struct xfrm_policy *
xfrm_policy_lookup_bytype(struct net *net, u8 type,
    const struct flowi *fl, u16 family, u8 dir)
{
    /* 1. (dst_addr, family) 기반 해시 버킷 선택 */
    /* 2. 버킷 내 정책 순회 → 셀렉터 매칭 검사 */
    /*    xfrm_selector_match(sel, fl, family) */
    /* 3. 매칭된 정책 중 priority 최소값 반환 */
    /* 4. action == BLOCK이면 패킷 드롭 (XfrmOutPolBlock) */
    /* 5. action == ALLOW이면 tmpl 배열로 SA 검색 */
}

/* 정책 방향 (dir) */
XFRM_POLICY_IN   0  /* 수신: 복호화 후 정책 검증 */
XFRM_POLICY_OUT  1  /* 송신: 패킷 나가기 전 정책 검색 */
XFRM_POLICY_FWD  2  /* 포워딩: 라우터 역할 시 터널 간 전달 */
xfrm_policy_check()와 Netfilter 통합: 수신 경로에서 xfrm_policy_check()는 Netfilter의 NF_INET_PRE_ROUTING 이후, ip_local_deliver() 이전에 호출됩니다. 복호화된 패킷의 셀렉터가 수신 정책(XFRM_POLICY_IN)과 일치하지 않으면 패킷이 드롭되어, 정책 우회 공격을 방지합니다. 이는 "수신 시에도 반드시 정책 검증"이라는 IPSec의 보안 원칙을 구현합니다.

NAT Traversal (NAT-T) 상세

ESP는 IP 프로토콜 번호 50을 사용하므로, 포트 번호가 없어 일반 NAT가 처리할 수 없습니다. NAT-T(NAT Traversal, RFC 3948)는 ESP 패킷을 UDP 4500 포트로 캡슐화하여 NAT 장비를 통과할 수 있게 합니다.

NAT-T 캡슐화 패킷 구조 일반 ESP: IP (proto=50) ESP Header Encrypted Payload + Trailer + ICV NAT-T ESP: IP (proto=17) UDP (4500→4500) ESP Header Encrypted Payload + Trailer + ICV Keep-alive: IP (proto=17) UDP (4500→4500) 0xFF (1 byte) NAT-T 판별: SPI의 첫 4바이트가 0x00000000이면 IKE, 0xFF 1바이트면 Keep-alive, 그 외 ESP
/* NAT-T 감지: IKEv2 NAT_DETECTION_*_IP payload */
/* IKE_SA_INIT 교환에서 양쪽이 NAT 감지 해시 전송:
 *   HASH = SHA-1(SPIi | SPIr | IP | port)
 * 수신 측에서 자신의 IP/port로 재계산한 해시와 비교
 * → 불일치하면 경로 상에 NAT 존재 → NAT-T 활성화
 */

/* 커널 NAT-T 처리: net/ipv4/esp4.c + net/ipv4/udp.c */
/* 수신: UDP 4500 소켓에 ESP-in-UDP 핸들러 등록 */
static int esp4_rcv_cb(struct sk_buff *skb)
{
    /* 1. UDP 헤더 제거 */
    /* 2. SPI로 xfrm_state 검색 */
    /* 3. encap_type = UDP_ENCAP_ESPINUDP 설정 */
    /* 4. esp_input()으로 복호화 진행 */
}

/* 송신: xfrm_state에 encap 정보가 있으면 UDP 래핑 */
struct xfrm_encap_tmpl {
    __u16 encap_type;   /* UDP_ENCAP_ESPINUDP (2) */
    __be16 encap_sport; /* 로컬 UDP 포트 (4500) */
    __be16 encap_dport; /* 원격 UDP 포트 (4500) */
    xfrm_address_t encap_oa; /* 원본 주소 (NAT 이전) */
};
NAT 유형별 문제:
  • Full Cone NAT — NAT-T로 문제 없이 통과
  • Restricted/Port Restricted NAT — Keep-alive 패킷(20~30초 간격)으로 NAT 매핑 유지 필요
  • Symmetric NAT — 목적지마다 다른 외부 포트 할당. IKE에서 감지한 포트와 ESP의 실제 매핑이 다를 수 있어 연결 실패 가능. MOBIKE의 주소 업데이트로 완화
  • 이중 NAT — 양쪽 모두 NAT 뒤에 있는 경우. NAT-T 필수이며, 양쪽 IKE 데몬이 모두 NAT를 감지해야 함

xfrm interface vs VTI

리눅스에서 route-based VPN을 구현하는 두 가지 방법이 있습니다: 레거시 VTI(Virtual Tunnel Interface)와 커널 4.19에서 도입된 xfrm interface입니다. xfrm interface는 VTI의 한계를 해결하고 현대 VPN 아키텍처에 필수적인 기능을 제공합니다.

특성VTI (ip_vti)xfrm interface (커널 4.19+)
인터페이스 생성 ip tunnel add vti0 mode vti ... ip link add xfrm0 type xfrm ...
SA 바인딩 터널 src/dst IP 주소로 매칭 if_id 정수값으로 매칭 (IP 무관)
다중 터널 동일 피어에 하나의 VTI만 가능 서로 다른 if_id로 다중 터널 가능
네트워크 네임스페이스 제한적 (SA와 같은 netns에만) 완전 지원 (인터페이스와 SA 분리 가능)
IPv4/IPv6 통합 vti (IPv4), vti6 (IPv6) 별도 단일 인터페이스로 IPv4/IPv6 모두 처리
멀티 테넌트 비실용적 VRF + netns + if_id 조합으로 완전 격리
라우팅 통합 기본적 완전한 route-based VPN (BGP/OSPF over IPSec)
# xfrm interface 생성 및 설정
# 1. xfrm interface 생성 (if_id=42로 SA와 바인딩)
ip link add xfrm0 type xfrm dev eth0 if_id 42
ip addr add 10.10.0.1/30 dev xfrm0
ip link set xfrm0 up

# 2. SA에 if_id 지정
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
    proto esp spi 0x1000 mode tunnel if_id 42 \
    aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128

# 3. 정책에 if_id 지정
ip xfrm policy add dir out if_id 42 \
    src 0.0.0.0/0 dst 0.0.0.0/0 \
    tmpl src 203.0.113.1 dst 198.51.100.1 proto esp mode tunnel

# 4. 라우팅: xfrm interface를 통해 터널 트래픽 라우팅
ip route add 192.168.2.0/24 dev xfrm0

# 멀티 터널 시나리오 (서로 다른 피어에 대해 별도 xfrm interface)
ip link add xfrm1 type xfrm dev eth0 if_id 43
ip link add xfrm2 type xfrm dev eth0 if_id 44
# → BGP/OSPF 동적 라우팅을 각 xfrm interface에서 실행 가능

# 네임스페이스 격리 (멀티 테넌트)
ip netns add tenant1
ip link set xfrm0 netns tenant1
ip netns exec tenant1 ip addr add 10.10.0.1/30 dev xfrm0
ip netns exec tenant1 ip link set xfrm0 up
# → tenant1 네임스페이스 내에서만 IPSec 터널 접근 가능
Route-based vs Policy-based VPN: Policy-based VPN은 ip xfrm policy의 셀렉터로 트래픽을 직접 매칭합니다. 설정이 간단하지만 동적 라우팅과 호환이 어렵습니다. Route-based VPN은 xfrm interface에 라우팅 엔트리를 추가하여 트래픽을 유도합니다. BGP/OSPF 같은 동적 라우팅 프로토콜을 IPSec 위에서 실행할 수 있어 대규모 사이트 간 VPN(수백 개 터널)에 필수적입니다.

Anti-replay 메커니즘

Anti-replay는 공격자가 캡처한 ESP 패킷을 재전송하는 것을 방지합니다. 수신 측은 슬라이딩 윈도우 비트맵을 유지하여 이미 처리한 시퀀스 번호의 패킷을 거부합니다.

Anti-replay 슬라이딩 윈도우 비트맵 1 seq 90 1 91 0 92 1 93 1 94 0 95 ··· 1 118 0 119 1 120 ← 윈도우 왼쪽 경계 ← 최신 수신 seq = 수신 완료 (비트 1) = 미수신/패킷 손실 (비트 0) seq < 윈도우 왼쪽 → 드롭 (너무 오래된 패킷) seq > 120 → 윈도우 오른쪽으로 슬라이드, 새 비트 설정 윈도우 내 비트=1 → 드롭 (중복/재전송 패킷)
/* net/xfrm/xfrm_replay.c — anti-replay 검사 (ESN 모드) */
static int xfrm_replay_check_esn(struct xfrm_state *x,
    struct sk_buff *skb, __be32 net_seq)
{
    u32 seq = ntohl(net_seq);
    struct xfrm_replay_state_esn *replay_esn = x->replay_esn;
    u32 wsize = replay_esn->replay_window;
    u32 top = replay_esn->seq;         /* 최신 수신 시퀀스 */
    u32 bottom = top - wsize + 1;     /* 윈도우 왼쪽 경계 */

    /* Case 1: 윈도우 오른쪽 밖 → 새 패킷, 수락 */
    if (likely(seq > top))
        return 0;

    /* Case 2: 윈도우 왼쪽 밖 → 너무 오래된 패킷, 드롭 */
    if (seq < bottom)
        return -EINVAL;  /* XfrmInSeqOutOfWindow */

    /* Case 3: 윈도우 내 → 비트맵 검사 */
    u32 diff = top - seq;
    u32 pos = diff / 32;
    u32 bit = 1 << (diff % 32);
    if (replay_esn->bmp[pos] & bit)
        return -EINVAL;  /* XfrmInStateReplay — 중복 패킷 */

    return 0;  /* 윈도우 내 미수신 패킷, 수락 */
}

/* 윈도우 업데이트: 패킷 수락 후 비트맵 갱신 */
static void xfrm_replay_advance_esn(struct xfrm_state *x, __be32 net_seq)
{
    /* seq > top이면 윈도우 오른쪽으로 슬라이드 */
    /* 이동 과정에서 벗어난 비트들은 0으로 클리어 */
    /* 새 seq 위치의 비트를 1로 설정 */
}
고대역 환경 윈도우 크기 튜닝: 기본 윈도우 크기는 32패킷으로 10Gbps 환경에서는 패킷 재정렬(RSS, 멀티큐)로 인해 정상 패킷이 윈도우 밖으로 밀려나 드롭될 수 있습니다. ip xfrm state add ... replay-window 2048로 확대하거나, ESN 활성화 시 최대 4096까지 설정 가능합니다. /proc/net/xfrm_statXfrmInSeqOutOfWindow 카운터가 증가하면 윈도우 확대가 필요합니다.

IPSec 하드웨어 오프로드

소프트웨어 ESP 처리는 CPU 집약적이어서 10Gbps 이상 환경에서 병목이 됩니다. 리눅스 커널은 3가지 수준의 하드웨어 오프로드를 지원합니다.

오프로드 모드처리 위치커널 관여지원 하드웨어성능
Crypto offload 암호화/복호화만 HW ESP 헤더 처리, 시퀀스 번호 관리는 커널 Intel QAT, AMD CCP CPU 50~70% 절감
Inline crypto NIC가 ESP 암호화/복호화 SA 설정만. 패킷 처리에서 커널 개입 최소 NVIDIA ConnectX-6 Dx, Intel E810 CPU 90%+ 절감, line-rate 근접
Full offload NIC/DPU가 전체 IPSec 처리 SA/SP 설정만. 패킷 경로 완전 HW NVIDIA BlueField DPU CPU 해방, 100Gbps+
/* include/net/xfrm.h — H/W 오프로드 구조체 */
struct xfrm_dev_offload {
    struct net_device *dev;     /* 오프로드 대상 NIC */
    struct net_device *real_dev; /* bond/vlan 하위 실제 디바이스 */
    unsigned long offload_handle; /* 드라이버 전용 핸들 */
    u8 dir : 2;               /* XFRM_DEV_OFFLOAD_IN / OUT */
    u8 type : 2;              /* CRYPTO / PACKET (inline) / FULL */
    u8 flags : 2;             /* XFRM_DEV_OFFLOAD_FLAG_ACE 등 */
};

/* NIC 드라이버가 구현하는 xdo_dev_* 콜백 */
struct xfrmdev_ops {
    int (*xdo_dev_state_add)(struct xfrm_state *x,
                             struct netlink_ext_ack *extack);
    void (*xdo_dev_state_delete)(struct xfrm_state *x);
    void (*xdo_dev_state_free)(struct xfrm_state *x);
    bool (*xdo_dev_offload_ok)(struct sk_buff *skb,
                              struct xfrm_state *x);
    /* PACKET/FULL 오프로드 시 정책 콜백도 구현 */
    int (*xdo_dev_policy_add)(struct xfrm_policy *p, ...);
};
# Inline crypto offload 설정 (NVIDIA ConnectX-6 Dx 예시)
# 1. SA 생성 시 offload 지정
ip xfrm state add src 203.0.113.1 dst 198.51.100.1 \
    proto esp spi 0x1000 mode tunnel \
    aead 'rfc4106(gcm(aes))' 0x$(openssl rand -hex 20) 128 \
    offload dev eth0 dir out

# 2. 오프로드 상태 확인
ip xfrm state list
# → "offload type packet dev eth0 dir out" 표시

# Intel QAT crypto offload (AEAD)
# QAT 드라이버 로드 → openssl engine → strongSwan에서 QAT 플러그인 사용
modprobe qat_4xxx                    # Intel 4세대 QAT 디바이스
# strongSwan: charon.plugins.openssl.engine_id = qatengine

# 오프로드 실패 시 자동 소프트웨어 폴백
# ethtool -k eth0 | grep esp
# esp-hw-offload: on → 하드웨어 오프로드 활성화됨
오프로드 선택 가이드:
  • Crypto offload (QAT): 기존 서버에 PCIe 카드 추가만으로 성능 개선. SA 수 제한 없음. 암호화 외 처리는 여전히 CPU
  • Inline crypto (ConnectX-6/E810): NIC 교체 필요하지만 line-rate 근접. SA 수는 NIC 메모리에 제한 (수천~수만). AES-GCM만 지원하는 경우 많음
  • Full offload (BlueField DPU): 가장 높은 성능이지만 비용이 높음. 클라우드/가상화 환경에서 OVS + IPSec을 DPU에 완전 오프로드

strongSwan/Libreswan 실전 설정

IKE 데몬은 커널 xfrm과 협력하여 SA의 자동 생성/갱신/삭제를 처리합니다. 현대 리눅스 환경에서는 strongSwan(swanctl)과 Libreswan(ipsec.conf)이 주로 사용됩니다.

# /etc/swanctl/swanctl.conf — strongSwan site-to-site 설정
connections {
    site-to-site {
        version = 2                    # IKEv2 전용
        local_addrs = 203.0.113.1
        remote_addrs = 198.51.100.1

        local {
            auth = pubkey                # X.509 인증서 인증
            certs = server.pem
            id = vpn.example.com
        }
        remote {
            auth = pubkey
            id = vpn.peer.com
        }

        proposals = aes256gcm128-x25519-sha256  # IKE SA 암호 스위트
        dpd_delay = 30s                 # DPD 간격

        children {
            lan-to-lan {
                local_ts = 192.168.1.0/24   # 로컬 트래픽 셀렉터
                remote_ts = 192.168.2.0/24  # 원격 트래픽 셀렉터
                esp_proposals = aes256gcm128-x25519  # CHILD SA 암호 스위트
                rekey_time = 3600s          # 1시간마다 rekey
                replay_window = 2048       # Anti-replay 윈도우
                start_action = start        # 부팅 시 자동 연결
                dpd_action = restart        # DPD 실패 시 재연결
                # hw_offload = packet      # inline crypto 오프로드 (지원 NIC)
            }
        }
    }
}

# Road Warrior (모바일 클라이언트) 설정
connections {
    roadwarrior {
        version = 2
        local_addrs = %any              # 서버: 모든 주소에서 수신
        pools = pool-ipv4               # 클라이언트에게 IP 할당

        local {
            auth = pubkey
            certs = server.pem
        }
        remote {
            auth = eap-mschapv2         # EAP 인증 (사용자/비밀번호)
            eap_id = %any
        }

        children {
            rw-child {
                local_ts = 0.0.0.0/0    # 모든 트래픽 터널링
            }
        }
    }
}

pools {
    pool-ipv4 {
        addrs = 10.10.0.0/24
        dns = 8.8.8.8, 8.8.4.4
    }
}
작업strongSwan (swanctl)Libreswan (ipsec)
설정 로드 swanctl --load-all ipsec auto --add conn-name
연결 시작 swanctl --initiate --child lan-to-lan ipsec auto --up conn-name
SA 목록 swanctl --list-sas ipsec whack --trafficstatus
연결 종료 swanctl --terminate --child lan-to-lan ipsec auto --down conn-name
디버그 로그 swanctl --log --level 2 ipsec whack --debug-all
인증서 목록 swanctl --list-certs ipsec whack --listcerts
설정 파일 /etc/swanctl/swanctl.conf /etc/ipsec.conf + /etc/ipsec.secrets
커널 연동 charon.plugins.kernel-netlink pluto 데몬 → NETLINK_XFRM
strongSwan 커널 연동 상세: strongSwan의 charon 데몬은 kernel-netlink 플러그인으로 NETLINK_XFRM 소켓을 통해 커널과 통신합니다. swanctl --load-all 실행 시 설정이 charon에 로드되고, IKEv2 교환 완료 후 XFRM_MSG_NEWSA/XFRM_MSG_NEWPOLICY로 SA/SP를 커널에 주입합니다. kernel-netlink 플러그인 설정: charon.plugins.kernel-netlink.xfrm_acq_expires = 165 (ACQUIRE 타임아웃), charon.plugins.kernel-netlink.set_mark = yes (fwmark 연동).

IPSec 성능 튜닝

IPSec 성능은 암호 알고리즘, CPU 아키텍처, 패킷 크기, NIC 설정에 크게 의존합니다. 고성능 환경에서는 체계적인 벤치마크와 프로파일링이 필수적입니다.

알고리즘x86_64 (AES-NI)ARM64 (NEON/CE)비고
AES-128-GCM ~40 Gbps ~8 Gbps (ARMv8 CE) AES-NI + CLMUL 하드웨어 가속. 가장 보편적
AES-256-GCM ~32 Gbps ~6 Gbps AES-128 대비 ~20% 느림 (4 라운드 추가)
ChaCha20-Poly1305 ~15 Gbps ~10 Gbps (NEON) AES-NI 없는 환경에서 고성능. ARM에서 AES-GCM보다 빠를 수 있음
AES-256-CBC + HMAC-SHA256 ~12 Gbps ~3 Gbps 2-pass 처리. 레거시 호환용. AEAD 대비 ~60% 느림
패킷 크기별 throughput 특성: 작은 패킷(64B)은 per-packet 오버헤드(ESP 헤더, 암호화 초기화, DMA 설정)가 지배적이어서 throughput이 크게 저하됩니다. 1500B 패킷 대비 64B 패킷은 ~10배 낮은 throughput을 보입니다. VoIP 등 소형 패킷 위주 트래픽에서는 하드웨어 오프로드 효과가 극대화됩니다.
# CPU affinity와 RPS/RFS 최적화
# ESP 처리를 특정 CPU에 고정하여 캐시 효율 극대화

# 1. NIC 인터럽트를 특정 CPU에 바인딩
# (CPU 0~3: 일반 트래픽, CPU 4~7: ESP 처리)
for i in /proc/irq/*/smp_affinity_list; do
    irq=$(echo $i | grep -oP '/proc/irq/\K[0-9]+')
    cat /proc/irq/$irq/actions | grep -q eth0 && echo "4-7" > $i
done

# 2. RPS (Receive Packet Steering) — 소프트웨어 수신 분산
echo f0 > /sys/class/net/eth0/queues/rx-0/rps_cpus   # CPU 4-7
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

# 3. xfrm 관련 sysctl 파라미터
sysctl -w net.core.xfrm_larval_drop=1    # SA 미완성 시 패킷 즉시 드롭 (대기 안 함)
sysctl -w net.core.xfrm_acq_expires=30   # ACQUIRE 타임아웃 (초)
sysctl -w net.core.xfrm_aevent_rseqth=2  # replay 이벤트 시퀀스 임계값
sysctl -w net.ipv4.xfrm4_gc_thresh=32768 # xfrm dst 가비지 컬렉션 임계값

# perf 프로파일링 — ESP 처리 핫스팟 식별
perf top -C 4-7 -g                        # ESP 처리 CPU에서 실시간 프로파일
perf record -C 4-7 -g -- sleep 10        # 10초 샘플링
perf report --sort=dso,sym                # 심볼별 CPU 사용량
# 주요 핫스팟: gcm_hash_crypt_*, aesni_ctr_enc, esp_output/input

# 암호 알고리즘 벤치마크 (커널 crypto API 테스트)
modprobe tcrypt sec=1 mode=211           # AES-GCM 벤치마크
dmesg | tail -50                          # 결과 확인
대규모 터널(수백~수천 SA) 환경 최적화:
  • SAD 해시 테이블 — SA 수가 많으면 해시 충돌 증가. xfrm4_gc_thresh를 SA 수의 2배 이상으로 설정
  • SPD 검색 — 정책이 많으면 선형 검색이 병목. 셀렉터를 최대한 구체적으로 설정하고, 불필요한 정책 제거
  • CHILD_SA rekey 폭풍 — 모든 터널이 동시에 rekey되면 CPU 스파이크. rekey_timerand_time을 추가하여 분산: rand_time = 600s
  • NAPI 배치 처리 — ESP 복호화가 비동기(crypto_aead)이므로 NAPI 폴링과 상호작용. net.core.busy_poll으로 레이턴시 최적화 가능
  • PCPU xfrm 캐시 — 커널 4.14+에서 per-CPU xfrm 정책 캐시 도입. 멀티코어 환경에서 lock contention 감소
  • 모니터링/proc/net/xfrm_stat의 각 카운터를 Prometheus 등으로 수집하여 이상 징후(XfrmInError 급증 등) 조기 감지

WireGuard 심화

WireGuard는 Jason A. Donenfeld가 설계한 차세대 VPN 프로토콜로, Linux 커널 5.6에 공식 통합되었습니다 (drivers/net/wireguard/). IPSec이나 OpenVPN 대비 코드량이 약 4,000줄(vs IPSec 수만 줄)로 극도로 간결하며, 최신 암호화 프리미티브만 사용하여 보안 감사가 용이합니다. 커널 레벨에서 동작하므로 유저스페이스 VPN 대비 높은 성능을 제공합니다.

WireGuard 설계 철학: "Cryptokey Routing" — 각 피어에 공개키와 허용 IP 대역을 매핑하여, 라우팅 테이블처럼 동작합니다. 복잡한 상태 머신이나 협상 과정 없이, 패킷이 도착하면 공개키 기반으로 자동 핸드셰이크를 수행합니다. 설정이 SSH authorized_keys 수준으로 단순합니다.

WireGuard vs IPSec vs OpenVPN 비교

특성WireGuardIPSec (strongSwan)OpenVPN
코드 라인 ~4,000 ~400,000+ ~100,000+
동작 레이어 커널 (L3 netdevice) 커널 (xfrm) 유저스페이스 (tun/tap)
프로토콜 UDP (단일 포트) ESP (IP 프로토콜 50) + IKEv2 TCP/UDP
암호화 협상 없음 (고정 암호 스위트) IKEv2로 협상 TLS 핸드셰이크
핵심 암호 ChaCha20-Poly1305, Curve25519 AES-GCM, RSA/ECDSA AES-GCM, RSA/ECDHE
키 교환 Noise IK (1-RTT) IKEv2 (2-RTT) TLS (2-3 RTT)
연결 상태 Stateless (Cryptokey Routing) Stateful (SA 관리) Stateful (TCP/TLS 세션)
로밍 지원 자동 (소스 IP 변경 감지) MOBIKE 확장 필요 재연결 필요
Throughput (1Gbps NIC) ~950 Mbps ~800 Mbps ~400 Mbps

암호화 프리미티브

WireGuard는 암호 민첩성(cipher agility)을 의도적으로 배제합니다. 단일 고정 암호 스위트만 사용하여 다운그레이드 공격을 원천 차단합니다:

용도알고리즘커널 구현설명
키 교환 (ECDH) Curve25519 lib/crypto/curve25519.c 타원곡선 Diffie-Hellman (X25519)
대칭 암호화 ChaCha20-Poly1305 lib/crypto/chacha20poly1305.c AEAD — 암호화 + 인증 동시 제공
해시 / MAC BLAKE2s lib/crypto/blake2s.c 핸드셰이크 해시 체인, 쿠키 MAC
키 유도 (KDF) HKDF (BLAKE2s 기반) drivers/net/wireguard/noise.c 세션 키 유도, 체인 키 갱신
DoS 방어 BLAKE2s(MAC) + Xchacha20Poly1305 drivers/net/wireguard/cookie.c 쿠키 기반 핸드셰이크 DoS 완화
/* drivers/net/wireguard/noise.h — Noise Protocol 상수 */
#define NOISE_PUBLIC_KEY_LEN     32   /* Curve25519 공개키 */
#define NOISE_SYMMETRIC_KEY_LEN  32   /* ChaCha20-Poly1305 키 */
#define NOISE_TIMESTAMP_LEN      12   /* TAI64N 타임스탬프 */
#define NOISE_AUTHTAG_LEN        16   /* Poly1305 인증 태그 */
#define NOISE_HASH_LEN           32   /* BLAKE2s 해시 길이 */

Noise Protocol Framework (IK 패턴)

WireGuard는 Noise Protocol FrameworkNoise_IKpsk2 패턴을 사용합니다. "IK"는 Initiator가 Responder의 정적 공개키를 미리 알고 있다(Known)는 의미이며, psk2는 선택적 사전 공유 키(Pre-Shared Key)를 두 번째 메시지에서 믹스한다는 뜻입니다.

/* Noise_IKpsk2 핸드셰이크 패턴 */
/*
 * ← s                              (Responder 정적 공개키를 미리 알고 있음)
 * ...
 * → e, es, s, ss                   (메시지 1: Initiation)
 * ← e, ee, se, psk                 (메시지 2: Response)
 *
 * e  = 임시(ephemeral) 키 생성
 * es = Initiator 임시키 × Responder 정적키 DH
 * s  = Initiator 정적키 (암호화하여 전송)
 * ss = Initiator 정적키 × Responder 정적키 DH
 * ee = Initiator 임시키 × Responder 임시키 DH
 * se = Responder 임시키 × Initiator 정적키 DH
 * psk = Pre-Shared Key 믹스 (양자 내성 레이어)
 */
/* 핸드셰이크 메시지 구조 — drivers/net/wireguard/messages.h */

/* 메시지 타입 */
enum message_type {
    MESSAGE_HANDSHAKE_INITIATION  = 1,
    MESSAGE_HANDSHAKE_RESPONSE    = 2,
    MESSAGE_HANDSHAKE_COOKIE      = 3,
    MESSAGE_DATA                  = 4,
};

/* 메시지 1: Handshake Initiation (148 바이트) */
struct message_handshake_initiation {
    struct message_header header;  /* type(1) + reserved(3) */
    __le32 sender_index;           /* Initiator 세션 ID */
    u8 unencrypted_ephemeral[NOISE_PUBLIC_KEY_LEN]; /* e: 임시 공개키 (평문) */
    u8 encrypted_static[NOISE_PUBLIC_KEY_LEN
                        + NOISE_AUTHTAG_LEN];  /* s: 정적 공개키 (암호화) */
    u8 encrypted_timestamp[NOISE_TIMESTAMP_LEN
                           + NOISE_AUTHTAG_LEN]; /* TAI64N 타임스탬프 (암호화) */
    struct message_macs macs;      /* mac1 + mac2 (DoS 방어) */
};

/* 메시지 2: Handshake Response (92 바이트) */
struct message_handshake_response {
    struct message_header header;
    __le32 sender_index;           /* Responder 세션 ID */
    __le32 receiver_index;         /* Initiator의 sender_index */
    u8 unencrypted_ephemeral[NOISE_PUBLIC_KEY_LEN]; /* e: 임시 공개키 */
    u8 encrypted_nothing[NOISE_AUTHTAG_LEN];       /* 빈 AEAD (인증만) */
    struct message_macs macs;
};

/* 메시지 4: 데이터 패킷 */
struct message_data {
    struct message_header header;
    __le32 key_idx;                /* 수신자 세션 인덱스 */
    __le64 counter;                /* nonce (8바이트, 리플레이 방지) */
    u8 encrypted_data[];           /* 암호화된 IP 패킷 + Poly1305 태그 */
};
💡

1-RTT 핸드셰이크: WireGuard의 Noise IK 패턴은 단 1회 왕복(RTT)으로 핸드셰이크를 완료합니다. Initiator가 메시지 1을 보내고 Responder가 메시지 2를 응답하면 즉시 양방향 데이터 전송이 가능합니다. 반면 IKEv2는 최소 2-RTT, TLS 1.3은 1-RTT(0-RTT 재연결 제외)가 필요합니다.

핸드셰이크 상세 과정

/* WireGuard Noise IK 핸드셰이크 상세 흐름 */

/*
 * 사전 조건:
 *   - Initiator (I): 자신의 정적 키쌍 (Si_priv, Si_pub)
 *   - Responder (R): 자신의 정적 키쌍 (Sr_priv, Sr_pub)
 *   - I는 Sr_pub를 미리 알고 있음 (설정 시 지정)
 *
 * 초기 상태:
 *   C = HASH("Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s")  ← chaining key
 *   H = HASH(C || "WireGuard v1 zx2c4 Jason@zx2c4.com...")  ← hash
 *   H = HASH(H || Sr_pub)  ← Responder 공개키를 해시에 믹스
 *
 * ─── 메시지 1: Initiation (I → R) ───
 *   Ei_priv, Ei_pub = DH_GENERATE()        임시 키쌍 생성
 *   H = HASH(H || Ei_pub)                  임시 공개키 해시
 *   C, k = KDF2(C, DH(Ei_priv, Sr_pub))   es: 임시×정적 DH → 키 유도
 *   encrypted_static = AEAD(k, 0, Si_pub, H)  정적 공개키 암호화
 *   H = HASH(H || encrypted_static)
 *   C, k = KDF2(C, DH(Si_priv, Sr_pub))   ss: 정적×정적 DH → 키 유도
 *   encrypted_timestamp = AEAD(k, 0, TAI64N(), H)  타임스탬프 암호화
 *   H = HASH(H || encrypted_timestamp)
 *
 * ─── 메시지 2: Response (R → I) ───
 *   Er_priv, Er_pub = DH_GENERATE()        임시 키쌍 생성
 *   H = HASH(H || Er_pub)
 *   C, k = KDF2(C, DH(Er_priv, Ei_pub))   ee: 양쪽 임시키 DH
 *   C, k = KDF2(C, DH(Er_priv, Si_pub))   se: Responder임시×Initiator정적
 *   C, k = KDF2(C, psk)                    psk 믹스 (없으면 all-zero)
 *   encrypted_nothing = AEAD(k, 0, "", H)  빈 페이로드 인증
 *   H = HASH(H || encrypted_nothing)
 *
 * ─── 세션 키 유도 ───
 *   T_send, T_recv = KDF2(C, "")           양방향 전송 키 생성
 *   → I는 T_send로 암호화, T_recv로 복호화
 *   → R는 T_recv로 암호화, T_send로 복호화
 */
/* drivers/net/wireguard/noise.c — 핸드셰이크 Initiation 생성 (단순화) */
bool wg_noise_handshake_create_initiation(
    struct message_handshake_initiation *dst,
    struct noise_handshake *handshake)
{
    struct noise_symmetric_key *key;

    down_read(&handshake->lock);

    /* 1. 임시 키쌍 생성 (Curve25519) */
    curve25519_generate_secret(handshake->ephemeral_private);
    curve25519_generate_public(dst->unencrypted_ephemeral,
                               handshake->ephemeral_private);

    /* 2. 임시 공개키를 해시 체인에 믹스 */
    mix_hash(handshake->hash,
             dst->unencrypted_ephemeral, NOISE_PUBLIC_KEY_LEN);

    /* 3. DH(Ei_priv, Sr_pub) → chaining key 갱신 */
    if (!mix_dh(handshake->chaining_key, key,
               handshake->ephemeral_private,
               handshake->remote_static))
        goto out;

    /* 4. 정적 공개키 암호화 전송 */
    message_encrypt(dst->encrypted_static,
                    handshake->static_identity->static_public,
                    NOISE_PUBLIC_KEY_LEN, key, handshake->hash);

    /* 5. DH(Si_priv, Sr_pub) → chaining key 갱신 */
    mix_dh(handshake->chaining_key, key,
           handshake->static_identity->static_private,
           handshake->remote_static);

    /* 6. TAI64N 타임스탬프 암호화 (리플레이 방지) */
    tai64n_now(timestamp);
    message_encrypt(dst->encrypted_timestamp,
                    timestamp, NOISE_TIMESTAMP_LEN,
                    key, handshake->hash);

    /* 7. MAC 계산 (DoS 방어) */
    message_macs(dst, dst->macs, handshake);

    up_read(&handshake->lock);
    return true;
out:
    up_read(&handshake->lock);
    return false;
}

DoS 방어: 쿠키 메커니즘

WireGuard는 핸드셰이크 패킷에 두 개의 MAC을 포함하여 DoS 공격을 완화합니다:

/* drivers/net/wireguard/messages.h */
struct message_macs {
    u8 mac1[COOKIE_LEN];  /* 항상 검증 — BLAKE2s(msg, peer_pubkey) */
    u8 mac2[COOKIE_LEN];  /* 부하 시에만 검증 — BLAKE2s(msg, cookie) */
};

/*
 * mac1: 메시지 무결성 + 피어 인증 (항상 검증)
 *   → mac1 = BLAKE2s(responder_pub, msg_without_macs)
 *   → 유효한 피어만 올바른 mac1을 생성 가능
 *
 * mac2: DoS 방어 토큰 (서버 부하 시에만 요구)
 *   → 서버가 과부하 감지 시 MESSAGE_HANDSHAKE_COOKIE 응답
 *   → cookie = Xchacha20Poly1305(BLAKE2s(client_ip:port), random_secret)
 *   → 클라이언트는 받은 cookie로 mac2를 재계산하여 재전송
 *   → IP 주소 기반이므로 스푸핑된 IP로는 cookie 수신 불가
 */
/* drivers/net/wireguard/cookie.c — 쿠키 검증 흐름 (단순화) */
enum cookie_mac_state
wg_cookie_validate_packet(struct cookie_checker *checker,
                          struct sk_buff *skb, bool check_cookie)
{
    struct message_macs *macs = (void *)(skb->data + skb->len
                                         - sizeof(*macs));

    /* 1단계: mac1 검증 (항상) */
    blake2s(computed_mac1, skb->data,
            skb->len - sizeof(*macs),
            checker->mac1_key, COOKIE_LEN);
    if (!crypto_memneq(computed_mac1, macs->mac1, COOKIE_LEN))
        return INVALID_MAC;

    /* 2단계: 부하 상태 시 mac2 검증 */
    if (!check_cookie)
        return VALID_MAC_BUT_NO_COOKIE;

    make_cookie(cookie, skb, checker);
    blake2s(computed_mac2, skb->data,
            skb->len - COOKIE_LEN,
            cookie, COOKIE_LEN);
    if (!crypto_memneq(computed_mac2, macs->mac2, COOKIE_LEN))
        return VALID_MAC_WITH_COOKIE_BUT_RATELIMITED;

    return VALID_MAC_WITH_COOKIE;
}

커널 구현 아키텍처

/* drivers/net/wireguard/ 소스 파일 구조 */
/*
 * drivers/net/wireguard/
 * ├── main.c           ← 모듈 초기화 (wg_mod_init)
 * ├── device.c         ← netdevice 생성/소멸, ndo_start_xmit
 * ├── device.h         ← struct wg_device 정의
 * ├── noise.c          ← Noise Protocol 핸드셰이크 엔진
 * ├── noise.h          ← noise_handshake, noise_keypair 구조체
 * ├── peer.c           ← struct wg_peer 생성/관리
 * ├── peer.h           ← 피어 구조체 정의
 * ├── allowedips.c     ← IP 기반 라우팅 트라이 (Cryptokey Routing)
 * ├── allowedips.h     ← allowedips_node 구조체
 * ├── queueing.c       ← 암호화/복호화 병렬 큐 관리
 * ├── queueing.h       ← crypt_queue, 큐잉 매크로
 * ├── send.c           ← 패킷 암호화 + 전송 (xmit)
 * ├── receive.c        ← 패킷 수신 + 복호화 (rx)
 * ├── socket.c         ← UDP 소켓 관리
 * ├── netlink.c        ← Generic Netlink 인터페이스 (wg set/show)
 * ├── cookie.c         ← MAC/쿠키 DoS 방어
 * ├── cookie.h         ← cookie_checker 구조체
 * ├── timers.c         ← 키 재협상, keepalive 타이머
 * ├── timers.h         ← 타이머 상수 정의
 * ├── peerlookup.c     ← 인덱스 해시 테이블 (세션 ID → keypair)
 * ├── peerlookup.h     ← index_hashtable 구조체
 * ├── ratelimiter.c    ← 핸드셰이크 속도 제한
 * └── ratelimiter.h    ← 레이트리미터 구조체
 */

주요 데이터 구조

/* drivers/net/wireguard/device.h — WireGuard 디바이스 (인터페이스당 1개) */
struct wg_device {
    struct net_device *dev;          /* 커널 net_device (wg0, wg1, ...) */
    struct crypt_queue encrypt_queue; /* 암호화 큐 (per-CPU 워커) */
    struct crypt_queue decrypt_queue; /* 복호화 큐 (per-CPU 워커) */
    struct sock __rcu *sock4;         /* IPv4 UDP 소켓 */
    struct sock __rcu *sock6;         /* IPv6 UDP 소켓 */
    struct noise_static_identity static_identity; /* 정적 키쌍 */
    struct list_head peer_list;      /* 피어 연결 리스트 */
    struct allowedips peer_allowedips;/* AllowedIPs 라우팅 테이블 */
    struct index_hashtable index_hashtable; /* 세션 인덱스 → keypair */
    struct cookie_checker cookie_checker;  /* DoS 방어 쿠키 */
    u16 incoming_port;               /* 수신 UDP 포트 (기본 51820) */
    u32 fwmark;                      /* 라우팅 마크 */
    struct mutex device_update_lock;  /* 디바이스 설정 변경 잠금 */
    struct mutex socket_update_lock;  /* 소켓 변경 잠금 */
};

/* drivers/net/wireguard/peer.h — 피어 (원격 엔드포인트당 1개) */
struct wg_peer {
    struct wg_device *device;        /* 소속 디바이스 */
    struct noise_handshake handshake; /* 핸드셰이크 상태 */
    struct noise_keypairs keypairs;   /* 현재/이전/다음 세션 키 */
    struct endpoint endpoint;         /* 최종 알려진 IP:port */
    struct allowedips_node *allowedips_list; /* 허용 IP 목록 */

    /* 타이머 */
    struct timer_list timer_retransmit_handshake;
    struct timer_list timer_send_keepalive;
    struct timer_list timer_new_handshake;
    struct timer_list timer_zero_key_material;
    struct timer_list timer_persistent_keepalive;

    /* 통계 */
    u64 rx_bytes, tx_bytes;
    struct timespec64 walltime_last_handshake;

    struct kref refcount;             /* 참조 카운트 */
    struct rcu_head rcu;              /* RCU 해제 */
    struct list_head peer_list;       /* wg_device→peer_list 연결 */

    /* 전송 큐 */
    struct prev_queue tx_queue, rx_queue; /* napi 기반 큐 */
    struct napi_struct tx_napi, rx_napi;  /* NAPI 폴링 */
};

/* drivers/net/wireguard/noise.h — 핸드셰이크 상태 */
struct noise_handshake {
    struct index_hashtable_entry entry; /* 인덱스 해시 테이블 엔트리 */
    u8 remote_static[NOISE_PUBLIC_KEY_LEN];   /* 상대 정적 공개키 */
    u8 remote_ephemeral[NOISE_PUBLIC_KEY_LEN]; /* 상대 임시 공개키 */
    u8 ephemeral_private[NOISE_PUBLIC_KEY_LEN]; /* 내 임시 비밀키 */
    u8 hash[NOISE_HASH_LEN];           /* 핸드셰이크 해시 (H) */
    u8 chaining_key[NOISE_HASH_LEN];   /* 체인 키 (C) */
    u8 preshared_key[NOISE_SYMMETRIC_KEY_LEN]; /* PSK (선택) */
    u8 latest_timestamp[NOISE_TIMESTAMP_LEN];  /* 리플레이 방지 */
    enum noise_handshake_state state; /* CREATED/CONSUMED/RESPONSE */
    struct noise_static_identity *static_identity; /* 디바이스 정적 키 */
    struct rw_semaphore lock;
};

/* 세션 키쌍 — 핸드셰이크 완료 후 데이터 전송에 사용 */
struct noise_keypair {
    struct index_hashtable_entry entry;
    u8 sending_key[NOISE_SYMMETRIC_KEY_LEN];   /* 송신 키 */
    u8 receiving_key[NOISE_SYMMETRIC_KEY_LEN]; /* 수신 키 */
    struct noise_replay_counter receiving_counter; /* 리플레이 윈도우 */
    atomic64_t sending_counter;         /* 송신 카운터 (nonce) */
    ktime_t birthdate;                   /* 키 생성 시각 */
    bool is_initiator;                   /* 이 핸드셰이크의 Initiator? */
    struct kref refcount;
    struct rcu_head rcu;
};

AllowedIPs: Cryptokey Routing

WireGuard의 핵심 개념인 Cryptokey Routing은 AllowedIPs 트라이(trie) 자료구조로 구현됩니다. 각 IP 대역은 특정 피어의 공개키에 매핑되어, 일반 라우팅 테이블처럼 최장 접두사 매칭(LPM)으로 패킷의 피어를 결정합니다:

/* drivers/net/wireguard/allowedips.h */
struct allowedips_node {
    struct wg_peer __rcu *peer;  /* 이 대역에 매핑된 피어 */
    struct allowedips_node __rcu *bit[2]; /* 0비트/1비트 자식 */
    u8 cidr;                     /* 서브넷 마스크 길이 */
    u8 bit_at_a, bit_at_b;       /* 압축된 비트 위치 */
    u8 bitlen;                   /* 주소 길이 (32=IPv4, 128=IPv6) */
    u8 bits[16] __aligned(sizeof(u64)); /* IP 주소 비트 */
};

struct allowedips {
    struct allowedips_node __rcu *root4; /* IPv4 트라이 루트 */
    struct allowedips_node __rcu *root6; /* IPv6 트라이 루트 */
    u64 seq;                             /* 시퀀스 (변경 추적) */
};
/* AllowedIPs 동작 예시:
 *
 * [Peer A] PublicKey = aaa...
 *   AllowedIPs = 10.0.0.0/24, fd00::1/128
 *
 * [Peer B] PublicKey = bbb...
 *   AllowedIPs = 10.0.1.0/24, 0.0.0.0/0
 *
 * 송신 결정:
 *   dst=10.0.0.5   → Peer A (10.0.0.0/24 매칭)
 *   dst=10.0.1.100 → Peer B (10.0.1.0/24 매칭)
 *   dst=8.8.8.8    → Peer B (0.0.0.0/0 기본 경로)
 *
 * 수신 검증:
 *   Peer A에서 수신한 패킷의 inner src가 10.0.0.5 → 허용
 *   Peer A에서 수신한 패킷의 inner src가 10.0.1.5 → 드롭 (AllowedIPs 불일치)
 *
 * → 송신: AllowedIPs로 피어 결정 (라우팅)
 * → 수신: AllowedIPs로 소스 IP 검증 (역방향 경로 필터링)
 */
/* drivers/net/wireguard/allowedips.c — 최장 접두사 매칭 (LPM) */
struct wg_peer *wg_allowedips_lookup_dst(
    struct allowedips *table,
    struct sk_buff *skb)
{
    if (skb->protocol == htons(ETH_P_IP))
        return lookup(table->root4, 32,
                      &ip_hdr(skb)->daddr);  /* IPv4 목적지 */
    else if (skb->protocol == htons(ETH_P_IPV6))
        return lookup(table->root6, 128,
                      &ipv6_hdr(skb)->daddr); /* IPv6 목적지 */
    return NULL;
}

/* 수신 시: 소스 IP가 피어의 AllowedIPs에 포함되는지 검증 */
struct wg_peer *wg_allowedips_lookup_src(
    struct allowedips *table,
    struct sk_buff *skb)
{
    if (skb->protocol == htons(ETH_P_IP))
        return lookup(table->root4, 32,
                      &ip_hdr(skb)->saddr);  /* IPv4 소스 */
    else if (skb->protocol == htons(ETH_P_IPV6))
        return lookup(table->root6, 128,
                      &ipv6_hdr(skb)->saddr);
    return NULL;
}

패킷 처리 흐름

/* WireGuard 송신 패킷 흐름 (Transmit Path)
 *
 * 애플리케이션 → socket → IP 라우팅 → wg0 dev
 *   │
 *   ↓  ndo_start_xmit() (device.c)
 *   ├─ AllowedIPs 테이블에서 dst IP로 피어 검색
 *   ├─ 피어 없음 → ICMP unreachable + 드롭
 *   │
 *   ├─ 세션 키 확인
 *   │   ├─ 유효한 keypair 있음 → 암호화 큐에 추가
 *   │   └─ 키 없음/만료 → 핸드셰이크 시작 + 패킷 대기열
 *   │
 *   ↓  encrypt_queue (queueing.c)
 *   ├─ per-CPU 워커가 ChaCha20-Poly1305로 암호화
 *   ├─ 카운터(nonce) 증가 (atomic64_inc_return)
 *   ├─ message_data 헤더 부착
 *   │
 *   ↓  NAPI tx_napi 폴링 (send.c)
 *   ├─ UDP 소켓으로 캡슐화 전송
 *   └─ peer->endpoint (IP:port)로 전송
 *
 *
 * WireGuard 수신 패킷 흐름 (Receive Path)
 *
 * NIC → netif_receive_skb → IP → UDP → wg 소켓
 *   │
 *   ↓  wg_receive() (socket.c → receive.c)
 *   ├─ 메시지 타입 판별
 *   │   ├─ MESSAGE_DATA (4):
 *   │   │   ├─ key_idx로 index_hashtable에서 keypair 검색
 *   │   │   ├─ 리플레이 윈도우 검사 (counter)
 *   │   │   ├─ decrypt_queue에 추가
 *   │   │   │
 *   │   │   ↓  per-CPU 워커가 복호화
 *   │   │   ├─ ChaCha20-Poly1305 복호화 + 인증 검증
 *   │   │   ├─ 실패 → 드롭 (인증 태그 불일치)
 *   │   │   │
 *   │   │   ↓  NAPI rx_napi 폴링
 *   │   │   ├─ AllowedIPs로 inner src IP 검증
 *   │   │   ├─ 불일치 → 드롭 (피어 스푸핑 방지)
 *   │   │   ├─ 피어 endpoint 업데이트 (로밍 지원)
 *   │   │   └─ netif_receive_skb() → 일반 IP 처리
 *   │   │
 *   │   ├─ MESSAGE_HANDSHAKE_INITIATION (1):
 *   │   │   ├─ mac1 검증 → 부하 시 mac2 검증
 *   │   │   ├─ Noise handshake 처리
 *   │   │   └─ Response 메시지 생성 + 전송
 *   │   │
 *   │   ├─ MESSAGE_HANDSHAKE_RESPONSE (2):
 *   │   │   ├─ 핸드셰이크 완료
 *   │   │   ├─ 세션 키 유도 (T_send, T_recv)
 *   │   │   └─ 대기 중인 패킷 전송
 *   │   │
 *   │   └─ MESSAGE_HANDSHAKE_COOKIE (3):
 *   │       └─ cookie 저장 → mac2 재계산 후 재전송
 */
/* drivers/net/wireguard/device.c — 패킷 송신 진입점 */
static netdev_tx_t wg_xmit(struct sk_buff *skb,
                            struct net_device *dev)
{
    struct wg_device *wg = netdev_priv(dev);
    struct wg_peer *peer;
    struct noise_keypair *keypair;
    int ret;

    /* 1. AllowedIPs에서 목적지 IP로 피어 검색 */
    peer = wg_allowedips_lookup_dst(&wg->peer_allowedips, skb);
    if (unlikely(!peer)) {
        ret = -ENOKEY;
        net_dbg_ratelimited("%s: No peer for dst\n", dev->name);
        goto err;
    }

    /* 2. 유효한 세션 키 확보 */
    keypair = wg_noise_keypair_current(peer);
    if (unlikely(!keypair)) {
        /* 키 없음 → 핸드셰이크 시작, 패킷 큐잉 */
        wg_packet_queue_staged_packets(peer, skb);
        wg_packet_send_handshake_initiation(peer);
        return NETDEV_TX_OK;
    }

    /* 3. 암호화 큐에 추가 (per-CPU 워커가 처리) */
    wg_packet_encrypt_worker(skb, keypair, peer);

    return NETDEV_TX_OK;
err:
    kfree_skb(skb);
    return NETDEV_TX_OK;
}
/* drivers/net/wireguard/send.c — 암호화 처리 (단순화) */
static void encrypt_packet(struct sk_buff *skb,
                           struct noise_keypair *keypair)
{
    struct message_data *header;
    u64 nonce;
    bool ret;

    /* nonce 원자적 증가 (리플레이 방지) */
    nonce = atomic64_inc_return(&keypair->sending_counter) - 1;
    if (nonce >= REJECT_AFTER_MESSAGES) {
        /* 2^64 - 2^13 - 1 메시지 초과 → 키 재협상 필요 */
        goto err;
    }

    /* 데이터 헤더 구성 */
    header = (struct message_data *)skb_push(skb,
              sizeof(struct message_data));
    header->header.type = cpu_to_le32(MESSAGE_DATA);
    header->key_idx = keypair->remote_index;
    header->counter = cpu_to_le64(nonce);

    /* ChaCha20-Poly1305 AEAD 암호화 */
    ret = chacha20poly1305_encrypt(
        skb->data + sizeof(*header),  /* 평문 IP 패킷 */
        skb->len - sizeof(*header),
        NULL, 0,                       /* AAD 없음 */
        nonce,
        keypair->sending_key);        /* 세션 송신 키 */

    /* Poly1305 인증 태그 (16바이트) 자동 부착 */
}
/* drivers/net/wireguard/receive.c — 수신 복호화 + AllowedIPs 검증 */
static void wg_packet_consume_data_done(
    struct wg_peer *peer,
    struct sk_buff *skb,
    struct endpoint *endpoint)
{
    struct net_device *dev = peer->device->dev;
    struct wg_peer *routed_peer;
    unsigned int len;

    /* inner 패킷 프로토콜 확인 */
    if (unlikely(skb_network_header(skb)[0] >> 4 != 4
                && skb_network_header(skb)[0] >> 4 != 6))
        goto dishonest;

    /* 핵심: AllowedIPs로 소스 IP 검증 (역방향 경로 필터링) */
    routed_peer = wg_allowedips_lookup_src(
        &peer->device->peer_allowedips, skb);
    if (unlikely(routed_peer != peer))
        goto dishonest;  /* 피어가 허용되지 않은 IP로 패킷 전송 → 드롭 */

    /* 피어 endpoint 업데이트 (로밍 지원) */
    wg_socket_set_peer_endpoint(peer, endpoint);

    /* 통계 갱신 */
    len = skb->len;
    wg_timers_data_received(peer);

    /* 복호화된 패킷을 네트워크 스택에 주입 */
    if (likely(netif_receive_skb(skb) == NET_RX_SUCCESS))
        peer->rx_bytes += len;
    return;

dishonest:
    net_dbg_ratelimited("%s: Packet from peer with wrong inner src\n",
                        dev->name);
    ++dev->stats.rx_errors;
    kfree_skb(skb);
}

병렬 암/복호화와 NAPI

WireGuard는 암호화 연산을 per-CPU 워크큐에서 병렬 처리하고, 결과를 NAPI 폴링으로 순서대로 전달합니다. 이 설계로 멀티코어 환경에서 선형적 성능 확장을 달성합니다:

/* drivers/net/wireguard/queueing.h — 병렬 암호화 큐 */
struct crypt_queue {
    struct ptr_ring ring;          /* lock-free 링 버퍼 (skb 포인터) */
    struct work_struct work;        /* per-CPU 워크큐 (암/복호 실행) */
    int last_cpu;                   /* 마지막 사용 CPU (로드 밸런싱) */
};

/*
 * 스레딩 모델:
 *
 *   [xmit 또는 UDP recv]
 *         │
 *         ↓
 *   crypt_queue.ring에 skb 추가
 *         │
 *         ↓
 *   per-CPU 워커 (encrypt/decrypt)  ← 실제 암호 연산 (CPU 집약)
 *   ┌─CPU0─┐ ┌─CPU1─┐ ┌─CPU2─┐
 *   │ skb1 │ │ skb2 │ │ skb3 │    ← 병렬 처리
 *   └──┬───┘ └──┬───┘ └──┬───┘
 *      │        │        │
 *      ↓        ↓        ↓
 *   peer->tx/rx_queue (prev_queue)  ← 순서 보장 큐
 *         │
 *         ↓
 *   NAPI 폴링 (tx_napi / rx_napi)  ← softirq에서 순서대로 전송/주입
 *
 * 핵심: 암호화는 병렬, 전달은 순서대로 (per-peer NAPI)
 */
/* drivers/net/wireguard/queueing.c — per-CPU 워커 분배 */
int wg_queue_enqueue_per_device_and_peer(
    struct crypt_queue *device_queue,
    struct prev_queue *peer_queue,
    struct sk_buff *skb)
{
    int cpu;

    /* 라운드 로빈으로 CPU 선택 */
    cpu = wg_cpumask_next_online(&device_queue->last_cpu);

    /* peer 큐에 순서 슬롯 예약 (FIFO 보장) */
    atomic_set(&PACKET_CB(skb)->state, PACKET_STATE_UNCRYPTED);
    prev_queue_enqueue(peer_queue, skb);

    /* device 큐에 추가 → per-CPU 워커 기동 */
    ptr_ring_produce(&device_queue->ring, skb);
    queue_work_on(cpu, wg_packet_crypt_wq,
                  &per_cpu_ptr(device_queue->worker, cpu)->work);
    return 0;
}

타이머 메커니즘

WireGuard는 5개의 타이머로 핸드셰이크 수명주기, keepalive, 키 제로화를 관리합니다:

타이머주기트리거 조건동작
timer_retransmit_handshake 5초 핸드셰이크 Initiation 후 Response 미수신 Initiation 재전송 (최대 MAX_TIMER_HANDSHAKES=90회/7.5분)
timer_send_keepalive 10초 데이터 수신 후 KEEPALIVE_TIMEOUT 내 송신 없음 빈 패킷 전송 (NAT 매핑 유지)
timer_new_handshake 5초 + jitter 데이터 송신 후 KEEPALIVE_TIMEOUT + REKEY_TIMEOUT 내 수신 없음 새 핸드셰이크 시작 (데드 피어 감지)
timer_zero_key_material 180초 (3×REJECT_AFTER_TIME) 핸드셰이크 완료 후 세션 키 메모리 제로화 (전방 비밀성 강화)
timer_persistent_keepalive 사용자 설정 설정 시 항상 주기적 빈 패킷 (NAT 뒤 서버용)
/* drivers/net/wireguard/timers.h — 타이머 상수 */
#define REKEY_AFTER_MESSAGES     (1ULL << 60)   /* 2^60 메시지 후 키 갱신 */
#define REJECT_AFTER_MESSAGES   (U64_MAX - (1ULL << 13) - 1) /* nonce 한계 */
#define REKEY_AFTER_TIME         120           /* 2분 후 키 갱신 */
#define REJECT_AFTER_TIME        180           /* 3분 후 키 거부 */
#define REKEY_TIMEOUT            5             /* 핸드셰이크 재전송 간격 */
#define KEEPALIVE_TIMEOUT        10            /* Passive keepalive */
#define MAX_TIMER_HANDSHAKES     90            /* 최대 재전송 횟수 */
/* drivers/net/wireguard/timers.c — 키 갱신 로직 (단순화) */
static void wg_expired_retransmit_handshake(struct timer_list *timer)
{
    struct wg_peer *peer = from_timer(peer, timer,
                                        timer_retransmit_handshake);

    if (peer->timer_handshake_attempts > MAX_TIMER_HANDSHAKES) {
        /* 7.5분 동안 응답 없음 → 키 소재 제로화 */
        pr_debug("%s: Handshake timed out for peer %llu\n",
                 peer->device->dev->name, peer->internal_id);
        wg_noise_handshake_clear(&peer->handshake);
        wg_noise_keypairs_clear(&peer->keypairs);
        return;
    }

    /* Initiation 재전송 + 지수 백오프 아닌 고정 5초 간격 */
    ++peer->timer_handshake_attempts;
    wg_packet_send_handshake_initiation(peer);
}

/* 데이터 수신 시 keepalive 타이머 재설정 */
void wg_timers_data_received(struct wg_peer *peer)
{
    if (!timer_pending(&peer->timer_send_keepalive))
        mod_timer(&peer->timer_send_keepalive,
                  jiffies + KEEPALIVE_TIMEOUT * HZ);
}

/* 데이터 송신 시 키 수명 확인 */
void wg_timers_data_sent(struct wg_peer *peer)
{
    struct noise_keypair *keypair =
        rcu_dereference(peer->keypairs.current_keypair);

    /* REKEY_AFTER_TIME (120초) 경과 → 새 핸드셰이크 */
    if (keypair && keypair->is_initiator &&
        wg_birthdate_has_expired(keypair, REKEY_AFTER_TIME))
        wg_packet_send_handshake_initiation(peer);
}

리플레이 방지 (Sliding Window)

/* drivers/net/wireguard/noise.h — 리플레이 카운터 */
struct noise_replay_counter {
    u64 counter;                  /* 수신된 최대 nonce */
    unsigned long backtrack[COUNTER_BITS_TOTAL / BITS_PER_LONG];
                                   /* 비트맵 슬라이딩 윈도우 (2048비트) */
    struct spinlock lock;
};

/*
 * 리플레이 방지 알고리즘:
 *
 * 1. 수신 nonce > counter → 윈도우 슬라이드, counter 갱신, 허용
 * 2. counter - COUNTER_WINDOW_SIZE < nonce <= counter
 *    → backtrack 비트맵에서 해당 비트 확인
 *    → 이미 설정됨 → 리플레이 → 드롭
 *    → 미설정 → 비트 설정 + 허용
 * 3. nonce <= counter - COUNTER_WINDOW_SIZE → 너무 오래됨 → 드롭
 *
 * COUNTER_WINDOW_SIZE = 2048
 * → 최대 2048개 패킷의 순서 뒤바뀜 허용
 * → UDP 전송 환경에서 충분한 여유
 */

/* 리플레이 검사 */
bool wg_noise_counter_validate(
    struct noise_replay_counter *counter,
    u64 their_counter)
{
    bool ret = false;
    unsigned long index;

    spin_lock_bh(&counter->lock);

    if (unlikely(their_counter >= REJECT_AFTER_MESSAGES))
        goto out;

    if (their_counter > counter->counter) {
        /* 새로운 최대값 → 윈도우 전진 */
        index = their_counter >> COUNTER_REDUNDANT_BITS;
        /* 이전 비트들 클리어 */
        bitmap_clear(counter->backtrack, ...);
        counter->counter = their_counter;
        ret = true;
    } else if (their_counter + COUNTER_WINDOW_SIZE >
               counter->counter) {
        /* 윈도우 범위 내 → 비트맵 확인 */
        index = their_counter >> COUNTER_REDUNDANT_BITS;
        if (test_and_set_bit(index & (COUNTER_BITS_TOTAL - 1),
                             counter->backtrack))
            ret = false;  /* 이미 수신됨 → 리플레이 */
        else
            ret = true;   /* 처음 수신 → 허용 */
    }
    /* else: 윈도우 밖 → ret = false (너무 오래됨) */

out:
    spin_unlock_bh(&counter->lock);
    return ret;
}

로밍 (Roaming) 메커니즘

WireGuard는 피어의 endpoint(IP:port)를 고정하지 않고, 정상적으로 인증된 패킷이 도착할 때마다 소스 주소를 업데이트합니다. 이로써 모바일 기기의 네트워크 전환(Wi-Fi ↔ LTE) 시 자동으로 VPN이 유지됩니다:

/* drivers/net/wireguard/socket.c — 엔드포인트 업데이트 */
void wg_socket_set_peer_endpoint(
    struct wg_peer *peer,
    const struct endpoint *endpoint)
{
    /* 인증된 패킷의 소스 주소가 변경된 경우에만 업데이트 */
    if (endpoint_eq(&peer->endpoint, endpoint))
        return;

    write_lock_bh(&peer->endpoint_lock);
    peer->endpoint.addr = endpoint->addr;
    write_unlock_bh(&peer->endpoint_lock);
}

/*
 * 로밍 시나리오:
 *
 * 1. 서버(S)는 클라이언트(C) endpoint = 1.2.3.4:51820
 * 2. C가 Wi-Fi → LTE로 전환 → IP가 5.6.7.8로 변경
 * 3. C는 기존 세션 키로 패킷을 5.6.7.8에서 전송
 * 4. S는 수신 → 복호화 성공 → AllowedIPs 검증 통과
 * 5. S는 C의 endpoint를 5.6.7.8:port로 자동 업데이트
 * 6. 이후 S→C 응답은 5.6.7.8로 전송
 *
 * → 재연결, 재인증 불필요 (세션 키가 유효한 한 즉시 전환)
 * → 단, 정적 공개키를 알고 유효한 세션 키로 인증된 패킷만
 *   endpoint 변경을 유발 → 스푸핑 불가
 */

WireGuard는 wg(8) 유틸리티와 Generic Netlink로 통신합니다. 패밀리 이름 "wireguard"로 등록되며, WG_CMD_SET_DEVICEWG_CMD_GET_DEVICE 두 개의 명령을 지원합니다:

/* drivers/net/wireguard/netlink.c — Generic Netlink 정의 */
static const struct genl_ops wg_genl_ops[] = {
    {
        .cmd    = WG_CMD_GET_DEVICE,
        .doit   = wg_get_device_dump,  /* wg show */
        .dumpit = wg_get_device_dump,
        .flags  = GENL_UNS_ADMIN_PERM,
    },
    {
        .cmd    = WG_CMD_SET_DEVICE,
        .doit   = wg_set_device,       /* wg set */
        .flags  = GENL_UNS_ADMIN_PERM,
    },
};

static struct genl_family wg_genl_family __ro_after_init = {
    .name     = WG_GENL_NAME,         /* "wireguard" */
    .version  = WG_GENL_VERSION,      /* 1 */
    .maxattr  = WGDEVICE_A_MAX,
    .module   = THIS_MODULE,
    .ops      = wg_genl_ops,
    .n_ops    = ARRAY_SIZE(wg_genl_ops),
};

/* Netlink Attribute 계층 구조:
 *
 * WGDEVICE_A_IFNAME        ← 인터페이스 이름 ("wg0")
 * WGDEVICE_A_PRIVATE_KEY   ← 디바이스 비밀키
 * WGDEVICE_A_PUBLIC_KEY    ← 디바이스 공개키
 * WGDEVICE_A_LISTEN_PORT   ← 수신 UDP 포트
 * WGDEVICE_A_FWMARK        ← 라우팅 마크
 * WGDEVICE_A_PEERS         ← 중첩 피어 목록
 *   └─ WGPEER_A_PUBLIC_KEY       ← 피어 공개키
 *      WGPEER_A_PRESHARED_KEY    ← PSK (선택)
 *      WGPEER_A_ENDPOINT         ← IP:port
 *      WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL
 *      WGPEER_A_ALLOWEDIPS       ← 중첩 IP 목록
 *        └─ WGALLOWEDIP_A_FAMILY   ← AF_INET/AF_INET6
 *           WGALLOWEDIP_A_IPADDR   ← IP 주소
 *           WGALLOWEDIP_A_CIDR_MASK ← 서브넷 마스크
 *      WGPEER_A_LAST_HANDSHAKE_TIME
 *      WGPEER_A_RX_BYTES
 *      WGPEER_A_TX_BYTES
 */

설정 예제와 커널 동작

# WireGuard 인터페이스 생성 (ip-link netlink → rtnl_newlink)
ip link add wg0 type wireguard
# → rtnl_link_ops.newlink() → wg_newlink()
#   → alloc_netdev() + register_netdevice()
#   → wg0 netdevice 생성 (net_device_ops = wg_netdev_ops)

# 키 생성 (유저스페이스에서 Curve25519 키쌍 생성)
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
# → wg genkey: /dev/urandom에서 32바이트 읽기 + Curve25519 클램핑
#   wg pubkey: curve25519(private_key, basepoint) → 공개키

# WireGuard 설정 (Generic Netlink → WG_CMD_SET_DEVICE)
wg set wg0 \
    listen-port 51820 \
    private-key /etc/wireguard/private.key \
    peer "peer_public_key_base64" \
        endpoint 203.0.113.1:51820 \
        allowed-ips 10.0.0.0/24,fd00::/64 \
        persistent-keepalive 25
# → sendmsg(genl_sock, WG_CMD_SET_DEVICE, attrs)
#   → wg_set_device() → 피어 생성, AllowedIPs 트라이 구축

# IP 주소 할당 + 인터페이스 활성화
ip addr add 10.0.0.1/24 dev wg0
ip link set wg0 up
# → dev_open() → ndo_open() → wg_open()
#   → UDP 소켓 바인드 (listen-port)

# 라우팅 설정 (선택: 전체 트래픽 VPN 터널링)
ip route add default dev wg0 table 51820
ip rule add not fwmark 51820 table 51820
ip rule add table main suppress_prefixlength 0
# → fwmark로 WireGuard 자체 UDP 트래픽은 기본 경로 사용
#   나머지 트래픽은 wg0으로 라우팅

# 상태 확인 (Generic Netlink → WG_CMD_GET_DEVICE)
wg show wg0
# interface: wg0
#   public key: (base64)
#   private key: (hidden)
#   listening port: 51820
#
# peer: (base64)
#   endpoint: 203.0.113.1:51820
#   allowed ips: 10.0.0.0/24, fd00::/64
#   latest handshake: 23 seconds ago
#   transfer: 2.45 GiB received, 1.23 GiB sent
#   persistent keepalive: every 25 seconds

네트워크 네임스페이스와 WireGuard

/* WireGuard의 네임스페이스 분리 설계
 *
 * WireGuard 인터페이스는 두 개의 네임스페이스에 걸칠 수 있습니다:
 *
 *   ┌─── 내부 네임스페이스 (wg0이 속한 곳) ──┐
 *   │  애플리케이션 → wg0 → 암/복호화        │
 *   │  (VPN 터널 내부 트래픽)                 │
 *   └──────────────┬─────────────────────────┘
 *                  │ UDP 소켓
 *   ┌──────────────↓─────────────────────────┐
 *   │  외부 네임스페이스 (UDP 소켓이 바인드)   │
 *   │  eth0 → 인터넷 (암호화된 UDP 패킷)      │
 *   └────────────────────────────────────────┘
 *
 * → 컨테이너(내부 NS)에 wg0을 넣고,
 *   호스트(외부 NS)에서 실제 네트워크 통신
 * → 컨테이너는 평문 트래픽만 보고, 키 접근 불가
 */

/* drivers/net/wireguard/socket.c — 네임스페이스 분리 */
int wg_socket_init(struct wg_device *wg, u16 port)
{
    struct net *net;
    struct udp_tunnel_sock_cfg cfg = { ... };

    /* creating_net: wg 디바이스가 생성된 네임스페이스 (외부)
     * → UDP 소켓은 여기에 바인드
     * dev_net(wg->dev): wg0이 현재 속한 네임스페이스 (내부)
     * → 평문 패킷은 여기로 주입 */
    net = wg->creating_net;  /* 소켓은 생성 네임스페이스에서 */

    udp_sock_create(net, &cfg, &new4);
    setup_udp_tunnel_sock(net, new4, &cfg);
    rcu_assign_pointer(wg->sock4, new4->sk);
    return 0;
}
# 네임스페이스 활용 예: 컨테이너에 WireGuard 제공

# 1. 호스트 네임스페이스에서 WireGuard 생성
ip link add wg0 type wireguard
wg set wg0 private-key /etc/wireguard/key peer "..." endpoint ... allowed-ips 0.0.0.0/0

# 2. wg0을 컨테이너 네임스페이스로 이동
ip link set wg0 netns container_ns

# 3. 컨테이너 내에서 IP 할당
ip -n container_ns addr add 10.0.0.2/24 dev wg0
ip -n container_ns link set wg0 up
ip -n container_ns route add default dev wg0

# → UDP 소켓은 호스트 NS에서 동작 (eth0으로 통신)
# → 컨테이너는 wg0만 보이고, 비밀키에 접근 불가
# → 완전한 네트워크 격리 + VPN 터널링

성능 특성과 최적화

요소설명성능 영향
ChaCha20-Poly1305 SIMD 가속 (AVX2/NEON/AVX-512) AES-NI 없는 ARM에서 AES-GCM보다 3~5배 빠름
Curve25519 ADX/BMI2 최적화 (x86_64) 핸드셰이크당 ~100μs
Per-CPU 병렬화 암/복호화 워크큐 분산 코어 수에 비례한 처리량 확장
NAPI 폴링 인터럽트 대신 폴링 높은 PPS에서 IRQ 오버헤드 감소
GSO/GRO 세그먼테이션 오프로드 지원 대용량 전송 시 per-packet 오버헤드 감소
AllowedIPs 트라이 비트 단위 기수 트라이 (O(32) / O(128)) 피어 수 무관 일정 시간 라우팅
최소 오버헤드 헤더 32B + 인증 태그 16B = 48B IPSec ESP(~40B+) 대비 유사, OpenVPN(~70B+) 대비 작음
# WireGuard 성능 측정

# iperf3 벤치마크 (서버 측)
iperf3 -s -B 10.0.0.1

# iperf3 벤치마크 (클라이언트 측, wg0 경유)
iperf3 -c 10.0.0.1 -t 30 -P 4  # 4 parallel streams

# SIMD 가속 확인
dmesg | grep -i wireguard
# wireguard: WireGuard 1.0.0 loaded. See www.wireguard.com
# wireguard: Copyright (C) 2015-2019 Jason A. Donenfeld.
#            All Rights Reserved.

# 암호화 구현 확인 (SIMD 사용 여부)
grep -r "chacha20" /proc/crypto
# name         : chacha20
# driver       : chacha20-simd      ← SIMD 가속 활성

# CPU별 암호화 처리 분포 확인
perf top -g -p $(pgrep -f "wg-crypt")

# MTU 최적화 (WireGuard 오버헤드 고려)
# 이더넷 1500 - IPv4(20) - UDP(8) - WG헤더(32) - AuthTag(16) = 1424
ip link set wg0 mtu 1420  # 기본값 (약간의 여유 포함)

보안 속성 분석

보안 속성메커니즘설명
전방 비밀성 (PFS) 임시 Curve25519 키 각 세션마다 새 임시 키쌍 생성. 정적 비밀키 유출 시에도 과거 세션 복호화 불가
신원 은닉 Noise IK + 암호화된 정적키 Initiator의 정적 공개키는 암호화하여 전송. 도청자는 통신 당사자 식별 불가
양자 내성 레이어 PSK (Pre-Shared Key) 선택적 256비트 PSK를 키 유도에 믹스. 양자 컴퓨터 공격에 대한 추가 방어
리플레이 방지 2048비트 슬라이딩 윈도우 nonce 기반 리플레이 감지. UDP 순서 뒤바뀜 허용하면서 중복 패킷 차단
키 소재 제로화 3분 후 자동 소거 REJECT_AFTER_TIME 경과 시 세션 키 메모리를 0으로 덮어씀
DoS 완화 쿠키 + 레이트 리미터 mac2 쿠키로 IP 기반 인증, 핸드셰이크 레이트 리미팅
타이밍 공격 방지 상수 시간 비교 모든 MAC/키 비교에 crypto_memneq() 사용
스텔스 (비응답) 인증 실패 시 무응답 유효하지 않은 패킷에 대해 일체 응답하지 않음 → 포트 스캔에 응답 없음

WireGuard의 스텔스 특성: WireGuard는 인증되지 않은 패킷에 절대 응답하지 않습니다. 유효한 mac1이 없는 패킷은 조용히 드롭됩니다. 이로써 외부에서 WireGuard 서버의 존재 자체를 탐지하기 어렵습니다. 다만 UDP 포트가 열려 있다는 사실은 OS 레벨 포트 스캔으로 감지될 수 있으므로, 필요시 방화벽으로 소스 IP를 제한하세요.

디버깅과 모니터링

# WireGuard 상태 확인
wg show all
wg show wg0 dump  # 스크립트 파싱용 탭 구분 출력

# 커널 로그 (동적 디버깅 활성화)
echo module wireguard +p > /sys/kernel/debug/dynamic_debug/control
dmesg -w | grep wireguard

# 인터페이스 통계
ip -s link show wg0
# RX: bytes packets errors dropped
# TX: bytes packets errors dropped

# Netlink 디버깅 (strace로 wg 유틸리티 추적)
strace -e trace=network wg show wg0 2>&1 | grep -E "sendmsg|recvmsg"

# 패킷 캡처 (외부 UDP 캡슐화된 패킷)
tcpdump -i eth0 udp port 51820 -nn
# → 암호화된 WireGuard 패킷만 보임 (평문 확인 불가)

# 내부 트래픽 캡처 (복호화된 평문)
tcpdump -i wg0 -nn
# → VPN 터널 내부의 평문 IP 패킷

# 핸드셰이크 모니터링
watch -n 1 'wg show wg0 latest-handshakes'

# perf로 CPU 프로파일링 (암호화 병목 확인)
perf record -g -p $(pgrep -f wg-crypt) -- sleep 10
perf report --sort=dso,symbol

# 트래픽 흐름 추적 (ftrace)
echo 1 > /sys/kernel/debug/tracing/events/net/netif_receive_skb/enable
echo 'dev == "wg0"' > /sys/kernel/debug/tracing/events/net/netif_receive_skb/filter
cat /sys/kernel/debug/tracing/trace_pipe
💡

WireGuard 트러블슈팅 체크리스트: (1) wg show에서 "latest handshake"가 갱신되는지 확인 — 갱신 안 되면 UDP 연결 문제, (2) AllowedIPs가 양쪽 피어에서 올바르게 설정되었는지 확인 — 비대칭이면 한 방향만 동작, (3) 방화벽에서 UDP 포트가 열려 있는지 확인, (4) NAT 뒤에 있다면 persistent-keepalive를 25초로 설정, (5) MTU 문제가 의심되면 ping -M do -s 1384로 PMTU 확인.

IPv4 심화

IPv4 헤더 구조와 커널 처리

/* include/uapi/linux/ip.h */
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8    ihl:4,         /* Internet Header Length (단위: 4바이트) */
            version:4;    /* IP 버전 (항상 4) */
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u8    version:4,
            ihl:4;
#endif
    __u8    tos;             /* Type of Service / DSCP + ECN */
    __be16  tot_len;         /* 패킷 전체 길이 (헤더 + 페이로드) */
    __be16  id;              /* 식별자 (단편화용) */
    __be16  frag_off;        /* 플래그(3bit) + Fragment Offset(13bit) */
    __u8    ttl;             /* Time To Live */
    __u8    protocol;        /* 상위 프로토콜 (6=TCP, 17=UDP, 132=SCTP) */
    __sum16 check;           /* 헤더 체크섬 */
    union {
        struct {
            __be32  saddr;   /* 소스 IP 주소 */
            __be32  daddr;   /* 목적지 IP 주소 */
        };
        __be32 addrs[2];
    };
    /* IP 옵션 (ihl > 5일 때, 최대 40바이트) */
};

tos 필드의 상위 6비트는 DSCP(Differentiated Services Code Point)이고, 하위 2비트는 ECN(Explicit Congestion Notification)입니다. ECN은 라우터가 혼잡을 감지하면 패킷을 드롭하는 대신 ECN 비트를 설정하여 송신자에게 알리는 메커니즘으로, TCP 혼잡 제어와 긴밀하게 연동됩니다.

IPv4 수신 경로 (ip_rcv)

/* net/ipv4/ip_input.c — IPv4 패킷 수신 진입점 */
int ip_rcv(struct sk_buff *skb, struct net_device *dev,
           struct packet_type *pt, struct net_device *orig_dev)
{
    struct iphdr *iph;

    /* 1. 기본 검증 */
    if (!pskb_may_pull(skb, sizeof(struct iphdr)))
        goto drop;

    iph = ip_hdr(skb);

    /* 2. 버전, IHL, 길이 검증 */
    if (iph->ihl < 5 || iph->version != 4)
        goto inhdr_error;
    if (ntohs(iph->tot_len) < (iph->ihl * 4))
        goto inhdr_error;

    /* 3. 헤더 체크섬 검증 */
    if (ip_fast_csum((u8 *)iph, iph->ihl))
        goto csum_error;

    /* 4. Netfilter PREROUTING 훅 */
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
                   dev_net(dev), NULL, skb, dev, NULL,
                   ip_rcv_finish);
    /* → ip_rcv_finish → 라우팅 결정 → ip_local_deliver 또는 ip_forward */
}

/* ip_rcv_finish → ip_route_input_noref → 라우팅 결정 */
/*   목적지가 로컬: ip_local_deliver → ip_local_deliver_finish */
/*   목적지가 외부: ip_forward → ip_forward_finish */
/*   브로드캐스트:  ip_local_deliver (+ 포워딩 가능) */
/*   멀티캐스트:    ip_mr_input (멀티캐스트 라우팅) */

IP 체크섬 (Checksum) 심화

IP 체크섬은 RFC 1071에 정의된 1의 보수(one's complement) 합 알고리즘을 사용합니다. IPv4 헤더의 무결성을 보장하는 핵심 메커니즘으로, 매 홉(hop)마다 TTL이 감소하므로 라우터는 체크섬을 매번 재계산해야 합니다.

체크섬 알고리즘 원리

IP 체크섬은 헤더를 16비트 워드 단위로 나누어 1의 보수 덧셈을 수행한 뒤, 결과의 1의 보수를 취합니다:

/*
 * RFC 1071 — Internet Checksum 알고리즘
 *
 * 1. 체크섬 필드를 0으로 설정
 * 2. 헤더를 16비트 워드 단위로 분할
 * 3. 모든 워드를 1의 보수 합산 (캐리 발생 시 하위에 더함)
 * 4. 최종 합의 1의 보수(비트 반전)가 체크섬 값
 *
 * 검증: 체크섬 포함하여 전체 합산 → 결과가 0xFFFF이면 유효
 */

/* 단순 구현 (이해용 — 실제 커널은 최적화 버전 사용) */
static __sum16 simple_ip_checksum(const void *data, int len)
{
    const __be16 *ptr = data;
    u32 sum = 0;
    int nwords = len / 2;

    while (nwords-- > 0)
        sum += *ptr++;

    /* 홀수 바이트 처리 (IP 헤더에서는 발생하지 않지만 범용 구현) */
    if (len & 1)
        sum += *(u8 *)ptr;

    /* 캐리 폴딩: 상위 16비트를 하위 16비트에 반복 합산 */
    while (sum >> 16)
        sum = (sum & 0xFFFF) + (sum >> 16);

    return (__sum16)~sum;
}

1의 보수 연산 특성: 일반 2의 보수 덧셈과 달리, 1의 보수 합에서는 캐리(carry)가 발생하면 결과에 1을 더합니다(end-around carry). 이 특성 덕분에 바이트 순서(endianness)에 독립적이며, 합산 순서를 바꿔도 결과가 동일합니다. 또한 체크섬 계산에 체크섬 필드 자체를 포함해도 최종 결과가 0xFFFF(또는 ~0)이 되어 검증이 단순합니다.

커널 체크섬 구현

리눅스 커널은 성능을 위해 아키텍처별 최적화된 체크섬 함수를 제공합니다:

/* arch/x86/include/asm/checksum.h — x86 최적화 */
static inline __sum16 ip_fast_csum(const void *iph, unsigned int ihl)
{
    unsigned int sum;

    asm(
        "  movl (%1), %0\n"          /* 첫 번째 32비트 워드 로드 */
        "  subl $4, %2\n"            /* ihl -= 4 (최소 5이므로 1부터 루프) */
        "  jbe 2f\n"
        "  addl 4(%1), %0\n"         /* 두 번째 워드 가산 */
        "  adcl 8(%1), %0\n"         /* ADC: 캐리 포함 가산 (1의 보수 합) */
        "  adcl 12(%1), %0\n"        /* 네 번째 워드 */
        "1: adcl 16(%1), %0\n"       /* 루프: IP 옵션 영역 */
        "  lea 4(%1), %1\n"
        "  decl %2\n"
        "  jne 1b\n"
        "  adcl $0, %0\n"            /* 마지막 캐리 추가 */
        "  movl %0, %2\n"
        "  shrl $16, %0\n"           /* 상위 16비트 */
        "  addw %w2, %w0\n"          /* 16비트 폴딩 */
        "  adcl $0, %0\n"
        "  notl %0\n"                 /* 비트 반전 (1의 보수) */
        "2:"
        : "=r"(sum), "=r"(iph), "=r"(ihl)
        : "1"(iph), "2"(ihl)
        : "memory"
    );
    return (__sum16)sum;
}

/*
 * ip_fast_csum 동작 요약:
 *   - 32비트 단위로 ADC(Add with Carry) 명령어 사용
 *   - 처음 4워드(20바이트 기본 헤더)는 언롤링으로 분기 없이 처리
 *   - IP 옵션이 있으면(ihl > 5) 루프로 추가 워드 처리
 *   - 최종 32비트 → 16비트 폴딩 + NOT
 *   - 반환값 0 = 체크섬 유효, 비-0 = 오류
 */
/* include/net/checksum.h — 범용 체크섬 유틸리티 */

/* 부분 체크섬 계산 (임의 길이 데이터) */
__wsum csum_partial(const void *buff, int len, __wsum wsum);
/*
 * 데이터 버퍼의 부분 체크섬을 계산하여 기존 wsum에 누적.
 * TCP/UDP 페이로드 체크섬 계산의 핵심 함수.
 * 아키텍처별 어셈블리 최적화 제공 (x86, ARM, MIPS 등).
 *
 * x86_64: ADCQ 명령어로 64비트 단위 처리 → 대용량 데이터에서 2배 빠름
 * ARM: LDMIA + ADC 조합으로 레지스터 파이프라인 최적화
 */

/* 32비트 합을 16비트 체크섬으로 최종 폴딩 */
static inline __sum16 csum_fold(__wsum csum)
{
    u32 sum = (__force u32)csum;
    sum = (sum & 0xFFFF) + (sum >> 16);  /* 첫 번째 폴딩 */
    sum = (sum & 0xFFFF) + (sum >> 16);  /* 두 번째 폴딩 (캐리 전파) */
    return (__sum16)~sum;
}

/* 체크섬 검증: 전체 합이 0이면 유효 */
static inline __sum16 csum_verify(__wsum csum)
{
    return csum_fold(csum);  /* 결과가 0이면 유효 */
}

증분 체크섬 갱신 (Incremental Update)

라우터가 패킷을 포워딩할 때 TTL을 감소시키면 체크섬도 갱신해야 합니다. RFC 1624에 따라 전체를 재계산하지 않고 변경된 필드만으로 증분 갱신하여 성능을 최적화합니다:

/* include/net/ip.h — TTL 감소 + 체크섬 증분 갱신 */
static inline int ip_decrease_ttl(struct iphdr *iph)
{
    u32 check = (__force u32)iph->check;
    check += (__force u32)htons(0x0100);
    /*
     * TTL은 8번째 바이트 (offset 8).
     * TTL이 1 감소하면 16비트 워드 관점에서 상위 바이트가 1 감소.
     * 1의 보수 합에서 필드 감소 = 체크섬에 해당 차이를 더함.
     * htons(0x0100) = 빅엔디안에서 TTL 바이트 위치의 +1.
     *
     * 1의 보수 산술: ~(C + (-m) + m') = ~C' (RFC 1624)
     *   C  = 기존 체크섬의 1의 보수
     *   m  = 변경 전 값
     *   m' = 변경 후 값
     *   C' = 새 체크섬의 1의 보수
     */
    iph->check = (__force __sum16)(check + (check >= 0xFFFF));
    /* check >= 0xFFFF: 캐리 발생 시 +1 (end-around carry) */
    return --iph->ttl;
}

/* net/core/utils.c — 범용 증분 체크섬 갱신 */
void inet_proto_csum_replace4(__sum16 *sum, struct sk_buff *skb,
                              __be32 from, __be32 to, bool pseudohdr)
{
    /*
     * NAT에서 IP 주소 변경 시 사용.
     * from: 변경 전 주소, to: 변경 후 주소
     * pseudohdr: true면 TCP/UDP 의사 헤더 체크섬도 갱신
     *
     * 내부적으로 csum_replace4() 호출:
     *   ~csum_partial(&to, 4, csum_partial(&from_complement, 4, ~old_csum))
     *
     * 핵심: 전체 패킷을 다시 순회하지 않고 O(1)에 체크섬 갱신
     */
    if (skb->ip_summed != CHECKSUM_PARTIAL) {
        *sum = csum_fold(
            csum_partial((u8 *)&to, 4,
                csum_partial((u8 *)&from, 4,
                    ~csum_unfold(*sum))));
    } else if (pseudohdr) {
        *sum = ~csum_fold(
            csum_partial((u8 *)&to, 4,
                csum_partial((u8 *)&from, 4,
                    csum_unfold(*sum))));
    }
}

/* 2바이트 필드 변경 (포트 번호 등) */
void inet_proto_csum_replace2(__sum16 *sum, struct sk_buff *skb,
                              __be16 from, __be16 to, bool pseudohdr);
/* NAT에서 포트 변환(NAPT) 시 TCP/UDP 체크섬 증분 갱신에 사용 */
💡

증분 갱신의 성능 이점: 표준 IP 헤더(20바이트)의 체크섬 재계산은 10회의 16비트 덧셈이 필요하지만, 증분 갱신은 단 1~2회의 32비트 연산으로 완료됩니다. 고속 라우팅(포워딩 경로)에서 매 패킷마다 수행되므로 이 최적화는 매우 중요합니다. Netfilter NAT에서 IP 주소와 포트를 동시에 변경할 때도 inet_proto_csum_replace4inet_proto_csum_replace2를 순차적으로 호출하여 O(1)에 처리합니다.

TCP/UDP 의사 헤더 체크섬

TCP와 UDP는 IP 계층의 주소 정보를 체크섬에 포함하기 위해 의사 헤더(pseudo-header)를 사용합니다. 이 설계는 잘못된 호스트로 배달된 패킷을 상위 계층에서 탐지할 수 있게 합니다:

/*
 * TCP/UDP 의사 헤더 (RFC 793, RFC 768)
 *
 * +--------+--------+--------+--------+
 * |           Source Address           |  (4 bytes)
 * +--------+--------+--------+--------+
 * |        Destination Address         |  (4 bytes)
 * +--------+--------+--------+--------+
 * |  Zero  |Protocol|   TCP/UDP Len   |  (4 bytes)
 * +--------+--------+--------+--------+
 *
 * 실제 전송되지 않지만, 체크섬 계산에만 포함됨
 */

/* include/net/ip.h — 의사 헤더 체크섬 계산 */
static inline __wsum csum_tcpudp_nofold(
    __be32 saddr, __be32 daddr,
    __u32 len, __u8 proto, __wsum sum)
{
    /* x86 최적화 구현 (인라인 어셈블리) */
    asm(
        "  addl %1, %0\n"    /* sum += saddr */
        "  adcl %2, %0\n"    /* sum += daddr + carry */
        "  adcl %3, %0\n"    /* sum += (proto << 8) + len + carry */
        "  adcl $0, %0\n"    /* 마지막 캐리 추가 */
        : "=r"(sum)
        : "g"(daddr), "g"(saddr),
          "g"((u32)((u32)len + ((u32)proto << 8))),
          "0"(sum)
    );
    return sum;
}

/* 최종 TCP/UDP 체크섬 = csum_fold(의사 헤더 + 헤더 + 페이로드) */
static inline __sum16 csum_tcpudp_magic(
    __be32 saddr, __be32 daddr,
    __u32 len, __u8 proto, __wsum sum)
{
    return csum_fold(csum_tcpudp_nofold(saddr, daddr, len, proto, sum));
}

/* 전송 경로에서 TCP 체크섬 계산 예시 */
/* tcp_v4_send_check() → tcp_v4_check() */
static inline __sum16 tcp_v4_check(int len, __be32 saddr,
                                    __be32 daddr, __wsum base)
{
    return csum_tcpudp_magic(saddr, daddr, len, IPPROTO_TCP, base);
    /*
     * base = csum_partial(TCP 헤더 + 페이로드)
     * 의사 헤더(src_ip, dst_ip, 프로토콜, 길이) + 실제 데이터를
     * 하나의 1의 보수 합으로 통합
     *
     * HW offload 시 (CHECKSUM_PARTIAL):
     *   의사 헤더 체크섬만 미리 계산하여 TCP 헤더의 check 필드에 기록
     *   NIC가 나머지(TCP 헤더 + 페이로드) 합산을 HW로 처리
     */
}

하드웨어 체크섬 오프로드 상세

최신 NIC는 체크섬 계산을 하드웨어로 수행하여 CPU 부담을 크게 줄입니다. sk_buffip_summed 필드와 관련 필드들이 이를 제어합니다:

/* sk_buff 체크섬 관련 필드 */
struct sk_buff {
    /* ... */
    __u8   ip_summed:2;       /* CHECKSUM_NONE/UNNECESSARY/COMPLETE/PARTIAL */
    union {
        __wsum csum;           /* 수신: HW가 계산한 raw 체크섬 (COMPLETE) */
        struct {
            __u16 csum_start;  /* 송신: 체크섬 계산 시작 오프셋 (skb->head 기준) */
            __u16 csum_offset; /* 송신: 체크섬 저장 위치 (csum_start 기준) */
        };
    };
    /* ... */
};

/* ======== 수신 경로 (RX) ======== */

/* 1) CHECKSUM_COMPLETE: NIC가 L4 전체 패킷 합을 계산하여 제공 */
/*    드라이버: skb->csum에 HW 체크섬 저장 */
/*    스택: skb_checksum_validate()에서 의사 헤더만 추가로 합산하여 검증 */
static inline bool __skb_checksum_validate_needed(
    struct sk_buff *skb, bool zero_okay, __wsum pseudohdr)
{
    if (skb->ip_summed == CHECKSUM_COMPLETE) {
        /* HW csum + 의사 헤더 합산 → 폴딩 → 0이면 유효 */
        if (!csum_fold(csum_add(skb->csum, pseudohdr)))
            return false;  /* 유효 → SW 재검증 불필요 */
    }
    return true;  /* SW 검증 필요 */
}

/* 2) CHECKSUM_UNNECESSARY: NIC가 체크섬 검증까지 완료 */
/*    가장 빠름. loopback, 일부 고급 NIC */

/* 3) CHECKSUM_NONE: HW 지원 없음 → SW 전체 검증 */

/* ======== 송신 경로 (TX) ======== */

/* CHECKSUM_PARTIAL: 프로토콜 스택이 의사 헤더 체크섬만 계산 */
/*   NIC가 csum_start부터 패킷 끝까지의 체크섬을 계산하여 */
/*   csum_start + csum_offset 위치에 기록 */
static inline void skb_set_transport_header_csum(
    struct sk_buff *skb, int offset)
{
    skb->ip_summed = CHECKSUM_PARTIAL;
    skb->csum_start = skb_headroom(skb) + offset;
    skb->csum_offset = offsetof(struct tcphdr, check);
    /* TCP: csum_offset = 16 (check 필드의 오프셋) */
    /* UDP: csum_offset = 6  (check 필드의 오프셋) */
}

/* NIC가 HW offload를 지원하지 않을 때의 SW fallback */
int skb_checksum_help(struct sk_buff *skb)
{
    /*
     * CHECKSUM_PARTIAL → CHECKSUM_NONE으로 변환
     * SW로 체크섬을 직접 계산하여 패킷에 기록
     *
     * 호출 시점:
     *   - NIC가 NETIF_F_HW_CSUM / NETIF_F_IP_CSUM 미지원
     *   - Netfilter가 패킷을 변조하여 HW offload 불가능
     *   - veth, bridge 등 가상 디바이스 경유
     */
    __wsum csum;
    int offset = skb->csum_start - skb_headroom(skb);

    csum = skb_checksum(skb, offset, skb->len - offset, 0);
    *(__sum16 *)(skb->data + offset + skb->csum_offset) = csum_fold(csum);
    skb->ip_summed = CHECKSUM_NONE;
    return 0;
}

체크섬 오프로드와 패킷 캡처: tcpdump/Wireshark에서 송신 패킷의 체크섬이 잘못된 것으로 표시되는 경우가 많습니다. 이는 CHECKSUM_PARTIAL 상태에서 캡처 시점에 아직 NIC가 체크섬을 계산하지 않았기 때문입니다. 수신 측에서는 정상 체크섬이 확인됩니다. ethtool -K eth0 tx-checksum-ipv4 off로 오프로드를 비활성화하면 SW가 체크섬을 계산하여 캡처에서도 올바른 값을 볼 수 있습니다.

NIC 체크섬 피처 플래그

피처 플래그설명적용 범위
NETIF_F_IP_CSUM IPv4 TCP/UDP HW 체크섬 (L4) IPv4 + TCP/UDP만. 의사 헤더 포함 HW 계산
NETIF_F_IPV6_CSUM IPv6 TCP/UDP HW 체크섬 (L4) IPv6 + TCP/UDP만
NETIF_F_HW_CSUM 범용 HW 체크섬 임의 프로토콜. csum_start/csum_offset 기반으로 NIC가 계산
NETIF_F_RXCSUM 수신 HW 체크섬 검증 NIC가 수신 패킷 체크섬을 검증하여 CHECKSUM_COMPLETE 또는 CHECKSUM_UNNECESSARY 설정
# 현재 NIC의 체크섬 오프로드 상태 확인
ethtool -k eth0 | grep checksum
# rx-checksumming: on
# tx-checksumming: on
#   tx-checksum-ipv4: on
#   tx-checksum-ipv6: on
#   tx-checksum-ip-generic: off [not requested]

# 개별 제어
ethtool -K eth0 rx off          # 수신 체크섬 오프로드 비활성화
ethtool -K eth0 tx-checksum-ipv4 off  # 송신 IPv4 체크섬 오프로드 비활성화

IPv4 vs IPv6 체크섬 차이

항목IPv4IPv6
IP 헤더 체크섬 있음 (iphdr->check). 매 홉마다 재계산 없음. L2(이더넷 CRC) + L4(TCP/UDP) 체크섬으로 대체
설계 이유 1980년대: L2 CRC가 불충분한 환경 고려 라우터 포워딩 성능 향상. L2/L4 체크섬이 충분히 강력
UDP 체크섬 선택 (0이면 미사용) 필수. IP 헤더 체크섬이 없으므로 의사 헤더로 보완
의사 헤더 크기 12바이트 (src4 + dst4 + zero + proto + len) 40바이트 (src16 + dst16 + len4 + zero3 + next_header1)
커널 함수 ip_fast_csum(), csum_tcpudp_magic() csum_ipv6_magic() (의사 헤더만, IP 헤더 체크섬 없음)
/* include/net/ip6_checksum.h — IPv6 의사 헤더 체크섬 */
__sum16 csum_ipv6_magic(
    const struct in6_addr *saddr,   /* 128비트 소스 주소 */
    const struct in6_addr *daddr,   /* 128비트 목적지 주소 */
    __u32 len,                       /* Upper-Layer Packet Length */
    __u8  proto,                      /* Next Header (6=TCP, 17=UDP) */
    __wsum csum                       /* 기존 부분 합 */
);
/*
 * IPv6 의사 헤더 (RFC 8200, Section 8.1):
 *   Source Address (16) + Dest Address (16) +
 *   Upper-Layer Packet Length (4) + zero (3) + Next Header (1)
 * = 40바이트
 *
 * IPv4보다 의사 헤더가 크므로 csum 연산이 조금 더 비싸지만,
 * IP 헤더 체크섬 자체가 없어 라우터 포워딩은 더 빠름
 */

체크섬의 한계: 1의 보수 체크섬은 단일 비트 오류는 항상 검출하지만, 16비트 워드 경계의 동일 위치에서 두 비트가 동시에 반전되는 경우 검출 실패할 수 있습니다. CRC-32(이더넷)는 최대 32비트 버스트 오류까지 검출하므로, L2 CRC와 L4 체크섬의 이중 보호가 중요합니다. 추가로 TCP는 선택적으로 TCP-AO(Authentication Option) 또는 레거시 TCP-MD5를 사용하여 암호학적 무결성을 보장할 수 있습니다.

IP 단편화와 재조합

IPv4 패킷이 MTU를 초과하면 단편화(fragmentation)가 발생합니다. 커널은 수신 시 ip_defrag()로 단편을 재조합합니다:

/* frag_off 필드 해석 */
#define IP_DF       0x4000   /* Don't Fragment 플래그 */
#define IP_MF       0x2000   /* More Fragments 플래그 */
#define IP_OFFSET   0x1FFF   /* Fragment Offset 마스크 (단위: 8바이트) */

/* 단편화 여부 확인 */
if (iph->frag_off & htons(IP_MF | IP_OFFSET))
    /* 이 패킷은 단편이다 → 재조합 필요 */
    return ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER);

/* net/ipv4/ip_fragment.c — 재조합 핵심 */
/* ip_defrag()는 해시 테이블(inet_frag_queue)에 단편을 수집
 *   키: (src_ip, dst_ip, id, protocol)
 *   모든 단편 도착 시 → ip_frag_reasm()으로 하나의 skb로 합침
 *
 * 타임아웃: ipfrag_time (기본 30초)
 *   → 시간 내 모든 단편 미도착 시 재조합 포기, skb 해제
 *
 * 메모리 제한: ipfrag_high_thresh / ipfrag_low_thresh
 *   → 재조합 대기 메모리가 high_thresh 초과 시 오래된 큐 강제 해제
 */
sysctl 매개변수기본값설명
net.ipv4.ipfrag_time 30 (초) 재조합 타임아웃. 초과 시 모든 단편 폐기
net.ipv4.ipfrag_high_thresh 4194304 (4MB) 재조합 대기 메모리 상한. 초과 시 오래된 큐 제거
net.ipv4.ipfrag_low_thresh 3145728 (3MB) 메모리 회수 후 목표 수준
net.ipv4.ipfrag_max_dist 64 동일 소스의 최대 비순차 단편 수 (공격 방어)
net.ipv4.ip_no_pmtu_disc 0 PMTU Discovery 비활성화 (1로 설정 시 항상 DF=0)

Path MTU Discovery (PMTUD) 심화

Path MTU Discovery는 송신자와 수신자 사이 경로에서 단편화 없이 전송 가능한 최대 패킷 크기를 동적으로 탐지하는 메커니즘입니다. IPv4에서는 RFC 1191, IPv6에서는 RFC 8201로 정의되며, 리눅스 커널은 기본적으로 PMTUD를 활성화합니다.

PMTUD 동작 원리

PMTUD의 핵심은 IP 헤더의 DF(Don't Fragment) 비트와 ICMP 메시지의 상호작용입니다:

송신 호스트 라우터 A MTU=1500 라우터 B MTU=1400 수신 호스트 1500B, DF=1 ICMP Type 3 Code 4 Next-Hop MTU=1400 1400B, DF=1 ✓ PMTUD 과정: ① 송신자가 DF=1로 설정한 패킷(1500B)을 전송 ② 라우터 B는 MTU(1400) 초과 패킷을 폐기하고 ICMP "Fragmentation Needed" (Type 3, Code 4) 반환 ③ 송신자가 PMTU를 1400으로 갱신하고 작은 패킷으로 재전송 → 성공 ※ IPv6: ICMPv6 "Packet Too Big" (Type 2) 사용, 최소 MTU 1280바이트 보장

커널 PMTUD 구현

리눅스 커널의 PMTUD는 라우팅 서브시스템전송 계층이 긴밀하게 협력하여 동작합니다. PMTU 값은 dst_entry 경로 정보에 저장되며, fib_nh_exception 구조체를 통해 per-destination으로 관리됩니다.

/* include/net/dst.h — 경로의 PMTU 조회 */
static inline u32 dst_mtu(const struct dst_entry *dst)
{
    /* dst_ops에 정의된 mtu 콜백 호출
     * IPv4: ipv4_mtu() → PMTU 캐시 또는 인터페이스 MTU 반환
     * IPv6: ip6_mtu() → PMTU 캐시 또는 인터페이스 MTU 반환 */
    return dst->ops->mtu(dst);
}

/* net/ipv4/route.c — IPv4 PMTU 조회 */
static unsigned int ipv4_mtu(const struct dst_entry *dst)
{
    unsigned int mtu = dst_metric_raw(dst, RTAX_MTU);
    struct net *net = dev_net(dst->dev);

    if (mtu)
        return mtu;            /* 명시적으로 설정된 route MTU */

    mtu = READ_ONCE(dst->dev->mtu);  /* 인터페이스 MTU */

    if (unlikely(ip_mtu_locked(dst)))
        return mtu;            /* 관리자가 lock한 MTU (변경 불가) */

    /* fib_nh_exception에 캐싱된 PMTU가 있으면 그 값 사용 */
    if (mtu > IPV4_MIN_MTU)   /* 68바이트 (RFC 791 최소) */
        return mtu;

    return IPV4_MIN_MTU;
}

ICMP "Fragmentation Needed" 수신 처리

ICMP Type 3, Code 4 메시지를 수신하면 커널은 해당 목적지에 대한 PMTU를 갱신합니다:

/* net/ipv4/route.c — PMTU 갱신 핵심 함수 */
static void __ip_rt_update_pmtu(struct rtable *rt,
                                struct flowi4 *fl4, u32 mtu)
{
    struct dst_entry *dst = &rt->dst;
    struct net *net = dev_net(dst->dev);
    struct fib_result res;
    bool lock = false;

    /* RFC 1191: PMTU는 최소 68바이트 (IPv4 최소 MTU) */
    if (mtu < IPV4_MIN_MTU) {
        lock = true;
        mtu = IPV4_MIN_MTU;
    }

    /* ip_no_pmtu_disc 설정 시 PMTUD 무시 */
    if (mtu < ip_rt_min_pmtu(net))
        mtu = ip_rt_min_pmtu(net);

    /* FIB nexthop exception에 PMTU 캐싱 */
    rcu_read_lock();
    if (fib_lookup(net, fl4, &res, 0) == 0) {
        struct fib_nh_common *nhc = FIB_RES_NHC(res);
        update_or_create_fnhe(nhc, fl4->daddr, 0, mtu, lock,
                              jiffies + ip_rt_mtu_expires(net));
    }
    rcu_read_unlock();
}

/* net/ipv4/icmp.c — ICMP "Fragmentation Needed" 수신 경로 */
/*
 * icmp_rcv()
 *   → icmp_unreach()             (Type 3 처리)
 *     → icmp_unreach_handler()
 *       → ICMP_MIB_INMSGS++
 *       → 내부 IP 헤더에서 원본 (src, dst, proto) 추출
 *       → 상위 프로토콜의 err_handler 호출:
 *         TCP: tcp_v4_err() → tcp_v4_mtu_reduced()
 *         UDP: udp_err()   → ip_icmp_error()
 *         → ip_rt_update_pmtu() 호출하여 라우팅 캐시 갱신
 */

TCP와 PMTUD 연동

TCP는 PMTU 변경에 가장 적극적으로 대응하는 프로토콜입니다. ICMP 에러 수신 시 MSS를 조정하고 필요하면 세그먼트를 재전송합니다:

/* net/ipv4/tcp_ipv4.c — PMTU 감소 시 TCP 처리 */
static void tcp_v4_mtu_reduced(struct sock *sk)
{
    struct inet_sock *inet = inet_sk(sk);
    struct dst_entry *dst;
    u32 mtu;

    /* LISTEN 상태에서는 무시 */
    if ((1 << sk->sk_state) & (TCPF_LISTEN | TCPF_CLOSE))
        return;

    dst = inet_csk_update_pmtu(sk, tcp_sk(sk)->mtu_info);
    if (!dst)
        return;

    mtu = dst_mtu(dst);

    /* 새 PMTU에 맞춰 MSS 재계산 */
    if (inet_csk(sk)->icsk_pmtu_cookie > mtu) {
        /* TCP MSS = PMTU - IP 헤더(20) - TCP 헤더(20+옵션)
         * 예: PMTU=1400 → MSS = 1400 - 20 - 32 = 1348 */
        tcp_sync_mss(sk, mtu);

        /* 이미 전송된 세그먼트가 새 PMTU 초과 시
         * 재전송 큐의 세그먼트를 분할하여 재전송 */
        tcp_simple_retransmit(sk);
    }
}

/* TCP MSS 클램핑 관련 sysctl */
/* net.ipv4.tcp_mtu_probing:
 *   0 = 비활성 (기본)
 *   1 = PMTUD 블랙홀 감지 시에만 probing 활성화
 *   2 = 항상 probing 활성화
 *
 * net.ipv4.tcp_base_mss:
 *   MTU probing 시작 MSS (기본: 1024)
 *   → PMTUD 블랙홀 감지 시 이 값부터 시작하여 점진적으로 증가
 *
 * net.ipv4.tcp_mtu_probe_floor:
 *   probing 최소 MSS (기본: 48)
 */

TCP MSS와 PMTU 관계: TCP SYN 패킷의 MSS 옵션은 인터페이스 MTU에서 IP+TCP 헤더를 뺀 값입니다. 예를 들어 Ethernet MTU 1500이면 MSS=1460 (IPv4) 또는 1440 (IPv6)입니다. PMTUD가 경로상 더 작은 MTU를 발견하면 MSS를 동적으로 줄여 단편화를 방지합니다.

PMTUD 블랙홀 문제

PMTUD의 가장 심각한 문제는 블랙홀(Black Hole)입니다. 중간 경로의 방화벽이나 라우터가 ICMP "Fragmentation Needed" 메시지를 차단하면, 송신자는 PMTU를 알 수 없어 큰 패킷이 사라지는 현상이 발생합니다:

# PMTUD 블랙홀 증상 진단
# 1. 작은 패킷(ping)은 정상, 큰 패킷(SSH, HTTP)이 멈춤
ping -c 3 -M do -s 1472 목적지     # DF=1, 1500B (20+8+1472) → 통과 확인
ping -c 3 -M do -s 1400 목적지     # 점진적으로 줄여 병목 MTU 탐지

# 2. tracepath로 PMTU 탐색 (ICMP 기반)
tracepath -n 목적지
# 출력 예:
#  1?: [LOCALHOST]      pmtu 1500
#  1:  192.168.1.1      0.345ms
#  2:  10.0.0.1         1.234ms pmtu 1400    ← 여기서 MTU 감소
#  3:  172.16.0.1       2.567ms reached
#      Resume: pmtu 1400

# 3. 커널 PMTU 캐시 확인
ip route get 목적지
# 출력 예:
# 목적지 via 192.168.1.1 dev eth0 src 192.168.1.100
#     cache expires 542sec mtu 1400

# 4. PMTU 캐시 강제 삭제 (재탐색 유도)
ip route flush cache

블랙홀 해결 방법

해결 방법적용 위치설명설정 예시
MSS Clamping Netfilter (라우터) TCP SYN의 MSS 옵션을 강제로 줄여 PMTUD 없이도 단편화 방지 iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
TCP MTU Probing 송신 호스트 ICMP 없이 TCP 계층에서 직접 적정 MSS를 탐색 (PLPMTUD 유사) sysctl net.ipv4.tcp_mtu_probing=1
인터페이스 MTU 축소 송신 호스트/터널 터널 오버헤드를 고려한 보수적 MTU 설정 ip link set dev tun0 mtu 1400
PMTUD 비활성화 송신 호스트 DF=0으로 설정하여 중간 라우터가 단편화 (성능 저하) sysctl net.ipv4.ip_no_pmtu_disc=1

TCP MTU Probing (PLPMTUD) 상세

RFC 4821/8899에 정의된 Packetization Layer PMTUD (PLPMTUD)는 ICMP에 의존하지 않고 전송 계층에서 직접 적정 패킷 크기를 탐색합니다. 리눅스의 tcp_mtu_probing이 이 메커니즘을 구현합니다:

/* net/ipv4/tcp_timer.c — TCP MTU Probing 구현 */
static void tcp_mtup_probe(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct sk_buff *skb;
    int probe_size;

    /* 이진 탐색으로 최적 MSS 탐색
     * search_low  = 현재 동작하는 MSS (확인된 하한)
     * search_high = 시도할 MSS 상한
     * probe_size  = (search_low + search_high) / 2 */
    probe_size = (icsk->icsk_mtup.search_low +
                  icsk->icsk_mtup.search_high) / 2;

    /* probe 패킷 전송:
     * - 일반 데이터를 probe_size 크기로 전송
     * - ACK 수신 → search_low = probe_size (성공, 상향 탐색)
     * - RTO 타임아웃 → search_high = probe_size (실패, 하향 탐색)
     * - search_high - search_low < 8 이면 탐색 종료 */
}

/* tcp_mtu_probing 동작 모드:
 *
 * 모드 0 (기본): MTU probing 비활성. 전통적 PMTUD만 사용.
 *
 * 모드 1 (블랙홀 감지):
 *   - 정상 시에는 전통적 PMTUD 사용
 *   - TCP 재전송 타임아웃(RTO) 반복 시 블랙홀로 판단
 *   - 블랙홀 감지 → tcp_base_mss부터 이진 탐색 시작
 *   - 장점: 오버헤드 최소, 블랙홀 자동 복구
 *
 * 모드 2 (항상 활성):
 *   - 연결 시작부터 MTU probing 수행
 *   - ICMP가 전혀 동작하지 않는 환경에 적합
 *   - 오버헤드가 있으므로 일반적으로 모드 1 권장
 */

IPv6 Path MTU Discovery

IPv6는 중간 라우터의 단편화를 금지하므로 PMTUD가 더욱 중요합니다. 최소 MTU는 1280바이트이며, ICMPv6 "Packet Too Big" (Type 2)를 사용합니다:

/* net/ipv6/route.c — IPv6 PMTU 갱신 */
static void ip6_rt_update_pmtu(struct dst_entry *dst, struct sock *sk,
                               struct sk_buff *skb, u32 mtu,
                               bool confirm_neigh)
{
    struct rt6_info *rt6 = (struct rt6_info *)dst;

    /* IPv6 최소 MTU = 1280 (RFC 8200) */
    if (mtu < IPV6_MIN_MTU)
        mtu = IPV6_MIN_MTU;

    /* rt6_exception에 PMTU 캐싱 (IPv4의 fib_nh_exception과 유사) */
    rt6_do_update_pmtu(rt6, mtu);
    rt6_update_exception_stamp_rt(rt6);
}

/* IPv4 vs IPv6 PMTUD 차이점:
 *
 * 항목               IPv4 (RFC 1191)          IPv6 (RFC 8201)
 * ─────────────────────────────────────────────────────────────
 * ICMP 메시지        Type 3 Code 4            ICMPv6 Type 2
 *                    "Frag Needed"            "Packet Too Big"
 * 최소 MTU           68 바이트               1280 바이트
 * 중간 라우터 단편화  허용 (DF=0일 때)        금지 (항상)
 * 송신측 단편화       선택적                   Fragment Extension Header 사용
 * DF 비트            명시적 설정 필요          암묵적 (항상 DF=1 동작)
 * PMTU 만료          ip_rt_mtu_expires        rt6_mtu_expires
 * 최소 PMTU sysctl   ip_rt_min_pmtu          (없음, 항상 1280)
 */

PMTUD 관련 sysctl 매개변수 종합

sysctl 매개변수기본값설명
net.ipv4.ip_no_pmtu_disc 0 전역 PMTUD 비활성화. 1=항상 DF=0, 2=DF 설정하되 PMTUD 하지 않음, 3=PMTU 정보 유지하되 사용 안 함
net.ipv4.route.min_pmtu 552 PMTU 최솟값. 이보다 작은 ICMP 응답은 이 값으로 클램핑 (RFC 1191의 68보다 높게 설정)
net.ipv4.route.mtu_expires 600 (초) 캐싱된 PMTU의 만료 시간. 만료 후 인터페이스 MTU로 복원 (경로 변경 대응)
net.ipv4.tcp_mtu_probing 0 TCP MTU probing 모드. 0=비활성, 1=블랙홀 시 활성, 2=항상 활성
net.ipv4.tcp_base_mss 1024 MTU probing 시작 MSS. 블랙홀 감지 시 이 크기부터 이진 탐색
net.ipv4.tcp_mtu_probe_floor 48 MTU probing 최소 MSS 하한. 이보다 낮은 MSS는 시도하지 않음
net.ipv4.ip_forward_use_pmtu 0 포워딩 시 PMTU 사용 여부. 1=PMTU 적용 (라우터에서 단편화 감소), 0=인터페이스 MTU 사용
net.ipv6.conf.*.mtu (인터페이스별) 인터페이스별 IPv6 MTU. PMTUD 상한값으로 사용

터널과 PMTUD

VPN, GRE, VXLAN 등 터널 환경에서는 캡슐화 오버헤드로 인해 PMTUD가 특히 중요합니다:

# 터널별 일반적인 오버헤드와 권장 내부 MTU (외부 MTU=1500 기준)
#
# 터널 유형         오버헤드          내부 MTU   비고
# ──────────────────────────────────────────────────────────────
# GRE               24B (IP+GRE)      1476      키/시퀀스 시 +4~8B
# GRE + IPsec ESP   ≈80~120B          ≈1380     암호화 알고리즘에 따라 변동
# VXLAN             50B (UDP+VXLAN)   1450      Jumbo Frame 사용 시 완화
# WireGuard         60B (UDP+WG)      1420      IPv6 외부: 1400
# IPsec ESP(전송)   36~73B            ≈1430     암호화 + 인증
# IPsec ESP(터널)   56~93B            ≈1400     + 외부 IP 헤더 20B
# GENEVE            50~258B           ≈1450     가변 옵션 길이
# IP-in-IP (IPIP)   20B               1480      최소 오버헤드

# 터널 PMTUD 설정 예시
ip tunnel add gre1 mode gre remote 10.0.0.2 local 10.0.0.1 \
    pmtudisc                         # DF 비트 설정 (PMTUD 활성, 기본값)

ip link set gre1 mtu 1476            # 내부 MTU 수동 설정

# 터널 인터페이스에 MSS clamping 적용
iptables -t mangle -A FORWARD -o gre1 -p tcp \
    --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

# 커널 내부: 터널 xmit 시 PMTU 처리
# ip_tunnel_xmit() → 내부 패킷 크기 > 터널 PMTU?
#   → DF=1인 내부 패킷: 원래 송신자에게 ICMP "Frag Needed" 전송
#   → DF=0인 내부 패킷: 내부 패킷을 단편화 후 각각 캡슐화

이중 단편화(Double Fragmentation) 주의: 터널에서 nopmtudisc를 설정하면 외부 패킷이 단편화됩니다. 수신 측에서 외부 IP 재조합 → 디캡슐화 → 내부 IP 재조합으로 이중 재조합이 필요할 수 있어 성능이 크게 저하됩니다. 가능하면 pmtudisc(기본값)를 유지하고 내부 MTU를 적절히 설정하세요.

PMTUD 디버깅

# 1. 현재 경로의 PMTU 확인
ip route get 203.0.113.50
# 203.0.113.50 via 192.168.1.1 dev eth0 src 192.168.1.100 uid 0
#     cache expires 542sec mtu 1400   ← PMTU가 캐싱됨

# 2. 모든 PMTU 예외 확인 (fib_nh_exception)
ip route show cache
# 203.0.113.50 via 192.168.1.1 dev eth0
#     cache expires 542sec mtu 1400
# 198.51.100.25 via 192.168.1.1 dev eth0
#     cache expires 310sec mtu 1380

# 3. ICMP "Fragmentation Needed" 수신 모니터링
tcpdump -i eth0 'icmp[icmptype] == 3 and icmp[icmpcode] == 4' -nn -v
# → IP 10.0.0.1 > 192.168.1.100: ICMP 203.0.113.50 unreachable -
#   need to frag (mtu 1400), length 556

# 4. ICMPv6 "Packet Too Big" 모니터링
tcpdump -i eth0 'icmp6 and ip6[40] == 2' -nn -v

# 5. nstat으로 PMTU 관련 통계 확인
nstat -az | grep -i -E 'Pmtu|Frag|Mtu'
# IpFragOKs          0     # 성공적으로 단편화한 패킷 수
# IpFragFails        5     # DF=1로 단편화 실패 (ICMP 전송됨)
# IpFragCreates      0     # 생성된 단편 수
# IpReasmReqds       12    # 재조합 요청 수
# IpReasmOKs         12    # 재조합 성공 수
# IpReasmFails       0     # 재조합 실패 수

# 6. ftrace로 PMTU 갱신 추적
echo 1 > /sys/kernel/debug/tracing/events/fib/fib_table_lookup/enable
echo 'nexthop_exceptions != 0' > /sys/kernel/debug/tracing/events/fib/fib_table_lookup/filter
cat /sys/kernel/debug/tracing/trace_pipe

# 7. 수동 PMTU 테스트 (이진 탐색)
# MTU 1500에서 시작하여 DF 비트로 테스트
for size in 1472 1400 1300 1200; do
    ping -c 1 -M do -s $size -W 2 목적지 &> /dev/null \
        && echo "MTU >= $((size + 28)): OK" \
        || echo "MTU <  $((size + 28)): FAIL"
done
💡

운영 권장 사항: (1) tcp_mtu_probing=1을 서버에 기본 설정하여 PMTUD 블랙홀을 자동 복구, (2) 터널/VPN 라우터에는 TCPMSS --clamp-mss-to-pmtu를 항상 적용, (3) route.mtu_expires를 환경에 맞게 조정 (동적 경로가 많으면 짧게, 안정적이면 길게), (4) IpFragFails 카운터를 모니터링하여 PMTUD 블랙홀 조기 감지.

IPv4 전송 경로

/* net/ipv4/ip_output.c — IPv4 전송 */
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
    /* 1. 라우팅 조회 (캐시된 dst 또는 새로 lookup) */
    rt = ip_route_output_flow(net, fl4, sk);

    /* 2. IP 헤더 구성 */
    iph = ip_hdr(skb);
    iph->version  = 4;
    iph->ihl      = 5;
    iph->tos      = inet->tos;           /* IP_TOS 소켓 옵션 */
    iph->tot_len  = htons(skb->len);
    iph->id       = ip_select_ident(...); /* per-destination 카운터 */
    iph->frag_off = htons(IP_DF);        /* PMTUD 활성 시 DF 설정 */
    iph->ttl      = ip_select_ttl(inet, &rt->dst);
    iph->protocol = sk->sk_protocol;
    iph->saddr    = fl4->saddr;
    iph->daddr    = fl4->daddr;

    /* 3. Netfilter LOCAL_OUT 훅 → ip_output */
    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_OUT,
                   net, sk, skb, NULL, rt->dst.dev,
                   dst_output);
}

/* ip_output → ip_finish_output → ip_fragment (필요 시) → dev_queue_xmit */
/* ip_fragment: skb->len > mtu && DF 미설정 시 단편화 수행
 *   → ICMP "Fragmentation Needed" (DF 설정 시) 또는 실제 단편화
 *   → GSO skb는 skb_gso_segment()으로 분할 후 각각 전송
 */

IP 옵션 처리

옵션타입 번호커널 처리보안 영향
Record Route 7 ip_options_compile()에서 파싱, 포워딩 시 자기 IP 기록 네트워크 토폴로지 노출 위험
Timestamp 68 타임스탬프 기록/검증 시스템 시간 노출
Loose Source Route 131 지정 경로 경유 (기본 비활성: accept_source_route=0) IP 스푸핑 악용 가능 → 서버에서 반드시 비활성화
Strict Source Route 137 정확한 경로 강제 Loose와 동일한 보안 위험
Router Alert 148 라우터가 패킷을 로컬 처리 (IGMP, RSVP) DoS 가능성 (라우터 CPU 소비)

IP Source Routing 보안: net.ipv4.accept_source_route=0 (기본값)은 source route 옵션이 포함된 패킷을 폐기합니다. 이 옵션을 절대 활성화하지 마세요. 공격자가 패킷 경로를 조작하여 방화벽을 우회하거나 IP 스푸핑에 악용할 수 있습니다.

IPv6 심화

IPv6 헤더 구조

IPv6 헤더는 고정 40바이트로 IPv4보다 단순하지만, 확장 헤더 체인으로 유연성을 제공합니다:

/* include/uapi/linux/ipv6.h */
struct ipv6hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u8    priority:4,      /* Traffic Class 상위 4비트 */
            version:4;       /* IP 버전 (항상 6) */
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u8    version:4,
            priority:4;
#endif
    __u8    flow_lbl[3];     /* Traffic Class 하위 4비트 + Flow Label 20비트 */
    __be16  payload_len;     /* 페이로드 길이 (확장 헤더 포함, 기본 헤더 제외) */
    __u8    nexthdr;         /* 다음 헤더 타입 (6=TCP, 17=UDP, 43=Routing 등) */
    __u8    hop_limit;       /* IPv4의 TTL에 해당 */
    struct in6_addr saddr;   /* 소스 주소 (128비트) */
    struct in6_addr daddr;   /* 목적지 주소 (128비트) */
};
/* 크기: 정확히 40바이트 (IPv4와 달리 가변 길이 아님) */

/* 커널에서 IPv6 헤더 접근 */
struct ipv6hdr *hdr = ipv6_hdr(skb);
pr_info("src=%pI6c dst=%pI6c nexthdr=%u\n",
        &hdr->saddr, &hdr->daddr, hdr->nexthdr);

확장 헤더 체인

IPv6는 옵션을 확장 헤더로 체인 연결합니다. nexthdr 필드가 다음 헤더 타입을 지정합니다:

확장 헤더Next Header 값용도커널 소스
Hop-by-Hop Options 0 모든 중간 라우터가 검사 (Router Alert, Jumbogram) net/ipv6/exthdrs.c
Routing 43 소스 라우팅 (Type 0은 보안상 비활성, SRv6는 Type 4) net/ipv6/exthdrs.c
Fragment 44 송신측 단편화 (IPv6는 중간 라우터 단편화 금지) net/ipv6/reassembly.c
Destination Options 60 목적지 노드만 검사하는 옵션 net/ipv6/exthdrs.c
Authentication Header (AH) 51 IPSec 인증 net/ipv6/ah6.c
ESP 50 IPSec 암호화 net/ipv6/esp6.c
Segment Routing (SRH) 43 (Type 4) SRv6 세그먼트 리스트 net/ipv6/seg6.c
/* 확장 헤더 순회: 커널 내부 패턴 */
int ipv6_find_tlv(struct sk_buff *skb, int offset, int type)
{
    /* nexthdr 체인을 따라가며 특정 TLV 옵션 검색 */
    /* 각 확장 헤더: nexthdr(1) + hdrlen(1) + data(가변) */
    /* hdrlen 단위: 8바이트 (실제 길이 = (hdrlen+1)*8) */
}

/* ipv6_skip_exthdr: 확장 헤더를 건너뛰고 실제 상위 프로토콜 위치 찾기 */
int nexthdr = ipv6_hdr(skb)->nexthdr;
int offset = sizeof(struct ipv6hdr);
__be16 frag_off;
offset = ipv6_skip_exthdr(skb, offset, &nexthdr, &frag_off);
/* 반환: nexthdr = 실제 프로토콜 (TCP/UDP 등), offset = 페이로드 시작 위치 */

/* Fragment 헤더 구조 */
struct frag_hdr {
    __u8    nexthdr;          /* 단편화된 원본의 상위 프로토콜 */
    __u8    reserved;
    __be16  frag_off;         /* Offset(13bit) + Res(2bit) + MF(1bit) */
    __be32  identification;   /* 단편 식별자 (per-destination) */
};

NDP (Neighbor Discovery Protocol)

IPv6에서 ARP를 대체하는 NDP는 ICMPv6 기반으로 주소 해석, 라우터 발견, 주소 자동 설정을 수행합니다:

ICMPv6 타입메시지용도커널 함수
133 Router Solicitation (RS) 호스트가 라우터 탐색 ndisc_send_rs()
134 Router Advertisement (RA) 라우터가 프리픽스, MTU, DNS 광고 ndisc_router_discovery()
135 Neighbor Solicitation (NS) 주소 해석 (ARP Request 역할) + DAD ndisc_send_ns()
136 Neighbor Advertisement (NA) 주소 응답 (ARP Reply 역할) ndisc_send_na()
137 Redirect 더 나은 next-hop 알림 ndisc_redirect_rcv()
/* net/ipv6/ndisc.c — NDP 핵심 */
/* Neighbor Solicitation 수신 처리 */
static void ndisc_recv_ns(struct sk_buff *skb)
{
    struct nd_msg *msg = (struct nd_msg *)skb_transport_header(skb);
    struct in6_addr *target = &msg->target;

    /* DAD (Duplicate Address Detection) 확인 */
    if (ipv6_addr_any(&ipv6_hdr(skb)->saddr)) {
        /* 소스 = :: → DAD 요청 (주소 중복 검사) */
        /* 같은 주소를 가진 인터페이스가 있으면 DAD 실패 */
    }

    /* 타겟 주소가 로컬이면 NA(Neighbor Advertisement) 응답 */
    if (ipv6_chk_addr(net, target, dev, 0))
        ndisc_send_na(dev, &saddr, target, ...);
}

/* SLAAC (Stateless Address AutoConfiguration) */
/* RA에서 prefix 정보를 받아 자동으로 IPv6 주소 생성:
 *   주소 = prefix (64bit) + interface ID (64bit, EUI-64 또는 랜덤)
 *   net.ipv6.conf.*.use_tempaddr = 2 → RFC 4941 Privacy Extension
 *     → 임시 주소 자동 생성 (기본 24시간 유효)
 */

Flow Label과 ECMP

/* Flow Label: 20비트 (IPv6 헤더 내) */
/* 동일 flow의 패킷을 동일 경로로 라우팅 (ECMP 해시 입력) */

/* 커널 자동 설정: net.ipv6.flowlabel_state_ranges */
/* TCP: 연결별 고유 flow label 자동 할당 (auto_flowlabels) */
/* sysctl: net.ipv6.auto_flowlabels = 1 (기본 활성) */

/* 소켓에서 flow label 명시적 설정 */
struct in6_flowlabel_req freq = {
    .flr_label  = htonl(0x12345),
    .flr_action = IPV6_FL_A_GET,
    .flr_share  = IPV6_FL_S_EXCL,
};
setsockopt(fd, SOL_IPV6, IPV6_FLOWLABEL_MGR, &freq, sizeof(freq));

/* ECMP에서의 활용:
 * 라우터가 5-tuple 대신 (src, dst, flow_label)로 해시
 * → UDP 멀티플렉싱 환경에서도 안정적 경로 고정
 * → 특히 QUIC처럼 하나의 UDP 포트에 다수 연결 시 효과적
 */

커널 듀얼 스택 구현

sysctl기본값설명
net.ipv6.conf.all.disable_ipv6 0 IPv6 전체 비활성화 (1로 설정 시)
net.ipv6.bindv6only 0 0: IPv6 소켓이 IPv4도 수신 (mapped address). 1: IPv6 전용
net.ipv6.conf.*.accept_ra 1 Router Advertisement 수락 (2: forwarding 활성 시에도 수락)
net.ipv6.conf.*.autoconf 1 SLAAC 주소 자동 설정
net.ipv6.conf.*.use_tempaddr 0 Privacy Extension (2: 임시 주소 선호)
net.ipv6.conf.*.dad_transmits 1 DAD NS 전송 횟수 (0: DAD 비활성)
/* IPv4-mapped IPv6 주소: ::ffff:a.b.c.d */
/* 듀얼 스택 소켓이 IPv4 패킷을 수신하면:
 *   커널이 IPv4 주소를 mapped 형태로 변환하여 IPv6 소켓에 전달
 *   → 애플리케이션은 하나의 IPv6 소켓으로 양쪽 모두 처리 가능
 *
 * IPV6_V6ONLY 소켓 옵션:
 *   setsockopt(fd, SOL_IPV6, IPV6_V6ONLY, &on, sizeof(on));
 *   → IPv6 전용으로 제한 (mapped address 거부)
 */

/* 커널 내부: IPv4/IPv6 프로토콜 핸들러 등록 */
static struct inet_protosw tcpv6_protosw = {
    .type       = SOCK_STREAM,
    .protocol   = IPPROTO_TCP,
    .prot       = &tcpv6_prot,          /* IPv6 TCP 핸들러 */
    .ops        = &inet6_stream_ops,
};
/* tcp_v6_rcv()에서 IPv4-mapped 주소 패킷도 처리:
 *   ipv6_addr_v4mapped(&hdr->daddr) → tcp_v4_rcv()로 fallback
 */

TCP 심화 — 커널 내부 메커니즘

3-Way Handshake와 커널 구조체

/* TCP 연결 수립 과정의 커널 내부 */

/* 1단계: 클라이언트 SYN 전송 */
/*   tcp_v4_connect() → tcp_connect() → tcp_transmit_skb()
 *   → TCP_SYN_SENT 상태 전이
 *   skb에 SYN 플래그 + 초기 시퀀스 번호(ISN) + MSS/Window Scale 옵션 설정
 *   ISN: secure_tcp_seq() → siphash 기반 (예측 불가)
 */

/* 2단계: 서버 SYN+ACK 수신 */
/*   tcp_v4_rcv() → tcp_v4_do_rcv() → tcp_rcv_state_process()
 *   LISTEN 소켓에서 수신 → request_sock (미니 소켓) 생성
 *   → inet_csk_reqsk_queue_hash_add()로 SYN 큐에 추가
 *   → SYN+ACK 응답 전송
 */

/* 3단계: 클라이언트 ACK 수신 */
/*   서버: tcp_check_req() → 전체 struct sock 생성
 *   → inet_csk_complete_hashdance()로 accept 큐에 이동
 *   → TCP_ESTABLISHED 상태
 */

/* Listen 소켓의 큐 구조:
 *   SYN 큐 (반개방 연결):  request_sock으로 관리
 *     → 크기: /proc/sys/net/ipv4/tcp_max_syn_backlog
 *   Accept 큐 (완전 연결):  listen() backlog 인자로 제한
 *     → 크기: min(backlog, /proc/sys/net/core/somaxconn)
 */

SYN Cookie 메커니즘

SYN Flood 공격 시 SYN 큐가 가득 차면 SYN Cookie가 발동합니다. 서버 상태를 저장하지 않고 SYN+ACK의 시퀀스 번호에 연결 정보를 인코딩합니다:

/* net/ipv4/syncookies.c */
/* SYN Cookie ISN 생성: */
/* ISN = hash(saddr, daddr, sport, dport, count) + (count << 24)
 *        + (MSS 인덱스 << 접근자)
 *
 * 인코딩 정보:
 *   - 타임스탬프 (분 단위 카운터, 상위 비트)
 *   - MSS 값 (8개 고정 값 중 하나로 양자화)
 *   - 상대방 IP/포트 해시
 *
 * ACK 수신 시: ISN 검증 → request_sock 없이 직접 sock 생성
 */

/* 제약사항:
 *   - Window Scale, SACK, Timestamp 옵션 정보 손실
 *     → TCP 성능 저하 가능 (큰 윈도우, SACK 불가)
 *   - 커널 4.4+: TCP_SAVED_SYN으로 일부 완화
 *   - tcp_syncookies = 1: SYN 큐 overflow 시에만 활성화 (권장)
 *   - tcp_syncookies = 2: 항상 활성화 (성능 저하 감수)
 */

/* sysctl 설정 */
/* net.ipv4.tcp_syncookies = 1   (기본: overflow 시 활성화) */
/* net.ipv4.tcp_max_syn_backlog = 4096 (SYN 큐 크기) */
/* net.core.somaxconn = 4096 (accept 큐 크기) */

Window Scaling과 수신 윈도우

/* TCP 윈도우: 16비트 필드 → 최대 65535바이트 */
/* Window Scale 옵션 (RFC 7323): 3-way handshake 시 협상 */
/* 실제 윈도우 = header의 window × 2^(scale factor) */
/* 최대 scale factor = 14 → 최대 윈도우 = 65535 × 16384 ≈ 1GB */

/* net/ipv4/tcp_output.c */
static u16 tcp_select_window(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* 가용 수신 버퍼 크기를 윈도우로 광고 */
    u32 cur_win = tcp_receive_window(tp);

    /* 윈도우 축소 방지 (RFC 규칙) */
    if (new_win < cur_win)
        new_win = cur_win;

    /* Window Scale 적용 */
    return new_win >> tp->rx_opt.rcv_wscale;
}

/* 커널 자동 튜닝 (tcp_rmem) */
/* net.ipv4.tcp_rmem = 4096  131072  6291456
 *                     min   default  max
 *   커널이 RTT와 BDP(Bandwidth-Delay Product)에 따라
 *   수신 버퍼를 default~max 범위에서 자동 조절
 *   → tcp_moderate_rcvbuf=1 (기본) 일 때 활성화
 */

SACK (Selective Acknowledgment)

/* SACK: 수신자가 비연속적으로 받은 블록을 명시적으로 알림 */
/* → 송신자가 손실된 세그먼트만 정확히 재전송 가능 */

/* TCP 헤더 옵션으로 SACK 블록 전달 (최대 4블록) */
/* [Kind=5] [Length] [Left Edge 1][Right Edge 1] [Left Edge 2][Right Edge 2] ... */

/* net/ipv4/tcp_input.c */
static int tcp_sacktag_write_queue(struct sock *sk,
    const struct sk_buff *ack_skb, u32 prior_snd_una, ...)
{
    /* SACK 블록을 파싱하여 재전송 큐의 skb에 마킹 */
    /* TCPCB_SACKED_ACKED:  상대가 수신 확인한 블록 */
    /* TCPCB_SACKED_RETRANS: 재전송된 블록 */
    /* TCPCB_LOST:           손실로 판단된 블록 → 재전송 대상 */
}

/* SACK 관련 sysctl */
/* net.ipv4.tcp_sack = 1          (기본 활성화) */
/* net.ipv4.tcp_dsack = 1         (D-SACK: 중복 수신 알림) */
/* net.ipv4.tcp_fack = 0          (FACK: 6.x에서 제거됨) */

/* SACK 없이 3-duplicate ACK만 사용하면:
 *   연속 손실 시 하나씩 재전송 → 복구 느림
 * SACK 활성화 시:
 *   손실된 세그먼트를 한 RTT 내에 모두 재전송 가능
 */

TCP Keepalive

/* TCP Keepalive: 유휴 연결의 생존 여부 확인 */
/* net/ipv4/tcp_timer.c */

/* keepalive 타이머 동작:
 *   1. 마지막 데이터 이후 tcp_keepalive_time 경과 → 첫 probe 전송
 *   2. 응답 없으면 tcp_keepalive_intvl 간격으로 반복
 *   3. tcp_keepalive_probes 회 응답 없으면 연결 종료 (RST)
 */

/* sysctl 기본값 */
/* net.ipv4.tcp_keepalive_time  = 7200  (2시간 유휴 후 시작) */
/* net.ipv4.tcp_keepalive_intvl = 75    (75초 간격 probe) */
/* net.ipv4.tcp_keepalive_probes = 9    (9회 실패 시 종료) */
/* → 총 ~2시간 11분 후 연결 종료 */

/* 소켓별 설정 (sysctl 기본값 오버라이드) */
int optval = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &optval, sizeof(optval));

int idle = 60;    /* 60초 유휴 후 시작 */
setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));

int interval = 10; /* 10초 간격 */
setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));

int maxpkt = 3;   /* 3회 실패 시 종료 */
setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &maxpkt, sizeof(maxpkt));

TCP Fast Open (TFO)

/* TCP Fast Open: SYN 패킷에 데이터를 포함하여 1-RTT 절감 */
/* RFC 7413, 커널 3.7+ */

/* 동작 원리:
 *   1. 최초 연결: 일반 3-way handshake + 서버가 TFO 쿠키 발급
 *   2. 이후 연결: SYN + 쿠키 + 데이터 → 서버 즉시 응답 가능
 *   → HTTP 요청/응답에서 1-RTT 절감
 */

/* sysctl 설정 */
/* net.ipv4.tcp_fastopen = 3
 *   비트 0: 클라이언트 TFO 활성화
 *   비트 1: 서버 TFO 활성화
 *   비트 2: 쿠키 없이 TFO 허용 (보안 위험)
 */

/* 서버 측 */
int qlen = 5;
setsockopt(fd, SOL_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
/* qlen: TFO 대기열 크기 */

/* 클라이언트 측 */
sendto(fd, data, len, MSG_FASTOPEN,
       (struct sockaddr *)&addr, sizeof(addr));
/* connect() 없이 첫 sendto()에서 SYN+데이터 전송 */

TCP 성능 관련 sysctl 종합

sysctl기본값설명
tcp_wmem 4096 16384 4194304 전송 버퍼 (min/default/max). 자동 튜닝 범위
tcp_rmem 4096 131072 6291456 수신 버퍼 (min/default/max). 자동 튜닝 범위
tcp_mem (시스템 메모리 기반) TCP 전체 메모리 제한 (페이지 단위: low/pressure/high)
tcp_moderate_rcvbuf 1 수신 버퍼 자동 조절 활성화
tcp_window_scaling 1 Window Scale 옵션 (비활성화 시 최대 64KB)
tcp_timestamps 1 Timestamp 옵션 (RTT 측정, PAWS 보호)
tcp_tw_reuse 2 TIME_WAIT 소켓 재사용 (2: loopback+timestamp 조건부)
tcp_fin_timeout 60 FIN_WAIT2 타임아웃 (초)
tcp_max_tw_buckets 262144 TIME_WAIT 소켓 최대 수 (초과 시 즉시 종료)
tcp_slow_start_after_idle 1 유휴 후 슬로 스타트 재시작 (0: cwnd 유지)
tcp_notsent_lowat UINT_MAX 미전송 데이터 한계. epoll 통지 기준 (값 설정 시 쓰기 효율↑)
tcp_ecn 2 ECN (0:비활성, 1:활성, 2:서버측만 응답)

tcp_sock 핵심 구조체

struct tcp_sockstruct inet_connection_sock을 확장하며, TCP 연결의 모든 상태를 관리합니다. 소켓 하나당 약 2KB 이상의 메모리를 차지합니다:

/* include/linux/tcp.h */
struct tcp_sock {
    struct inet_connection_sock  inet_conn;

    /* === 시퀀스 번호 관리 === */
    u32     snd_una;        /* 전송 완료 확인된 첫 바이트 (send unacknowledged) */
    u32     snd_nxt;        /* 다음 전송할 시퀀스 번호 */
    u32     snd_wnd;        /* 상대방이 광고한 수신 윈도우 크기 */
    u32     rcv_nxt;        /* 다음 수신 기대 시퀀스 번호 */
    u32     rcv_wnd;        /* 광고할 수신 윈도우 크기 */
    u32     write_seq;      /* 유저가 write()한 마지막 바이트 다음 */
    u32     copied_seq;     /* 유저가 read()한 마지막 바이트 다음 */

    /* === 혼잡 제어 상태 === */
    u32     snd_cwnd;       /* 혼잡 윈도우 (cwnd) — 패킷 단위 */
    u32     snd_ssthresh;   /* 슬로 스타트 임계값 */
    u32     prior_cwnd;     /* 손실 복구 전 cwnd (undo용) */
    u32     prr_delivered;  /* PRR 알고리즘: 복구 중 전달된 세그먼트 */
    u32     prr_out;        /* PRR: 복구 중 전송한 세그먼트 */

    /* === RTT 측정 === */
    u32     srtt_us;        /* smoothed RTT (마이크로초 × 8) */
    u32     mdev_us;        /* RTT 편차 (마이크로초 × 4) */
    u32     rttvar_us;      /* RTT 분산 (RTO 계산용) */
    u32     rto;            /* 재전송 타임아웃 (jiffies) */

    /* === 재전송 관리 === */
    u32     retrans_out;    /* 현재 네트워크에 있는 재전송 세그먼트 수 */
    u32     lost_out;       /* 손실로 판단된 세그먼트 수 */
    u32     sacked_out;     /* SACK 확인된 세그먼트 수 */
    u8      reordering;     /* 현재 관측된 재정렬 수준 */

    /* === TCP 옵션 === */
    struct tcp_options_received rx_opt;
    /*   .rcv_wscale    수신 Window Scale factor
     *   .snd_wscale    전송 Window Scale factor
     *   .tstamp_ok     Timestamp 옵션 협상 여부
     *   .sack_ok       SACK 옵션 협상 여부
     *   .wscale_ok     Window Scale 협상 여부
     */

    /* === Pacing === */
    u64     tcp_mstamp;     /* 가장 최근 전송 시각 */
    u32     sk_pacing_rate; /* bytes/sec 단위 전송 속도 */

    /* === 혼잡 제어 알고리즘 private 데이터 === */
    u64     ca_priv[104 / sizeof(u64)];
    /* CUBIC: bic_K, bic_origin_point, cnt 등
     * BBR:   bw[], min_rtt_us, mode, cycle_idx 등
     */
};
관계 체인: struct sockstruct inet_sockstruct inet_connection_sockstruct tcp_sock. tcp_sk(sk) 매크로로 struct sock *에서 struct tcp_sock *로 캐스팅합니다. 각 계층이 프로토콜 독립적인 필드를 추가하는 상속 구조입니다.

TCP 연결 종료와 TIME_WAIT

TCP 연결 종료는 4-Way Handshake 또는 동시 종료(simultaneous close)로 진행됩니다. TIME_WAIT 상태는 지연 패킷 처리와 연결 식별자 재사용 방지를 위해 필수적입니다:

/* TCP 4-Way Handshake 연결 종료 */

/* 능동 종료자 (Active Close) — close() 호출 측 */
/*
 * ESTABLISHED → FIN_WAIT1 → FIN_WAIT2 → TIME_WAIT → CLOSED
 *
 * 1. close()/shutdown() 호출
 *    → tcp_close() → tcp_send_fin()
 *    → FIN 세그먼트 전송, 상태 → FIN_WAIT1
 *
 * 2. 상대방 ACK 수신
 *    → tcp_rcv_state_process() → FIN_WAIT2
 *    → tcp_fin_timeout (기본 60초) 타이머 시작
 *
 * 3. 상대방 FIN 수신
 *    → tcp_fin() → ACK 전송
 *    → TIME_WAIT 상태 전이
 *
 * 4. TIME_WAIT: 2 × MSL (Maximum Segment Lifetime) 동안 대기
 *    → Linux: 60초 고정 (TCP_TIMEWAIT_LEN)
 *    → 이유: 지연 패킷 흡수 + 마지막 ACK 재전송 보장
 */

/* 수동 종료자 (Passive Close) — FIN 수신 측 */
/*
 * ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
 *
 * 1. 상대방 FIN 수신
 *    → ACK 자동 전송, 상태 → CLOSE_WAIT
 *    → 애플리케이션에 EOF(read()=0) 전달
 *
 * 2. 애플리케이션 close() 호출
 *    → tcp_send_fin(), 상태 → LAST_ACK
 *
 * 3. 마지막 ACK 수신
 *    → CLOSED, 소켓 해제
 */

/* TIME_WAIT 소켓 최적화 */
struct tcp_timewait_sock {
    struct inet_timewait_sock tw_sk;
    u32    tw_rcv_nxt;      /* 기대 수신 시퀀스 */
    u32    tw_snd_nxt;      /* 마지막 전송 시퀀스 */
    u32    tw_rcv_wnd;      /* 수신 윈도우 */
    u32    tw_ts_recent;    /* 최근 타임스탬프 */
    long   tw_ts_recent_stamp;
};
/* → 전체 tcp_sock (~2KB) 대신 경량 구조체 (~240B) 사용
 *   → TIME_WAIT 소켓 수만 개에도 메모리 절약
 */
CLOSE_WAIT 누적: 수동 종료 측에서 애플리케이션이 close()를 호출하지 않으면 CLOSE_WAIT 상태가 무한히 쌓입니다. 이는 애플리케이션 버그(FD 누수)이며, ss -s로 모니터링해야 합니다. 커널은 이 상태를 강제로 정리하지 않습니다.
# TIME_WAIT 관련 sysctl 튜닝

# TIME_WAIT 소켓 재사용 (동일 4-tuple에 한해)
net.ipv4.tcp_tw_reuse = 1
# 조건: Timestamp 옵션 활성화 + 이전 타임스탬프보다 큰 값
# 2: loopback에서만 활성화 (기본값, 커널 5.7+)

# TIME_WAIT 소켓 최대 수
net.ipv4.tcp_max_tw_buckets = 262144
# 초과 시 새 TIME_WAIT 즉시 종료 (로그: "time wait bucket table overflow")

# FIN_WAIT2 타임아웃 (orphan 소켓)
net.ipv4.tcp_fin_timeout = 30
# 기본 60초 → 고부하 서버에서 30초로 단축 권장

# orphan 소켓 최대 수
net.ipv4.tcp_max_orphans = 65536
# close() 후 아직 FIN 교환 중인 소켓

TCP 재전송 메커니즘

TCP의 신뢰성 보장 핵심은 재전송입니다. 커널은 타이머 기반 재전송(RTO)과 빠른 재전송(Fast Retransmit) 두 가지 메커니즘을 사용합니다:

/* === RTO (Retransmission Timeout) 계산 — RFC 6298 === */
/* net/ipv4/tcp_input.c: tcp_rtt_estimator() */

/* RTT 샘플 수집 (Timestamp 옵션 또는 전송 시각 기록) */
/*
 * SRTT = (1 - α) × SRTT + α × RTT_sample     (α = 1/8)
 * RTTVAR = (1 - β) × RTTVAR + β × |SRTT - RTT_sample|  (β = 1/4)
 * RTO = SRTT + max(G, 4 × RTTVAR)             (G = clock granularity)
 *
 * 커널 구현 (정수 연산, 스케일링):
 *   tp→srtt_us    = srtt × 8     (마이크로초)
 *   tp→mdev_us    = rttvar × 4   (마이크로초)
 *   tp→rto        = jiffies 단위
 */

static void tcp_rtt_estimator(struct sock *sk, long mrtt_us)
{
    struct tcp_sock *tp = tcp_sk(sk);
    long m = mrtt_us;  /* 새 RTT 샘플 */
    u32 srtt = tp->srtt_us;

    if (srtt != 0) {
        m -= (srtt >> 3);         /* m = sample - srtt/8 */
        srtt += m;                  /* srtt = 7/8 × srtt + 1/8 × sample */
        if (m < 0) m = -m;
        m -= (tp->mdev_us >> 2);   /* mdev 갱신 */
        tp->mdev_us += m;
    } else {
        /* 첫 번째 RTT 샘플 */
        srtt = m << 3;             /* srtt = sample × 8 */
        tp->mdev_us = m << 1;      /* mdev = sample × 2 */
        tp->rttvar_us = max(tp->mdev_us, tcp_rto_min_us(sk));
    }
    tp->srtt_us = max(1U, srtt);
}

/* RTO 범위 제한 */
/* 최소: TCP_RTO_MIN = 200ms (HZ/5)
 * 최대: TCP_RTO_MAX = 120초 (120*HZ)
 * 초기: TCP_TIMEOUT_INIT = 1초 (SYN 재전송 시작값)
 */
/* === 재전송 타이머와 지수 백오프 === */
/* net/ipv4/tcp_timer.c: tcp_retransmit_timer() */

/*
 * RTO 만료 시 동작:
 *   1. snd_una 이후 첫 번째 미확인 skb를 재전송
 *   2. RTO를 2배로 증가 (지수 백오프: exponential backoff)
 *   3. snd_cwnd = 1 MSS (혼잡 윈도우 최소화)
 *   4. snd_ssthresh = max(flight_size/2, 2)
 *   5. 재전송 횟수 카운터 증가
 *
 * 최대 재전송 횟수:
 *   tcp_retries1 = 3   → 이 횟수 초과 시 라우팅 테이블 갱신 시도
 *   tcp_retries2 = 15  → 이 횟수 초과 시 연결 종료 (RST)
 *     → ~13~30분 (RTO 백오프에 따라 변동)
 *
 * SYN 재전송 횟수:
 *   tcp_syn_retries = 6    → 약 127초
 *   tcp_synack_retries = 5 → 약 63초
 */

void tcp_retransmit_timer(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (!tp->packets_out)  /* 미확인 패킷 없으면 무시 */
        return;

    /* 재전송 실행 */
    tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1);

    /* 지수 백오프: RTO × 2 */
    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
        min(tp->rto << 1, TCP_RTO_MAX), TCP_RTO_MAX);
}
/* === Fast Retransmit / Fast Recovery (RFC 5681, RFC 6675) === */

/*
 * 빠른 재전송 (Fast Retransmit):
 *   3개의 중복 ACK (duplicate ACK) 수신 시
 *   → RTO 만료를 기다리지 않고 즉시 재전송
 *   → SACK 기반: 3개 이상의 세그먼트가 SACK 확인되면 gap을 손실로 간주
 *
 * 빠른 복구 (Fast Recovery):
 *   → ssthresh = max(flight_size / 2, 2)
 *   → cwnd = ssthresh + 3 (중복 ACK 수만큼)
 *   → 중복 ACK마다 cwnd++ (새 세그먼트 전송 가능)
 *   → 새 ACK(snd_una 전진) 수신 시 cwnd = ssthresh, 복구 종료
 *
 * PRR (Proportional Rate Reduction, RFC 6937):
 *   → 커널 기본 복구 알고리즘 (3.2+)
 *   → 기존 Fast Recovery의 버스트 문제 해결
 *   → 손실 복구 중에도 일정한 비율로 세그먼트 전송
 *   → prr_delivered, prr_out으로 전송량 조절
 */

/* 손실 감지 상태 머신 (tcp_ca_state) */
enum tcp_ca_state {
    TCP_CA_Open     = 0,  /* 정상 동작 (cwnd 증가) */
    TCP_CA_Disorder = 1,  /* 중복 ACK/SACK 감지 (아직 손실 미확정) */
    TCP_CA_CWR      = 2,  /* ECN-Echo 수신 → cwnd 감소 중 */
    TCP_CA_Recovery = 3,  /* Fast Retransmit 진입 (SACK 기반 복구) */
    TCP_CA_Loss     = 4,  /* RTO 만료 → cwnd=1, 전체 재전송 */
};
RACK (Recent ACKnowledgment): 커널 4.15+에서 기본 활성화된 손실 감지 알고리즘입니다. 시간 기반으로 동작하여 3 dupACK 규칙보다 정확합니다. tcp_rack_detect_loss()에서 가장 최근 ACK된 세그먼트의 전송 시각과 비교하여, 일정 시간(min_rtt/4) 이상 지난 미확인 세그먼트를 손실로 판단합니다. net.ipv4.tcp_recovery = 1 (기본)로 활성화됩니다.

혼잡 제어 심화 — CUBIC과 BBR

Linux의 혼잡 제어 프레임워크는 struct tcp_congestion_ops 인터페이스를 통해 알고리즘을 플러그인으로 교체할 수 있습니다:

/* include/net/tcp.h */
struct tcp_congestion_ops {
    /* 필수 콜백 */
    void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
    /* → ACK 수신 시 cwnd 조절 (Slow Start / Congestion Avoidance) */

    u32  (*ssthresh)(struct sock *sk);
    /* → 손실 감지 시 새 ssthresh 계산 */

    /* 선택적 콜백 */
    void (*init)(struct sock *sk);
    void (*release)(struct sock *sk);
    void (*set_state)(struct sock *sk, u8 new_state);
    void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
    void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
    u32  (*undo_cwnd)(struct sock *sk);
    u32  (*sndbuf_expand)(struct sock *sk);

    /* BBR 등에서 사용 */
    u32  (*min_tso_segs)(struct sock *sk);
    void (*cong_control)(struct sock *sk, const struct rate_sample *rs);
    /* → cong_control이 정의되면 cong_avoid/ssthresh 대신 호출
     *   BBR은 이 콜백에서 cwnd와 pacing_rate를 직접 설정 */

    char            name[TCP_CA_NAME_MAX];
    struct module   *owner;
};

CUBIC 알고리즘 내부

/* net/ipv4/tcp_cubic.c — Linux 기본 혼잡 제어 (커널 2.6.19+) */

/* CUBIC 핵심 아이디어:
 *   cwnd 증가를 3차 함수(cubic function)로 모델링
 *   → 손실 전 cwnd(Wmax)에 빠르게 접근, 이후 느리게 탐색
 *   → 고대역폭·고지연 네트워크에서 Reno보다 빠른 대역폭 활용
 */

/* CUBIC 윈도우 함수:
 *   W(t) = C × (t - K)³ + Wmax
 *
 *   C = 0.4 (스케일링 상수)
 *   K = ³√(Wmax × β / C)  — 원점에서 Wmax까지 도달 시간
 *   β = 0.7 (손실 시 cwnd 감소 비율: new_cwnd = Wmax × 0.7)
 *   t = 마지막 손실 이후 경과 시간
 */

struct bictcp {
    u32  cnt;             /* cwnd 증가 속도 (ACK당 1/cnt MSS) */
    u32  last_max_cwnd;    /* Wmax: 마지막 손실 시점 cwnd */
    u32  last_cwnd;        /* 직전 cwnd 값 */
    u32  last_time;        /* 직전 갱신 시각 */
    u32  bic_origin_point; /* CUBIC 함수 원점 */
    u32  bic_K;            /* Wmax 도달 시간 K */
    u32  epoch_start;      /* 현재 에포크 시작 시각 */
    u32  ack_cnt;          /* 에포크 내 ACK 카운트 */
    u32  tcp_cwnd;         /* Reno 모드 cwnd (하이브리드용) */
};

/* CUBIC 슬로 스타트: Hystart++
 *   → 표준 슬로 스타트의 과도한 오버슈트 방지
 *   → ACK 지연 변화량으로 BDP 근처 감지
 *   → 탐지 시 ssthresh 설정하고 congestion avoidance 전환
 *   → net.ipv4.tcp_hystart = 1 (기본 활성화)
 */

BBR 알고리즘 내부

/* net/ipv4/tcp_bbr.c — Google BBR (커널 4.9+) */

/* BBR 핵심 원리:
 *   손실이 아닌 "대역폭(BtlBw)"과 "최소 RTT(RTprop)"를 측정하여
 *   최적 전송 속도를 결정
 *
 *   pacing_rate = BtlBw × pacing_gain
 *   cwnd = BDP × cwnd_gain = BtlBw × RTprop × cwnd_gain
 */

struct bbr {
    u32  min_rtt_us;          /* 관측된 최소 RTT (10초 윈도우) */
    u32  min_rtt_stamp;       /* min_rtt 측정 시각 */
    u32  bw[2];              /* 최대 대역폭 샘플 (windowed max) */
    u32  mode:3,             /* 현재 상태: STARTUP/DRAIN/PROBE_BW/PROBE_RTT */
         prev_ca_state:3,
         round_start:1,
         idle_restart:1,
         probe_rtt_round_done:1;
    u32  cycle_idx;           /* PROBE_BW 사이클 위치 (0~7) */
    u32  pacing_gain;         /* 현재 pacing gain (× BBR_UNIT) */
    u32  cwnd_gain;           /* 현재 cwnd gain */
};

/* BBR 상태 머신 */
/*
 * 1. STARTUP (슬로 스타트 대응)
 *    pacing_gain = 2.89 (= 2/ln2), cwnd_gain = 2.89
 *    → 대역폭이 3 라운드 연속 25% 미만 증가하면 DRAIN 전환
 *
 * 2. DRAIN
 *    pacing_gain = 1/2.89 ≈ 0.35
 *    → 큐에 쌓인 데이터 배출 (inflight ≤ BDP까지)
 *
 * 3. PROBE_BW (정상 상태 — 대부분 시간)
 *    8-phase 사이클:
 *    [1.25, 0.75, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
 *    → 1.25: 대역폭 탐색 (약간 공격적 전송)
 *    → 0.75: 큐 배출
 *    → 1.0 × 6: 안정 상태 유지
 *
 * 4. PROBE_RTT (10초마다)
 *    cwnd = 4 패킷으로 축소, 200ms 유지
 *    → 최소 RTT 재측정 (큐 비우고 순수 전파 지연 측정)
 */
BBR vs CUBIC 선택 기준:
  • CUBIC 권장: 낮은 패킷 손실률, 일반적인 인터넷/데이터센터 환경
  • BBR 권장: 높은 패킷 손실률(무선, 위성), 고지연(WAN), 버퍼블로트 환경
  • 주의: BBR은 손실을 무시하므로 CUBIC 플로우와 공존 시 대역폭을 불공평하게 점유할 수 있습니다. BBRv2(커널 6.x 패치)에서 개선 진행 중
소켓별 설정: setsockopt(fd, SOL_TCP, TCP_CONGESTION, "bbr", 3)

TCP 데이터 전송/수신 경로

유저 프로세스의 send()/recv() 호출이 커널 내부에서 처리되는 전체 경로입니다:

/* === 전송 경로 (send → wire) === */

/* 1. 시스템 콜 진입 */
/* send(fd, buf, len, flags)
 *   → sys_sendto() → sock_sendmsg() → tcp_sendmsg()
 */

/* 2. tcp_sendmsg() — net/ipv4/tcp.c */
/*
 *   a) 유저 데이터를 sk_buff 체인으로 복사 (send buffer)
 *      → sk->sk_write_queue에 추가
 *      → 가능하면 기존 skb의 남은 공간에 append (coalescence)
 *      → copy_from_iter()로 유저 → 커널 복사
 *
 *   b) write_seq 갱신
 *
 *   c) 전송 조건 확인 후 tcp_push() 호출
 *      → Nagle 알고리즘 확인 (TCP_NODELAY 아니면)
 *      → 혼잡 윈도우 / 수신 윈도우 확인
 */

/* 3. tcp_write_xmit() — 실제 세그먼트 전송 루프 */
/*
 *   while (cwnd에 여유 && 전송 대기 skb 있음) {
 *     tcp_transmit_skb():
 *       a) TCP 헤더 구성 (seq, ack, window, options)
 *       b) 체크섬 계산 (또는 hw checksum offload 설정)
 *       c) IP 계층 전달: ip_queue_xmit()
 *       d) retransmit queue에 skb 유지 (ACK 대기)
 *
 *     pacing 적용: sk→sk_pacing_rate에 따라 전송 간격 조절
 *     TSO 적용: 대형 세그먼트를 NIC에서 분할하도록 설정
 *   }
 */

/* 4. IP → 디바이스 → NIC */
/*   ip_queue_xmit() → ip_local_out() → NF_INET_LOCAL_OUT
 *   → dst_output() → ip_output() → NF_INET_POST_ROUTING
 *   → ip_finish_output() → neigh_output() → dev_queue_xmit()
 *   → qdisc → NIC 드라이버 → 하드웨어 전송
 */
/* === 수신 경로 (wire → recv) === */

/* 1. NIC → NAPI → IP */
/*   NIC 인터럽트 → NAPI poll → napi_gro_receive()
 *   → netif_receive_skb() → ip_rcv()
 *   → NF_INET_PRE_ROUTING → ip_rcv_finish()
 *   → ip_local_deliver() → NF_INET_LOCAL_IN
 *   → tcp_v4_rcv()
 */

/* 2. tcp_v4_rcv() — TCP 수신 핵심 */
/*
 *   a) 4-tuple (src_ip, dst_ip, src_port, dst_port)로 소켓 조회
 *      → inet_lookup_established() 또는 inet_lookup_listener()
 *      → Early Demux 최적화 적용 가능
 *
 *   b) 체크섬 검증 (HW offload 또는 SW)
 *
 *   c) tcp_v4_do_rcv() → tcp_rcv_established() (대부분의 경우)
 */

/* 3. tcp_rcv_established() — Fast Path / Slow Path */
/*
 *   Fast Path (예측 기반 — 일반적 경우):
 *     → 다음 기대 시퀀스와 일치하는 순서 데이터
 *     → ACK 번호가 유효
 *     → 윈도우 변화 없음
 *     → 직접 sk→sk_receive_queue에 추가
 *     → 매우 빠름 (헤더 예측으로 분기 최소화)
 *
 *   Slow Path:
 *     → 비순서 데이터 → Out-of-Order 큐에 추가
 *     → SACK 처리, 윈도우 업데이트, URG 등
 *     → tcp_data_queue() → tcp_ofo_queue() (재정렬)
 */

/* 4. 유저 read() */
/*   recv()/read() → tcp_recvmsg()
 *     → sk_receive_queue에서 데이터를 유저 버퍼로 복사
 *     → copied_seq 갱신
 *     → 수신 윈도우 갱신 → ACK 전송 (조건부)
 *
 *   Delayed ACK: 즉시 ACK 대신 최대 40ms 지연 (tcp_delack_timer)
 *     → 데이터 응답에 ACK를 피기백(piggyback)하여 패킷 수 절감
 *     → 2번째 세그먼트마다 즉시 ACK (quick_ack)
 */
TCP 전송 경로 상세 send(fd, buf, len) — User Space tcp_sendmsg() — 데이터 → skb 복사 tcp_write_xmit() — cwnd/rwnd 확인 Nagle | Pacing | TSO | 혼잡 제어 tcp_transmit_skb() — TCP 헤더 생성 ip_queue_xmit() — IP 헤더 + 라우팅 Netfilter (LOCAL_OUT → POST_ROUTING) dev_queue_xmit() → qdisc NIC Driver → Hardware TX retransmit queue 에 skb 유지 (ACK 대기) cwnd 초과 시 전송 보류
TCP 전송 경로: send()에서 NIC까지의 커널 내부 처리 단계

TCP 메모리 관리와 TSQ

/* === TCP 메모리 관리 3단계 === */
/* net.ipv4.tcp_mem = low pressure high  (페이지 단위)
 *
 * 전체 TCP 소켓이 사용하는 메모리를 3단계로 관리:
 *
 * 1. low 미만: 정상 동작
 *    → 소켓별 버퍼 자동 튜닝 정상 작동
 *
 * 2. pressure (low < 현재 < high):
 *    → tcp_memory_pressure 플래그 설정
 *    → 소켓별 버퍼 축소 시작
 *    → 새 버퍼 할당에 제한
 *    → sk_stream_moderate_sndbuf()로 전송 버퍼 감소
 *
 * 3. high 이상:
 *    → 새 메모리 할당 거부 (소켓 write 블로킹)
 *    → 기존 연결의 전송도 지연될 수 있음
 *    → OOM 방지를 위한 최후 방어선
 *
 * 현재 사용량 확인: cat /proc/net/sockstat
 *   TCP: inuse 1234 orphan 0 tw 56 alloc 1234 mem 789
 *   (mem = 현재 사용 페이지 수)
 */

/* === 소켓별 메모리 관리 === */
/*
 * sk→sk_wmem_queued:  전송 버퍼에 쌓인 바이트
 * sk→sk_rmem_alloc:   수신 버퍼에 쌓인 바이트
 * sk→sk_sndbuf:       전송 버퍼 상한 (tcp_wmem 기반 자동 조절)
 * sk→sk_rcvbuf:       수신 버퍼 상한 (tcp_rmem 기반 자동 조절)
 *
 * sk_wmem_queued ≥ sk_sndbuf 이면:
 *   → sk_stream_wait_memory(): write() 블로킹
 *   → epoll: EPOLLOUT 해제
 */
/* === TSQ (TCP Small Queues) — net/ipv4/tcp_output.c === */
/* 커널 3.6+ (commit 46d3ceab) */

/* 문제: 대량의 TCP 세그먼트가 qdisc 큐에 쌓이면
 *   → 지연 시간 증가 (버퍼블로트)
 *   → 다른 플로우에 대한 공정성 저하
 *   → 혼잡 제어의 피드백 루프가 느려짐
 *
 * 해결: 소켓당 qdisc/NIC에 대기 중인 바이트 수를 제한
 *   → sk→sk_pacing_status로 추적
 *   → 제한: sysctl net.ipv4.tcp_limit_output_bytes (기본 1MB)
 *
 * 동작:
 *   tcp_write_xmit()에서 전송 전 확인:
 *   if (sk→sk_wmem_queued - sk→sk_wmem_alloc > limit)
 *       → 전송 보류, tasklet으로 나중에 재시도
 *
 * TSQ tasklet:
 *   NIC 드라이버가 skb 전송 완료 → skb_orphan()
 *   → sk→sk_wmem_alloc 감소
 *   → tcp_tsq_handler() → tcp_write_xmit() 재개
 *
 * 효과:
 *   → qdisc 큐 깊이 감소 → 지연 시간 대폭 개선
 *   → BBR의 pacing과 함께 사용하면 버퍼블로트 근본 해결
 */

TCP Segmentation Offload (TSO/GSO)

TSO/GSO는 TCP 전송 성능의 핵심입니다. 커널이 MSS보다 훨씬 큰 대형 skb를 생성하여 네트워크 스택을 한 번만 통과시킨 후, 최종 단계에서 분할합니다.

/* === TSO (TCP Segmentation Offload) — 하드웨어 오프로드 === */
/*
 * 일반 전송 (오프로드 없음):
 *   write(fd, buf, 64000) → 커널이 MSS(1460) 단위로 44개 skb 생성
 *   → 각 skb마다: TCP 헤더 생성, IP 헤더, 체크섬, qdisc, NIC DMA
 *   → 매우 높은 per-packet CPU 오버헤드
 *
 * TSO 활성:
 *   → 커널이 64KB 대형 skb 1개 생성
 *   → 네트워크 스택(IP, Netfilter, TC, qdisc) 1번 통과
 *   → NIC 하드웨어가 MSS 단위 분할:
 *     - 각 세그먼트에 TCP 헤더 복사 (seq 증가, PSH/FIN 조정)
 *     - IP 헤더 복사 (total_length, ID 증가)
 *     - 체크섬 계산 (TCP pseudo-header + payload)
 *   → CPU 부하 대폭 절감 — 10Gbps+ 환경에서 필수
 *
 * 확인: ethtool -k eth0 | grep tcp-segmentation
 *   tcp-segmentation-offload: on
 */

/* === GSO (Generic Segmentation Offload) — 소프트웨어 fallback === */
/*
 * TSO의 소프트웨어 일반화 (커널 2.6.18+, Herbert Xu):
 *   → TSO와 동일하게 대형 skb를 생성하여 스택 통과
 *   → validate_xmit_skb()에서 분할 결정:
 *     NIC가 TSO 지원 (NETIF_F_TSO) → 그대로 NIC에 전달
 *     NIC가 TSO 미지원 → skb_gso_segment()로 소프트웨어 분할
 *
 * GSO의 핵심 가치:
 *   1. TSO 미지원 NIC에서도 중간 계층 처리 비용 절감
 *   2. 터널(VXLAN, GRE), 가상화(veth, bridge) 환경에서 동작
 *   3. TCP 외 프로토콜 지원: UDP GSO, SCTP GSO, ESP GSO
 *   4. GRO ↔ GSO 대칭: 포워딩 시 GRO로 병합된 skb를 GSO로 재분할
 *
 * 전송 경로:
 *   tcp_sendmsg() → tcp_write_xmit() [대형 skb 생성, gso_size=MSS]
 *   → ip_queue_xmit() → __dev_queue_xmit() → qdisc (1개 skb만 처리)
 *   → validate_xmit_skb() → NIC feature 확인
 *     ├→ HW TSO 가능: skb 그대로 NIC 전달
 *     └→ HW 미지원: skb_gso_segment() → N개 세그먼트로 분할
 */

/* GSO 유형 (skb_shinfo→gso_type 비트마스크):
 *   SKB_GSO_TCPV4         IPv4 TCP (기본 TSO/GSO)
 *   SKB_GSO_TCPV6         IPv6 TCP
 *   SKB_GSO_UDP_L4        UDP L4 세그먼트 (4.18+, QUIC/WireGuard)
 *   SKB_GSO_UDP           UDP IP 단편화 (UFO)
 *   SKB_GSO_GRE           GRE 터널 내부 GSO
 *   SKB_GSO_UDP_TUNNEL    VXLAN/Geneve 내부 GSO
 *   SKB_GSO_PARTIAL       부분 GSO: 외부 HW + 내부 SW
 *   SKB_GSO_ESP           IPsec ESP GSO
 *   SKB_GSO_SCTP          SCTP 청크 GSO
 *   SKB_GSO_FRAGLIST      frag_list 기반 (GRO→포워딩→GSO)
 *   SKB_GSO_TCP_ECN       ECN 활성 TCP GSO
 *   SKB_GSO_DODGY         신뢰할 수 없는 GSO (VM 전달 등)
 */

struct sk_buff *skb;
/* TSO/GSO 관련 skb 필드 (skb_shared_info) */
skb_shinfo(skb)->gso_size;    /* 분할 단위 크기 (MSS)
                               * TCP: MSS (예: 1460)
                               * UDP GSO: 데이터그램 크기 (예: 1472)
                               * 0이면 GSO 미사용 */
skb_shinfo(skb)->gso_segs;    /* 예상 세그먼트 수 (힌트)
                               * DIV_ROUND_UP(payload_len, gso_size)
                               * BQL(Byte Queue Limit) 계산에 활용 */
skb_shinfo(skb)->gso_type;    /* SKB_GSO_* 비트마스크 (OR 조합)
                               * 예: SKB_GSO_TCPV4 | SKB_GSO_TCP_ECN */

/* 예: 64KB 데이터 + MSS=1460인 경우
 *   → gso_size = 1460 (분할 단위)
 *   → gso_segs = 64000/1460 ≈ 44개 세그먼트
 *   → 커널은 1개의 skb만 처리:
 *     - ip_queue_xmit() 1회, qdisc 1회
 *     - Netfilter/conntrack/NAT 1회
 *   → 최종 분할:
 *     NIC TSO → 하드웨어가 44개 와이어 프레임 생성
 *     SW GSO → validate_xmit_skb()에서 44개 skb 분할
 *
 * GSO 최대 크기:
 *   net_device→gso_max_size (기본 65536)
 *   BIG TCP (6.3+): IPv6에서 ~185KB까지 확장 가능
 *   ip link set dev eth0 gso_max_size 185000
 */

/* 유용한 헬퍼 함수 */
skb_is_gso(skb);               /* gso_size != 0이면 true */
skb_gso_network_seglen(skb);   /* 세그먼트의 실제 와이어 크기 */
skb_gso_segment(skb, features);/* SW GSO 분할 수행 → skb 리스트 반환 */

GSO/GRO 심화: GSO 전송 경로 상세, skb_gso_segment() 내부 동작, GSO_PARTIAL 터널 처리, GRO 병합 기준/flush 메커니즘, HW-GRO, 성능 튜닝(sysctl) 등은 sk_buff — GSO/GRO 심화 섹션을 참고하세요.

Nagle 알고리즘, TCP_NODELAY, TCP_CORK

/* === Nagle 알고리즘 (RFC 896) === */
/*
 * 목적: 작은 패킷(tinygram) 과다 전송 방지
 *
 * 규칙:
 *   미확인 데이터(unACKed)가 있는 경우:
 *     → 새 데이터가 MSS 미만이면 전송 보류 (버퍼에 합침)
 *     → ACK 도착하면 합쳐진 데이터를 한 번에 전송
 *   미확인 데이터가 없으면: 즉시 전송
 *
 * 효과: 작은 write() 여러 번 → 하나의 세그먼트로 합침
 * 부작용: 지연 시간 증가 (ACK 대기 필요)
 */

/* net/ipv4/tcp_output.c */
static bool tcp_nagle_check(bool partial,
    const struct tcp_sock *tp, int nonagle)
{
    return partial &&          /* MSS 미만 세그먼트 */
           ((nonagle & TCP_NAGLE_CORK) ||
            (!nonagle && tp->packets_out &&  /* 미확인 패킷 존재 */
             !tcp_minshall_check(tp)));
}

/* === TCP_NODELAY — Nagle 비활성화 === */
/*
 * 사용 시나리오:
 *   → 대화형 프로토콜 (SSH, 게임, 실시간 통신)
 *   → 작은 메시지라도 즉시 전송 필요
 *   → 이미 애플리케이션에서 버퍼링하는 경우
 */
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));

/* === TCP_CORK — 명시적 코르크 === */
/*
 * 코르크를 끼우면: 모든 전송을 보류 (MSS 미만 세그먼트 억제)
 * 코르크를 빼면: 합쳐진 데이터를 한꺼번에 전송
 *
 * Nagle과 차이:
 *   Nagle: ACK 도착 시 자동 전송
 *   CORK:  명시적으로 코르크를 뺄 때만 전송
 *
 * 사용 시나리오:
 *   → sendfile() + 헤더/트레일러 조합
 *   → 여러 write()를 하나의 세그먼트로 합치고 싶을 때
 *   → 200ms 타임아웃으로 자동 해제 (안전장치)
 */
int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
/* ... 여러 write() 호출 ... */
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(cork));
/* → 합쳐진 데이터가 한 번에 전송됨 */
옵션동작지연사용 사례
기본 (Nagle ON) 작은 세그먼트 합침, ACK 시 전송 중간 (~RTT) 일반적 벌크 전송
TCP_NODELAY 즉시 전송, 합침 없음 최소 대화형, 실시간
TCP_CORK 코르크 해제 시까지 전부 보류 제어 가능 sendfile 조합, 배치 전송
TCP_NODELAY + MSG_MORE MSG_MORE 동안 보류, 마지막 전송 시 즉시 최소 프레임워크 내부 최적화

Zero-Copy 전송

커널 ↔ 유저 공간 간 데이터 복사를 제거하여 CPU 사용률과 지연을 줄이는 기법들입니다:

/* === sendfile() — 커널 2.2+ === */
/*
 * 파일 → 소켓 전송 시 유저 공간 버퍼를 거치지 않음
 *
 * 일반 경로:  read(file, buf) → write(sock, buf)
 *   디스크 → 커널 버퍼 → 유저 버퍼 → 커널 소켓 버퍼 → NIC
 *   (2번의 유저↔커널 복사)
 *
 * sendfile():  sendfile(sock_fd, file_fd, offset, count)
 *   디스크 → 커널 Page Cache → 커널 소켓 버퍼 → NIC
 *   (유저 공간 복사 제거 — 복사 1회로 줄임)
 *
 * NIC가 scatter-gather 지원 시:
 *   Page Cache 페이지를 직접 NIC DMA에 매핑
 *   → 복사 0회 (진정한 zero-copy)
 */
ssize_t sendfile(int out_fd, int in_fd,
                 off_t *offset, size_t count);

/* === MSG_ZEROCOPY — 커널 4.14+ === */
/*
 * 유저 버퍼를 직접 NIC DMA에 매핑 (복사 없이 전송)
 *
 * 동작 원리:
 *   1. 유저 페이지를 pin (get_user_pages)
 *   2. skb의 frag으로 유저 페이지를 참조
 *   3. NIC가 DMA로 직접 유저 메모리 읽기
 *   4. 전송 완료 통지: SO_EE_ORIGIN_ZEROCOPY (errqueue)
 *   5. 유저가 통지 받은 후에야 버퍼 수정/해제 가능
 *
 * 적합한 경우:
 *   → 대용량 전송 (10KB+ per send, 권장 수십KB~)
 *   → 높은 처리량 필요 (10Gbps+)
 *   → 복사 비용 > pin + 통지 오버헤드
 *
 * 부적합한 경우:
 *   → 작은 메시지 (오버헤드 > 이득)
 *   → 전송 완료 전 버퍼를 빠르게 재사용해야 하는 경우
 */

/* 설정 */
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one));

/* 전송 */
send(fd, buf, len, MSG_ZEROCOPY);

/* 완료 통지 수신 (errqueue에서) */
struct msghdr msg = {};
struct sock_extended_err *serr;
recvmsg(fd, &msg, MSG_ERRQUEUE);
/* serr→ee_origin == SO_EE_ORIGIN_ZEROCOPY
 * serr→ee_data == 전송 완료된 send() 호출의 카운터
 * → 이 통지 이후 유저 버퍼 안전하게 재사용 가능
 */
/* === splice() / vmsplice() — 파이프 기반 zero-copy === */
/*
 * splice(): 파일 → 파이프 → 소켓 (또는 역방향)
 *   → 파이프를 중개자로 사용하여 커널 내부 페이지 이동
 *   → 유저 공간 복사 없음
 *
 * vmsplice(): 유저 버퍼 → 파이프
 *   → 유저 페이지를 파이프에 zero-copy로 연결
 *
 * 사용 예: 프록시 서버
 *   splice(client_fd → pipe) + splice(pipe → upstream_fd)
 *   → 프록시가 데이터를 한 번도 복사하지 않음
 */
ssize_t splice(int fd_in, loff_t *off_in,
               int fd_out, loff_t *off_out,
               size_t len, unsigned int flags);

TCP Pacing

/* === TCP Pacing — 균일한 전송 속도 제어 === */
/* 커널 3.12+ (FQ qdisc 기반), 4.20+ (내장 pacing) */

/* 문제: TCP가 cwnd만큼 한꺼번에 전송 (burst)
 *   → 네트워크 버퍼에 순간적 과부하
 *   → 패킷 드롭 → 재전송 → 성능 저하
 *   → 특히 고대역폭 환경에서 심각
 *
 * Pacing: 세그먼트를 일정한 시간 간격으로 전송
 *   → 버스트 제거 → 큐잉 지연 감소
 *   → BBR 알고리즘의 핵심 구성요소
 */

/* 커널 내부 pacing 구현 */
/*
 * sk→sk_pacing_rate:  bytes/sec 단위 전송 속도
 * sk→sk_pacing_status:
 *   SK_PACING_NONE    pacing 미사용
 *   SK_PACING_NEEDED  pacing 활성화됨
 *   SK_PACING_FQ      FQ qdisc가 pacing 수행
 *
 * 방법 1: FQ (Fair Queue) qdisc 사용 (권장)
 *   tc qdisc add dev eth0 root fq
 *   → qdisc 레벨에서 per-flow pacing
 *   → sk→sk_pacing_rate를 읽어 패킷 전송 간격 조절
 *   → EDT (Earliest Departure Time) 모델: skb→tstamp에 전송 시각 기록
 *
 * 방법 2: 내장 pacing (FQ 없을 때)
 *   → tcp_internal_pacing() → hrtimer 기반
 *   → FQ보다 정밀도 낮고 CPU 오버헤드 높음
 *
 * BBR + FQ 조합:
 *   BBR이 sk_pacing_rate = BtlBw × pacing_gain으로 설정
 *   → FQ가 해당 속도에 맞춰 패킷 간격 조절
 *   → 버퍼블로트 없는 고성능 전송
 */
# FQ qdisc 설정 (BBR + pacing 최적 조합)
tc qdisc replace dev eth0 root fq

# EDT 기반 pacing 확인
tc -s qdisc show dev eth0
# → flows 127 (gcflows 0) throttled 45231
#   throttled: pacing에 의해 지연된 패킷 수

# 소켓별 전송 속도 제한 (SO_MAX_PACING_RATE)
# → 혼잡 제어와 별도로 상한선 설정 가능

Early Demux 최적화

/* === Early Demux — IP 계층에서의 사전 소켓 조회 === */
/* net/ipv4/ip_input.c, 커널 3.6+ */

/* 일반 경로:
 *   ip_rcv() → ip_rcv_finish() → ip_local_deliver()
 *   → tcp_v4_rcv() → 소켓 조회 (inet_lookup)
 *
 * Early Demux 경로:
 *   ip_rcv_finish()에서 미리 소켓 조회 수행
 *   → skb→sk에 소켓 캐싱
 *   → 라우팅 조회를 소켓의 캐시된 dst_entry로 대체 (FIB 조회 스킵)
 *   → tcp_v4_rcv()에서 중복 조회 회피
 *
 * 성능 효과:
 *   → ESTABLISHED 연결의 수신 경로에서 ~5% CPU 절감
 *   → FIB 조회 비용이 높은 대규모 라우팅 테이블 환경에서 효과 극대화
 */

/* net/ipv4/ip_input.c */
static int ip_rcv_finish_core(struct net *net,
    struct sock *sk, struct sk_buff *skb, ...)
{
    /* Early Demux: 소켓과 연관된 dst 캐시 활용 */
    if (net->ipv4.sysctl_ip_early_demux &&
        !skb_dst(skb) && !skb->sk) {
        tcp_v4_early_demux(skb);
        /* → skb→sk = 매칭된 소켓
         * → skb→dst = 소켓의 캐시된 라우팅 엔트리
         */
    }
    /* skb→dst가 설정되어 있으면 FIB 조회 스킵 */
    if (!skb_valid_dst(skb))
        ip_route_input_noref(skb, ...);  /* FIB 조회 */
}

/* sysctl 제어 */
/* net.ipv4.ip_early_demux = 1   (기본: 활성화) */
/* net.ipv4.tcp_early_demux = 1  (TCP용, 커널 4.15+) */
/* net.ipv4.udp_early_demux = 1  (UDP용, 커널 4.15+) */

/* 비활성화 고려:
 *   → 라우터/포워더: ESTABLISHED 소켓이 적고 포워딩이 대부분
 *     → Early Demux의 소켓 조회가 불필요한 오버헤드
 *   → 대규모 서버: 소켓 수 많으면 조회 비용 > 캐시 이득
 *     → 벤치마크로 확인 필요
 */

TCP 인증 (MD5 Signature / TCP-AO)

/* === TCP MD5 Signature (RFC 2385) === */
/*
 * BGP 세션 보호를 위해 설계된 TCP 세그먼트 인증
 * → TCP 옵션(Kind=19)에 16바이트 MD5 해시 추가
 * → 해시 입력: TCP pseudo-header + 세그먼트 데이터 + 비밀 키
 * → 잘못된 해시의 세그먼트는 조용히 폐기
 *
 * 사용: BGP 피어 간 RST injection/hijacking 방어
 */

/* 커널 API (소켓 옵션) */
struct tcp_md5sig md5sig = {
    .tcpm_addr = {  /* 피어 주소 */
        .ss_family = AF_INET,
    },
    .tcpm_keylen = 16,
};
memcpy(md5sig.tcpm_key, "secret_key_here!", 16);
setsockopt(fd, IPPROTO_TCP, TCP_MD5SIG, &md5sig, sizeof(md5sig));

/* === TCP-AO (Authentication Option, RFC 5925) === */
/*
 * TCP MD5의 후속 — 커널 6.7+
 * 개선점:
 *   - 알고리즘 선택 가능 (HMAC-SHA1, AES-128-CMAC 등)
 *   - 키 롤오버 지원 (키 ID 기반으로 무중단 교체)
 *   - 주소 바인딩 유연 (prefix 매칭)
 *   - MD5보다 강력한 암호화 지원
 *
 * 커널 구조체:
 *   struct tcp_ao_key: 키 정보 (알고리즘, ID, 키 데이터)
 *   struct tcp_ao_info: 소켓별 TCP-AO 정보
 *
 * 사용: BGP, LDP 등 장시간 연결 보호
 * 설정: TCP_AO_ADD_KEY, TCP_AO_DEL_KEY, TCP_AO_INFO setsockopt
 */

TCP 디버깅과 모니터링

# === 연결 상태 모니터링 ===

# ss: 소켓 통계 (netstat 대체, 커널 정보 직접 조회)
ss -tnpi
# -t: TCP, -n: 숫자 표시, -p: 프로세스, -i: 내부 TCP 정보
# 출력 예:
#   cubic wscale:7,7 rto:204 rtt:1.5/0.75 ato:40
#   cwnd:10 ssthresh:7 send 77.9Mbps retrans:0/3
#   → cwnd=10, rtt=1.5ms, 재전송 3회(현재 0 in-flight)

# 상태별 연결 수
ss -s
# TCP:   1234 (estab 890, closed 12, orphaned 0, timewait 332)

# 특정 상태 필터
ss -tn state time-wait
ss -tn state established '( dport = 443 )'

# === TCP 내부 통계 ===
# /proc/net/snmp — MIB-II 카운터
cat /proc/net/snmp | grep Tcp
# 주요 필드:
#   ActiveOpens:    connect() 성공 수
#   PassiveOpens:   accept() 성공 수
#   RetransSegs:    재전송된 세그먼트 총수
#   InErrs:         수신 오류 (체크섬, 길이 등)
#   OutRsts:        전송된 RST 수

# /proc/net/netstat — 확장 TCP 통계
nstat -az | grep -i tcp
# 주요 카운터:
#   TcpExtTCPTimeouts      RTO 타임아웃 횟수
#   TcpExtTCPLossProbes    TLP (Tail Loss Probe) 전송 수
#   TcpExtTCPFastRetrans   Fast Retransmit 횟수
#   TcpExtTCPSACKRecovery  SACK 기반 복구 횟수
#   TcpExtTCPMemoryPressures  메모리 pressure 진입 횟수
#   TcpExtTCPBacklogDrop   backlog 큐 드롭 수
#   TcpExtListenOverflows  accept 큐 오버플로우
#   TcpExtListenDrops      listen 드롭 총수

# === 패킷 수준 디버깅 ===
# tcpdump: TCP 핸드셰이크, 재전송, 윈도우 분석
tcpdump -i eth0 -nn tcp port 443 -v
# -v: TCP 옵션 (MSS, SACK, Window Scale, Timestamp) 표시

# === ftrace: 커널 함수 추적 ===
# TCP 재전송 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/tcp/tcp_retransmit_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe
# → sport, dport, saddr, daddr, state, 재전송 시퀀스 번호 출력

# TCP probe (혼잡 제어 디버깅)
echo 1 > /sys/kernel/debug/tracing/events/tcp/tcp_probe/enable
# → cwnd, ssthresh, snd_wnd, srtt 실시간 추적
bpftrace 한 줄 스크립트: bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb { printf("%s:%d → %s:%d state=%d\n", ntop(args->saddr), args->sport, ntop(args->daddr), args->dport, args->state); }' — TCP 재전송 발생 시 실시간으로 소스/목적지와 상태를 출력합니다.

kTLS (Kernel TLS) 심화

kTLS (Kernel TLS)는 TLS 레코드 계층의 암호화/복호화를 커널 소켓 계층에서 수행하는 메커니즘입니다. 전통적으로 TLS는 OpenSSL, GnuTLS 등 유저스페이스 라이브러리가 전담했지만, 커널 4.13(TX)과 4.17(RX)부터 대칭키 암호화 처리를 커널로 이동시켜 sendfile() 제로카피, splice, NIC 하드웨어 오프로드 등의 이점을 제공합니다.

kTLS 핵심 포인트: kTLS는 TLS 핸드셰이크를 커널에서 수행하지 않습니다. 핸드셰이크(인증서 검증, 키 교환 등)는 여전히 유저스페이스 TLS 라이브러리가 처리하며, 핸드셰이크 완료 후 협상된 대칭키(session key)를 setsockopt(SOL_TLS)로 커널에 전달합니다. 이후의 레코드 암호화/복호화만 커널이 처리합니다.

kTLS 아키텍처

User Space Application (nginx, HAProxy, ...) TLS Library (OpenSSL, GnuTLS) sendfile() 제로카피 전송 splice() 파이프 전달 Kernel Space kTLS (net/tls/) TLS 레코드 암호화/복호화 · sendfile 제로카피 Crypto API AES-GCM, ChaCha20-Poly1305 TCP 전송/수신 큐 NIC TLS Offload (선택적) — tls-hw-tx/rx-offload NIC Hardware SW kTLS: 커널 CPU 암호화 HW kTLS TX: NIC가 암호화 HW kTLS RX: NIC가 복호화 (대부분의 NIC) (ConnectX-6+, E810, CX7) (ConnectX-6 Dx+, CX7) setsockopt(SOL_TLS)

유저스페이스 TLS vs kTLS 비교

항목유저스페이스 TLS (전통적)kTLS (커널 TLS)
암호화 위치 유저스페이스 (OpenSSL 등) 커널 소켓 계층 (net/tls/)
핸드셰이크 유저스페이스 유저스페이스 (동일)
sendfile() 지원 불가 — 데이터를 유저스페이스로 읽고 암호화 후 send() 가능 — 커널이 파일 → 암호화 → TCP 직접 전달 (제로카피)
splice() 지원 불가 가능 — 파이프를 통한 커널 내 데이터 전달
컨텍스트 스위칭 read() → 유저스페이스 암호화 → write() (2회 syscall) sendfile() 1회 syscall로 완료
메모리 복사 커널→유저→(암호화)→커널 (2~3회 복사) 커널 내 처리 (0~1회 복사)
HW 오프로드 불가 (NIC가 유저스페이스 버퍼에 접근 불가) 가능 — NIC가 TLS 레코드 암/복호화 수행
정적 파일 서빙 성능 기준 (1x) sendfile kTLS: ~2-4x 처리량 향상
커널 버전 요구 제한 없음 TX: 4.13+, RX: 4.17+, TLS 1.3: 5.1+

kTLS 커널 내부 구조체

/* include/net/tls.h — kTLS 핵심 구조체 */

/* TLS 버전 및 암호 스위트 정보를 담는 컨텍스트 */
struct tls_context {
    struct tls_prot_info  prot_info;     /* TLS 버전, 암호 알고리즘 정보 */

    u8 tx_conf : 3;                      /* TX 모드: SW, HW, HW_RECORD */
    u8 rx_conf : 3;                      /* RX 모드: SW, HW, HW_RECORD */
    u8 zerocopy_sendfile : 1;            /* 제로카피 sendfile 지원 여부 */
    u8 rx_no_pad : 1;                    /* TLS 1.3 패딩 비활성화 */

    int (*push_pending_record)(struct sock *sk, int flags);
    void (*sk_write_space)(struct sock *sk);

    void *priv_ctx_tx;                  /* TX 구현별 컨텍스트 (SW 또는 HW) */
    void *priv_ctx_rx;                  /* RX 구현별 컨텍스트 */

    struct net_device *netdev;           /* HW offload 디바이스 (NULL = SW) */

    struct cipher_context tx;            /* 송신 암호 컨텍스트 */
    struct cipher_context rx;            /* 수신 암호 컨텍스트 */

    struct scatterlist *partially_sent_record;
    u16 partially_sent_offset;

    struct list_head list;              /* 전역 컨텍스트 리스트 */
    refcount_t refcount;
    struct rcu_head rcu;
};

/* TLS 프로토콜 정보 */
struct tls_prot_info {
    u16 version;                        /* TLS_1_2_VERSION / TLS_1_3_VERSION */
    u16 cipher_type;                    /* TLS_CIPHER_AES_GCM_128 등 */
    u16 prepend_size;                   /* 레코드 헤더 크기 */
    u16 tag_size;                       /* AEAD 태그 크기 (16바이트 for GCM) */
    u16 overhead_size;                  /* 전체 오버헤드 = prepend + tag */
    u16 iv_size;                        /* IV 크기 */
    u16 salt_size;                      /* salt 크기 (GCM: 4바이트) */
    u16 rec_seq_size;                   /* 레코드 시퀀스 번호 크기 */
    u16 aad_size;                       /* AAD (Additional Auth Data) 크기 */
    u16 tail_size;                      /* TLS 1.3 content type (1바이트) */
};

/* 소프트웨어 TX 컨텍스트 */
struct tls_sw_context_tx {
    struct crypto_aead *aead_send;      /* AEAD 암호 인스턴스 (AES-GCM 등) */
    struct tls_strparser strp;          /* TLS 레코드 파서 */
    struct sk_msg tx_msg;
    struct list_head tx_list;           /* 전송 대기 레코드 리스트 */
    atomic_t encrypt_pending;           /* 비동기 암호화 진행 카운트 */
    spinlock_t encrypt_compl_lock;
    int async_notify;
    u8 async_capable : 1;               /* 비동기 암호화 가능 여부 */

    /* 비동기 암호화: 암호화 완료를 기다리지 않고
     * 다음 레코드를 처리하여 파이프라인 효율 향상.
     * 완료 콜백에서 TCP 전송 큐에 데이터 삽입. */
};

/* 소프트웨어 RX 컨텍스트 */
struct tls_sw_context_rx {
    struct crypto_aead *aead_recv;      /* AEAD 복호 인스턴스 */
    struct strparser strp;              /* TCP 바이트스트림 → TLS 레코드 분리 */
    struct sk_buff_head rx_list;        /* 복호화된 레코드 리스트 */
    void (*saved_data_ready)(struct sock *sk);
    struct sk_buff *recv_pkt;          /* 현재 처리 중인 수신 레코드 */
    u8 reader_present;
    u8 async_capable : 1;
    u8 zc_capable : 1;                 /* 수신 제로카피 가능 */
    u8 reader_contended : 1;
    atomic_t decrypt_pending;
};
strparser의 역할: TCP는 바이트 스트림이므로 TLS 레코드 경계가 TCP 세그먼트와 일치하지 않을 수 있습니다. strparser(net/strparser/)는 TCP 수신 데이터를 파싱하여 완전한 TLS 레코드를 추출합니다. TLS 레코드 헤더의 길이 필드를 읽어 레코드가 완성될 때까지 데이터를 축적하고, 완전한 레코드가 모이면 복호화 콜백을 호출합니다.

kTLS 설정 API — setsockopt() 흐름

kTLS 활성화는 TLS 핸드셰이크 완료 후 setsockopt()로 대칭키를 커널에 전달하는 것으로 시작됩니다. OpenSSL 3.0+에서는 SSL_set_options(ssl, SSL_OP_ENABLE_KTLS)로 자동 처리됩니다.

/* include/uapi/linux/tls.h — 유저스페이스 API 정의 */

/* TLS 소켓 옵션 레벨 */
#define SOL_TLS     282

/* TLS 소켓 옵션 */
#define TLS_TX      1     /* 송신(TX) kTLS 활성화 */
#define TLS_RX      2     /* 수신(RX) kTLS 활성화 */

/* 지원 TLS 버전 */
#define TLS_1_2_VERSION    0x0303
#define TLS_1_3_VERSION    0x0304

/* 지원 암호 스위트 */
#define TLS_CIPHER_AES_GCM_128            51
#define TLS_CIPHER_AES_GCM_256            52
#define TLS_CIPHER_AES_CCM_128            53
#define TLS_CIPHER_CHACHA20_POLY1305      54
#define TLS_CIPHER_SM4_GCM                55  /* 커널 6.0+ */
#define TLS_CIPHER_SM4_CCM                56  /* 커널 6.0+ */
#define TLS_CIPHER_ARIA_GCM_128           57  /* 커널 6.2+ */
#define TLS_CIPHER_ARIA_GCM_256           58  /* 커널 6.2+ */

/* AES-128-GCM 암호 정보 구조체 (가장 널리 사용) */
struct tls12_crypto_info_aes_gcm_128 {
    struct tls_crypto_info info;       /* version + cipher_type */
    unsigned char iv[8];              /* 명시적 IV (nonce의 가변 부분) */
    unsigned char key[16];            /* AES-128 대칭키 */
    unsigned char salt[4];            /* 암묵적 nonce (고정 부분) */
    unsigned char rec_seq[8];         /* 초기 레코드 시퀀스 번호 */
};

/* AES-256-GCM 암호 정보 구조체 */
struct tls12_crypto_info_aes_gcm_256 {
    struct tls_crypto_info info;
    unsigned char iv[8];
    unsigned char key[32];            /* AES-256 대칭키 */
    unsigned char salt[4];
    unsigned char rec_seq[8];
};

/* ChaCha20-Poly1305 암호 정보 구조체 */
struct tls12_crypto_info_chacha20_poly1305 {
    struct tls_crypto_info info;
    unsigned char iv[12];
    unsigned char key[32];
    unsigned char salt[0];            /* ChaCha20에는 salt 없음 */
    unsigned char rec_seq[8];
};
/* ━━━ kTLS 활성화 예제 (유저스페이스 C 코드) ━━━ */

#include <linux/tls.h>
#include <netinet/tcp.h>

/* 1. 일반 TCP 소켓 생성 + TLS 핸드셰이크 (OpenSSL 등) */
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
/* ... connect() + SSL_do_handshake() ... */

/* 2. 핸드셰이크 후 대칭키 추출 (TLS 1.2 + AES-128-GCM 예시) */
struct tls12_crypto_info_aes_gcm_128 crypto_info;
memset(&crypto_info, 0, sizeof(crypto_info));

crypto_info.info.version = TLS_1_2_VERSION;
crypto_info.info.cipher_type = TLS_CIPHER_AES_GCM_128;

/* OpenSSL에서 키/IV/시퀀스 번호 추출 */
SSL_get_key_material(ssl, crypto_info.key, crypto_info.iv,
                     crypto_info.salt, crypto_info.rec_seq);

/* 3. SOL_TLS + TLS_TX: 커널에 TX 대칭키 전달 → kTLS TX 활성화 */
setsockopt(sockfd, SOL_TLS, TLS_TX,
           &crypto_info, sizeof(crypto_info));

/* 4. SOL_TLS + TLS_RX: 커널에 RX 대칭키 전달 → kTLS RX 활성화 */
struct tls12_crypto_info_aes_gcm_128 crypto_info_rx;
/* ... RX 키 설정 ... */
setsockopt(sockfd, SOL_TLS, TLS_RX,
           &crypto_info_rx, sizeof(crypto_info_rx));

/* 5. 이후 send()/recv()는 커널이 자동으로 TLS 레코드 암/복호화
 *    sendfile()도 제로카피로 동작 */
sendfile(sockfd, filefd, &offset, count);
/* → 커널: 파일 페이지 → AES-GCM 암호화 → TLS 레코드 → TCP 전송
 *   유저스페이스 복사 없음! */

kTLS TX 경로 — 송신 처리

/* net/tls/tls_main.c — kTLS 초기화 */

/* setsockopt(SOL_TLS, TLS_TX) 호출 시 진입 */
static int do_tls_setsockopt_conf(struct sock *sk,
                                  sockptr_t optval, unsigned int optlen,
                                  int tx)
{
    struct tls_context *ctx = tls_get_ctx(sk);
    struct tls_crypto_info *crypto_info;

    /* 1. 유저스페이스에서 전달한 암호 정보 복사 */
    copy_from_sockptr(&crypto_info, optval, ...);

    /* 2. NIC HW offload 시도 */
    if (tx)
        rc = tls_set_device_offload(sk, ctx);  /* HW 가능 시 HW 모드 */

    /* 3. HW 불가 → SW 모드 폴백 */
    if (rc)
        rc = tls_set_sw_offload(sk, ctx, tx);
    /* → AEAD 인스턴스(aes-gcm) 할당
     * → TCP prot을 tls_prots[TLS_SW]로 교체
     * → sendmsg/sendpage 콜백이 kTLS 함수로 후킹 */
}

/* net/tls/tls_sw.c — 소프트웨어 TX 경로 */

/* send() / sendmsg() 시 호출되는 kTLS TX 함수 */
int tls_sw_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    struct tls_context *tls_ctx = tls_get_ctx(sk);
    struct tls_sw_context_tx *ctx = tls_ctx->priv_ctx_tx;

    /* 루프: 유저 데이터를 TLS 레코드 크기(max 16KB)로 분할 */
    while (msg_data_left(msg)) {
        /* a) 유저 데이터를 scatterlist에 수집 */
        tls_push_data(sk, msg, size, flags, TLS_RECORD_TYPE_DATA);

        /* b) TLS 레코드 헤더 생성 (content type, version, length) */
        tls_fill_prepend(tls_ctx, rec->aad_space, ...);

        /* c) AEAD 암호화 (AES-128-GCM) */
        crypto_aead_encrypt(aead_req);
        /* → IV(nonce) = salt(4B) || explicit_iv(8B)
         * → AAD = seq_num(8B) || record_header(5B)  [TLS 1.2]
         * → AAD = record_header(5B)                   [TLS 1.3]
         * → 평문 → AES-GCM → 암호문 + 16B 태그 */

        /* d) 암호화된 레코드를 TCP 전송 큐에 삽입 */
        tls_push_record(sk, flags, record_type);
    }
}

/* sendfile() 제로카피 경로 */
int tls_sw_sendpage(struct sock *sk, struct page *page,
                    int offset, size_t size, int flags)
{
    /* sendfile() → do_sendpage() → tls_sw_sendpage()
     *
     * 파일 페이지 캐시를 직접 scatterlist에 매핑:
     *   page cache → sg_set_page(sg, page, ...) → AEAD 암호화 → TCP
     *
     * 유저스페이스로의 데이터 복사 없음!
     * 기존 방식: read(file→user) + SSL_write(user→kernel) = 2회 복사
     * kTLS sendfile: 파일 → 커널 암호화 → TCP = 0회 유저 복사 */
}

kTLS RX 경로 — 수신 처리

/* net/tls/tls_sw.c — 소프트웨어 RX 경로 */

/* strparser가 완전한 TLS 레코드를 수신하면 호출 */
static void tls_strp_msg_ready(struct tls_strparser *strp,
                               struct sk_buff *skb)
{
    /* TCP 바이트 스트림에서 완전한 TLS 레코드를 감지
     *
     * TLS Record 구조:
     * ┌──────────┬─────────┬────────┬───────────┬─────┐
     * │ContentType│ Version │ Length │ Encrypted │ Tag │
     * │  (1B)    │  (2B)   │ (2B)   │  Payload  │(16B)│
     * └──────────┴─────────┴────────┴───────────┴─────┘
     * ← 5B header →        ← Length bytes →
     *
     * strparser가 Length 필드를 파싱하여 완전한 레코드 경계 결정
     */
}

/* recv() / recvmsg() 시 호출 */
int tls_sw_recvmsg(struct sock *sk, struct msghdr *msg,
                   size_t len, int flags, int *addr_len)
{
    struct tls_sw_context_rx *ctx = ...;

    /* 1. strparser가 축적한 완전한 TLS 레코드 가져오기 */
    skb = tls_strp_msg_dequeue(&ctx->strp);

    /* 2. TLS 레코드 헤더에서 content type 확인 */
    /* - TLS_RECORD_TYPE_DATA(23): 일반 데이터 → 복호화 후 유저에 전달
     * - TLS_RECORD_TYPE_ALERT(21): TLS 경고 → 에러 처리
     * - TLS_RECORD_TYPE_HANDSHAKE(22): 재협상 → 유저스페이스에 전달
     * - TLS 1.3: 항목이 항상 APPLICATION_DATA(23)이고,
     *   실제 content type은 복호화 후 마지막 바이트에서 확인 */

    /* 3. AEAD 복호화 */
    err = decrypt_skb(sk, skb, msg);
    /* → crypto_aead_decrypt(aead_req)
     * → MAC(GCM 태그) 검증 + 복호화
     * → 실패 시 TLS_ALERT_BAD_RECORD_MAC 반환 */

    /* 4. 복호화된 평문을 유저스페이스 버퍼에 복사 */
    err = skb_copy_datagram_msg(skb, rxm->offset, msg, chunk);

    /* 제로카피 RX (TLS_RX_EXPECT_NO_PAD 설정 시):
     * 유저 버퍼에 직접 복호화하여 중간 커널 버퍼 복사 제거 */
}

sendfile() 제로카피 — kTLS의 핵심 이점

kTLS의 가장 큰 이점은 sendfile() 시스템 콜을 TLS 연결에서 사용할 수 있다는 것입니다. 웹서버의 정적 파일 서빙에서 유저스페이스 데이터 복사를 완전히 제거하여 처리량을 크게 향상시킵니다.

기존 방식 (유저스페이스 TLS) Page Cache ①복사 User Buffer ②암호화 SSL_write() ③복사 TCP → read() + OpenSSL 암호화 + send() = 2회 syscall, 2~3회 메모리 복사 kTLS SW sendfile() Page Cache 직접 참조 kTLS 암호화 TCP → sendfile() 1회 syscall, 유저스페이스 복사 0회 (페이지 캐시 직접 암호화) kTLS HW Offload sendfile() Page Cache DMA TCP NIC 암호화 (HW AES-GCM) → sendfile() 1회 syscall, CPU 암호화 0% (NIC가 전담)
/* ━━━ nginx kTLS sendfile 예시 ━━━ */

/* nginx 1.21.4+ 에서 kTLS 활성화 시:
 *
 * 정적 파일 서빙 흐름:
 * 1. 클라이언트 TLS 핸드셰이크 (OpenSSL)
 * 2. OpenSSL이 setsockopt(SOL_TLS, TLS_TX) 호출 → kTLS TX 활성화
 * 3. 파일 요청 시 sendfile(client_fd, file_fd, ...) 직접 호출
 * 4. 커널: 페이지 캐시 → kTLS 암호화 → TCP 전송
 *
 * nginx.conf 설정:
 * http {
 *     ssl_conf_command Options KTLS;  ← kTLS 활성화
 *     sendfile on;                     ← sendfile 활성화
 * }
 */

/* 커널 sendfile 경로 (kTLS 활성화 시):
 *
 * sys_sendfile64()
 *   → do_sendfile()
 *     → do_splice_direct()
 *       → splice_file_to_pipe()     ← 파일 → pipe (페이지 참조)
 *       → pipe_to_sendpage()
 *         → tls_sw_sendpage()       ← kTLS: 페이지 → 암호화 → TCP
 *           → tls_push_data()
 *           → tls_do_encryption()   ← AES-GCM 암호화
 *           → tls_push_record()     ← TCP 전송 큐 삽입
 */

/* TLS_TX_ZEROCOPY_RO: 읽기 전용 페이지의 진정한 제로카피 (커널 5.19+)
 *
 * 일반 sendfile: 페이지 복사 후 암호화 (COW 방지)
 * ZEROCOPY_RO:   페이지 직접 암호화 → NIC DMA (복사 없음)
 *
 * 조건: 페이지가 읽기 전용이어야 함 (파일 시스템 페이지 캐시 = OK) */
setsockopt(sockfd, SOL_TLS, TLS_TX_ZEROCOPY_RO, &val, sizeof(val));

kTLS 하드웨어 오프로드

kTLS HW 오프로드는 TLS 레코드의 암호화/복호화를 NIC 하드웨어에서 수행합니다. CPU가 암호 연산을 전혀 수행하지 않아 100Gbps+ 환경에서도 라인 레이트 TLS를 달성할 수 있습니다.

오프로드 모드커널 설정동작CPU 부하
SW kTLS CONFIG_TLS=y 커널 CPU에서 AES-GCM 암호화/복호화 AES-NI 사용 시 적당 (코어당 ~10-20Gbps)
HW kTLS TX CONFIG_TLS_DEVICE=y NIC가 송신 데이터를 암호화 (TLS 레코드 생성) 암호화 CPU 0%
HW kTLS RX CONFIG_TLS_DEVICE=y NIC가 수신 데이터를 복호화 (TLS 레코드 해제) 복호화 CPU 0%
HW kTLS Record CONFIG_TLS_DEVICE=y NIC가 TLS 레코드 프레이밍까지 수행 TLS 전체 처리 CPU 0%
/* net/tls/tls_device.c — HW offload 설정 경로 */

int tls_set_device_offload(struct sock *sk, struct tls_context *ctx)
{
    struct net_device *netdev;
    struct tls_offload_context_tx *offload_ctx;

    /* 1. 소켓이 바인딩된 NIC 디바이스 확인 */
    netdev = get_netdev_for_sock(sk);

    /* 2. NIC가 kTLS offload를 지원하는지 확인 */
    if (!(netdev->features & NETIF_F_HW_TLS_TX))
        return -EOPNOTSUPP;

    /* 3. NIC 드라이버의 tls_dev_add 콜백 호출 */
    rc = netdev->tlsdev_ops->tls_dev_add(netdev, sk,
            TLS_OFFLOAD_CTX_DIR_TX, &ctx->crypto_send.info,
            tcp_sk(sk)->write_seq);
    /* → 드라이버: TLS 연결 정보(키, IV, seq)를 NIC HW에 설치
     * → NIC가 이 연결의 모든 TX 패킷을 자동 암호화
     *
     * 지원 NIC:
     * - Mellanox ConnectX-6 Dx / ConnectX-7: mlx5e_ktls_add_tx()
     * - Intel E810 (ICE): ice_tls_dev_add()
     * - Chelsio T6: cxgb4_ktls_dev_add()
     */

    /* 4. TCP prot을 HW offload용으로 교체 */
    tls_update_rx_zc_capable(ctx);
    ctx->tx_conf = TLS_HW;
}

/* include/net/tls.h — NIC 드라이버가 구현하는 TLS offload ops */
struct tlsdev_ops {
    int (*tls_dev_add)(struct net_device *netdev, struct sock *sk,
                       enum tls_offload_ctx_dir direction,
                       struct tls_crypto_info *crypto_info,
                       u32 start_offload_tcp_sn);
    void (*tls_dev_del)(struct net_device *netdev,
                        struct tls_context *ctx,
                        enum tls_offload_ctx_dir direction);
    int (*tls_dev_resync)(struct net_device *netdev,
                          struct sock *sk, u32 seq,
                          u8 *rcd_sn, enum tls_offload_ctx_dir direction);
};

/* NIC HW TX offload 패킷 전송 경로:
 *
 * tls_device_sendmsg() / tls_device_sendpage()
 *   → tls_push_data()        ← 데이터를 SG 리스트에 수집
 *   → tls_push_record()
 *     → tcp_sendmsg_locked()  ← 평문을 TCP로 전달
 *       → NIC TX 큐에 enqueue
 *         → NIC HW가 TLS 레코드 헤더 + 암호화 + 태그를 자동 생성
 *           → 와이어에 암호화된 TLS 레코드 전송
 *
 * CPU는 평문을 TCP에 전달만 → 암호화는 NIC가 라인 레이트로 처리
 */
# ━━━ kTLS HW Offload 상태 확인 ━━━

# NIC의 kTLS offload 지원 확인
ethtool -k eth0 | grep tls
# tls-hw-tx-offload: on      ← TX 오프로드 지원
# tls-hw-rx-offload: on      ← RX 오프로드 지원
# tls-hw-record: on          ← 레코드 프레이밍 오프로드

# kTLS offload 활성화/비활성화
ethtool -K eth0 tls-hw-tx-offload on
ethtool -K eth0 tls-hw-rx-offload on

# kTLS 통계 확인 (HW offload 카운터)
ethtool -S eth0 | grep tls
# tx_tls_encrypted_packets: 1234567
# tx_tls_encrypted_bytes: 987654321
# tx_tls_ooo: 0                    ← Out-of-order 패킷 (재전송)
# tx_tls_drop_no_sync_data: 0
# rx_tls_decrypted_packets: 654321
# rx_tls_decrypted_bytes: 543210987
# rx_tls_resync_req_pkt: 0         ← RX 동기화 재요청
# rx_tls_resync_req_start: 0
# rx_tls_resync_req_end: 0
# rx_tls_resync_res_ok: 0

# 커널 전역 kTLS 통계
cat /proc/net/tls_stat
# TlsCurrTxSw                  42    ← 현재 SW TX 연결 수
# TlsCurrRxSw                  38    ← 현재 SW RX 연결 수
# TlsCurrTxDevice               8    ← 현재 HW TX 연결 수
# TlsCurrRxDevice               6    ← 현재 HW RX 연결 수
# TlsTxSw                    5000    ← 누적 SW TX 연결
# TlsRxSw                    4500    ← 누적 SW RX 연결
# TlsTxDevice                1200    ← 누적 HW TX 연결
# TlsRxDevice                1000    ← 누적 HW RX 연결
# TlsDecryptError               0    ← 복호화 에러 (MAC 검증 실패 등)
# TlsRxDeviceResync             3    ← HW RX 재동기화 횟수
# TlsDecryptRetry               0
# TlsRxNoPadViolation           0    ← no_pad 위반

kTLS HW RX 재동기화 (Resync) 메커니즘

NIC RX 오프로드에서 TCP 재전송, 패킷 유실, 순서 변경이 발생하면 NIC의 TLS 레코드 시퀀스 번호와 실제 TCP 스트림이 불일치할 수 있습니다. 이때 커널과 NIC 사이의 재동기화(resync) 프로토콜이 동작합니다.

/* NIC RX offload 재동기화 흐름:
 *
 * 정상 상태:
 *   NIC가 수신 패킷의 TCP seq → TLS record seq 매핑을 유지
 *   → 패킷 도착 시 자동으로 복호화 후 커널에 전달
 *
 * 비정상 상태 (패킷 유실/재전송 등):
 *   NIC가 TLS 레코드 경계를 놓침 → 복호화 실패
 *
 *   1. NIC: 복호화 실패한 패킷을 암호문 상태로 커널에 전달
 *      (skb->decrypted = 0)
 *
 *   2. 커널 (tls_device.c):
 *      → SW fallback으로 해당 레코드 복호화
 *      → 정확한 TCP seq ↔ TLS record seq 매핑 계산
 *
 *   3. 커널 → NIC: tls_dev_resync() 호출
 *      → "TCP seq X부터 TLS record seq Y" 정보 전달
 *
 *   4. NIC: 새 매핑으로 HW 테이블 업데이트
 *      → 이후 패킷부터 다시 HW 복호화 재개
 *
 * resync 모드:
 * - TLS_OFFLOAD_SYNC_TYPE_DRIVER_REQ: 드라이버가 resync 요청
 * - TLS_OFFLOAD_SYNC_TYPE_CORE_NEXT_HINT: 커널이 힌트 제공
 */

/* mlx5 드라이버의 resync 구현 예시 */
/* drivers/net/ethernet/mellanox/mlx5/core/en_accel/ktls_rx.c */
static void mlx5e_ktls_rx_resync(struct net_device *netdev,
                                  struct sock *sk,
                                  u32 seq, u8 *rcd_sn)
{
    /* NIC HW의 TLS RX 컨텍스트를 새 시퀀스로 업데이트
     * → Flow Steering 규칙에 새 TCP seq/TLS seq 매핑 설치
     * → 다음 패킷부터 HW 복호화 재개 */
}

kTLS에서 TLS 1.2 vs TLS 1.3 차이

항목TLS 1.2TLS 1.3
kTLS 커널 지원 4.13+ (TX), 4.17+ (RX) 5.1+
Content Type 위치 레코드 헤더 (평문) 암호문 뒤 마지막 바이트 (inner content type)
레코드 헤더 실제 content type 포함 항상 APPLICATION_DATA(23)로 위장
AEAD nonce salt(4B) + explicit_iv(8B) = 12B salt(4B) XOR padded_seq(12B) = 12B
AAD 구성 seq_num(8B) + header(5B) = 13B header(5B) = 5B
패딩 없음 선택적 패딩 (content type 뒤에 0바이트 추가)
0-RTT 데이터 미지원 Early Data 지원 (kTLS에서는 유저스페이스에서 처리)
키 업데이트 재핸드셰이크 (kTLS와 비호환) KeyUpdate 메시지 (커널 6.0+에서 setsockopt으로 새 키 전달)
/* TLS 1.3 레코드 처리의 커널 구현 차이 */

/* net/tls/tls_sw.c — TLS 1.3 암호화 시 content type 처리 */
static void tls_fill_prepend(struct tls_context *ctx,
                            char *buf, size_t plaintext_len,
                            unsigned char record_type)
{
    /* TLS 1.2: 실제 content type을 헤더에 기록 */
    /* TLS 1.3: 헤더에 항상 APPLICATION_DATA(23) 기록
     *          실제 content type은 평문 끝에 추가 (inner content type)
     *
     * TLS 1.3 레코드 구조:
     * ┌───────────────┬─────────┬──────────┬────────────────┐
     * │ Header (5B)   │ Encrypt(│  Inner   │   AEAD Tag     │
     * │ type=23,ver,  │ Payload │ Content  │   (16B)        │
     * │ length        │         │ Type(1B) │                │
     * └───────────────┴─────────┴──────────┴────────────────┘
     * ← 평문 헤더 →   ← 암호화 영역 →
     *
     * 장점: 외부 관찰자가 content type을 알 수 없음 (프라이버시) */

    if (prot->version == TLS_1_3_VERSION) {
        buf[0] = TLS_RECORD_TYPE_DATA;  /* 항상 23 */
        /* plaintext 뒤에 실제 record_type 1바이트 추가 */
    } else {
        buf[0] = record_type;  /* 실제 타입 (23, 22, 21 등) */
    }
}

/* TLS 1.3 nonce 생성 (salt XOR seq) */
/* TLS 1.2: nonce = salt(4B) || explicit_iv(8B)
 * TLS 1.3: nonce = salt(4B padded to 12B) XOR seq_num(padded to 12B)
 *
 * TLS 1.3에서는 explicit IV가 전송되지 않아 레코드당 8바이트 절약 */

kTLS 커널 빌드 및 설정

# ━━━ 커널 빌드 옵션 ━━━

# kTLS 소프트웨어 지원 (필수)
CONFIG_TLS=y             # 또는 m (모듈)

# kTLS HW offload 지원 (선택)
CONFIG_TLS_DEVICE=y

# 관련 의존성
CONFIG_NET=y
CONFIG_INET=y
CONFIG_CRYPTO=y
CONFIG_CRYPTO_AEAD=y
CONFIG_CRYPTO_GCM=y      # AES-GCM 지원
CONFIG_CRYPTO_CHACHA20POLY1305=y  # ChaCha20-Poly1305 지원 (선택)
CONFIG_STREAM_PARSER=y   # strparser (TLS 레코드 파싱)

# ━━━ 모듈 로드 확인 ━━━
lsmod | grep tls
# tls                   126976  2

modinfo tls
# filename:  /lib/modules/.../net/tls/tls.ko
# description: Transport Layer Security Support
# license:  Dual BSD/GPL

# ━━━ OpenSSL kTLS 지원 확인 ━━━

# OpenSSL 3.0+에서 kTLS 활성화 확인
openssl version -a | grep KTLS
# OPENSSL_KTLS

# OpenSSL 빌드 시 kTLS 활성화
# ./Configure enable-ktls
# 런타임: SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS)

# GnuTLS: ktls 자동 감지 (3.7.2+)
# 커널 TLS 모듈이 로드되어 있으면 자동 사용

# ━━━ nginx kTLS 설정 ━━━

# nginx 1.21.4+ (OpenSSL 3.0+ 링크 필수)
# nginx.conf:
# http {
#     ssl_conf_command Options KTLS;
#     sendfile on;
#     ssl_protocols TLSv1.2 TLSv1.3;
# }

# ━━━ HAProxy kTLS 설정 ━━━

# HAProxy 2.6+ (OpenSSL 3.0+ 링크 필수)
# haproxy.cfg:
# global
#     ssl-engine ktls
#     # 또는 환경변수: OPENSSL_KTLS=1

kTLS 성능 특성

시나리오방식처리량 (단일 코어)CPU 사용률비고
정적 파일 서빙
(100KB 파일)
유저스페이스 TLS ~3-5 Gbps 100% read() + SSL_write()
kTLS SW sendfile ~8-12 Gbps 100% sendfile() 제로카피
kTLS HW offload ~25-40 Gbps ~20-30% NIC 암호화 (ConnectX-6 Dx)
동적 콘텐츠
(짧은 응답)
유저스페이스 TLS ~2-4 Gbps 100% send() + SSL_write()
kTLS SW ~3-5 Gbps 100% syscall 감소 효과 작음
대용량 전송
(1GB+ 파일)
kTLS SW sendfile ~15-20 Gbps 100% 대용량에서 제로카피 이점 극대
kTLS HW sendfile ~40-100 Gbps ~10-15% NIC 라인 레이트
kTLS 성능 주의사항:
  • 동적 콘텐츠에서의 이점 제한: kTLS의 주요 이점은 sendfile() 제로카피입니다. 동적으로 생성된 응답은 이미 유저스페이스 버퍼에 있으므로 제로카피 이점이 제한적. send()/sendmsg()에서는 syscall 오버헤드 감소와 유저-커널 경계 복사 1회 감소 정도
  • 소량 데이터 오버헤드: TLS 레코드 헤더(5B) + AEAD 태그(16B) = 최소 21바이트 오버헤드. 매우 작은 메시지에서는 비율이 높아질 수 있음
  • HW offload 연결 수 제한: NIC의 TLS 컨텍스트 테이블 크기에 따라 동시 오프로드 가능한 연결 수가 제한됨 (ConnectX-6 Dx: 수십만 연결). 초과 시 SW fallback
  • TCP 재전송과 HW offload: TCP 재전송 시 NIC가 이미 전송한 레코드를 재암호화해야 하며, 이때 커널과 NIC 사이 동기화가 필요 (성능 일시 저하)
  • renegotiation 비호환: TLS 1.2 renegotiation은 kTLS와 호환되지 않음. TLS 1.3 KeyUpdate는 커널 6.0+에서 지원

kTLS 디버깅 및 모니터링

# ━━━ kTLS 상태 모니터링 ━━━

# 1. 전역 kTLS 통계
cat /proc/net/tls_stat
# 주요 지표:
# TlsCurrTxSw / TlsCurrRxSw       — 현재 SW 모드 연결 수
# TlsCurrTxDevice / TlsCurrRxDevice — 현재 HW 모드 연결 수
# TlsDecryptError                   — 복호화 실패 (MAC 불일치)
# TlsRxDeviceResync                 — HW RX 재동기화 횟수

# 2. 소켓별 kTLS 상태 확인 (ss 도구)
ss -tni | grep -A1 "kTLS"
# ss -e 옵션으로 소켓 확장 정보에서 kTLS 모드 확인

# 3. NIC HW offload 카운터
ethtool -S eth0 | grep tls
# tx_tls_encrypted_packets / bytes — HW 암호화된 패킷/바이트
# tx_tls_ooo — out-of-order (재전송 등)
# rx_tls_decrypted_packets / bytes — HW 복호화된 패킷/바이트

# ━━━ ftrace / tracepoints ━━━

# kTLS 관련 tracepoints 확인
ls /sys/kernel/debug/tracing/events/tls/ 2>/dev/null || echo "TLS tracepoints 없음"

# 대안: kretprobe로 kTLS 함수 추적
bpftrace -e '
kprobe:tls_sw_sendmsg {
    printf("kTLS TX: pid=%d comm=%s size=%d\n",
           pid, comm, arg2);
}
kprobe:tls_sw_recvmsg {
    printf("kTLS RX: pid=%d comm=%s\n", pid, comm);
}
'

# kTLS 암호화 지연 측정
bpftrace -e '
kprobe:tls_do_encryption { @start[tid] = nsecs; }
kretprobe:tls_do_encryption /@start[tid]/ {
    @latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
'

# ━━━ 문제 해결 ━━━

# kTLS 활성화 실패 디버깅
dmesg | grep -i tls
# TLS module loaded
# 또는 에러: "kTLS offload not supported" 등

# OpenSSL kTLS 활성화 확인 (strace)
strace -e setsockopt nginx -t 2>&1 | grep SOL_TLS
# setsockopt(5, SOL_TLS, TLS_TX, ...) = 0  ← 성공
# setsockopt(5, SOL_TLS, TLS_TX, ...) = -1 ENOPROTOOPT ← TLS 모듈 미로드

# TLS 모듈이 로드되지 않은 경우
modprobe tls
# ENOPROTOOPT(92) → modprobe tls 후 재시도

# HW offload 실패 시 SW fallback 확인
cat /proc/net/tls_stat | grep -E "TlsCurr|TlsTx|TlsRx"
# TlsCurrTxDevice = 0이면 HW offload 미작동
# → ethtool -k eth0 | grep tls 로 NIC 지원 확인
# → NIC 드라이버가 tlsdev_ops를 구현했는지 확인
kTLS + BPF sockmap: BPF sockmap과 kTLS를 조합하면 커널 내에서 TLS 복호화 → BPF 프로그램 → 다른 소켓으로 전달을 유저스페이스 개입 없이 처리할 수 있습니다. 이는 L7 프록시(Envoy, Cilium)에서 커널 사이드카 가속에 활용됩니다. bpf_msg_redirect_map()을 사용하여 kTLS 소켓 간 데이터를 커널 내에서 직접 전달합니다.

kTLS HW Offload 지원 NIC

NIC 벤더제품TX OffloadRX Offload커널 드라이버최소 커널
NVIDIA/Mellanox ConnectX-6 Dx / ConnectX-7 TLS 1.2/1.3 TLS 1.2/1.3 mlx5_core 5.3+ (TX), 5.8+ (RX)
Intel E810 (ICE) TLS 1.2/1.3 ice 5.14+
Chelsio T6 TLS 1.2 cxgb4 5.3+
Broadcom NetXtreme-E (BCM5750X) TLS 1.2 bnxt_en 5.8+
Netronome Agilio SmartNIC TLS 1.2 nfp 5.2+
NETIF_F_HW_TLS 피처 플래그:
  • NETIF_F_HW_TLS_TX — NIC가 TX TLS offload를 지원 (ethtool -k에서 tls-hw-tx-offload)
  • NETIF_F_HW_TLS_RX — NIC가 RX TLS offload를 지원 (tls-hw-rx-offload)
  • NETIF_F_HW_TLS_RECORD — NIC가 TLS 레코드 프레이밍까지 수행 (tls-hw-record)
  • NIC가 지원하지 않으면 자동으로 SW kTLS로 fallback (유저에게 투명)

UDP 심화

UDP 헤더와 커널 처리

/* include/uapi/linux/udp.h */
struct udphdr {
    __be16  source;      /* 소스 포트 */
    __be16  dest;        /* 목적지 포트 */
    __be16  len;         /* UDP 길이 (헤더 8바이트 + 페이로드) */
    __sum16 check;       /* 체크섬 (IPv4: 선택, IPv6: 필수) */
};
/* 고정 8바이트 헤더 — 연결 상태 없음, 순서 보장 없음 */

/* net/ipv4/udp.c — UDP 수신 경로 */
int udp_rcv(struct sk_buff *skb)
{
    return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);
}

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable, int proto)
{
    struct udphdr *uh = udp_hdr(skb);

    /* 1. 체크섬 검증 */
    if (udp4_csum_init(skb, uh, proto))
        goto csum_error;

    /* 2. 소켓 lookup (4-tuple 해시) */
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

    if (sk) {
        /* 3a. 소켓 발견 → 수신 큐에 전달 */
        return udp_unicast_rcv_skb(sk, skb, uh);
    } else {
        /* 3b. 소켓 없음 → ICMP Port Unreachable 전송 */
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
        kfree_skb(skb);
    }
}

UDP GRO/GSO (커널 4.18+)

UDP GSO/GRO는 QUIC, WireGuard, DNS-over-HTTPS 등 고성능 UDP 애플리케이션을 위해 도입되었습니다. TCP의 TSO/GRO와 유사한 원리를 UDP에 적용하여 시스템콜 오버헤드를 줄입니다.

/* === UDP GSO (전송 방향) ===
 *
 * 기존 UDP 전송:
 *   sendmsg() × N회 → N개 skb → N번 스택 통과 → N개 패킷
 *   → 시스템콜 오버헤드 + per-packet 처리 비용
 *
 * UDP GSO:
 *   sendmsg() 1회 (대형 버퍼) → 1개 대형 skb
 *   → 1번 스택 통과 (routing, Netfilter, qdisc)
 *   → validate_xmit_skb()에서 __udp_gso_segment()로 분할
 *   → N개 UDP 데이터그램으로 전송
 *
 * 핵심 차이 (TCP GSO와):
 *   TCP: 시퀀스 번호 연속 → 하드웨어도 분할 가능 (TSO)
 *   UDP: 각 데이터그램 독립 → IP ID 증가, UDP length 조정만 필요
 *        → SKB_GSO_UDP_L4 타입 사용
 *
 * 성능 효과 (QUIC 벤치마크 기준):
 *   UDP GSO OFF: ~200K 패킷/초 (1 sendmsg = 1 packet)
 *   UDP GSO ON:  ~1.5M 패킷/초 (1 sendmsg = ~44 packets)
 *   → ~7.5배 처리량 향상
 */

/* 사용자 공간: cmsg로 UDP_SEGMENT 세그먼트 크기 지정 */
struct msghdr msg = {};
struct iovec iov = { .iov_base = buf, .iov_len = 64000 };
msg.msg_iov = &iov;
msg.msg_iovlen = 1;

char cmsgbuf[CMSG_SPACE(sizeof(uint16_t))];
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);

struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
cm->cmsg_level = SOL_UDP;
cm->cmsg_type  = UDP_SEGMENT;               /* GSO 세그먼트 크기 지정 */
cm->cmsg_len   = CMSG_LEN(sizeof(uint16_t));
*(uint16_t *)CMSG_DATA(cm) = 1472;          /* MTU(1500) - IP(20) - UDP(8) = 1472 */

/* 64KB 데이터를 한 번에 전송
 * → 커널이 1472바이트씩 ~43개 UDP 데이터그램으로 분할
 * → 마지막 세그먼트는 나머지 크기 (64000 % 1472 바이트)
 */
sendmsg(fd, &msg, 0);

/* 커널 내부 처리:
 * 1. udp_sendmsg() → 대형 skb 생성
 *    skb_shinfo(skb)->gso_size = 1472
 *    skb_shinfo(skb)->gso_type = SKB_GSO_UDP_L4
 * 2. ip_make_skb() → IP 계층 통과
 * 3. __udp_gso_segment() → 세그먼트 분할:
 *    - 각 세그먼트에 UDP 헤더 복사 (동일 src/dst port)
 *    - UDP length = 세그먼트 크기 + 8 (UDP 헤더)
 *    - IP ID 순차 증가
 *    - 마지막 세그먼트: 남은 데이터 + 올바른 UDP length
 */

/* === UDP GRO (수신 방향) ===
 *
 * TCP GRO와 달리, UDP GRO는 소켓 옵션으로 명시적 활성화 필요:
 *   - TCP: 시퀀스 번호로 연속성 판단 → 자동 병합
 *   - UDP: 시퀀스 번호 없음 → 소켓이 GRO 허용 의사를 밝혀야 함
 *
 * 병합 기준:
 *   - 동일 src/dst IP + port
 *   - 동일 데이터그램 크기 (마지막 제외)
 *   - UDP 체크섬 일관성
 *
 * 수신 측은 recvmsg()로 대형 버퍼를 받고,
 * GRO_UDP_SEGMENT cmsg로 원래 세그먼트 크기를 확인하여
 * 애플리케이션 레벨에서 분리
 */
int val = 1;
setsockopt(fd, SOL_UDP, UDP_GRO, &val, sizeof(val));

/* 수신 시: recvmsg()로 병합된 대형 버퍼 수신 */
struct msghdr rmsg = {};
char rcmsgbuf[CMSG_SPACE(sizeof(uint16_t))];
rmsg.msg_control = rcmsgbuf;
rmsg.msg_controllen = sizeof(rcmsgbuf);

recvmsg(fd, &rmsg, 0);

/* GRO_UDP_SEGMENT cmsg로 원래 세그먼트 크기 확인 */
struct cmsghdr *rcm;
for (rcm = CMSG_FIRSTHDR(&rmsg); rcm; rcm = CMSG_NXTHDR(&rmsg, rcm)) {
    if (rcm->cmsg_level == SOL_UDP && rcm->cmsg_type == UDP_GRO) {
        uint16_t gso_size = *(uint16_t *)CMSG_DATA(rcm);
        /* gso_size = 각 UDP 데이터그램의 페이로드 크기
         * 총 수신 바이트 / gso_size = 병합된 데이터그램 수
         * 마지막 데이터그램은 gso_size보다 작을 수 있음 */
    }
}

/* NIC 레벨에서 UDP GSO HW 오프로드 확인 */
/* # ethtool -k eth0 | grep udp-segmentation
 *   tx-udp-segmentation: on           # USO (HW UDP 세그먼트)
 *   tx-udp_tnl-segmentation: on       # 터널 내부 UDP GSO */
QUIC과 UDP GSO/GRO: QUIC(HTTP/3)은 UDP 위에 구현되므로 UDP GSO/GRO가 성능에 직접적 영향을 줍니다. Google의 QUICHE, cloudflare/quiche 등 주요 QUIC 구현체는 UDP GSO/GRO를 적극 활용합니다. WireGuard VPN도 UDP GSO를 사용하여 암호화된 터널 처리량을 극대화합니다.

UDP Encapsulation

UDP는 터널 프로토콜의 캡슐화 계층으로 광범위하게 사용됩니다:

프로토콜UDP 포트용도커널 모듈
VXLAN 4789 L2-over-UDP 가상 네트워크 (클라우드 오버레이) drivers/net/vxlan/
Geneve 6081 Generic Network Virtualization Encapsulation drivers/net/geneve.c
WireGuard 51820 VPN 터널 (Noise Protocol + ChaCha20) drivers/net/wireguard/
IPSec NAT-T 4500 ESP-in-UDP 캡슐화 (NAT 통과) net/ipv4/udp.c (encap)
L2TP 1701 L2 터널링 (PPP over UDP) net/l2tp/
GTP-U 2152 모바일 네트워크 사용자 평면 drivers/net/gtp.c
/* 커널 UDP encap 등록 패턴 (VXLAN, WireGuard 등이 사용) */
struct udp_tunnel_sock_cfg cfg = {
    .encap_type  = UDP_ENCAP_VXLAN,
    .encap_rcv   = vxlan_rcv,          /* 수신 콜백 */
    .encap_err_rcv = vxlan_err_rcv,    /* 에러 콜백 */
    .encap_destroy = vxlan_del_work,   /* 소멸 콜백 */
    .gro_receive = vxlan_gro_receive,  /* GRO 콜백 */
    .gro_complete = vxlan_gro_complete,
};
setup_udp_tunnel_sock(net, sock, &cfg);
/* → UDP 소켓이 특정 포트에서 터널 패킷을 수신하면
 *   일반 UDP 처리 대신 encap_rcv 콜백을 호출
 *   → 내부 패킷을 역캡슐화하여 다시 네트워크 스택에 주입 */

UDP-Lite (RFC 3828)

/* UDP-Lite: 부분 체크섬 지원 UDP 변형 (IP 프로토콜 136) */
/* 오디오/비디오 스트리밍에서 일부 비트 오류를 허용하되 전달 보장 */

/* 소켓 생성 */
int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDPLITE);

/* 체크섬 커버리지 설정 (기본: 전체 패킷) */
int coverage = 8;  /* 헤더 8바이트만 체크섬 보호, 페이로드는 무검증 */
setsockopt(fd, IPPROTO_UDPLITE, UDPLITE_SEND_CSCOV,
           &coverage, sizeof(coverage));
setsockopt(fd, IPPROTO_UDPLITE, UDPLITE_RECV_CSCOV,
           &coverage, sizeof(coverage));

/* 커널 내부: net/ipv4/udplite.c
 *   udplite4_lib_rcv() → 체크섬 커버리지 범위만 검증
 *   커버리지 미달 패킷은 수신 거부
 */

SCTP (Stream Control Transmission Protocol)

SCTP(IP 프로토콜 132)는 TCP의 신뢰성과 UDP의 메시지 경계 보존을 결합한 전송 프로토콜입니다. 멀티스트리밍, 멀티호밍, 4-way handshake 등 고유 기능을 제공합니다.

SCTP 핵심 특성

특성TCPUDPSCTP
연결 지향 O X O (Association)
신뢰적 전달 O X O (선택적 비순서 전달도 가능)
메시지 경계 보존 X (바이트 스트림) O O (청크 단위)
멀티스트리밍 X X O (독립 스트림, HoL blocking 방지)
멀티호밍 X X O (다수 IP 주소 바인딩, failover)
SYN Flood 방어 SYN Cookie 해당 없음 4-way handshake + Cookie (내장)

SCTP Association과 4-Way Handshake

/* SCTP 4-way handshake (INIT → INIT-ACK → COOKIE-ECHO → COOKIE-ACK) */
/*
 * 1. 클라이언트 → INIT 청크 (자신의 Tag, 스트림 수, 주소 목록)
 * 2. 서버 → INIT-ACK 청크 (자신의 Tag + State Cookie)
 *    → 서버는 이 시점에서 상태를 저장하지 않음 (SYN Flood 면역)
 *    → State Cookie에 검증 정보를 HMAC으로 서명하여 인코딩
 * 3. 클라이언트 → COOKIE-ECHO (State Cookie 그대로 반환)
 * 4. 서버 → COOKIE-ACK (Cookie 검증 후 Association 생성)
 *    → 3단계에서 이미 데이터 포함 가능 (TCP Fast Open과 유사)
 */

/* include/net/sctp/structs.h */
struct sctp_association {
    struct sctp_ep_common base;
    struct list_head transports;     /* 원격 주소 목록 (멀티호밍) */
    struct sctp_stream stream;       /* 스트림 관리 */
    __u16 c.sinit_num_ostreams;      /* 출력 스트림 수 */
    __u16 c.sinit_max_instreams;     /* 입력 스트림 수 */
    __u32 c.my_vtag;                 /* 자신의 Verification Tag */
    __u32 c.peer_vtag;               /* 상대의 Verification Tag */
    /* ... */
};

멀티스트리밍

/* SCTP 멀티스트리밍: 하나의 Association 내에 독립적인 스트림들 */
/* 장점: 한 스트림의 패킷 손실이 다른 스트림에 영향을 주지 않음
 *       → HTTP/2의 Head-of-Line blocking 문제를 프로토콜 수준에서 해결
 */

/* 사용자 공간: sctp_sendmsg로 스트림 지정 */
struct sctp_sndrcvinfo sinfo = {
    .sinfo_stream = 3,              /* 스트림 번호 3으로 전송 */
    .sinfo_flags  = SCTP_UNORDERED, /* 비순서 전달 (선택) */
    .sinfo_ppid   = htonl(42),     /* Payload Protocol ID */
};
sctp_sendmsg(fd, data, len, NULL, 0,
             sinfo.sinfo_ppid, sinfo.sinfo_flags,
             sinfo.sinfo_stream, 0, 0);

/* 수신: sctp_recvmsg로 스트림 정보 확인 */
struct sctp_sndrcvinfo rinfo;
int flags = 0;
sctp_recvmsg(fd, buf, buflen, NULL, 0, &rinfo, &flags);
printf("stream=%u ppid=%u\n", rinfo.sinfo_stream, ntohl(rinfo.sinfo_ppid));

멀티호밍과 Failover

/* SCTP 멀티호밍: 양쪽 엔드포인트가 여러 IP 주소를 가질 수 있음 */
/* → 한 경로 실패 시 자동으로 다른 경로로 전환 (heartbeat 기반) */

/* 다수 주소 바인딩 */
struct sockaddr_in addrs[2];
addrs[0].sin_addr.s_addr = inet_addr("10.0.0.1");
addrs[0].sin_port = htons(9999);
addrs[1].sin_addr.s_addr = inet_addr("10.0.1.1");
addrs[1].sin_port = htons(9999);
sctp_bindx(fd, (struct sockaddr *)addrs, 2, SCTP_BINDX_ADD_ADDR);

/* Heartbeat: 보조 경로의 상태를 주기적으로 확인
 *   net.sctp.hb_interval = 30000 (ms, 기본 30초)
 *   연속 path_max_retrans 회 실패 시 경로 비활성화
 *   → primary 경로 실패 시 active 보조 경로로 자동 전환
 */

/* 커널 내부: net/sctp/transport.c */
/* sctp_assoc_control_transport():
 *   SCTP_TRANSPORT_DOWN → 해당 경로 비활성화
 *   → sctp_assoc_update_retran_path()로 재전송 경로 변경
 */

SCTP 청크 타입

청크타입용도
DATA0사용자 데이터 전달 (TSN, 스트림 번호, 시퀀스 번호 포함)
INIT1Association 시작 요청
INIT ACK2INIT 응답 + State Cookie
SACK3선택적 확인응답 (TCP SACK와 유사)
HEARTBEAT4경로 활성 확인 (멀티호밍)
HEARTBEAT ACK5Heartbeat 응답
ABORT6Association 즉시 종료
SHUTDOWN7정상 종료 시작
COOKIE ECHO10State Cookie 반환 (handshake 3단계)
COOKIE ACK11Cookie 확인 (handshake 4단계)
FORWARD TSN192수신 불필요한 TSN 건너뛰기 (부분 신뢰성)
💡

SCTP 사용 사례: 텔레콤 시그널링(Diameter, SIGTRAN), WebRTC DataChannel(SCTP over DTLS over UDP), 고가용성 클러스터 통신. 커널 모듈 sctp를 로드해야 하며, lksctp-tools 패키지가 사용자 공간 유틸리티를 제공합니다.

ICMP 심화

ICMP(Internet Control Message Protocol, IP 프로토콜 1)는 IP 네트워크의 제어 평면 프로토콜입니다. 패킷 전달 실패 보고, 경로 변경 알림, 연결 진단(ping/traceroute) 등 네트워크 운영의 핵심 기능을 담당합니다. RFC 792(IPv4 ICMP)와 RFC 4443(ICMPv6)에 정의되어 있으며, 커널의 net/ipv4/icmp.cnet/ipv6/icmp.c에 구현되어 있습니다.

ICMP 헤더 구조와 커널 구조체

/* include/uapi/linux/icmp.h — ICMP 헤더 (고정 8바이트) */
struct icmphdr {
    __u8    type;       /* 메시지 타입 (0-255) */
    __u8    code;       /* 타입별 세부 코드 */
    __sum16 checksum;   /* ICMP 헤더 + 데이터 전체의 체크섬 */
    union {
        struct {
            __be16  id;          /* Echo: 식별자 (프로세스 구분) */
            __be16  sequence;    /* Echo: 시퀀스 번호 */
        } echo;
        __be32  gateway;        /* Redirect: 게이트웨이 주소 */
        struct {
            __be16  __unused;
            __be16  mtu;         /* Frag Needed: 다음 홉 MTU */
        } frag;
        __u8    reserved[4];    /* 기타 타입에서 사용 */
    } un;
};

/*
 * ICMP 패킷 구조 (에러 메시지의 경우):
 *
 *  ┌──────────────────────────────────┐
 *  │ IP Header (20+ bytes)            │  ← 외부 IP 헤더
 *  ├──────────────────────────────────┤
 *  │ ICMP Header (8 bytes)            │  ← type, code, checksum, un
 *  ├──────────────────────────────────┤
 *  │ Original IP Header (20+ bytes)   │  ← 에러 유발 패킷의 IP 헤더
 *  │ + Original L4 Header (8+ bytes)  │  ← 원본 TCP/UDP 헤더 (포트 포함)
 *  └──────────────────────────────────┘
 *
 * → 에러 ICMP는 원본 패킷 헤더를 포함해 어떤 연결에서 발생한 에러인지 식별 가능
 * → RFC 1122: 최소 원본 IP 헤더 + 8바이트 포함 필수
 */

ICMP 메시지 타입/코드 종합

타입이름주요 코드용도커널 처리 함수
0 Echo Reply 0 ping 응답 ping_rcv()
3 Destination Unreachable 0: Net
1: Host
2: Protocol
3: Port
4: Frag Needed (DF set)
13: Filtered
패킷 전달 불가 보고 icmp_unreach()
4 Source Quench 0 (폐기) 혼잡 알림 무시 (RFC 6633)
5 Redirect 0: Network
1: Host
2: TOS+Net
3: TOS+Host
더 나은 경로 알림 icmp_redirect()
8 Echo Request 0 ping 요청 icmp_echo()
11 Time Exceeded 0: TTL expired
1: Frag reassembly timeout
TTL 만료 / 재조합 실패 icmp_unreach()
12 Parameter Problem 0: Pointer
1: Missing option
2: Bad length
헤더 오류 보고 icmp_unreach()
13/14 Timestamp / Reply 0 시간 동기화 (거의 미사용) icmp_timestamp()

에러 vs 조회 메시지: ICMP 메시지는 두 범주로 나뉩니다. 에러 메시지(Type 3, 4, 5, 11, 12)는 다른 패킷의 처리 실패를 보고하며, 원본 패킷의 IP 헤더+8바이트를 페이로드에 포함합니다. 조회 메시지(Type 0/8, 13/14)는 요청-응답 쌍으로 네트워크 진단에 사용됩니다. 에러 메시지에 대해 ICMP 에러를 생성하지 않는 것이 핵심 규칙입니다 (무한 루프 방지).

ICMP 수신 경로 (icmp_rcv)

/* net/ipv4/icmp.c — ICMP 수신 진입점 */
int icmp_rcv(struct sk_buff *skb)
{
    struct icmphdr *icmph;
    struct net *net = dev_net(skb->dev);

    /* 1. 체크섬 검증 */
    if (skb_checksum_simple_validate(skb))
        goto csum_error;

    icmph = icmp_hdr(skb);

    /* 2. 브로드캐스트/멀티캐스트 ICMP 처리 */
    if (skb->pkt_type != PACKET_HOST) {
        /* Echo Request to broadcast: icmp_echo_ignore_broadcasts 확인 */
        if (icmph->type == ICMP_ECHO &&
            net->ipv4.sysctl_icmp_echo_ignore_broadcasts)
            goto drop;
    }

    /* 3. icmp_pointers[] 디스패치 테이블로 타입별 핸들러 호출 */
    if (icmph->type < NR_ICMP_TYPES) {
        int ret = icmp_pointers[icmph->type].handler(skb);
        return ret;
    }
    goto drop;
}

/* icmp_pointers[] — 타입별 핸들러 디스패치 테이블 */
static const struct icmp_control icmp_pointers[NR_ICMP_TYPES + 1] = {
    [ICMP_ECHOREPLY]      = { .handler = ping_rcv,     },
    [ICMP_DEST_UNREACH]   = { .handler = icmp_unreach, .error = 1, },
    [ICMP_SOURCE_QUENCH]  = { .handler = icmp_unreach, .error = 1, },
    [ICMP_REDIRECT]       = { .handler = icmp_redirect,.error = 1, },
    [ICMP_ECHO]           = { .handler = icmp_echo,    },
    [ICMP_TIME_EXCEEDED]  = { .handler = icmp_unreach, .error = 1, },
    [ICMP_PARAMETERPROB]  = { .handler = icmp_unreach, .error = 1, },
    [ICMP_TIMESTAMP]      = { .handler = icmp_timestamp, },
    [ICMP_TIMESTAMPREPLY] = { .handler = ping_rcv,     },
    /* ... 나머지는 icmp_discard()로 무시 */
};

/* icmp_control.error = 1인 타입은 ICMP 에러 메시지
 * → icmp_unreach()가 원본 패킷 정보를 추출해
 *   상위 프로토콜(TCP/UDP)의 에러 핸들러에 전달
 */

icmp_unreach() — 에러 메시지 처리

/* net/ipv4/icmp.c — Destination Unreachable / Time Exceeded 처리 */
static bool icmp_unreach(struct sk_buff *skb)
{
    struct icmphdr *icmph = icmp_hdr(skb);
    struct iphdr *iph;      /* 에러 유발 원본 패킷의 IP 헤더 */

    /* 1. ICMP 페이로드에서 원본 IP 헤더 추출 */
    iph = (struct iphdr *)skb->data;

    /* 2. Fragmentation Needed (Type 3, Code 4) → PMTUD 처리 */
    if (icmph->type == ICMP_DEST_UNREACH &&
        icmph->code == ICMP_FRAG_NEEDED) {
        /* Path MTU 업데이트:
         * icmph->un.frag.mtu에 다음 홉 MTU가 포함됨
         * → ip_rt_frag_needed()로 라우팅 캐시 MTU 갱신 */
        ipv4_update_pmtu(skb, net, ntohs(icmph->un.frag.mtu),
                         iph->daddr);
    }

    /* 3. 원본 IP 헤더의 프로토콜 번호로 상위 에러 핸들러 호출 */
    protocol = iph->protocol;
    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot && ipprot->err_handler)
        ipprot->err_handler(skb, /* info */);
        /* TCP → tcp_v4_err(): 연결 RST, 재전송 등 처리
         * UDP → udp_err():  소켓에 에러 전파
         * SCTP → sctp_v4_err(): association 에러 처리 */

    /* 4. SNMP 카운터 갱신 */
    __ICMP_INC_STATS(net, ICMP_MIB_INDESTUNREACHS);
}

/* TCP가 ICMP Destination Unreachable를 받았을 때:
 * - Code 0,1 (Net/Host Unreachable):
 *     → soft error 기록 (즉시 종료하지 않음)
 *     → 재전송 타이머 만료 시 EHOSTUNREACH 반환
 * - Code 2 (Protocol Unreachable):
 *     → 연결 RST (상대방에 TCP 스택 없음)
 * - Code 3 (Port Unreachable):
 *     → TCP에서는 일반적으로 무시 (TCP는 RST 사용)
 * - Code 4 (Frag Needed):
 *     → MSS 조정 후 재전송 (Path MTU Discovery)
 * - Code 13 (Admin Filtered):
 *     → soft error (방화벽 차단)
 */

ICMP 전송 메커니즘 (icmp_send)

/* net/ipv4/icmp.c — ICMP 에러 메시지 전송 */
void icmp_send(struct sk_buff *skb_in,
               int type, int code, __be32 info)
{
    struct iphdr *iph = ip_hdr(skb_in);

    /* ===== RFC 1122 규칙: ICMP 에러를 보내지 않는 경우 ===== */

    /* 규칙 1: ICMP 에러 메시지에 대해 ICMP 에러를 보내지 않음
     *   → 무한 루프 방지 */
    if (icmp_is_err_type(type) &&
        iph->protocol == IPPROTO_ICMP) {
        struct icmphdr *inner = icmp_hdr(skb_in);
        if (icmp_pointers[inner->type].error)
            return;  /* 원본이 ICMP 에러 → 이중 에러 금지 */
    }

    /* 규칙 2: 첫 번째 단편이 아닌 패킷에 대해 보내지 않음 */
    if (ntohs(iph->frag_off) & IP_OFFSET)
        return;

    /* 규칙 3: 브로드캐스트/멀티캐스트 목적지에 대해 보내지 않음 */
    if (skb_in->pkt_type != PACKET_HOST &&
        skb_in->pkt_type != PACKET_OUTGOING)
        return;

    /* 규칙 4: 소스 주소가 0.0.0.0이면 보내지 않음 */
    if (!iph->saddr)
        return;

    /* ===== Rate Limiting 확인 ===== */
    if (!icmpv4_global_allow(net, type, code))
        return;
    if (!icmpv4_xrlim_allow(net, type, code, skb_in))
        return;

    /* ===== ICMP 패킷 구성 및 전송 ===== */
    /* per-CPU icmp_sk 소켓 사용 (락 경쟁 최소화) */
    sk = icmp_sk(net);

    /* 에러 유발 패킷의 IP 헤더 + 8바이트를 페이로드에 복사 */
    room = dst_mtu(dst) - sizeof(struct iphdr)
                         - sizeof(struct icmphdr);
    /* RFC 4884: 가능하면 더 많은 원본 데이터 포함 */

    icmp_push_reply(sk, &icmp_param, &fl4, &ipc);
}

Echo Request/Reply 구현 (ping)

/* net/ipv4/icmp.c — Echo Request 처리 */
static bool icmp_echo(struct sk_buff *skb)
{
    struct net *net = dev_net(skb->dev);
    struct icmphdr *icmph = icmp_hdr(skb);

    /* sysctl로 Echo 응답 비활성화 가능 */
    if (net->ipv4.sysctl_icmp_echo_ignore_all)
        return true;

    /* Echo Reply 구성: type=0, id/seq 그대로 복사, 데이터 복사 */
    icmp_param.data.icmph      = *icmph;
    icmp_param.data.icmph.type = ICMP_ECHOREPLY;
    icmp_param.skb             = skb;

    /* icmp_reply()로 응답 전송 (소스 주소 = 수신 주소) */
    icmp_reply(&icmp_param, skb);
    return true;
}

/* ===== ping 소켓 (IPPROTO_ICMP) ===== */
/* 커널 3.0+: 비특권 사용자도 ping 가능
 *
 * 기존: raw socket(SOCK_RAW) 필요 → CAP_NET_RAW 권한 필수
 * 현재: SOCK_DGRAM + IPPROTO_ICMP → "ping socket" 자동 생성
 *
 * net.ipv4.ping_group_range = "0 2147483647"
 *   → 모든 GID의 사용자가 ping 가능
 *   → setuid 없이 /bin/ping 실행
 *
 * 커널 처리:
 * - id 필드를 소켓 포트처럼 사용 (소켓 demux)
 * - Echo Reply를 해당 소켓으로 직접 전달 (ping_rcv)
 */
int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
/* → 커널이 자동으로 id 할당, Echo Request 전송 시:
 *    sendto(fd, payload, len, 0, &dst, sizeof(dst));
 *    → 커널이 ICMP 헤더 구성 (type=8, code=0, id=소켓 id)
 *    recvfrom(fd, buf, sizeof(buf), 0, ...);
 *    → Echo Reply 수신 (type=0 응답만 필터링됨) */

ICMP Rate Limiting 메커니즘

/* net/ipv4/icmp.c — 전역 Rate Limiting */
static bool icmpv4_global_allow(struct net *net, int type, int code)
{
    /* Token Bucket 알고리즘 기반
     *
     * net.ipv4.icmp_msgs_per_sec (기본: 1000)
     *   → 초당 최대 ICMP 메시지 전송 수
     *   → 토큰이 이 속도로 리필됨
     *
     * net.ipv4.icmp_msgs_burst (기본: 50)
     *   → 버스트 허용 크기 (토큰 버킷 최대 토큰 수)
     *
     * 동작: 토큰이 있으면 전송 허용 + 토큰 1개 소비
     *       토큰이 없으면 ICMP 전송 억제
     */

    if (icmp_global_allow())
        return true;
    __ICMP_INC_STATS(net, ICMP_MIB_RATELIMITGLOBAL);
    return false;
}

/* 목적지별 Rate Limiting */
static bool icmpv4_xrlim_allow(struct net *net, int type, int code,
                                struct sk_buff *skb)
{
    struct dst_entry *dst = skb_dst(skb);

    /* net.ipv4.icmp_ratelimit (기본: 1000 ms)
     *   → 동일 목적지에 대한 ICMP 에러 최소 간격
     *
     * net.ipv4.icmp_ratemask (기본: 6168 = 0x1818)
     *   → rate limit 적용 대상 ICMP 타입 비트마스크
     *   → 비트가 설정된 타입만 rate limiting 적용
     *   → 기본값: Type 3 (Dest Unreach), 11 (Time Exceeded), 12 (Param Problem)
     *   → Type 0 (Echo Reply), 8 (Echo)은 기본적으로 rate limit 미적용
     */

    /* Destination Unreachable(Type 3)은 항상 rate limit */
    if (type == ICMP_DEST_UNREACH)
        return dst_output_okfn(...);  /* per-route rate check */

    /* Echo Reply는 rate limit 미적용 (별도 제어) */
    if (type == ICMP_ECHOREPLY)
        return true;

    return inet_peer_xrlim_allow(dst, net->ipv4.sysctl_icmp_ratelimit);
}

Path MTU Discovery (PMTUD)

/* PMTUD: IP 경로의 최소 MTU를 동적으로 탐지
 *
 * 동작 원리:
 * 1. 송신자가 DF(Don't Fragment) 비트를 설정해 패킷 전송
 * 2. 경로상 라우터가 패킷 > 자신의 MTU이면:
 *    → ICMP Type 3 Code 4 (Fragmentation Needed) + next-hop MTU 반환
 * 3. 송신자가 Path MTU를 줄이고 재전송
 * 4. 종단까지 도달할 때까지 반복
 *
 *    ┌──────┐    MTU 1500     ┌──────┐   MTU 1400    ┌──────┐
 *    │ Host ├────────────────▶│Router├──────────────▶│ Host │
 *    │  A   │                 │  R1  │    ▲          │  B   │
 *    └──────┘                 └──────┘    │          └──────┘
 *       ▲                                 │
 *       │ ICMP Frag Needed (MTU=1400)     │
 *       └─────────────────────────────────┘
 */

/* net/ipv4/route.c — PMTUD: MTU 갱신 */
void ipv4_update_pmtu(struct sk_buff *skb, struct net *net,
                       u32 mtu, __be32 daddr)
{
    struct rtable *rt;

    /* mtu 유효성 검사: 최소 68바이트 (RFC 791) */
    if (mtu < 68)
        return;

    /* 라우팅 캐시에서 해당 목적지의 PMTU 갱신 */
    rt = ip_route_output(net, daddr, ...);
    if (rt) {
        rt_update_pmtu(rt, mtu);
        /* → dst_entry->metrics[RTAX_MTU]를 mtu로 설정
         * → PMTU 만료 타이머 시작 (기본 10분)
         * → 만료 시 원래 interface MTU로 복귀 (경로 변경 감지) */
    }
}

/* TCP에서의 PMTUD 연동 */
/* tcp_v4_err()가 Frag Needed 수신 시:
 * 1. tp->mtu_info = mtu  (새 Path MTU 저장)
 * 2. tcp_sync_mss(sk, mtu) 호출
 *    → MSS = mtu - IP header - TCP header
 * 3. 현재 전송 큐의 세그먼트를 새 MSS로 재분할
 * 4. 재전송 트리거 (cwnd는 유지)
 */

/* PMTUD 문제와 대안 */
/* 문제: ICMP Frag Needed가 방화벽에서 차단되면 PMTUD 실패
 *   → "PMTUD Black Hole": 패킷이 무한 드롭
 *
 * 대안 1: TCP MSS Clamping
 *   iptables -A FORWARD -p tcp --tcp-flags SYN,RST SYN \
 *     -j TCPMSS --clamp-mss-to-pmtu
 *   → SYN의 MSS 옵션을 경로 MTU에 맞춤 (ICMP 불필요)
 *
 * 대안 2: PLPMTUD (RFC 8899, Packetization Layer PMTUD)
 *   → ICMP에 의존하지 않고 프로브 패킷으로 MTU 탐색
 *   → TCP: net.ipv4.tcp_mtu_probing = 1
 *   → SCTP, QUIC도 지원
 *
 * 대안 3: net.ipv4.ip_no_pmtu_disc = 1
 *   → PMTUD 비활성화 (DF 비트 미설정 → IP 단편화 허용)
 *   → 성능 저하 주의
 */

PMTUD Black Hole 감지: net.ipv4.tcp_mtu_probing = 1을 설정하면, TCP 재전송이 반복될 때 커널이 자동으로 MSS를 줄여가며 프로브합니다 (tcp_base_mss부터 시작). = 2이면 초기 연결부터 프로빙을 시작합니다. 이는 ICMP가 차단된 환경(많은 클라우드/기업 네트워크)에서 필수적인 설정입니다.

ICMP Redirect와 라우팅 캐시

/* ICMP Redirect (Type 5):
 * 라우터가 "너에게 더 좋은 next-hop이 있다"고 알리는 메시지
 *
 * 발생 조건: 라우터가 패킷을 수신한 인터페이스와 동일한 인터페이스로
 * 포워딩해야 할 때 (더 직접적인 경로가 존재)
 *
 *   ┌──────┐  default gw=R1    ┌──────┐
 *   │ Host ├───────────────────▶│  R1  │
 *   │  A   │◀─── Redirect ─────┤      │
 *   └──┬───┘  (use R2 for dst) └──────┘
 *      │                            │
 *      │     direct route to dst    │
 *      │         ┌──────┐          │
 *      └────────▶│  R2  │◀─────────┘
 *                └──────┘
 */

/* net/ipv4/icmp.c — ICMP Redirect 수신 */
static bool icmp_redirect(struct sk_buff *skb)
{
    struct icmphdr *icmph = icmp_hdr(skb);
    __be32 new_gw = icmph->un.gateway;  /* 새 게이트웨이 */

    /* 보안 검증:
     * 1. Redirect 소스가 현재 게이트웨이인지 확인
     * 2. 새 게이트웨이가 같은 서브넷에 있는지 확인
     * 3. 새 게이트웨이가 멀티캐스트/브로드캐스트가 아닌지 확인
     */
    if (!ip_route_input(skb, iph->daddr, iph->saddr, ...))
        goto reject;

    /* 라우팅 테이블에 redirect 경로 추가 */
    ip_rt_redirect(new_gw, iph->daddr, iph->saddr, skb->dev);
    return true;
}

/* ICMP Redirect 관련 sysctl */
/*
 * net.ipv4.conf.{iface}.accept_redirects
 *   = 1: Redirect 수신 허용 (호스트 기본값)
 *   = 0: 무시 (라우터/보안 환경 권장)
 *
 * net.ipv4.conf.{iface}.secure_redirects
 *   = 1: 기본 게이트웨이에서 온 Redirect만 수락 (기본)
 *   = 0: 모든 게이트웨이의 Redirect 수락 (위험)
 *
 * net.ipv4.conf.{iface}.send_redirects
 *   = 1: 포워딩 시 Redirect 전송 (라우터 기본값)
 *   = 0: Redirect 전송 안 함
 *
 * 보안 경고: ICMP Redirect는 MITM 공격에 악용 가능
 *   → 서버/라우터에서는 반드시 accept_redirects=0 설정
 *   → IPv6: net.ipv6.conf.{iface}.accept_redirects = 0
 */

커널 ICMP 소켓과 per-CPU 구조

/* net/ipv4/icmp.c — per-CPU ICMP 소켓 */
/* 커널은 ICMP 전송을 위해 네트워크 네임스페이스 + CPU별 전용 소켓을 유지
 *
 * 이유: icmp_send()는 softirq 컨텍스트에서 호출될 수 있으므로
 *       소켓 할당/해제 오버헤드를 줄이고 락 경쟁을 방지
 *
 * 초기화: icmp_init() → icmp_sk_init() (네임스페이스별)
 */

struct icmp_bxm {     /* ICMP 빌드 + 전송 매개변수 */
    struct sk_buff  *skb;           /* 에러 유발 원본 패킷 */
    int             offset;          /* 데이터 오프셋 */
    int             data_len;        /* 복사할 원본 데이터 길이 */
    struct {
        struct icmphdr icmph;        /* 전송할 ICMP 헤더 */
        __be32         times[3];    /* timestamp용 */
    } data;
    int             head_len;        /* 헤더 길이 */
    struct ip_options_data replyopts; /* IP 옵션 복사 */
};

/* icmp_reply() vs icmp_send():
 * - icmp_reply(): 수신된 ICMP에 대한 응답 (Echo Reply 등)
 *   → 소스 주소 = 수신 패킷의 목적지 주소
 *   → 목적지 = 수신 패킷의 소스 주소
 *
 * - icmp_send(): 에러 ICMP 생성 (Dest Unreach 등)
 *   → 소스 주소 = 에러를 감지한 인터페이스의 주소
 *   → 목적지 = 에러 유발 패킷의 소스 주소
 *   → 원본 패킷의 IP 헤더 + 8바이트를 페이로드에 포함
 */

ICMPv6 심화

/* include/uapi/linux/icmpv6.h — ICMPv6 헤더 */
struct icmp6hdr {
    __u8    icmp6_type;      /* 메시지 타입 */
    __u8    icmp6_code;      /* 타입별 코드 */
    __sum16 icmp6_cksum;     /* 체크섬 (IPv6 pseudo-header 포함!) */
    union {
        __be32             un_data32[1];
        __be16             un_data16[2];
        __u8               un_data8[4];
        struct icmpv6_echo u_echo;      /* id + seq */
        struct icmpv6_nd_advt u_nd_advt; /* NDP 광고 플래그 */
        struct icmpv6_nd_ra   u_nd_ra;   /* Router Advert */
    } icmp6_dataun;
};

/* ICMPv6 vs ICMPv4 주요 차이:
 * 1. 체크섬에 IPv6 pseudo-header 포함 (ICMPv4는 ICMP 자체만)
 * 2. 에러 메시지: 타입 0-127, 정보 메시지: 타입 128-255
 * 3. ICMPv6가 ARP/IGMP 역할 흡수 (NDP, MLD)
 * 4. Path MTU Discovery: Type 2 (Packet Too Big)
 */
타입이름ICMPv4 대응용도
1 Destination Unreachable Type 3 전달 불가 (no route, admin prohibited, port unreach 등)
2 Packet Too Big Type 3 Code 4 PMTUD — IPv6에서는 라우터가 단편화하지 않으므로 필수
3 Time Exceeded Type 11 Hop Limit 만료 / 재조합 타임아웃
4 Parameter Problem Type 12 헤더 필드 오류 / 인식 불가 Next Header
128/129 Echo Request/Reply Type 8/0 ping6
130-132 MLD (v1) IGMP 멀티캐스트 리스너 관리
133-137 NDP (RS/RA/NS/NA/Redirect) ARP + ICMP Redirect 주소 해석, 라우터 발견, DAD, SLAAC
143 MLDv2 IGMPv3 소스 특정 멀티캐스트 그룹 관리
/* net/ipv6/icmp.c — ICMPv6 수신 */
int icmpv6_rcv(struct sk_buff *skb)
{
    struct icmp6hdr *hdr = icmp6_hdr(skb);

    /* ICMPv6 체크섬 검증 (pseudo-header 포함) */
    if (skb_checksum_validate(skb, IPPROTO_ICMPV6, ...))
        goto csum_error;

    switch (hdr->icmp6_type) {
    case ICMPV6_ECHO_REQUEST:
        if (net->ipv6.sysctl.icmpv6_echo_ignore_all)
            break;
        icmpv6_echo_reply(skb);
        break;

    case ICMPV6_PKT_TOOBIG:
        /* IPv6 PMTUD: Packet Too Big → Path MTU 갱신 */
        icmpv6_notify(skb, hdr->icmp6_type, hdr->icmp6_code,
                      hdr->icmp6_mtu);
        break;

    case NDISC_ROUTER_SOLICITATION:
    case NDISC_ROUTER_ADVERTISEMENT:
    case NDISC_NEIGHBOUR_SOLICITATION:
    case NDISC_NEIGHBOUR_ADVERTISEMENT:
    case NDISC_REDIRECT:
        ndisc_rcv(skb);    /* NDP 서브시스템으로 전달 */
        break;

    case ICMPV6_MGM_QUERY:
    case ICMPV6_MGM_REPORT:
    case ICMPV6_MGM_REDUCTION:
    case ICMPV6_MLD2_REPORT:
        igmp6_event_query(skb); /* MLD → 멀티캐스트 그룹 관리 */
        break;

    case ICMPV6_DEST_UNREACH:
    case ICMPV6_TIME_EXCEED:
    case ICMPV6_PARAMPROB:
        icmpv6_notify(skb, ...); /* 상위 프로토콜에 에러 전파 */
        break;
    }
}

traceroute와 ICMP

/* traceroute 동작 원리:
 *
 * 방법 1: UDP 기반 (전통적 Unix traceroute)
 *   → 높은 포트 번호(33434+)로 UDP 패킷 전송, TTL을 1부터 증가
 *   → 각 홉에서 TTL 만료 시 ICMP Time Exceeded (Type 11, Code 0) 반환
 *   → 최종 목적지는 ICMP Port Unreachable (Type 3, Code 3) 반환
 *
 * 방법 2: ICMP 기반 (Windows tracert, traceroute -I)
 *   → ICMP Echo Request(Type 8) 전송, TTL을 1부터 증가
 *   → 각 홉에서 TTL 만료 시 ICMP Time Exceeded 반환
 *   → 최종 목적지는 ICMP Echo Reply (Type 0) 반환
 *
 * 방법 3: TCP SYN 기반 (traceroute -T, tcptraceroute)
 *   → TCP SYN 패킷 전송 (포트 80 등), TTL을 1부터 증가
 *   → 방화벽을 더 잘 통과함
 *
 * 커널에서 TTL 만료 처리:
 */

/* net/ipv4/ip_forward.c — TTL 감소와 만료 처리 */
int ip_forward(struct sk_buff *skb)
{
    struct iphdr *iph = ip_hdr(skb);

    /* TTL 검사 */
    if (iph->ttl <= 1) {
        /* TTL 만료 → ICMP Time Exceeded 전송 */
        icmp_send(skb, ICMP_TIME_EXCEEDED, ICMP_EXC_TTL, 0);
        __IP_INC_STATS(net, IPSTATS_MIB_INHDRERRORS);
        kfree_skb(skb);
        return NET_RX_DROP;
    }

    /* TTL 감소 */
    ip_decrease_ttl(iph);
    /* ... 라우팅 후 전송 */
}

/* ICMP Time Exceeded 응답에 포함되는 정보:
 * - 원본 IP 헤더 전체 (소스/목적지 주소, 프로토콜 등)
 * - 원본 L4 헤더 8바이트 (UDP: src/dst 포트, TCP: src/dst 포트 + seq)
 * → traceroute가 어떤 프로브에 대한 응답인지 매칭 가능
 *
 * traceroute의 RTT 계산:
 * - 프로브 전송 시각과 ICMP 응답 수신 시각의 차이
 * - 보통 홉당 3개 프로브 전송 → 3개 RTT 표시
 * - * 표시: 해당 홉에서 ICMP 응답 없음 (방화벽 차단 또는 rate limit)
 */

Raw Socket과 ICMP 프로그래밍

/* ICMP 패킷 직접 제어: Raw Socket 사용 */

/* 방법 1: Raw Socket (CAP_NET_RAW 필요) */
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
/* → 모든 ICMP 타입을 수신/전송 가능
 * → ICMP 헤더를 직접 구성해야 함
 * → 체크섬은 커널이 자동 계산 (IP_HDRINCL이 아닌 경우)
 */

/* ICMP Echo Request 전송 예시 */
struct icmphdr hdr = {
    .type     = ICMP_ECHO,       /* 8 */
    .code     = 0,
    .checksum = 0,                /* 커널이 계산 */
    .un.echo.id       = htons(getpid() & 0xFFFF),
    .un.echo.sequence = htons(seq++),
};
sendto(sockfd, &hdr, sizeof(hdr), 0,
       (struct sockaddr *)&dst, sizeof(dst));

/* ICMP 수신 — 모든 ICMP 메시지가 raw socket에 복사됨 */
struct sockaddr_in src;
socklen_t slen = sizeof(src);
char buf[1500];
ssize_t n = recvfrom(sockfd, buf, sizeof(buf), 0,
                      (struct sockaddr *)&src, &slen);
/* buf에 IP 헤더 + ICMP 메시지가 포함됨
 * → ip_hdr를 파싱해 ICMP 오프셋 계산 필요 */

/* 방법 2: Ping Socket (비특권, 커널 3.0+) */
int pingfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);
/* → Echo Request/Reply만 가능 (타 타입 접근 불가)
 * → IP 헤더 없이 ICMP 페이로드만 송수신
 * → 커널이 id 필드를 소켓 포트처럼 관리
 * → CAP_NET_RAW 불필요 (ping_group_range 범위 내) */

/* ICMP 소켓 필터: 특정 타입만 수신 */
struct icmp_filter filt;
filt.data = ~(1 << ICMP_ECHOREPLY);  /* Echo Reply만 허용 */
setsockopt(sockfd, SOL_RAW, ICMP_FILTER, &filt, sizeof(filt));

ICMP 관련 sysctl 종합

sysctl 경로기본값설명
net.ipv4.icmp_echo_ignore_all 0 1이면 모든 Echo Request 무시 (ping 차단)
net.ipv4.icmp_echo_ignore_broadcasts 1 브로드캐스트/멀티캐스트 Echo 무시 (Smurf 방어)
net.ipv4.icmp_echo_enable_probe 0 1이면 RFC 8335 Extended Echo (Probe) 지원 (커널 5.7+)
net.ipv4.icmp_msgs_per_sec 1000 초당 ICMP 전송 최대 수 (전역 token bucket)
net.ipv4.icmp_msgs_burst 50 ICMP 버스트 허용량 (token bucket 용량)
net.ipv4.icmp_ratelimit 1000 동일 목적지 ICMP 에러 최소 간격 (ms)
net.ipv4.icmp_ratemask 6168 rate limit 적용 ICMP 타입 마스크 (비트 필드)
net.ipv4.icmp_ignore_bogus_error_responses 1 비정상 ICMP 에러 무시 (로그 오염 방지)
net.ipv4.icmp_errors_use_inbound_ifaddr 0 1이면 ICMP 에러의 소스 주소를 수신 인터페이스 주소로 설정
net.ipv4.conf.*.accept_redirects 호스트:1
라우터:0
ICMP Redirect 수락 여부
net.ipv4.conf.*.secure_redirects 1 기본 게이트웨이의 Redirect만 수락
net.ipv4.conf.*.send_redirects 1 포워딩 시 ICMP Redirect 전송 여부
net.ipv4.ping_group_range "1 0" ping 소켓 허용 GID 범위 (0 2147483647 = 모두 허용)
net.ipv4.ip_no_pmtu_disc 0 1이면 PMTUD 비활성화 (DF 비트 미설정)
net.ipv4.tcp_mtu_probing 0 1: PMTUD 블랙홀 시 프로빙, 2: 항상 프로빙
net.ipv6.icmp.ratelimit 1000 ICMPv6 에러 rate limit (ms)
net.ipv6.icmp.echo_ignore_all 0 ICMPv6 Echo 무시 여부

ICMP와 Connection Tracking (conntrack)

/* Netfilter conntrack에서의 ICMP 처리
 *
 * 핵심 개념: ICMP 에러 메시지는 독립 연결이 아닌
 * 기존 연결과 "RELATED" 관계로 추적됨
 *
 * 예시: Host A → Host B (TCP SYN)
 *       → 중간 라우터가 ICMP Dest Unreachable 반환
 *       → conntrack이 이 ICMP를 원본 TCP 연결과 연결(RELATED)
 */

/* net/netfilter/nf_conntrack_proto_icmp.c */
static int icmp_error_message(struct nf_conn *tmpl,
                               struct sk_buff *skb, ...)
{
    /* ICMP 에러 페이로드에서 원본 패킷 헤더 추출 */
    struct nf_conntrack_tuple innertuple;

    /* 내부 패킷(원본)으로 conntrack 엔트리 검색 */
    h = nf_conntrack_find_get(net, zone, &innertuple);

    if (h) {
        /* 기존 연결 발견 → ICMP를 RELATED로 분류
         * → "iptables -A INPUT -m state --state RELATED -j ACCEPT"
         *   규칙에 의해 허용됨 */
        nf_ct_set(skb, nf_ct_tuplehash_to_ctrack(h), IP_CT_RELATED);
    }
}

/* ICMP Echo의 conntrack:
 * - Echo Request/Reply는 별도의 ICMP 연결로 추적
 * - tuple: (src_ip, dst_ip, type, code, id)
 *   → id 필드가 TCP/UDP의 포트 역할
 * - timeout: net.netfilter.nf_conntrack_icmp_timeout (기본 30초)
 *
 * conntrack 확인:
 *   conntrack -L -p icmp
 *   → icmp     1 29s src=10.0.0.1 dst=10.0.0.2 type=8 code=0 id=1234
 *              src=10.0.0.2 dst=10.0.0.1 type=0 code=0 id=1234
 */

ICMP 디버깅과 모니터링

# 1. ICMP SNMP 통계 확인
cat /proc/net/snmp | grep Icmp
# InMsgs OutMsgs InErrors OutErrors InDestUnreachs OutDestUnreachs
# InTimeExcds OutTimeExcds InEchos OutEchos InEchoReps OutEchoReps ...

# 상세 ICMP 통계 (타입별 카운터)
cat /proc/net/snmp | grep IcmpMsg
# InType0  (Echo Reply 수신)
# InType3  (Dest Unreachable 수신)
# InType8  (Echo Request 수신)
# OutType0 (Echo Reply 전송)
# OutType3 (Dest Unreachable 전송)
# OutType11 (Time Exceeded 전송) 등

# 2. nstat으로 ICMP 카운터 모니터링 (증분 확인)
nstat -s | grep -i icmp
# IcmpInMsgs, IcmpOutMsgs, IcmpInDestUnreachs, ...
# IcmpOutRateLimitGlobal  ← rate limit으로 억제된 ICMP 수!
nstat -z | grep -i icmp   # 0인 카운터도 표시

# 3. tcpdump로 ICMP 패킷 캡처
tcpdump -ni eth0 icmp
# 특정 타입만: ICMP Dest Unreachable
tcpdump -ni eth0 'icmp[icmptype] == 3'
# ICMP Frag Needed (PMTUD)
tcpdump -ni eth0 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'
# ICMPv6 전체
tcpdump -ni eth0 icmp6

# 4. ftrace로 커널 ICMP 함수 추적
echo icmp_rcv > /sys/kernel/tracing/set_ftrace_filter
echo icmp_send >> /sys/kernel/tracing/set_ftrace_filter
echo icmp_echo >> /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
# → /sys/kernel/tracing/trace 에서 호출 확인

# 5. perf로 ICMP 관련 커널 이벤트
perf trace -e 'net:*icmp*' -- ping -c 3 10.0.0.1

# 6. dropwatch로 ICMP 드롭 위치 추적
dropwatch -l kas
# → icmp_rcv+0x... 에서 드롭 발생 시 원인 파악 가능

# 7. 현재 ICMP sysctl 설정 일괄 확인
sysctl -a 2>/dev/null | grep icmp
sysctl -a 2>/dev/null | grep pmtu
💡

ICMP 보안 모범 사례: 서버에서는 accept_redirects=0send_redirects=0을 설정하고, icmp_echo_ignore_broadcasts=1을 유지합니다. ping을 완전 차단(icmp_echo_ignore_all=1)하면 네트워크 진단이 불가능해지므로, 대신 nftables rate limit으로 적절히 제어하는 것이 권장됩니다. ICMP Destination Unreachable은 절대 차단하지 마세요 — PMTUD와 TCP 연결 관리에 필수적입니다.

LLC/SNAP (Logical Link Control)

LLC(IEEE 802.2)는 OSI 모델의 데이터 링크 계층 상위에 위치하며, SNAP(Sub-Network Access Protocol)과 함께 L2 프레임의 프로토콜 식별을 담당합니다.

IEEE 802.2 LLC 프레임 구조

/* Ethernet II vs IEEE 802.3 + LLC/SNAP */
/*
 * Ethernet II (DIX):
 *   [Dst MAC 6B][Src MAC 6B][EtherType 2B][Payload][FCS]
 *   EtherType ≥ 0x0600 → 프로토콜 식별 (0x0800=IPv4, 0x86DD=IPv6)
 *
 * IEEE 802.3 + LLC:
 *   [Dst MAC 6B][Src MAC 6B][Length 2B][LLC Header 3B][Payload][FCS]
 *   Length < 0x0600 → IEEE 802.3 프레임
 *   LLC Header: [DSAP 1B][SSAP 1B][Control 1~2B]
 *
 * IEEE 802.3 + LLC/SNAP:
 *   [Dst MAC][Src MAC][Length][LLC(AA:AA:03)][SNAP(OUI 3B + Type 2B)][Payload]
 *   DSAP=0xAA, SSAP=0xAA, Ctrl=0x03 → SNAP 식별
 *   OUI=00:00:00 + Type → EtherType와 동일한 프로토콜 식별
 */

/* include/linux/llc.h */
struct llc_snap_hdr {
    __u8  dsap;        /* Destination SAP (0xAA for SNAP) */
    __u8  ssap;        /* Source SAP (0xAA for SNAP) */
    __u8  ctrl;        /* Control (0x03 = UI frame) */
    __u8  oui[3];     /* Organization Unique Identifier */
    __be16 ethertype;  /* SNAP 프로토콜 타입 */
};

커널 LLC 구현

/* net/llc/ — LLC 서브시스템 */
/* llc_rcv(): LLC 프레임 수신 진입점
 *   → eth_type_trans()에서 skb->protocol = htons(ETH_P_802_2)로 설정
 *   → llc_rcv()에서 DSAP/SSAP에 따라 적절한 SAP으로 전달
 */

/* SAP (Service Access Point) 등록 */
struct llc_sap *sap = llc_sap_open(0xAA, NULL);
/* → DSAP 0xAA로 오는 프레임을 이 SAP에서 수신 */

/* LLC 사용 프로토콜:
 *   DSAP 0xFE: ISO CLNP (IS-IS 라우팅)
 *   DSAP 0x42: STP (Spanning Tree Protocol)
 *   DSAP 0xAA: SNAP (대부분의 상위 프로토콜)
 *   DSAP 0x06: IP over IEEE 802.2 (거의 미사용)
 */

/* STP (Spanning Tree Protocol) — 브리지 루프 방지 */
/* net/bridge/br_stp_bpdu.c */
/* BPDU 수신: LLC DSAP=0x42 → br_stp_rcv()
 *   → STP 상태 머신 업데이트 (BLOCKING, LISTENING, LEARNING, FORWARDING)
 */
프레임 유형EtherType/LengthLLC 헤더사용 사례
Ethernet II ≥ 0x0600 (EtherType) 없음 IPv4/IPv6, ARP 등 대부분의 현대 프로토콜
802.3 + LLC < 0x0600 (Length) DSAP/SSAP/Ctrl STP, NetBEUI, IPX (레거시)
802.3 + LLC/SNAP < 0x0600 (Length) AA:AA:03 + OUI + Type 802.11 (WiFi), AppleTalk, 일부 VLAN
/* net/ethernet/eth.c — 프레임 타입 판별 */
__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
{
    struct ethhdr *eth = (struct ethhdr *) skb->data;
    __be16 proto = eth->h_proto;

    if (ntohs(proto) >= ETH_P_802_3_MIN) {
        /* Ethernet II: EtherType ≥ 1536 */
        return proto;  /* 0x0800(IPv4), 0x86DD(IPv6), 0x0806(ARP) 등 */
    }

    /* IEEE 802.3: length 필드 → LLC/SNAP 검사 */
    if (skb_at_tc_ingress(skb))
        skb->protocol = eth->h_proto;
    else if (*(__be16 *)(skb->data) == htons(0xAAAA))
        return htons(ETH_P_SNAP);  /* LLC/SNAP */
    else
        return htons(ETH_P_802_2); /* 순수 LLC */
}

네트워크 Flooding 공격과 커널 방어

네트워크 Flooding은 대량의 패킷을 보내 시스템 자원을 고갈시키는 DoS 공격 유형입니다. Linux 커널은 다양한 계층에서 방어 메커니즘을 내장하고 있습니다.

SYN Flood 방어

TCP SYN Flood는 대량의 SYN 패킷으로 서버의 SYN 큐(반개방 연결 큐)를 포화시킵니다:

/* 커널 SYN Flood 방어 체계 */

/* 1단계: SYN 큐 관리 */
/* net.ipv4.tcp_max_syn_backlog = 4096
 *   SYN 큐 크기. 초과 시 새 SYN 거부 또는 SYN Cookie 발동
 *
 * 2단계: SYN Cookie (앞서 상세 설명)
 * net.ipv4.tcp_syncookies = 1
 *   큐 overflow 시 상태 저장 없이 SYN+ACK 응답
 *   → 서버 메모리 소비 없이 연결 수립 가능
 *
 * 3단계: SYN 재전송 제한
 * net.ipv4.tcp_synack_retries = 5
 *   SYN+ACK 재전송 횟수 (5 → ~63초 후 포기)
 *   → 공격 시 2~3으로 감소 권장
 *
 * 4단계: Netfilter rate limiting
 */

/* net/ipv4/tcp_input.c — SYN 수신 처리 */
int tcp_conn_request(struct request_sock_ops *rsk_ops,
                     const struct tcp_request_sock_ops *af_ops,
                     struct sock *sk, struct sk_buff *skb)
{
    /* SYN 큐 overflow 확인 */
    if (inet_csk_reqsk_queue_is_full(sk)) {
        if (!net->ipv4.sysctl_tcp_syncookies) {
            /* SYN Cookie 미활성 → SYN 드롭 */
            goto drop;
        }
        want_cookie = true;
        /* SYN Cookie 모드: request_sock 할당 없이 진행 */
    }
    /* ... */
}

UDP Flood 방어

/* UDP Flood: 닫힌 포트로 대량 UDP 전송 → ICMP 응답 생성 부하 */

/* 방어 1: ICMP Rate Limiting */
/* net.ipv4.icmp_ratelimit = 1000 (ms)
 *   → ICMP 에러 메시지 전송 빈도 제한 (기본 1초에 1번)
 * net.ipv4.icmp_ratemask = 6168
 *   → rate limit 적용 대상 ICMP 타입 마스크
 */

/* 방어 2: conntrack 기반 차단 */
/* iptables -A INPUT -p udp -m state --state NEW -m recent \
 *   --set --name UDP_FLOOD
 * iptables -A INPUT -p udp -m state --state NEW -m recent \
 *   --update --seconds 1 --hitcount 20 --name UDP_FLOOD -j DROP
 */

/* 방어 3: XDP 레벨 조기 드롭 (가장 효과적) */
/* BPF 프로그램에서 특정 패턴의 UDP 패킷을 XDP_DROP
 * → 네트워크 스택 진입 전 NIC 드라이버 수준에서 차단
 * → 수백만 pps 처리 가능 */

ICMP Flood 방어

/* ICMP Flood (Ping Flood / Smurf Attack) */

/* 방어 1: ICMP Echo 응답 비활성화 (극단적) */
/* net.ipv4.icmp_echo_ignore_all = 1
 *   → 모든 ping 무시 (모니터링 도구에 영향)
 */

/* 방어 2: 브로드캐스트 Echo 무시 (Smurf 공격 방어) */
/* net.ipv4.icmp_echo_ignore_broadcasts = 1  (기본 활성)
 *   → 브로드캐스트/멀티캐스트 ICMP Echo Request 무시
 *   → Smurf 증폭 공격의 반사체 역할 방지
 */

/* 방어 3: Rate Limiting */
/* net.ipv4.icmp_ratelimit = 1000
 * → ICMP 응답 전송 빈도 제한 */

/* 방어 4: bogus ICMP 응답 차단 */
/* net.ipv4.icmp_ignore_bogus_error_responses = 1  (기본 활성)
 *   → 비정상적인 ICMP 에러 응답 무시 (로그 오염 방지)
 */

ARP Flood / ARP Spoofing 방어

/* ARP Spoofing: 위조된 ARP Reply로 MAC-IP 매핑 변조 */
/* ARP Flood: 대량 ARP Request로 ARP 캐시 오염 */

/* 방어 1: ARP 필터링 */
/* net.ipv4.conf.all.arp_filter = 1
 *   → 수신 인터페이스의 IP와 매칭되는 ARP만 응답
 */

/* 방어 2: Reverse Path Filtering (BCP38) */
/* net.ipv4.conf.all.rp_filter = 1
 *   → 소스 IP의 역방향 라우팅 검증
 *   → IP 스푸핑 패킷 차단 (소스 IP 위변조 감지)
 *   0: 비활성, 1: strict, 2: loose
 */

/* 방어 3: Static ARP 엔트리 */
/* ip neigh add 10.0.0.1 lladdr aa:bb:cc:dd:ee:ff nud permanent dev eth0 */

/* 방어 4: Dynamic ARP Inspection (스위치 레벨)
 *   커널 측: ebtables로 ARP 필터링 가능
 *   ebtables -A INPUT --protocol arp --arp-ip-src ! 10.0.0.0/24 -j DROP
 */

종합 Flooding 방어 체계

계층방어 기법처리 위치성능
L2 (Driver/XDP) XDP BPF 프로그램 (XDP_DROP) NIC 드라이버 내부 수천만 pps 처리 가능
L3 (Netfilter) nftables/iptables rate limit, conntrack PREROUTING/INPUT 수백만 pps
L4 (TCP) SYN Cookie, SYN 큐 크기, 재전송 제한 TCP 스택 커널 내장
L4 (UDP) ICMP rate limit, conntrack, BPF UDP/ICMP 스택 커널 내장
Application SO_REUSEPORT + BPF steering, backpressure 소켓 계층 애플리케이션 의존
# 실시간 Flooding 탐지 및 모니터링

# 1. 인터페이스 패킷 카운터 모니터링
watch -n 1 'ethtool -S eth0 | grep -E "rx_packets|rx_dropped|rx_errors"'

# 2. conntrack 테이블 상태 확인
conntrack -C        # 현재 엔트리 수
conntrack -S        # 통계 (drop 카운터 확인)

# 3. SYN 큐 상태 확인
ss -tnl | awk '{print $2, $3, $4}'  # Recv-Q, Send-Q, Local Address
# Recv-Q가 큰 값이면 accept 큐 포화 의심

# 4. softnet_stat으로 드롭 확인
cat /proc/net/softnet_stat
# 두 번째 컬럼이 0이 아니면 → softirq 처리 과부하 (패킷 드롭)
# 세 번째 컬럼이 0이 아니면 → time_squeeze (budget 부족)

# 5. nftables rate limit 설정 예시
nft add rule inet filter input ip protocol icmp limit rate 10/second accept
nft add rule inet filter input ip protocol icmp drop
nft add rule inet filter input tcp flags syn limit rate 100/second accept
nft add rule inet filter input tcp flags syn drop

XDP 기반 방어의 중요성: 대규모 DDoS(수백Gbps)에서는 Netfilter 수준 방어로는 CPU가 포화됩니다. XDP는 NIC 드라이버 내에서(또는 HW offload로 NIC 자체에서) 패킷을 처리하므로, 커널 네트워크 스택 진입 전에 차단이 가능합니다. Cloudflare, Facebook 등이 XDP 기반 DDoS 방어를 실전에 적용하고 있습니다. 자세한 내용은 BPF/XDP 페이지를 참고하세요.

Netlink는 Linux 커널과 유저스페이스 프로세스, 또는 커널 서브시스템 간 통신을 위한 소켓 기반 IPC 메커니즘입니다. ioctl의 한계를 극복하기 위해 설계되었으며, 비동기 전이중(full-duplex) 통신, 멀티캐스트 그룹, 커널→유저 알림 등을 지원합니다.

User Space ip / ss / tc (iproute2) NetworkManager wpa_supplicant conntrackd nft / iptables udev / systemd (KOBJECT_UEVENT) Custom (libnl/libmnl) AF_NETLINK Socket Layer (net/netlink/af_netlink.c) Kernel Subsystems NETLINK_ROUTE rtnetlink (ip) NETLINK_NETFILTER nfnetlink NETLINK_GENERIC Generic Netlink NETLINK_KOBJECT _UEVENT (udev) NETLINK_XFRM IPSec / NETLINK_AUDIT unicast (1:1) + multicast group (1:N) 지원
프로토콜상수용도주요 사용자
NETLINK_ROUTE 0 라우팅, 링크, 주소, 이웃, 규칙, qdisc 관리 ip, tc, bridge (iproute2)
NETLINK_USERSOCK 2 유저-유저 통신용 (커널 미개입) 사용자 정의 데몬 간 IPC
NETLINK_FIREWALL 3 (레거시) 패킷을 유저스페이스로 전달 제거됨 → NFQUEUE 사용
NETLINK_SOCK_DIAG 4 소켓 진단 정보 조회 ss 명령어
NETLINK_NFLOG 5 Netfilter 패킷 로깅 ulogd2
NETLINK_XFRM 6 IPSec SA/SP 관리 ip xfrm, strongSwan
NETLINK_SELINUX 7 SELinux 이벤트 알림 audit
NETLINK_AUDIT 9 감사(audit) 메시지 전달 auditd
NETLINK_NETFILTER 12 Netfilter 설정 (nftables, conntrack, NFQUEUE) nft, conntrack, iptables
NETLINK_KOBJECT_UEVENT 15 디바이스 핫플러그 이벤트 udev, systemd-udevd
NETLINK_GENERIC 16 범용 확장 프레임워크 (동적 패밀리 등록) nl80211, taskstats, devlink 등
NETLINK_CRYPTO 21 Crypto API 알고리즘 정보 조회 커널 암호화 서브시스템
/* include/uapi/linux/netlink.h */

/* Netlink 메시지 = nlmsghdr + payload (+ padding) */
/*
 * ┌─────────────────────────────────────────┐
 * │  nlmsghdr (16바이트)                     │
 * │  ┌─────────────┬──────────┬──────────┐  │
 * │  │ nlmsg_len   │ nlmsg_type│nlmsg_flags│ │
 * │  │ nlmsg_seq   │ nlmsg_pid            │  │
 * │  └─────────────┴──────────┴──────────┘  │
 * ├─────────────────────────────────────────┤
 * │  Payload (프로토콜별 구조)               │
 * │  ┌───────────────────────┐              │
 * │  │  Protocol Header      │              │
 * │  │  (ifinfomsg, rtmsg 등) │              │
 * │  ├───────────────────────┤              │
 * │  │  Attributes (NLA TLV)  │              │
 * │  │  [nla_len|nla_type|data|pad] ...     │
 * │  └───────────────────────┘              │
 * └─────────────────────────────────────────┘
 */

struct nlmsghdr {
    __u32  nlmsg_len;    /* 메시지 전체 길이 (헤더 포함) */
    __u16  nlmsg_type;   /* 메시지 타입 (RTM_NEWLINK 등) */
    __u16  nlmsg_flags;  /* 플래그 (NLM_F_REQUEST, NLM_F_DUMP 등) */
    __u32  nlmsg_seq;    /* 시퀀스 번호 (요청-응답 매칭) */
    __u32  nlmsg_pid;    /* 포트 ID (보통 프로세스 PID) */
};

/* 주요 플래그 */
#define NLM_F_REQUEST   0x01   /* 요청 메시지 (필수) */
#define NLM_F_MULTI     0x02   /* 멀티파트 응답 (NLMSG_DONE으로 종료) */
#define NLM_F_ACK       0x04   /* ACK 응답 요청 */
#define NLM_F_DUMP      0x300  /* 전체 덤프 요청 (NLM_F_ROOT|NLM_F_MATCH) */
#define NLM_F_CREATE    0x400  /* 새 객체 생성 (NEW 타입과 함께) */
#define NLM_F_EXCL      0x200  /* 이미 존재하면 에러 */
#define NLM_F_REPLACE   0x100  /* 기존 객체 대체 */
#define NLM_F_APPEND    0x800  /* 리스트 끝에 추가 */

/* Netlink Attribute (NLA) — TLV 형식 */
struct nlattr {
    __u16  nla_len;      /* 속성 길이 (헤더 + 데이터) */
    __u16  nla_type;     /* 속성 타입 */
    /* 데이터가 바로 뒤따름 (4바이트 정렬 패딩) */
};
/* include/net/netlink.h — 커널 NLA 유틸리티 */

/* 속성 추가 (커널 → 유저 응답 구성) */
nla_put_u32(skb, IFLA_MTU, dev->mtu);       /* u32 속성 */
nla_put_string(skb, IFLA_IFNAME, dev->name); /* 문자열 속성 */
nla_put(skb, IFLA_ADDRESS, dev->addr_len, dev->dev_addr); /* 바이너리 */

/* 중첩 속성 (nested attribute) */
struct nlattr *nest = nla_nest_start(skb, IFLA_AF_SPEC);
nla_put_u32(skb, IFLA_INET_CONF, val);
nla_nest_end(skb, nest);

/* 속성 파싱 (유저 → 커널 요청 파싱) */
static const struct nla_policy my_policy[MY_ATTR_MAX + 1] = {
    [MY_ATTR_NAME]  = { .type = NLA_NUL_STRING, .len = 64 },
    [MY_ATTR_VALUE] = { .type = NLA_U32 },
    [MY_ATTR_ADDR]  = { .type = NLA_BINARY, .len = ETH_ALEN },
    [MY_ATTR_FLAGS] = { .type = NLA_BITFIELD32,
                        .validation_type = NLA_VALIDATE_FUNCTION,
                        .validate = my_validate_flags },
    [MY_ATTR_NESTED] = { .type = NLA_NESTED },
};

/* nla_parse: 속성 배열로 파싱 */
struct nlattr *tb[MY_ATTR_MAX + 1];
nla_parse(tb, MY_ATTR_MAX, nlmsg_attrdata(nlh, hdrlen),
          nlmsg_attrlen(nlh, hdrlen), my_policy, extack);

if (tb[MY_ATTR_NAME])
    name = nla_get_string(tb[MY_ATTR_NAME]);
if (tb[MY_ATTR_VALUE])
    val = nla_get_u32(tb[MY_ATTR_VALUE]);

nla_policy는 커널 5.2+에서 strict validation이 기본입니다. 알려지지 않은 속성, 길이 초과, 타입 불일치 등이 자동으로 거부되어 보안이 강화되었습니다. NL_VALIDATE_STRICT 플래그가 Generic Netlink에서는 자동 적용되며, rtnetlink에서는 NL_SET_ERR_MSG로 상세 에러를 extack에 기록합니다.

가장 널리 사용되는 Netlink 패밀리입니다. ip 명령어의 모든 기능이 rtnetlink를 통해 구현됩니다:

메시지 타입동작대상 객체ip 명령어
RTM_NEWLINK / DELLINK / GETLINK생성/삭제/조회네트워크 인터페이스ip link
RTM_NEWADDR / DELADDR / GETADDR생성/삭제/조회IP 주소ip addr
RTM_NEWROUTE / DELROUTE / GETROUTE생성/삭제/조회라우팅 엔트리ip route
RTM_NEWNEIGH / DELNEIGH / GETNEIGH생성/삭제/조회이웃 (ARP/NDP)ip neigh
RTM_NEWRULE / DELRULE / GETRULE생성/삭제/조회Policy Routing 규칙ip rule
RTM_NEWQDISC / DELQDISC / GETQDISC생성/삭제/조회TC qdisctc qdisc
RTM_NEWTCLASS / DELTCLASS / GETTCLASS생성/삭제/조회TC 클래스tc class
RTM_NEWTFILTER / DELTFILTER / GETTFILTER생성/삭제/조회TC 필터tc filter
RTM_NEWNETCONF / GETNETCONF설정/조회인터페이스 네트워크 설정ip netconf
RTM_NEWNEXTHOP / DELNEXTHOP / GETNEXTHOP생성/삭제/조회Nexthop 객체 (5.3+)ip nexthop
/* rtnetlink 프로토콜 헤더 예시 */

/* 인터페이스 정보 (RTM_*LINK) */
struct ifinfomsg {
    unsigned char  ifi_family;  /* AF_UNSPEC */
    unsigned short ifi_type;    /* ARPHRD_ETHER 등 */
    int            ifi_index;   /* 인터페이스 인덱스 */
    unsigned       ifi_flags;   /* IFF_UP, IFF_RUNNING 등 */
    unsigned       ifi_change;  /* 변경된 플래그 마스크 */
};

/* 라우팅 엔트리 (RTM_*ROUTE) */
struct rtmsg {
    unsigned char  rtm_family;   /* AF_INET / AF_INET6 */
    unsigned char  rtm_dst_len;  /* 목적지 프리픽스 길이 */
    unsigned char  rtm_src_len;  /* 소스 프리픽스 길이 */
    unsigned char  rtm_tos;      /* TOS 필터 */
    unsigned char  rtm_table;    /* 라우팅 테이블 ID */
    unsigned char  rtm_protocol; /* 출처: RTPROT_BOOT, RTPROT_STATIC 등 */
    unsigned char  rtm_scope;    /* RT_SCOPE_UNIVERSE, LINK, HOST */
    unsigned char  rtm_type;     /* RTN_UNICAST, BLACKHOLE, UNREACHABLE */
    unsigned       rtm_flags;
};

/* IP 주소 (RTM_*ADDR) */
struct ifaddrmsg {
    __u8  ifa_family;    /* AF_INET / AF_INET6 */
    __u8  ifa_prefixlen; /* 프리픽스 길이 (/24 등) */
    __u8  ifa_flags;     /* IFA_F_PERMANENT 등 */
    __u8  ifa_scope;     /* RT_SCOPE_UNIVERSE 등 */
    __u32 ifa_index;     /* 인터페이스 인덱스 */
};

커널은 상태 변화 시 Netlink 멀티캐스트 그룹에 이벤트를 브로드캐스트합니다. 유저스페이스가 특정 그룹에 가입하면 실시간 알림을 수신합니다:

/* rtnetlink 멀티캐스트 그룹 (include/uapi/linux/rtnetlink.h) */
#define RTNLGRP_LINK       1   /* 인터페이스 up/down, 생성/삭제 */
#define RTNLGRP_NEIGH      3   /* ARP/NDP 테이블 변경 */
#define RTNLGRP_IPV4_IFADDR 5  /* IPv4 주소 추가/삭제 */
#define RTNLGRP_IPV4_ROUTE  7  /* IPv4 라우팅 테이블 변경 */
#define RTNLGRP_IPV6_IFADDR 9  /* IPv6 주소 추가/삭제 */
#define RTNLGRP_IPV6_ROUTE  11 /* IPv6 라우팅 테이블 변경 */
#define RTNLGRP_IPV4_RULE   8  /* Policy Routing 규칙 변경 */

/* 유저스페이스: 멀티캐스트 그룹에 가입하여 이벤트 수신 */
struct sockaddr_nl sa = {
    .nl_family = AF_NETLINK,
    .nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV4_ROUTE,
};
int fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
bind(fd, (struct sockaddr *)&sa, sizeof(sa));

/* 이벤트 수신 루프 */
char buf[8192];
while (1) {
    int len = recv(fd, buf, sizeof(buf), 0);
    struct nlmsghdr *nlh = (struct nlmsghdr *)buf;
    for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
        switch (nlh->nlmsg_type) {
        case RTM_NEWLINK:
        case RTM_DELLINK:
            /* 인터페이스 변경 이벤트 처리 */
            break;
        case RTM_NEWADDR:
        case RTM_DELADDR:
            /* IP 주소 변경 이벤트 처리 */
            break;
        }
    }
}

/* 커널 측: 이벤트 브로드캐스트 */
/* net/core/rtnetlink.c */
rtmsg_ifinfo(RTM_NEWLINK, dev, 0, GFP_KERNEL);
/* → 모든 RTNLGRP_LINK 구독자에게 인터페이스 정보 전달 */
/* 예제: 모든 네트워크 인터페이스 목록 조회 (ip link show) */
#include <linux/netlink.h>
#include <linux/rtnetlink.h>

int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

/* 요청 메시지 구성 */
struct {
    struct nlmsghdr nlh;
    struct ifinfomsg ifm;
} req = {
    .nlh = {
        .nlmsg_len   = NLMSG_LENGTH(sizeof(struct ifinfomsg)),
        .nlmsg_type  = RTM_GETLINK,
        .nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP,  /* 전체 덤프 */
        .nlmsg_seq   = 1,
    },
    .ifm = {
        .ifi_family = AF_UNSPEC,  /* 모든 패밀리 */
    },
};

send(fd, &req, req.nlh.nlmsg_len, 0);

/* 응답 수신: 멀티파트 메시지 (NLM_F_MULTI) */
char buf[32768];
int done = 0;
while (!done) {
    int len = recv(fd, buf, sizeof(buf), 0);
    struct nlmsghdr *nlh = (struct nlmsghdr *)buf;

    for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
        if (nlh->nlmsg_type == NLMSG_DONE) {
            done = 1;
            break;
        }
        if (nlh->nlmsg_type == NLMSG_ERROR) {
            struct nlmsgerr *err = NLMSG_DATA(nlh);
            fprintf(stderr, "error: %d\n", err->error);
            break;
        }

        /* 인터페이스 정보 파싱 */
        struct ifinfomsg *ifi = NLMSG_DATA(nlh);
        struct rtattr *rta = IFLA_RTA(ifi);
        int rtalen = IFLA_PAYLOAD(nlh);

        for (; RTA_OK(rta, rtalen); rta = RTA_NEXT(rta, rtalen)) {
            if (rta->rta_type == IFLA_IFNAME)
                printf("[%d] %s flags=0x%x\n",
                       ifi->ifi_index,
                       (char *)RTA_DATA(rta),
                       ifi->ifi_flags);
        }
    }
}
/* 커널 모듈에서 rtnetlink 메시지 핸들러 등록 */
/* net/core/rtnetlink.c */

/* rtnl_register: rtnetlink 메시지 핸들러 등록 */
rtnl_register(PF_UNSPEC, RTM_GETLINK, rtnl_getlink,
              rtnl_dump_ifinfo, 0);
/*
 * 인자:
 *   PF_UNSPEC    — 프로토콜 패밀리 (AF_UNSPEC = 모든 패밀리)
 *   RTM_GETLINK  — 처리할 메시지 타입
 *   rtnl_getlink — 단일 조회 핸들러 (doit)
 *   rtnl_dump_ifinfo — 전체 덤프 핸들러 (dumpit)
 */

/* doit 핸들러 시그니처 */
static int rtnl_getlink(struct sk_buff *skb, struct nlmsghdr *nlh,
                        struct netlink_ext_ack *extack)
{
    /* nlh에서 요청 파싱 → 결과를 skb에 구성 → 응답 전송 */
    struct ifinfomsg *ifm = nlmsg_data(nlh);
    struct net_device *dev;

    dev = __dev_get_by_index(net, ifm->ifi_index);
    if (!dev) {
        NL_SET_ERR_MSG(extack, "Unknown interface index");
        return -ENODEV;
    }

    /* 응답 skb 할당 및 구성 */
    nskb = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
    rtnl_fill_ifinfo(nskb, dev, ...);
    return nlmsg_unicast(rtnl, nskb, portid);
}

/* dumpit 핸들러 — 전체 인터페이스 열거 */
static int rtnl_dump_ifinfo(struct sk_buff *skb,
                            struct netlink_callback *cb)
{
    /* cb->args[]로 상태 유지 (다음 호출에서 이어서 덤프) */
    struct net_device *dev;
    int idx = 0;

    for_each_netdev(net, dev) {
        if (idx < cb->args[0]) { idx++; continue; }
        if (rtnl_fill_ifinfo(skb, dev, ...) < 0)
            break;  /* skb 공간 부족 → 다음 호출에서 계속 */
        idx++;
    }
    cb->args[0] = idx;
    return skb->len;
}

/* 멀티캐스트 브로드캐스트 (커널 → 유저스페이스 알림) */
rtnl_notify(skb, net, portid, RTNLGRP_LINK, nlh, GFP_KERNEL);
/* → RTNLGRP_LINK 그룹에 가입한 모든 소켓에 전달 */

새로운 커널 서브시스템은 고정 Netlink 프로토콜 번호를 할당받을 필요 없이 Generic Netlink를 통해 동적으로 패밀리를 등록합니다. nl80211(WiFi), devlink, taskstats, thermal 등이 Generic Netlink를 사용합니다.

/* Generic Netlink 패밀리 등록 (커널 모듈) */
/* include/net/genetlink.h */

/* 1. 패밀리 정의 */
static struct genl_family my_genl_family = {
    .name     = "MY_GENL",         /* 유저가 이 이름으로 ID 조회 */
    .version  = 1,
    .maxattr  = MY_ATTR_MAX,
    .policy   = my_genl_policy,    /* NLA 검증 정책 */
    .module   = THIS_MODULE,
    .ops      = my_genl_ops,       /* 명령어 핸들러 목록 */
    .n_ops    = ARRAY_SIZE(my_genl_ops),
    .mcgrps   = my_genl_mcgrps,    /* 멀티캐스트 그룹 */
    .n_mcgrps = ARRAY_SIZE(my_genl_mcgrps),
};

/* 2. 명령어(ops) 정의 */
static const struct genl_split_ops my_genl_ops[] = {
    {
        .cmd    = MY_CMD_GET_INFO,
        .doit   = my_cmd_get_info,      /* 단일 조회 */
        .dumpit = my_cmd_dump_info,     /* 전체 덤프 */
        .flags  = GENL_CMD_CAP_DO | GENL_CMD_CAP_DUMP,
    },
    {
        .cmd    = MY_CMD_SET_CONFIG,
        .doit   = my_cmd_set_config,    /* 설정 변경 */
        .flags  = GENL_ADMIN_PERM,      /* CAP_NET_ADMIN 필요 */
    },
};

/* 3. 멀티캐스트 그룹 정의 */
static const struct genl_multicast_group my_genl_mcgrps[] = {
    { .name = "my_events" },
};

/* 4. doit 핸들러 구현 */
static int my_cmd_get_info(struct sk_buff *skb, struct genl_info *info)
{
    struct sk_buff *msg;
    void *hdr;

    /* 입력 속성 접근 */
    if (info->attrs[MY_ATTR_NAME])
        name = nla_get_string(info->attrs[MY_ATTR_NAME]);

    /* 응답 구성 */
    msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
    hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq,
                       &my_genl_family, 0, MY_CMD_GET_INFO);

    nla_put_string(msg, MY_ATTR_NAME, "result");
    nla_put_u32(msg, MY_ATTR_VALUE, 42);

    genlmsg_end(msg, hdr);
    return genlmsg_reply(msg, info);
}

/* 5. 이벤트 멀티캐스트 전송 */
static void my_send_event(struct net *net, u32 value)
{
    struct sk_buff *msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
    void *hdr = genlmsg_put(msg, 0, 0, &my_genl_family, 0, MY_CMD_EVENT);
    nla_put_u32(msg, MY_ATTR_VALUE, value);
    genlmsg_end(msg, hdr);
    genlmsg_multicast_netns(&my_genl_family, net, msg, 0,
                            0 /* mcgrp index */, GFP_KERNEL);
}

/* 6. 모듈 init/exit */
genl_register_family(&my_genl_family);   /* 등록 */
genl_unregister_family(&my_genl_family); /* 해제 */
패밀리 이름용도유저 도구커널 소스
nl80211 WiFi 설정 (SSID, 인증, 스캔, 채널) iw, wpa_supplicant net/wireless/nl80211.c
devlink 네트워크 디바이스 관리 (firmware, SR-IOV) devlink (iproute2) net/devlink/
taskstats 프로세스별 리소스 사용 통계 getdelays kernel/taskstats.c
ethtool NIC 설정 (속도, 기능, ring 크기) ethtool (5.6+) net/ethtool/netlink.c
thermal 열 관리 이벤트 systemd-thermald drivers/thermal/thermal_genl.c
team 링크 본딩 (teaming) teamd drivers/net/team/
wireguard WireGuard VPN 설정 wg drivers/net/wireguard/netlink.c
mptcp_pm MPTCP 경로 관리 ip mptcp net/mptcp/pm_netlink.c

ss 명령어의 백엔드입니다. netstat/proc/net/tcp 파일을 파싱하는 것과 달리, ss는 SOCK_DIAG Netlink로 커널에 직접 쿼리하여 훨씬 빠릅니다:

/* include/uapi/linux/sock_diag.h */
struct sock_diag_req {
    __u8  sdiag_family;    /* AF_INET, AF_INET6, AF_UNIX */
    __u8  sdiag_protocol;  /* IPPROTO_TCP, IPPROTO_UDP */
};

/* TCP 소켓 진단 요청 (ss -tn의 내부 동작) */
struct inet_diag_req_v2 {
    __u8  sdiag_family;
    __u8  sdiag_protocol;
    __u8  idiag_ext;       /* 확장 정보 요청 (메모리, 혼잡 제어 등) */
    __u8  pad;
    __u32 idiag_states;    /* 조회할 TCP 상태 비트마스크 */
    struct inet_diag_sockid id; /* 필터 (소스/목적지 IP+포트) */
};

/* 확장 정보 플래그 */
#define INET_DIAG_MEMINFO  1  /* 소켓 메모리 사용량 */
#define INET_DIAG_INFO     2  /* TCP info (cwnd, rtt, mss 등) */
#define INET_DIAG_CONG     4  /* 혼잡 제어 알고리즘 이름 */
#define INET_DIAG_TOS      5  /* IP TOS */
#define INET_DIAG_SKMEMINFO 8 /* sk_meminfo (rmem, wmem, fwd, drop) */

/* 사용 예: ss -tnpi
 * → INET_DIAG_INFO로 tcp_info 조회 (cwnd, rtt, retrans 등)
 * → INET_DIAG_SKMEMINFO로 메모리 사용량 조회
 */
# ss 명령어 내부: NETLINK_SOCK_DIAG 사용
ss -tnp                # TCP 소켓 + 프로세스 정보
ss -tnpi               # + TCP 내부 정보 (cwnd, rtt, mss)
ss -tnpm               # + 메모리 사용량
ss state established   # ESTABLISHED 상태만 필터
ss 'sport = :80'       # 소스 포트 80 필터
ss -u state all        # UDP 소켓 전체 조회

# strace로 netlink 통신 확인
strace -e socket,sendmsg,recvmsg ss -tn 2>&1 | head -20
# socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC, NETLINK_SOCK_DIAG) = 3
# sendmsg(3, {msg_name={sa_family=AF_NETLINK, nl_pid=0, ...}, ...})
특성Netlinkioctlsysfs / procfs
통신 방향 양방향 (비동기, 전이중) 유저→커널 (동기, 반이중) 양방향 (read/write, 동기)
커널→유저 알림 멀티캐스트 이벤트 지원 불가 (폴링 필요) poll/inotify (제한적)
다중 수신자 멀티캐스트 그룹 1:1만 여러 프로세스 읽기 가능
메시지 크기 제한 없음 (멀티파트) 고정 크기 구조체 문자열 기반 (파싱 필요)
확장성 NLA 속성으로 유연한 확장 ABI 깨짐 위험 새 파일 추가
성능 (대량 조회) 뛰어남 (dump 모드) 각 호출마다 시스템콜 느림 (파일 읽기+파싱)
바이너리 호환 NLA 기반 전후 호환 구조체 변경 시 호환 깨짐 문자열이라 유연하지만 파싱 부담
적합 용도 네트워크 설정, 대량 데이터 조회 디바이스 제어 (레거시) 단순 설정, 디바이스 속성
💡

Netlink 설계 원칙: 새로운 커널-유저 인터페이스를 설계할 때는 ioctl보다 Generic Netlink를 권장합니다. NLA(Netlink Attribute) 기반 TLV 형식은 필드 추가/제거 시 ABI 호환성을 유지하기 쉽고, 멀티캐스트 이벤트로 상태 변화를 즉시 알릴 수 있습니다. ethtool도 5.6+부터 ioctl에서 Generic Netlink로 마이그레이션했습니다.

/* Netlink 소켓 버퍼 크기 — 대량 데이터 조회 시 중요 */
int rcvbuf = 1048576;  /* 1MB */
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
/* 기본 212992바이트는 수만 개의 라우팅 엔트리 덤프 시 부족
 * → ENOBUFS 에러 발생 → 데이터 유실
 * → SO_RCVBUFFORCE (CAP_NET_ADMIN)로 rmem_max 초과 설정 가능
 */

/* NETLINK_NO_ENOBUFS: 멀티캐스트 수신 시 버퍼 초과 에러 무시 */
int one = 1;
setsockopt(fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &one, sizeof(one));
/* 모니터링 용도: 일부 이벤트 유실보다 에러로 인한 중단이 더 문제
 * → 이 옵션으로 ENOBUFS 대신 메시지 드롭 (graceful degradation)
 */

/* NETLINK_CAP_ACK: ACK 메시지에서 원본 요청 페이로드 생략 */
setsockopt(fd, SOL_NETLINK, NETLINK_CAP_ACK, &one, sizeof(one));
/* 대량 요청 시 ACK 크기 감소 → 메모리/대역폭 절약 */

/* NETLINK_EXT_ACK: 에러 시 상세 메시지(extack) 수신 */
setsockopt(fd, SOL_NETLINK, NETLINK_EXT_ACK, &one, sizeof(one));
/* 에러 응답에 NL_SET_ERR_MSG()로 설정된 텍스트 포함
 * 예: "Nexthop has invalid gateway" → 디버깅 시 매우 유용
 */
# Netlink 메시지 추적

# 1. ip 명령어의 디버그 모드 (실제 Netlink 메시지 덤프)
ip -d -d link show       # 상세 속성 출력
strace -e sendmsg,recvmsg ip route show 2>&1  # syscall 수준 추적

# 2. nlmon: Netlink 트래픽 캡처 (커널 4.0+)
ip link add nlmon0 type nlmon          # Netlink 모니터 인터페이스 생성
ip link set nlmon0 up
tcpdump -i nlmon0 -w netlink.pcap      # Wireshark에서 분석 가능

# 3. ip monitor: 실시간 Netlink 이벤트 모니터링
ip monitor all            # 모든 rtnetlink 이벤트
ip monitor link           # 인터페이스 변경만
ip monitor route          # 라우팅 변경만
ip monitor neigh          # ARP/NDP 변경만

# 4. conntrack 이벤트 모니터링 (NETLINK_NETFILTER)
conntrack -E              # conntrack 이벤트 실시간 출력

# 5. udevadm 모니터링 (NETLINK_KOBJECT_UEVENT)
udevadm monitor --kernel  # 커널 uevent 실시간 출력

# 6. Generic Netlink 패밀리 목록
genl ctrl list            # 등록된 모든 genl 패밀리 표시 (iproute2)
# 또는
python3 -c "
import socket, struct
s = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, 16)
# NETLINK_GENERIC=16, CTRL_CMD_GETFAMILY dump
"

Netlink 보안 고려사항:

  • 권한 검사: 대부분의 Netlink 쓰기 작업은 CAP_NET_ADMIN이 필요합니다. 읽기(DUMP)는 일부 패밀리에서 비특권 접근을 허용하지만, 5.10+에서 SOCK_DIAG 등은 네트워크 네임스페이스 격리를 강화했습니다.
  • 네임스페이스 격리: Netlink 메시지는 네트워크 네임스페이스 경계 내에서만 동작합니다. 컨테이너의 Netlink 소켓은 해당 netns의 인터페이스/라우팅만 접근 가능합니다.
  • 메시지 검증: 커널 측에서 nla_policy strict validation을 반드시 사용하세요. 유저스페이스에서 조작된 NLA를 통한 커널 메모리 corruption이 과거 다수의 CVE 원인이었습니다.

Unix Domain Socket 심화

Unix domain socket(UDS)은 동일 호스트 내 프로세스 간 통신(IPC)을 위한 소켓 인터페이스입니다. 네트워크 스택(IP, TCP/UDP 헤더, 체크섬, 라우팅)을 완전히 우회하여 커널 내부에서 직접 메모리를 복사하므로, localhost TCP/UDP 대비 현저히 높은 성능을 제공합니다. D-Bus, systemd, X11/Wayland, Docker, PostgreSQL, MySQL, nginx 등 대부분의 주요 리눅스 서비스가 UDS를 IPC 기본 수단으로 사용합니다.

주소 체계와 소켓 타입

/* include/uapi/linux/un.h */
struct sockaddr_un {
    __kernel_sa_family_t sun_family;   /* AF_UNIX (= AF_LOCAL = 1) */
    char                 sun_path[108]; /* 경로명 또는 추상 이름 */
};
/* sun_path 최대 108바이트 — 이 제한은 BSD 호환성에서 유래
 * 절대/상대 경로 + NUL 종단 → 실제 사용 가능 107자
 * Abstract namespace: sun_path[0] = '\0', NUL 종단 불필요 → 108바이트 전체 사용 가능 */

/* 지원되는 소켓 타입 */
socket(AF_UNIX, SOCK_STREAM, 0);    /* 연결 지향 바이트 스트림 (TCP 유사) */
socket(AF_UNIX, SOCK_DGRAM, 0);     /* 비연결 데이터그램 (UDP 유사, 경계 보존) */
socket(AF_UNIX, SOCK_SEQPACKET, 0); /* 연결 지향 + 메시지 경계 보존 (커널 2.6.4+) */

세 가지 주소 유형이 존재합니다:

유형sun_path 형식파일시스템 가시성특징
Pathname /var/run/app.sock inode 생성됨 (S_IFSOCK) bind 시 파일 생성, unlink 필요, 파일 권한으로 접근 제어
Abstract \0app-socket 파일시스템에 없음 Linux 전용, 자동 정리, 네임스페이스 격리 (커널 5.1+)
Unnamed (없음) 파일시스템에 없음 socketpair()로 생성, 부모-자식 프로세스 간 사용

커널 내부 구조체

/* net/unix/af_unix.c, include/net/af_unix.h */

struct unix_sock {
    struct sock         sk;            /* 기본 소켓 (sock 임베딩) */
    struct unix_address *addr;          /* bind된 주소 */
    struct path         path;           /* pathname 소켓의 dentry/vfsmount */
    struct mutex        iolock;         /* 단일 reader 직렬화 */
    struct sock         *peer;          /* 연결된 상대 소켓 */
    struct sock         *other;         /* DGRAM: 연결 대상 */
    struct list_head    link;           /* 글로벌 Unix 소켓 리스트 */
    atomic_long_t       inflight;       /* SCM_RIGHTS로 전송 중인 fd 수 */
    spinlock_t          lock;           /* 상태 보호 */
    unsigned long       gc_flags;       /* GC 마킹 플래그 */
#define UNIX_GC_CANDIDATE   0
#define UNIX_GC_MAYBE_CYCLE 1
    struct socket_wq    peer_wq;        /* 피어 대기 큐 */
    wait_queue_entry_t  peer_wake;      /* 피어 깨우기 */
    struct scm_stat     scm_stat;       /* SCM 통계 */
};

struct unix_address {
    refcount_t          refcnt;         /* 참조 카운트 */
    int                 len;            /* sockaddr_un 전체 길이 */
    struct sockaddr_un  name[];         /* 가변 길이 주소 (flexible array) */
};

/* Unix 소켓 해시 테이블 — pathname과 abstract 각각 별도 */
struct unix_table {
    spinlock_t          *locks;
    struct hlist_head    *buckets;
    int                 mask;
};
/* net->unx.table: 네트워크 네임스페이스별 해시 테이블 (커널 5.1+)
 * 이전에는 글로벌 해시 테이블이었으나, 네임스페이스 격리를 위해 per-netns로 변경 */

데이터 전송 메커니즘

UDS의 핵심 성능 이점은 네트워크 스택을 우회하는 커널 내 직접 메모리 복사입니다.

/* net/unix/af_unix.c — SOCK_STREAM 전송 경로 (간략화) */
static int unix_stream_sendmsg(struct socket *sock,
                               struct msghdr *msg, size_t len)
{
    struct sock *sk = sock->sk;
    struct sock *other = unix_peer(sk);   /* 연결된 피어 */
    struct sk_buff *skb;

    /* 1. SCM (Socket Control Message) 처리 — fd 전달, 자격증명 등 */
    err = scm_send(sock, msg, &scm, false);

    while (sent < len) {
        size = min_t(int, len - sent, SKB_MAX_ORDER(0, 0));

        /* 2. skb 할당 — 네트워크 헤더 공간 불필요 */
        skb = sock_alloc_send_pskb(sk, 0, size, msg->msg_flags & MSG_DONTWAIT, &err, 0);

        /* 3. 유저스페이스 → skb로 데이터 복사 */
        err = skb_copy_datagram_from_iter(skb, 0, &msg->msg_iter, size);

        /* 4. 피어의 수신 큐에 직접 enqueue */
        skb_queue_tail(&other->sk_receive_queue, skb);

        /* 5. 피어 깨우기 */
        unix_state_lock(other);
        other->sk_data_ready(other);     /* epoll/poll/select 통지 */
        unix_state_unlock(other);

        sent += size;
    }
    return sent;
}
/*
 * TCP/UDP와의 핵심 차이:
 * - IP 헤더/체크섬/라우팅 테이블 lookup 없음
 * - sk_buff에 네트워크 헤더 공간 예약 없음 (headroom 최소화)
 * - 송신자의 skb가 수신자 큐에 직접 들어감 (제로 라우팅)
 * - congestion control, flow control 없음 (sk_sndbuf/sk_rcvbuf로만 제어)
 */
💡

SOCK_STREAM vs SOCK_SEQPACKET: 두 타입 모두 연결 지향이지만, SOCK_STREAM은 바이트 스트림(경계 없음)이고 SOCK_SEQPACKET은 메시지 경계를 보존합니다. 프로토콜 프레이밍이 필요한 IPC에서는 SOCK_SEQPACKET이 파싱 로직을 단순화합니다. SOCK_DGRAM은 비연결이며 메시지 경계를 보존하지만, 수신 버퍼 초과 시 메시지가 드롭될 수 있습니다 (TCP처럼 재전송하지 않음).

SCM_RIGHTS — 파일 디스크립터 전달

Unix domain socket의 가장 강력한 기능은 프로세스 간 파일 디스크립터 전달입니다. 이는 ancillary data(보조 데이터)를 통해 구현되며, 커널이 송신 프로세스의 fd를 수신 프로세스의 fd 테이블에 복제합니다.

/* SCM_RIGHTS: 파일 디스크립터 전송 — 송신 측 */
int send_fd(int sock_fd, int fd_to_send)
{
    struct msghdr msg = {};
    struct cmsghdr *cmsg;
    char buf[CMSG_SPACE(sizeof(int))];
    char dummy = 'x';               /* 최소 1바이트 데이터 필수 */
    struct iovec iov = { .iov_base = &dummy, .iov_len = 1 };

    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_SOCKET;
    cmsg->cmsg_type  = SCM_RIGHTS;           /* fd 전달 타입 */
    cmsg->cmsg_len   = CMSG_LEN(sizeof(int));
    *(int *)CMSG_DATA(cmsg) = fd_to_send;

    return sendmsg(sock_fd, &msg, 0);
}

/* SCM_RIGHTS: 파일 디스크립터 수신 */
int recv_fd(int sock_fd)
{
    struct msghdr msg = {};
    struct cmsghdr *cmsg;
    char buf[CMSG_SPACE(sizeof(int))];
    char dummy;
    struct iovec iov = { .iov_base = &dummy, .iov_len = 1 };

    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = buf;
    msg.msg_controllen = sizeof(buf);

    recvmsg(sock_fd, &msg, 0);

    cmsg = CMSG_FIRSTHDR(&msg);
    if (cmsg && cmsg->cmsg_level == SOL_SOCKET
             && cmsg->cmsg_type == SCM_RIGHTS) {
        return *(int *)CMSG_DATA(cmsg);   /* 수신된 fd (새 번호 할당됨) */
    }
    return -1;
}
/* 커널 내부: SCM_RIGHTS 처리 경로 (net/core/scm.c) */

/* 송신 시: fd → struct file* 변환 후 skb에 부착 */
int __scm_send(struct socket *sock, struct msghdr *msg,
               struct scm_cookie *scm)
{
    /* cmsg 타입별 분기 */
    case SCM_RIGHTS:
        /* fget()으로 fd → struct file* 변환, refcount 증가 */
        err = scm_fp_copy(cmsg, &scm->fp);
        /* scm->fp->fp[i]에 struct file* 포인터 배열 저장 */
        break;
}

/* 수신 시: struct file* → 수신자 fd 테이블에 설치 */
void scm_detach_fds(struct msghdr *msg, struct scm_cookie *scm)
{
    for (i = 0; i < fdmax; i++) {
        /* 수신 프로세스의 fd 테이블에서 빈 슬롯 할당 */
        new_fd = get_unused_fd_flags(MSG_CMSG_CLOEXEC);
        /* struct file*을 새 fd에 설치 */
        fd_install(new_fd, get_file(fp[i]));
    }
}
/* 핵심: fd 번호 자체가 전달되는 것이 아님!
 * 커널이 struct file*을 공유하며, 수신자에게 새 fd 번호를 할당
 * → 같은 파일을 가리키는 서로 다른 fd가 두 프로세스에 존재 */

SCM_RIGHTS fd 누수 주의:

  • recvmsg()로 수신한 fd는 반드시 close()해야 합니다. 수신만 하고 CMSG를 파싱하지 않으면 커널이 fd를 수신자 테이블에 설치하지만 애플리케이션이 그 번호를 모르게 되어 fd leak이 발생합니다.
  • MSG_PEEK으로 SCM_RIGHTS 메시지를 peek하면 fd가 매번 새로 설치됩니다. 반복 peek = fd 누수.
  • 전송 중인 fd(inflight)가 순환 참조를 만들 수 있어 커널에 Unix GC(unix_gc())가 존재합니다.

SCM_CREDENTIALS — 프로세스 자격증명 전달

/* include/uapi/linux/socket.h */
struct ucred {
    __u32 pid;    /* 송신자 PID */
    __u32 uid;    /* 송신자 UID */
    __u32 gid;    /* 송신자 GID */
};

/* 송신 측: SO_PASSCRED 옵션 활성화 필요 */
int optval = 1;
setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof(optval));

/* 수신 측: ancillary data로 자격증명 수신 */
struct ucred *cred;
cmsg = CMSG_FIRSTHDR(&msg);
while (cmsg) {
    if (cmsg->cmsg_level == SOL_SOCKET &&
        cmsg->cmsg_type == SCM_CREDENTIALS) {
        cred = (struct ucred *)CMSG_DATA(cmsg);
        printf("pid=%d uid=%d gid=%d\n", cred->pid, cred->uid, cred->gid);
    }
    cmsg = CMSG_NXTHDR(&msg, cmsg);
}

/* 커널 내부: 자격증명 검증 (net/core/scm.c)
 *
 * - 비특권 프로세스는 자신의 실제 PID/UID/GID만 전송 가능
 * - root(CAP_SYS_ADMIN)는 임의의 자격증명을 위조할 수 있음
 * - SO_PEERCRED: connect/accept 시점의 자격증명 (getsockopt으로 조회)
 * - SCM_CREDENTIALS: 매 메시지마다 자격증명 전달 (더 세밀한 제어)
 *
 * D-Bus, systemd, polkit 등이 클라이언트 인증에 SO_PEERCRED 사용
 */
/* SO_PEERCRED: 연결 시점 자격증명 조회 (connect/accept 시 커널이 자동 기록) */
struct ucred peer;
socklen_t len = sizeof(peer);
getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED, &peer, &len);
/* peer.pid, peer.uid, peer.gid 사용 가능
 * DGRAM 소켓에서는 connect()를 호출한 후에만 유효 */

/* SO_PEERPIDFD (커널 6.5+): PID 대신 pidfd 획득 — PID 재사용 문제 방지 */
int pidfd;
socklen_t len = sizeof(pidfd);
getsockopt(client_fd, SOL_SOCKET, SO_PEERPIDFD, &pidfd, &len);
/* pidfd는 프로세스가 종료되어도 유효한 핸들을 유지 — 보안 인증에 더 안전 */

Abstract Namespace 심화

/* Abstract namespace: sun_path[0] = '\0'으로 시작 (Linux 전용) */
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
addr.sun_path[0] = '\0';                           /* abstract 마커 */
memcpy(addr.sun_path + 1, "my-service", 10);

/* bind 시 addrlen이 중요 — NUL 종단이 아닌 길이로 이름 결정 */
socklen_t addrlen = offsetof(struct sockaddr_un, sun_path)
                  + 1 + 10;  /* '\0' + "my-service" */
bind(fd, (struct sockaddr *)&addr, addrlen);

/*
 * Abstract namespace 특성:
 * - 파일시스템에 파일이 생성되지 않음 → unlink() 불필요
 * - 소켓을 닫으면 이름이 자동으로 해제됨
 * - 파일 권한 기반 접근 제어 불가 → 보안에 주의
 * - 커널 5.1 이전: 모든 네트워크 네임스페이스에서 동일 이름 접근 가능 (보안 문제)
 * - 커널 5.1+: 네트워크 네임스페이스별 격리 (컨테이너 보안 강화)
 *
 * Android는 abstract namespace를 광범위하게 사용 (Zygote, SurfaceFlinger 등)
 * → SELinux로 접근 제어 보완
 */

Pathname vs Abstract 선택 기준:

  • Pathname: 파일 권한(chmod/chown)으로 접근 제어가 필요할 때, 다른 유닉스 시스템과 호환이 필요할 때
  • Abstract: 파일시스템 정리가 번거로울 때(크래시 후 stale 소켓 파일 문제 방지), 컨테이너 내부 IPC에서, 네임스페이스 격리가 충분할 때 (5.1+)
Android Abstract Namespace 상세: Android는 abstract namespace UDS를 Zygote↔ActivityManager, SurfaceFlinger, logd 등 핵심 서비스 간 통신에 광범위하게 사용한다. 파일시스템에 소켓 파일이 남지 않아 관리가 간편하지만, 파일 권한으로 접근 제어할 수 없으므로 SELinux가 필수다. Android init이 .rc 파일에서 socket 지시어로 소켓을 생성하면, SELinux의 unix_stream_socket 클래스가 접근을 제어한다. 한편 Android의 주력 IPC는 Binder이며, UDS는 보조적 역할(로그, 트레이스, 디버그 등)에 주로 사용된다.

Unix Socket GC (Garbage Collection)

SCM_RIGHTS로 소켓 fd 자체를 다른 Unix 소켓을 통해 전달하면 순환 참조가 발생할 수 있습니다. 예: 소켓 A의 fd를 소켓 B로 전달, 소켓 B의 fd를 소켓 A로 전달. 이 경우 두 소켓 모두 참조 카운트가 0이 되지 않아 메모리 누수가 발생합니다.

/* net/unix/garbage.c — Unix socket GC
 *
 * Mark-and-sweep 알고리즘으로 inflight fd 순환 참조 탐지:
 *
 * 1단계 (Mark): 모든 inflight Unix 소켓을 후보(CANDIDATE)로 표시
 *   - inflight: SCM_RIGHTS로 전송되어 아직 수신되지 않은 fd
 *
 * 2단계 (Scan): 외부에서 접근 가능한 소켓을 후보에서 제외
 *   - 유저스페이스 fd 테이블에 참조가 있으면 → 외부 접근 가능
 *   - 외부 접근 가능한 소켓의 수신 큐에 있는 fd도 접근 가능
 *
 * 3단계 (Sweep): 여전히 CANDIDATE인 소켓 = 순환 참조 → 강제 정리
 *   - 수신 큐의 skb를 purge하여 inflight fd 해제
 *   - SKB_DROP_REASON_UNIX_GC로 드롭 카운트 기록
 */

void unix_gc(void)
{
    /* gc_in_progress 플래그로 동시 실행 방지 */
    if (gc_in_progress)
        return;
    gc_in_progress = true;

    /* Phase 1: inflight 소켓을 gc_candidates 리스트에 추가 */
    list_for_each_entry_safe(u, next, &gc_inflight_list, link) {
        struct sock *sk = &u->sk;
        total_refs = file_count(sk->sk_socket->file);
        inflight_refs = atomic_long_read(&u->inflight);

        /* 외부 참조 = 전체 참조 - inflight 참조 */
        if (total_refs == inflight_refs)
            __set_bit(UNIX_GC_CANDIDATE, &u->gc_flags);
    }

    /* Phase 2: 외부 도달 가능한 소켓에서 시작하여 전파 */
    scan_children(&not_cycle_list, inc_inflight_move_tail);

    /* Phase 3: 남은 candidates = 순환 → 수신 큐 purge */
    list_for_each_entry_safe(u, next, &gc_candidates, link) {
        skb_queue_purge(&u->sk.sk_receive_queue);
    }
    gc_in_progress = false;
}

GC 성능 영향: unix_gc()는 글로벌 unix_gc_lock을 잡고 모든 inflight 소켓을 스캔합니다. SCM_RIGHTS를 대량으로 사용하는 시스템(예: 컨테이너 런타임)에서 GC 빈도가 높아지면 성능 저하가 발생할 수 있습니다. 커널 6.10+에서는 개선된 unix_gc() 알고리즘이 도입되어 lock 경합이 줄었습니다.

socketpair()와 파이프 대체

/* socketpair(): 이미 연결된 소켓 쌍 생성 (unnamed socket) */
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
/* sv[0]과 sv[1]은 양방향 통신 가능 — pipe()는 단방향
 *
 * fork() 후 부모-자식 간 IPC에 이상적:
 *   부모: close(sv[1]), sv[0]으로 읽기/쓰기
 *   자식: close(sv[0]), sv[1]으로 읽기/쓰기
 *
 * pipe() 대비 장점:
 * - 양방향 (pipe는 단방향, 양방향 필요 시 2개 필요)
 * - SCM_RIGHTS/SCM_CREDENTIALS 사용 가능
 * - SOCK_SEQPACKET으로 메시지 경계 보존 가능
 * - shutdown(SHUT_WR)로 한 방향만 닫기 가능
 *
 * pipe() 대비 단점:
 * - splice()/vmsplice() 미지원 (파이프는 제로카피 가능)
 * - 약간 더 많은 메모리 사용 (struct sock 오버헤드)
 */

/* SOCK_SEQPACKET socketpair: 메시지 경계 보존 IPC */
socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, sv);
/* SOCK_CLOEXEC: exec 시 자동 close (fd leak 방지)
 * SOCK_NONBLOCK: 비차단 모드 (이벤트 루프 호환) */

성능 특성과 튜닝

항목Unix Domain SocketTCP loopbackpipe
지연시간 (64B msg) ~2-5 us ~8-15 us ~2-4 us
처리량 (bulk) ~8-12 GB/s ~4-6 GB/s ~10-14 GB/s
시스템콜 오버헤드 sendmsg/recvmsg sendmsg/recvmsg + TCP 처리 write/read (더 가벼움)
fd 전달 SCM_RIGHTS 불가 불가 (pidfd_getfd로 대체)
양방향 가능 가능 단방향
# Unix socket 버퍼 크기 튜닝
sysctl net.core.wmem_default        # 기본 송신 버퍼 (212992)
sysctl net.core.rmem_default        # 기본 수신 버퍼 (212992)
sysctl net.core.wmem_max            # 최대 송신 버퍼
sysctl net.core.rmem_max            # 최대 수신 버퍼

# 소켓별 버퍼 크기 설정
setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &size, sizeof(size));
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));

# Unix socket 전용 sysctl (커널 5.4+)
sysctl net.unix.max_dgram_qlen      # DGRAM 소켓 최대 큐 길이 (기본 10)
# 이 값이 작으면 DGRAM 전송 시 블로킹 또는 EAGAIN 발생 빈도 증가
/* 고성능 UDS 패턴: io_uring과 조합 (커널 5.6+)
 *
 * io_uring은 UDS의 sendmsg/recvmsg를 비동기로 처리 가능
 * → 시스템콜 오버헤드 제거 (SQ polling 모드)
 * → 대량의 소규모 메시지 처리에 특히 효과적
 */
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_sendmsg(sqe, sock_fd, &msg, 0);
sqe->flags |= IOSQE_ASYNC;             /* 강제 비동기 실행 */

/* 또는 io_uring_prep_send_zc() (커널 6.0+)
 * 제로카피 전송 — 대용량 데이터 전달 시 memcpy 제거 */

보안 고려사항

/* 1. Pathname 소켓 파일 권한 */
umask(0077);          /* bind 전에 umask 설정 → 소유자만 접근 */
bind(fd, ...);
chmod("/var/run/app.sock", 0660);  /* 소유자+그룹만 읽기/쓰기 */
/* 디렉토리 권한도 중요: /var/run/의 권한이 너무 느슨하면 무의미 */

/* 2. LSM (Linux Security Module) 후크 */
/*
 * SELinux, AppArmor 등은 Unix 소켓에 대해 세밀한 접근 제어 제공:
 *   - unix_stream_connect: STREAM 연결 허용/거부
 *   - unix_may_send: DGRAM 전송 허용/거부
 *   - unix_listen: listen() 허용/거부
 *
 * SELinux 정책 예:
 *   allow client_t server_t:unix_stream_socket connectto;
 */

/* 3. SO_PEERSEC: 피어의 보안 컨텍스트 조회 (SELinux) */
char secctx[256];
socklen_t len = sizeof(secctx);
getsockopt(fd, SOL_SOCKET, SO_PEERSEC, secctx, &len);
/* 예: "system_u:system_r:httpd_t:s0" */

/* 4. Abstract namespace 보안 한계 */
/*
 * - 파일 권한 없음 → 같은 네트워크 네임스페이스의 모든 프로세스가 접근 가능
 * - 커널 5.1 이전: 네트워크 네임스페이스 간 격리 없음 → 컨테이너 탈출 벡터
 * - 대응: SELinux/AppArmor로 보안 강화, 또는 pathname 소켓 + 파일 권한 사용
 */

프로토콜 연산 테이블

/* net/unix/af_unix.c — 소켓 타입별 proto_ops */

static const struct proto_ops unix_stream_ops = {
    .family     = PF_UNIX,
    .owner      = THIS_MODULE,
    .bind       = unix_bind,
    .connect    = unix_stream_connect,
    .accept     = unix_accept,
    .listen     = unix_listen,
    .sendmsg    = unix_stream_sendmsg,
    .recvmsg    = unix_stream_recvmsg,
    .poll       = unix_poll,
    .socketpair = unix_socketpair,
    .shutdown   = unix_shutdown,
    .ioctl      = unix_ioctl,
    .splice_read = unix_stream_splice_read,  /* splice 지원 (커널 5.1+) */
};

static const struct proto_ops unix_dgram_ops = {
    .family     = PF_UNIX,
    .bind       = unix_bind,
    .connect    = unix_dgram_connect,
    .accept     = sock_no_accept,          /* DGRAM은 accept 없음 */
    .listen     = sock_no_listen,          /* DGRAM은 listen 없음 */
    .sendmsg    = unix_dgram_sendmsg,
    .recvmsg    = unix_dgram_recvmsg,
    .poll       = unix_dgram_poll,
    .socketpair = unix_socketpair,
};

static const struct proto_ops unix_seqpacket_ops = {
    .family     = PF_UNIX,
    .bind       = unix_bind,
    .connect    = unix_stream_connect,      /* STREAM과 동일한 connect */
    .accept     = unix_accept,
    .listen     = unix_listen,
    .sendmsg    = unix_seqpacket_sendmsg,
    .recvmsg    = unix_seqpacket_recvmsg,   /* MSG_TRUNC으로 경계 감지 */
    .poll       = unix_poll,
    .socketpair = unix_socketpair,
};

/* unix_bind() 내부 — pathname vs abstract 분기 */
static int unix_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
    struct sockaddr_un *sunaddr = (struct sockaddr_un *)uaddr;

    if (sunaddr->sun_path[0]) {
        /* Pathname: 파일시스템에 소켓 inode 생성 */
        err = unix_bind_bsd(sock, sunaddr, addr_len);
        /* kern_path_create() → mknod(S_IFSOCK) → dentry 생성 */
    } else {
        /* Abstract: 해시 테이블에만 등록 (파일시스템 무관) */
        err = unix_bind_abstract(sock, sunaddr, addr_len);
        /* 해시 키: sun_path 전체 바이트 (길이 기반, NUL 포함) */
    }
}

디버깅과 모니터링

# 1. /proc/net/unix — 열린 Unix 소켓 목록
cat /proc/net/unix
# Num  RefCount Protocol Flags    Type St Inode Path
# 00000000  2     0      00010000 0001 01 12345 /var/run/docker.sock
#
# Type: 0001=STREAM, 0002=DGRAM, 0005=SEQPACKET
# St:   01=UNCONNECTED, 02=CONNECTING, 03=CONNECTED, 04=DISCONNECTING
# Flags: 00010000=ACC (listening)

# 2. ss 명령어 — 상세 Unix 소켓 정보
ss -xlnp                              # listening Unix 소켓 + PID
ss -xa                                # 모든 Unix 소켓 (연결 포함)
ss -x -e state connected              # 연결된 소켓만 + 확장 정보
ss -x --filter 'dst = /var/run/docker.sock'  # 특정 경로 필터

# 3. lsof — 소켓-프로세스 매핑
lsof -U                               # 모든 Unix 소켓과 프로세스
lsof /var/run/docker.sock             # 특정 소켓 파일의 사용자

# 4. SOCK_DIAG (Netlink) — 프로그래매틱 소켓 진단
# ss는 내부적으로 NETLINK_SOCK_DIAG를 사용
ss -xp -e -i                          # 확장 정보 + 내부 상태

# 5. strace로 UDS 통신 추적
strace -e trace=sendmsg,recvmsg,connect,bind -f -p <PID>

# 6. bpftrace로 Unix 소켓 이벤트 추적
bpftrace -e '
  kprobe:unix_stream_connect {
    printf("pid=%d comm=%s connecting\n", pid, comm);
  }
  kprobe:unix_stream_sendmsg {
    printf("pid=%d comm=%s sendmsg len=%d\n", pid, comm, arg2);
  }
'

# 7. Abstract namespace 소켓 확인 (@ 접두어로 표시)
ss -xlnp | grep '@'
# 예: u_str LISTEN 0  128  @/tmp/.X11-unix/X0 12345 * 0

# 8. Stale 소켓 파일 정리 (pathname 소켓)
# 프로세스 크래시 후 소켓 파일이 남아있으면 bind() 시 EADDRINUSE 발생
# 해결: bind 전 connect 시도 → ECONNREFUSED면 stale → unlink 후 bind
/* Stale 소켓 파일 안전 정리 패턴 */
static int safe_bind(int fd, const char *path)
{
    struct sockaddr_un addr = { .sun_family = AF_UNIX };
    strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1);

    if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0)
        return 0;

    if (errno != EADDRINUSE)
        return -1;

    /* EADDRINUSE: 이미 존재 — connect로 살아있는지 확인 */
    int probe = socket(AF_UNIX, SOCK_STREAM, 0);
    if (connect(probe, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
        close(probe);
        errno = EADDRINUSE;     /* 실제로 사용 중 */
        return -1;
    }
    close(probe);

    /* ECONNREFUSED = stale → 안전하게 제거 후 재시도 */
    unlink(path);
    return bind(fd, (struct sockaddr *)&addr, sizeof(addr));
}
💡

systemd 소켓 활성화: systemd는 서비스 대신 소켓 파일을 먼저 생성(ListenStream=/var/run/app.sock)하고, 첫 연결이 들어오면 서비스를 시작합니다. 이 방식은 stale 소켓 문제를 근본적으로 해결하며, 서비스 재시작 시에도 클라이언트 연결이 큐에 유지됩니다. FileDescriptorStoreMax=를 사용하면 서비스 재시작 간 fd도 보존 가능합니다.

네트워크 스택 주요 버그 사례

리눅스 커널 네트워크 스택은 수십만 줄의 코드와 수백 개의 프로토콜 구현으로 구성된 복잡한 서브시스템입니다. 이 섹션에서는 실제 발생한 주요 버그와 취약점 사례를 분석하여 커널 네트워크 개발 시 주의해야 할 패턴을 살펴봅니다.

AF_PACKET 경쟁 조건 (CVE-2016-8655)

AF_PACKET 소켓의 TPACKET_V3 링 버퍼 설정 과정에서 타이머 핸들러와 소켓 종료 사이의 경쟁 조건(race condition)이 발견되었습니다. packet_set_ring() 함수에서 링 버퍼를 해제하는 동안 타이머 콜백이 이미 해제된 메모리에 접근하여 use-after-free가 발생했습니다.

CVE-2016-8655 (CVSS 7.8): 로컬 비권한 사용자가 AF_PACKET 소켓의 TPACKET_V3 타이머 경쟁 조건을 악용하여 use-after-free를 트리거하고, 커널 코드 실행을 통해 root 권한 상승이 가능합니다. Linux 4.8.14 이전 커널이 영향을 받습니다.

취약한 코드 경로를 단순화하면 다음과 같습니다:

/* net/packet/af_packet.c — 취약한 코드 (단순화) */

/* Thread A: packet_set_ring() — 링 버퍼 해제 */
static int packet_set_ring(struct sock *sk,
                            union tpacket_req_u *req_u,
                            int closing, int tx_ring)
{
    struct packet_sock *po = pkt_sk(sk);
    struct packet_ring_buffer *rb;

    rb = tx_ring ? &po->tx_ring : &po->rx_ring;

    /* 문제: 타이머가 아직 실행 중일 수 있음 */
    if (closing) {
        kfree(rb->prb_bdqc);      /* 메모리 해제 */
        rb->prb_bdqc = NULL;
    }
    return 0;
}

/* Thread B: 타이머 콜백 — 해제된 메모리 접근! */
static void prb_retire_rx_blk_timer_expired(struct timer_list *t)
{
    struct tpacket_kbdq_core *pkc =
        from_timer(pkc, t, retire_blk_timer);

    /* pkc가 이미 kfree()된 메모리를 가리킴 → use-after-free! */
    prb_retire_current_block(pkc);  /* BOOM */
}

수정 패치에서는 fanout_mutex를 사용하여 링 버퍼 설정과 해제 과정 전체를 동기화하고, 타이머를 확실히 취소한 후에만 메모리를 해제하도록 변경했습니다:

/* 수정된 코드: fanout_mutex로 동기화 보강 */
static int packet_set_ring(struct sock *sk,
                            union tpacket_req_u *req_u,
                            int closing, int tx_ring)
{
    struct packet_sock *po = pkt_sk(sk);

    mutex_lock(&fanout_mutex);    /* 전역 뮤텍스 획득 */

    if (closing && po->tp_version == TPACKET_V3) {
        /* 타이머 완전 취소 후 메모리 해제 */
        del_timer_sync(&rb->prb_bdqc->retire_blk_timer);
        kfree(rb->prb_bdqc);
        rb->prb_bdqc = NULL;
    }

    mutex_unlock(&fanout_mutex);  /* 뮤텍스 해제 */
    return 0;
}
💡

교훈 — 네트워크 소켓 라이프사이클 관리: 소켓 종료 경로에서 타이머, 워크큐, tasklet 등 비동기 핸들러가 모두 완료되었는지 반드시 확인해야 합니다. del_timer_sync(), cancel_work_sync() 등의 동기식 취소 함수를 사용하고, 필요시 뮤텍스로 전체 해제 경로를 보호하세요. 단순 del_timer()는 이미 실행 중인 콜백을 기다리지 않으므로 race condition에 취약합니다.

SYN Flood 공격과 SYN Cookie 메커니즘

SYN Flood는 TCP 3-way handshake의 설계를 악용한 대표적인 DoS(Denial of Service) 공격입니다. 공격자가 위조된 소스 IP로 대량의 SYN 패킷을 전송하면 서버는 각 SYN에 대해 반-열림(half-open) 연결 상태를 SYN 큐에 유지해야 하며, 큐가 가득 차면 정상적인 연결 요청도 거부됩니다.

/* TCP 3-way handshake 정상 흐름 */

/*
 * Client                    Server
 *   |                         |
 *   |--- SYN (seq=x) -----→  |  ← 서버: SYN 큐에 상태 저장
 *   |                         |
 *   |←-- SYN-ACK (seq=y,  ---|  ← 서버: 응답 전송
 *   |         ack=x+1)       |
 *   |                         |
 *   |--- ACK (ack=y+1) ---→  |  ← 서버: 큐에서 제거, Accept 큐로 이동
 *   |                         |
 *
 * SYN Flood 공격:
 *   공격자가 위조 IP로 SYN만 대량 전송
 *   → 서버 SYN 큐 고갈 → 정상 연결 불가
 */

SYN Cookie는 서버가 SYN 큐에 상태를 저장하지 않고도 정상적인 연결을 수립할 수 있게 해주는 방어 메커니즘입니다. 서버는 SYN-ACK의 시퀀스 번호(ISN)에 연결 정보를 암호학적으로 인코딩하여, 나중에 ACK가 돌아왔을 때 이를 디코딩하여 연결을 복원합니다.

/* net/ipv4/syncookies.c — SYN Cookie 생성 원리 (단순화) */

/*
 * SYN Cookie ISN(Initial Sequence Number) 구성:
 *
 *  31          24 23      0
 * ┌──────────────┬────────┐
 * │  t (5 bits)  │        │
 * │ (타이머 카운터)│        │
 * ├──────────────┤        │
 * │ MSS idx      │ hash   │
 * │ (3 bits)     │(24bits)│
 * └──────────────┴────────┘
 *
 * hash = SHA-1(saddr, daddr, sport, dport, t, secret)
 * MSS index: 8개 사전 정의된 MSS 값 중 하나
 */

static __u32 cookie_v4_init_sequence(
    const struct sk_buff *skb,
    __u16 *mssp)
{
    const struct iphdr *iph = ip_hdr(skb);
    const struct tcphdr *th = tcp_hdr(skb);
    int mssind;
    const __u16 mss = *mssp;

    /* 클라이언트 MSS를 사전 정의된 테이블에서 가장 가까운 값으로 매핑 */
    for (mssind = ARRAY_SIZE(msstab) - 1; mssind; mssind--)
        if (mss >= msstab[mssind])
            break;

    *mssp = msstab[mssind];

    /* ISN에 연결 정보를 인코딩 */
    return secure_tcp_syn_cookie(
        iph->saddr, iph->daddr,
        th->source, th->dest,
        ntohl(th->seq),
        mssind);
}

SYN Cookie 활성화 및 관련 커널 파라미터 설정:

# SYN Cookie 활성화 (기본값: 1)
sysctl -w net.ipv4.tcp_syncookies=1

# SYN 큐(백로그) 크기 증가
sysctl -w net.ipv4.tcp_max_syn_backlog=65536

# SYN-ACK 재전송 횟수 감소 (빠른 타임아웃)
sysctl -w net.ipv4.tcp_synack_retries=2

# 확인 방법
netstat -s | grep "SYNs to LISTEN"
cat /proc/net/netstat | grep "TCPReqQFullDoCookies"

SYN Cookie의 제한사항: SYN Cookie를 사용하면 초기 SYN 패킷에 포함된 TCP 옵션 정보가 손실됩니다. 특히 window scaling과 SACK(Selective ACK) 옵션을 ISN에 인코딩할 공간이 부족하여, SYN Cookie로 수립된 연결은 이러한 성능 최적화 기능을 사용할 수 없습니다. 따라서 SYN Cookie는 SYN 큐가 가득 찼을 때의 fallback 메커니즘으로만 동작하며, 정상 상황에서는 일반적인 SYN 큐 방식이 사용됩니다.

💡

실무 권장 사항: SYN Flood 방어는 커널 수준의 SYN Cookie만으로 충분하지 않습니다. iptables/nftables의 --syn --limit 규칙으로 SYN 패킷 속도를 제한하고, tcp_max_syn_backlogsomaxconn 값을 워크로드에 맞게 튜닝하며, 대규모 공격에는 네트워크 장비 수준의 SYN Proxy를 함께 활용하세요.

Netfilter nf_conntrack 테이블 고갈

Netfilter의 연결 추적(connection tracking) 모듈인 nf_conntrack은 NAT, stateful 방화벽, 연결 기반 매칭의 핵심입니다. 그러나 대량의 동시 연결을 처리하는 서버에서 conntrack 테이블이 가득 차면 새로운 패킷이 드롭되어 심각한 서비스 장애가 발생합니다.

/* nf_conntrack 테이블 고갈 시 커널 로그 */

/*
 * dmesg 출력:
 * [123456.789] nf_conntrack: table full, dropping packet
 * [123456.790] nf_conntrack: table full, dropping packet
 * [123456.791] nf_conntrack: table full, dropping packet
 *
 * 이 메시지가 반복되면 새로운 연결이 모두 거부됨
 * → 웹 서버 접속 불가, SSH 연결 실패 등
 */

conntrack 테이블의 현재 상태를 확인하고 튜닝하는 방법:

# 현재 conntrack 사용량 확인
cat /proc/sys/net/netfilter/nf_conntrack_count   # 현재 추적 중인 연결 수
cat /proc/sys/net/netfilter/nf_conntrack_max     # 최대 허용 연결 수

# conntrack 테이블 사용률 (%) 계산
# usage = (nf_conntrack_count / nf_conntrack_max) * 100

# 해시 테이블 버킷 수 확인
cat /sys/module/nf_conntrack/parameters/hashsize

# ── 튜닝: 최대 연결 수 증가 ──
sysctl -w net.netfilter.nf_conntrack_max=1048576

# ── 튜닝: 해시 테이블 크기 조정 (보통 nf_conntrack_max / 4) ──
echo 262144 > /sys/module/nf_conntrack/parameters/hashsize

# ── 튜닝: 타임아웃 감소로 만료된 연결 빠르게 제거 ──
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_close_wait=30

# ── 영구 설정: /etc/sysctl.d/99-conntrack.conf ──
# net.netfilter.nf_conntrack_max = 1048576
# net.netfilter.nf_conntrack_tcp_timeout_established = 3600

특정 트래픽에 대해 연결 추적을 완전히 비활성화하여 conntrack 테이블 부하를 줄일 수 있습니다:

# iptables raw 테이블: 특정 트래픽의 conntrack 제외 (NOTRACK)

# 고트래픽 웹 서버의 HTTP/HTTPS 연결 추적 제외
iptables -t raw -A PREROUTING -p tcp --dport 80 -j NOTRACK
iptables -t raw -A PREROUTING -p tcp --dport 443 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp --sport 80 -j NOTRACK
iptables -t raw -A OUTPUT -p tcp --sport 443 -j NOTRACK

# nftables 동등 설정
# nft add table ip raw
# nft add chain ip raw prerouting \
#     '{ type filter hook prerouting priority -300; }'
# nft add rule ip raw prerouting tcp dport { 80, 443 } notrack
# nft add chain ip raw output \
#     '{ type filter hook output priority -300; }'
# nft add rule ip raw output tcp sport { 80, 443 } notrack
💡

conntrack 모니터링 모범 사례: 프로덕션 환경에서는 nf_conntrack_countnf_conntrack_max의 비율을 지속적으로 모니터링하고, 80% 도달 시 경고 알림을 설정하세요. conntrack -L 명령으로 현재 추적 중인 연결 목록을 확인하고, conntrack -S로 CPU별 통계(drop 카운트 포함)를 점검할 수 있습니다. NOTRACK 규칙은 NAT가 필요 없고 stateful 매칭이 불필요한 트래픽에만 적용해야 합니다.

TCP SACK Panic (CVE-2019-11477, SACK of Death)

2019년 Netflix 보안팀이 발견한 이 취약점은 TCP Selective Acknowledgment(SACK) 처리 과정에서 정수 오버플로우가 발생하여, 원격 공격자가 특수하게 조작된 SACK 시퀀스를 전송하는 것만으로 대상 시스템의 커널 패닉을 유발할 수 있었습니다.

CVE-2019-11477 — SACK Panic (CVSS 7.5): 원격 공격자가 TCP 연결이 수립된 상태에서 특수 조작된 SACK 블록 시퀀스를 전송하여 tcp_gso_highat()에서 정수 오버플로우를 유발합니다. 이로 인해 BUG_ON()이 트리거되어 커널 패닉이 발생합니다. Linux 2.6.29 이후 모든 커널이 영향을 받으며, TCP 연결만 가능하면 인증 없이 원격 DoS 공격이 가능합니다.

/* net/ipv4/tcp_output.c — 취약한 코드 (단순화) */

/*
 * TCP SACK 처리 시 SKB(socket buffer)는 여러 fragment로 분할될 수 있음.
 * 문제: SACK 블록 처리로 SKB가 과도하게 분할되면
 *       tcp_gso_segs (17-bit 필드)에서 정수 오버플로우 발생
 */

static int tcp_sendmsg_locked(struct sock *sk,
                               struct msghdr *msg,
                               size_t size)
{
    /* ... */
    struct sk_buff *skb;

    /*
     * skb_shinfo(skb)->gso_segs는 unsigned 16-bit (최대 65535)
     * SACK 처리로 SKB fragment가 계속 분할되면
     * gso_segs가 오버플로우 → BUG_ON() 트리거
     *
     * BUG_ON(skb_shinfo(skb)->gso_segs > MAX_SKB_FRAGS + 1);
     * → 커널 패닉!
     */
}

/* 공격 시나리오:
 *
 * 1. 공격자가 TCP 연결 수립
 * 2. MSS를 매우 작은 값(48 bytes)으로 협상
 * 3. 서버가 대량 데이터 전송하도록 유도
 * 4. 특수 조작된 SACK 블록 전송:
 *    - 작은 간격으로 SACK 블록을 반복적으로 전송
 *    - 서버의 write queue에서 SKB가 과도하게 분할됨
 * 5. gso_segs 오버플로우 → BUG_ON() → 커널 패닉
 */

수정 패치에서는 SKB fragment 카운트에 대한 상한 검증을 추가했습니다:

/* 수정: tcp_write_queue에서 SKB fragment 카운트 검증 */

/* 패치 1: MAX_SKB_FRAGS 제한 추가 */
static bool tcp_shifted_skb(struct sock *sk,
                             struct sk_buff *prev,
                             struct sk_buff *skb, ...)
{
    /* fragment 수가 한계에 도달하면 더 이상 병합하지 않음 */
    if (skb_shinfo(prev)->nr_frags >= MAX_SKB_FRAGS)
        return false;

    /* ... */
}

/* 패치 2: gso_segs에 안전한 상한 적용 */
static int tcp_gso_segs(struct sk_buff *skb,
                         unsigned int mss_now)
{
    u32 segs;

    segs = DIV_ROUND_UP(skb->len, mss_now);

    /* 오버플로우 방지: 최대값 제한 */
    if (segs > GSO_MAX_SEGS)
        segs = GSO_MAX_SEGS;

    return segs;
}

임시 완화 조치와 영구 수정 방법:

# ── 임시 완화: SACK 비활성화 (성능 저하 주의) ──
sysctl -w net.ipv4.tcp_sack=0

# ── 대안: MSS 하한 설정으로 공격 벡터 차단 ──
# iptables로 비정상적으로 작은 MSS 차단
iptables -A INPUT -p tcp -m tcpmss --mss 1:500 -j DROP

# ── 영구 수정: 커널 업데이트 ──
# Linux 4.4.182, 4.9.182, 4.14.127, 4.19.52, 5.1.11 이후 수정됨
# 또는 배포판별 보안 패치 적용
uname -r   # 현재 커널 버전 확인

# ── 관련 CVE (같은 시기 발견된 TCP 취약점) ──
# CVE-2019-11478 (SACK Slowness): SACK 처리로 과도한 리소스 소비
# CVE-2019-11479 (Excess Resource Consumption): 작은 MSS로 리소스 고갈
💡

TCP 스택 보안 점검 체크리스트: (1) 커널을 최신 보안 패치 버전으로 유지합니다. (2) net.ipv4.tcp_sack=1은 성능에 중요하므로, SACK을 비활성화하기보다 커널 업데이트로 대응합니다. (3) iptables/nftables에서 비정상 MSS 값(--mss 1:500)을 차단합니다. (4) TCP 관련 커널 로그(dmesg)와 /proc/net/netstat의 비정상 카운터를 모니터링합니다. (5) Netflix가 공개한 tcp_sack_panic_test 도구로 취약점 여부를 사전에 점검할 수 있습니다.