네트워크 스택(Network Stack) 개요 (Network Stack Overview)

Linux 커널 네트워크 스택의 전체 처리 경로를 RX 인터럽트(Interrupt)부터 사용자 공간(User Space) 소켓(Socket)까지 단계별로 해설합니다. NAPI 폴링(Polling)과 softirq, sk_buff 수명주기, IPv4/IPv6·TCP/UDP 처리, 라우팅(Routing)/FIB 조회, Netfilter 훅, qdisc/TC 송신 경로를 연결해 설명하고, 실제 운영에서 자주 마주치는 지연(Latency)·드롭·재전송(Retransmission) 문제를 계측하고 튜닝하는 실무 절차까지 포함합니다.

문서 구조 재정렬: 이 문서는 코어 패킷(Packet) 경로 이해 중심으로 유지합니다. 운영 튜닝/오프로드는 Network Device 드라이버, TC, 라우팅, Netfilter, BPF/XDP, AF_XDP 문서를 참고하세요.
전제 조건: 커널 아키텍처sk_buff 문서를 먼저 읽으세요. 전체 네트워크 스택은 패킷 수명 주기와 큐 전이가 중심이므로, 먼저 공통 버퍼(Buffer) 모델을 이해하면 이후 문서 연결이 쉬워집니다.
일상 비유: 이 주제는 물류 허브 전체 지도와 비슷합니다. 입고, 분류, 전달, 반송 지점을 한 장으로 보면 세부 프로토콜 문서를 읽을 때 길을 잃지 않습니다.

핵심 요약

  • 패킷 수명주기 — ingress, 처리, egress 경로를 연결합니다.
  • 큐/버퍼 모델 — sk_buff와 큐 지점의 역할을 분리합니다.
  • 정책/데이터 분리 — 제어 평면과 데이터 평면을 구분합니다.
  • 성능 지표 — PPS, 지연, 드롭 원인을 함께 분석합니다.
  • 오프로딩(Offloading) 경계 — NIC/XDP/DPDK 경계를 명확히 유지합니다.

단계별 이해

  1. 경로 고정
    문제가 발생한 ingress/egress 지점을 먼저 특정합니다.
  2. 큐 관찰
    백로그와 드롭 위치를 계측합니다.
  3. 정책 반영 확인
    라우팅/필터 변경이 데이터 경로에 반영됐는지 봅니다.
  4. 부하 검증
    실제 트래픽 패턴에서 재현성을 확인합니다.
관련 표준: RFC 791 (IPv4), RFC 8200 (IPv6), RFC 793 (TCP), RFC 768 (UDP), IEEE 802.3 (Ethernet) — 커널 네트워크 스택이 구현하는 핵심 프로토콜 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

전체 네트워크 스택 계층도

Linux 커널의 네트워크 스택은 유저 공간 애플리케이션에서 물리 NIC까지 7개 주요 계층으로 구성됩니다. 아래 대형 다이어그램은 각 계층의 핵심 모듈과 데이터 흐름을 한눈에 보여줍니다. 수신(RX) 경로는 아래에서 위로, 송신(TX) 경로는 위에서 아래로 진행합니다.

Linux 전체 네트워크 스택 계층도: 유저 공간부터 NIC 하드웨어까지의 7개 계층과 주요 모듈 Linux 네트워크 스택 전체 계층도 유저 공간 (User Space) socket() / bind() send() / recv() sendfile() / splice() setsockopt() / ioctl() syscall 소켓 계층 (Socket Layer) struct socket struct sock (sk) proto_ops AF_INET AF_PACKET 전송 계층 (Transport Layer) TCP (tcp_prot) UDP (udp_prot) SCTP (sctp_prot) RAW (raw_prot) 네트워크 계층 (Network Layer) IPv4 (ip_rcv/output) IPv6 (ipv6_rcv) FIB / Routing ARP / NDP ICMP Netfilter 훅 PREROUTING / INPUT / FORWARD / OUTPUT / POSTROUTING TC / qdisc fq_codel / htb / clsact / ingress / egress 데이터 링크 / 디바이스 계층 net_device / dev.c NAPI poll() GRO / GSO XDP (bpf_prog_run) NIC 드라이버 ndo_start_xmit() IRQ handler DMA map/unmap NIC 하드웨어 RX/TX Ring Buffer RSS / TSO / LRO 체크섬 / VLAN HW TX (송신) RX (수신)
Linux 네트워크 스택 전체 계층도: 유저 공간에서 NIC 하드웨어까지의 7개 주요 계층과 핵심 모듈

위 다이어그램에서 주목할 점은 다음과 같습니다:

커널 내부 함수 호출 흐름 상세

아래 다이어그램들은 Linux 커널 네트워크 스택 내부의 함수 호출 흐름을 상세히 보여줍니다. 수신(RX)부터 송신(TX), IPSec 처리, 브리지 흐름까지 5개의 다이어그램으로 구성됩니다. sk_buff 구조체 상세는 sk_buff 문서를 참고하세요.

① RX 수신 경로: NIC 드라이버 → XDP → NAPI → GRO → 프로토콜 디스패치

패킷이 NIC 하드웨어에 도착하면 DMA로 메모리에 복사된 뒤, XDP 필터를 거치고 NAPI 폴링 루프에서 GRO 병합을 수행합니다. 이후 netif_receive_skb()를 통해 프로토콜 타입별 핸들러로 디스패치됩니다.

RX 수신 경로: NIC 드라이버에서 프로토콜 디스패치까지 NIC 드라이버 (NIC Drivers) e1000 e1000e igb ixgbe i40e ice/bnxt virtio_net Crypto DPU/NPU Checksum SPLIT FLOW/NAT RSS → RXQ 분산 / DMA → napi_alloc_skb() / page_pool XDP 처리 (eXpress Data Path) XDP BPF prog drop XDP_DROP 패킷 폐기 tx XDP_TX → NIC 재전송 redirect XDP_REDIRECT → other NIC / AF_XDP pass NAPI & GRO napi_poll() napi_gro_receive() GRO 콜백 목록: inet_gro_receive() ipv6_gro_receive() tcp4_gro_receive() udp4_gro_receive() Backlog & RPS RPS enabled? (Remote CPU) Yes enqueue_to_backlog() process_backlog() (remote CPU) No 직접 처리 (current CPU) 프로토콜 디스패치 (Protocol Dispatch) netif_receive_skb() __netif_receive_skb_core() ptype_base 해시 → protocol 핸들러: ip_rcv() ETH_P_IP (0x0800) ipv6_rcv() ETH_P_IPV6 (0x86DD) arp_rcv() ETH_P_ARP (0x0806) • NF_INET_PRE_ROUTING → conntrack, NAT, iptables/nftables • tc ingress: sch_ingress → cls_bpf / cls_flower
RX 수신 경로: NIC 드라이버 → XDP → NAPI/GRO → Backlog/RPS → 프로토콜 디스패치 (ip_rcv, ipv6_rcv, arp_rcv)

② IP 라우팅 & 분기: PRE_ROUTING → 라우팅 결정 → Local-In / Forward / Local-Out

ip_rcv()로 진입한 패킷은 Netfilter NF_INET_PRE_ROUTING 훅을 거친 뒤 FIB 테이블을 조회하여 세 가지 경로(로컬 수신, 포워딩, 로컬 출력)로 분기합니다.

Linux 커널 IP 라우팅 및 세 갈래 분기: Local-In, Forward, Local-Out ip_rcv() AF_INET ipv6_rcv() AF_INET6 Netfilter: NF_INET_PRE_ROUTING ip_rcv_finish() 헤더 검증, 옵션 처리, dst_entry 조회 ip_route_input_slow() → fib_lookup() FIB 테이블 조회, 정책 라우팅 rx_handler (가상 디바이스) tap_handle_frame br_handle_frame Destination? dst->input 결정 Local-In Forward Local-Out ip_local_deliver() 역단편화 처리 NF_INET_LOCAL_IN ip_local_deliver_finish() 프로토콜 분기 inet_protos[] tcp_v4_rcv() udp_rcv() icmp_rcv() xfrm4_esp_rcv() Socket RX Queue sk->sk_receive_queue ip_forward() TTL 감소, MTU 검사 xfrm_policy_check() NF_INET_FORWARD ip_forward_finish() dst_output() → TX 송신 경로 Socket Layer (AF_INET) sendmsg / write ip_queue_xmit() ip_send_skb() ip_local_out() NF_INET_LOCAL_OUT 로컬 출력 훅 필터링 dst_output() → TX 출력 경로 ip_rcv → PRE_ROUTING → fib_lookup → { Local-In | Forward | Local-Out } → dst_output → TX
IP 라우팅 & 분기: ip_rcv → PRE_ROUTING → FIB 조회 → Local-In / Forward / Local-Out 세 경로

③ xfrm/IPSec 처리: 복호화(RX) & 암호화(TX)

IPSec은 커널의 xfrm 서브시스템이 담당합니다. 수신 시 SAD를 조회해 ESP/AH를 복호화하고, 송신 시 SPD에 따라 암호화한 뒤 NF_INET_LOCAL_OUT으로 재주입합니다.

xfrm/IPSec 처리 흐름 (RX 복호화 / TX 암호화) xfrm / IPSec 처리 흐름 RX — 복호화 (Decapsulation) Encrypted packet received Protocol Dispatch xfrm4_esp_rcv / xfrm4_ah_rcv xfrm4_ipcomp_rcv encap_rcv (UDP) → xfrm4_rcv xfrm Decapsulation SAD lookup ESP/AH processing Crypto API call Tunnel mode? Yes skb_dst_drop / XFRM_MODE_FLAG_TUNNEL → re-inject PRE_ROUTING No Transport: xfrm4_transport_finish Decapsulated Packet → ip_local_deliver TX — 암호화 (Encapsulation) Plain packet (from Local-Out) xfrm_lookup() SPD check (Security Policy Database) NF_INET_POST_ROUTING __xfrm4_output → xfrm_output → xfrm_output2 → xfrm_output_resume xfrm Encapsulation Crypto API call xfrm_state → x->type->output(x, skb) HW offload? Yes x->type_offload ->encap(x,skb) No (SW) Encapsulated Packet skb_dst_pop(skb) → goto NF_INET_LOCAL_OUT SAD/SPD: Security Assoc. ← RX | TX →
xfrm/IPSec 처리: 좌측 RX 복호화(SAD → Crypto → 역캡슐화) / 우측 TX 암호화(SPD → Crypto → 캡슐화 → LOCAL_OUT 재주입)

④ TX 출력 경로: dst_output → ip_output → Neighbour → NIC

라우팅/Local-Out/Forward에서 결정된 패킷은 dst_output()을 시작으로 Netfilter POST_ROUTING, GSO/단편화 판단, Neighbour 서브시스템의 L2 헤더 추가, TC/qdisc 스케줄링을 거쳐 NIC 드라이버로 전달됩니다.

TX 출력 경로 (dst_output → NIC) From: Routing / Local-Out / Forward 패킷 송신 진입점 dst_output() ip_output() ip_mc_output() xfrm4_output() IPsec 암호화 ip_rt_bug() 라우팅 오류 NF_INET_POST_ROUTING Netfilter Hook ip_finish_output() GSO needed? Yes ip_finish_ output_gso() No MTU exceeded? Yes ip_fragment() IP 단편화 No ip_finish_output2() neigh_output() Neighbour Subsystem neigh_hh_output() cached HW header neighbour->output() 함수 포인터 디스패치 ARP / NDP resolve neigh_resolve_output() dev_queue_xmit() — TC / qdisc TC/qdisc 처리 tc_classify · pfifo_fast qdisc_run() / dequeue 큐 디스크에서 dequeue net_tx_action() softirq · NET_TX_SOFTIRQ ndo_start_xmit() DMA mapping TX ring buffer Wire (TX)
TX 출력 경로: dst_output → POST_ROUTING → GSO/단편화 → Neighbour(ARP) → TC/qdisc → NIC Driver → 전송

⑤ Bridge 처리 흐름: br_handle_frame → NF_BR 훅 → FDB 분기

브리지 디바이스는 rx_handler로 등록된 br_handle_frame()에서 패킷을 가로채고, FDB 조회 결과에 따라 유니캐스트 포워딩, 플러딩, 멀티캐스트 전달, 로컬 수신으로 분기합니다.

Linux 브리지 처리 흐름: br_handle_frame부터 dev_queue_xmit까지 rx_handler: br_handle_frame() 수신 패킷 진입점 NF_BR_PRE_ROUTING ebtables PREROUTING hook br_handle_frame_finish() 멀티캐스트 처리 + FDB 조회 FDB entry found? 포워딩 DB 검색 found br_forward() NF_BR_FORWARD br_forward_ finish() not found br_flood() flood to all ports NF_BR_FORWARD multicast br_multicast_flood() MDB 조회 선택된 포트로 forward local br_pass_ frame_up() NF_BR_LOCAL_IN netif_receive_skb() re-inject to IP stack NF_BR_POST_ROUTING ebtables POSTROUTING br_forward_finish() br_dev_queue_push_xmit() dev_queue_xmit() → TX path br_dev_xmit() bridge device 출력 NF_BR_LOCAL_OUT br_forward_finish()
Bridge 처리 흐름: br_handle_frame → NF_BR_PRE_ROUTING → FDB 조회 → forward/flood/multicast/local 분기 → NF_BR_POST_ROUTING → TX

네트워크 스택 개요

Linux 네트워크 스택은 OSI 7계층 모델에 대응하는 계층적 구조로 설계되어 있습니다. 패킷은 sk_buff(소켓 버퍼) 구조체(Struct)로 표현되며, 각 계층을 통과하면서 헤더가 추가/제거됩니다. 커널 소스에서 네트워크 코드는 net/ 디렉터리에 위치하며, 프로토콜별(net/ipv4/, net/ipv6/, net/core/)로 분리되어 있습니다.

네트워크 스택의 주요 설계 원칙은 다음과 같습니다:

Linux 네트워크 스택 기본 계층: 소켓에서 NIC 드라이버까지 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 드라이버까지의 패킷 경로

커널 소스 디렉토리 구조

Linux 커널의 네트워크 코드는 크게 세 디렉토리 트리에 분산되어 있습니다: 프로토콜 구현(net/), NIC 드라이버(drivers/net/), 헤더 파일(include/net/, include/linux/). 아래 표는 각 하위 디렉토리의 역할과 주요 소스 파일을 상세하게 정리합니다.

net/ 디렉토리 상세 맵

디렉토리역할주요 파일줄 수 (대략)
net/core/프로토콜 독립 코어 인프라dev.c(디바이스 관리), skbuff.c(sk_buff), sock.c(소켓 공통), filter.c(BPF), flow_dissector.c~70,000
net/ipv4/IPv4 프로토콜 스택ip_input.c, ip_output.c, tcp.c, tcp_input.c, tcp_output.c, udp.c, route.c, fib_trie.c~120,000
net/ipv6/IPv6 프로토콜 스택ip6_input.c, ip6_output.c, tcp_ipv6.c, udp_ipv6.c, route.c, ndisc.c~60,000
net/netfilter/Netfilter 프레임워크nf_conntrack_core.c, nf_tables_api.c, nf_nat_core.c, nf_flow_table_core.c~80,000
net/sched/TC(Traffic Control)sch_generic.c, cls_api.c, sch_fq_codel.c, sch_htb.c, act_api.c~40,000
net/xdp/XDP 프레임워크xdp_umem.c, xsk.c, xsk_buff_pool.c~5,000
net/bridge/Linux 브릿지br_input.c, br_forward.c, br_fdb.c, br_vlan.c~15,000
net/unix/Unix 도메인 소켓af_unix.c, garbage.c~4,000
net/packet/AF_PACKET (raw 패킷)af_packet.c~5,000
net/netlink/Netlink 소켓af_netlink.c, genetlink.c~4,000
net/sctp/SCTP 프로토콜sm_statefuns.c, associola.c, output.c~30,000
net/openvswitch/Open vSwitch 커널 모듈(Kernel Module)datapath.c, flow.c, actions.c~10,000
net/tls/커널 TLS (kTLS)tls_main.c, tls_sw.c, tls_device.c~6,000

drivers/net/ 디렉토리 맵

디렉토리역할주요 드라이버
drivers/net/ethernet/intel/Intel NIC 드라이버ixgbe/(10G), i40e/(40G), ice/(100G), e1000e/(1G), igc/(2.5G)
drivers/net/ethernet/mellanox/NVIDIA/Mellanox NICmlx5/(ConnectX-5/6/7), mlx4/(ConnectX-3)
drivers/net/ethernet/broadcom/Broadcom NICbnxt/(NetXtreme-E), tg3.c
drivers/net/ethernet/realtek/Realtek NICr8169.c(RTL8111/8168)
drivers/net/virtio_net.c가상화(Virtualization) NICvirtio-net 드라이버 (KVM/QEMU)
drivers/net/veth.c가상 이더넷 쌍네트워크 네임스페이스 연결
drivers/net/bonding/본딩(Bonding) 드라이버NIC 결합 (active-backup, 802.3ad 등)
drivers/net/macvlan.cMACVLAN 드라이버MAC 기반 가상 NIC
drivers/net/tun.cTUN/TAP 드라이버유저 공간 네트워크 터널(Tunnel)

include/ 헤더 파일 맵

디렉토리역할핵심 헤더
include/linux/공통 커널 네트워크 헤더skbuff.h, netdevice.h, socket.h, tcp.h, if_ether.h
include/net/네트워크 서브시스템 내부 헤더sock.h, tcp.h, ip.h, dst.h, netfilter.h, flow.h, xdp.h
include/uapi/linux/UAPI (유저 공간 API)tcp.h, in.h, socket.h, bpf.h, if_link.h

커널 네트워크 코드 규모

Linux 커널의 네트워크 코드는 커널 전체 코드의 약 10-15%를 차지하는 대규모 서브시스템입니다. 아래 표는 주요 디렉토리별 대략적인 코드 규모를 보여줍니다:

디렉토리파일 수코드 줄 수 (대략)비율
net/ (전체)~2,500~800,000가장 큰 서브시스템
drivers/net/ (전체)~4,000~1,200,000NIC 드라이버
include/net/ + include/linux/ (네트워크)~300~100,000헤더 파일
합계~6,800~2,100,000전체 커널의 약 12%
소스 탐색 팁: 네트워크 관련 커널 함수를 빠르게 찾으려면 git grep -n 'EXPORT_SYMBOL.*함수명' net/ 또는 Bootlin Elixir 온라인 소스 브라우저를 활용하세요. net/core/dev.c__netif_receive_skb()부터 추적하면 수신 경로의 핵심 분기점을 파악할 수 있습니다.

핵심 소스 파일과 진입점(Entry Point)

네트워크 스택을 처음 분석할 때 아래 소스 파일부터 읽으면 전체 구조를 빠르게 파악할 수 있습니다:

파일핵심 함수역할
net/core/dev.c__netif_receive_skb(), __dev_queue_xmit()수신/송신 경로의 중앙 허브
net/core/skbuff.calloc_skb(), skb_clone(), kfree_skb()sk_buff 할당/해제/조작
net/core/sock.csock_sendmsg(), sock_recvmsg()소켓 공통 인터페이스
net/ipv4/ip_input.cip_rcv(), ip_local_deliver()IPv4 수신 경로
net/ipv4/ip_output.cip_output(), ip_queue_xmit()IPv4 송신 경로
net/ipv4/tcp.ctcp_sendmsg(), tcp_recvmsg()TCP 소켓 인터페이스
net/ipv4/tcp_input.ctcp_v4_rcv(), tcp_rcv_state_process()TCP 수신/상태 머신
net/ipv4/tcp_output.ctcp_write_xmit(), tcp_transmit_skb()TCP 송신/세그먼트 생성
net/ipv4/route.cip_route_input_noref(), ip_route_output_key()IPv4 라우팅 캐시(Cache)
net/ipv4/fib_trie.cfib_table_lookup()FIB LC-trie 조회
net/sched/sch_generic.cqdisc_run(), dev_hard_start_xmit()TC/qdisc 프레임워크

패킷 수신 경로 (RX Path)

패킷이 NIC에 도달한 순간부터 유저 공간 recv()가 데이터를 받을 때까지의 전체 경로를 단계별로 추적합니다. 이 경로를 정확히 이해하면 지연 원인 분석과 드롭 포인트 추적이 훨씬 용이해집니다.

패킷 수신 경로(RX Path) 상세: NIC DMA부터 소켓 수신 큐까지의 8단계 패킷 수신 경로 (RX Path) 상세 1. NIC: DMA로 RX Ring Buffer에 패킷 기록 / IRQ 발생 2. Hard IRQ: napi_schedule() 호출 / IRQ 비활성화 3. NET_RX_SOFTIRQ: net_rx_action() / NAPI poll() 콜백 4a. GRO: napi_gro_receive() 4b. XDP: bpf_prog_run_xdp() 5. __netif_receive_skb(): L2 프로토콜 디멀티플렉싱 6a. ip_rcv() / Netfilter PREROUTING 6b. ip_rcv_finish() / FIB lookup 7. ip_local_deliver() / Netfilter INPUT / tcp_v4_rcv() or udp_rcv() 8. sock_queue_rcv_skb() / sk_receive_queue / wake_up(sk_wq)
NIC에서 소켓 수신 큐까지의 패킷 수신 경로

단계별 상세 설명

1단계 - NIC DMA 전송: NIC가 패킷을 수신하면 DMA(Direct Memory Access)를 통해 미리 할당된 RX Ring Buffer의 빈 슬롯에 패킷 데이터를 기록합니다. RX Ring Buffer는 드라이버 초기화 시 napi_alloc_skb() 또는 page_pool을 사용하여 할당한 메모리 영역입니다. DMA 완료 후 NIC는 하드웨어 인터럽트(IRQ)를 발생시킵니다.

2단계 - Hard IRQ 처리: 드라이버의 인터럽트 핸들러(Handler)가 실행됩니다. 이 핸들러는 최소한의 작업만 수행합니다: napi_schedule()를 호출하여 NAPI 폴링을 예약하고, 해당 큐의 인터럽트를 비활성화합니다. 이렇게 함으로써 고부하 시 인터럽트 폭풍(interrupt storm)을 방지합니다.

/* 드라이버 인터럽트 핸들러 전형적 패턴 */
static irqreturn_t driver_irq_handler(int irq, void *data)
{
    struct driver_priv *priv = data;

    /* 인터럽트 비활성화 (폴링 모드 전환) */
    driver_disable_irq(priv);

    /* NAPI 폴링 예약 */
    napi_schedule(&priv->napi);

    return IRQ_HANDLED;
}

3단계 - SoftIRQ / NAPI 폴링: NET_RX_SOFTIRQ가 트리거되면 net_rx_action()이 실행되어 예약된 NAPI 구조체의 poll() 콜백을 호출합니다. 이 콜백에서 드라이버는 RX Ring에서 수신된 패킷을 budget(기본 64) 개수만큼 배치로 처리합니다. budget 내에서 모든 패킷을 처리하면 napi_complete_done()으로 인터럽트를 재활성화합니다.

4단계 - GRO/XDP 처리: NAPI poll 내부에서 napi_gro_receive()가 호출되면 GRO(Generic Receive Offload)가 동일 플로우의 연속 패킷을 병합하여 상위 계층의 처리 횟수를 줄입니다. XDP 프로그램이 연결되어 있다면 bpf_prog_run_xdp()에서 XDP_DROP, XDP_TX, XDP_REDIRECT, XDP_PASS 중 하나를 결정합니다.

5단계 - L2 디멀티플렉싱: __netif_receive_skb()에서 skb->protocol 필드(이더넷의 EtherType)를 기반으로 적절한 L3 프로토콜 핸들러를 찾습니다. IPv4(ETH_P_IP), IPv6(ETH_P_IPV6), ARP(ETH_P_ARP) 등을 구분합니다. AF_PACKET 소켓(tcpdump 등)이 있다면 이 시점에 패킷 복사본이 전달됩니다.

6단계 - IP 계층 처리: ip_rcv()에서 IP 헤더 유효성을 검증하고 Netfilter NF_INET_PRE_ROUTING 훅을 통과시킵니다. ip_rcv_finish()에서 FIB(Forwarding Information Base) 조회를 수행하여 패킷의 목적지가 로컬인지, 포워딩 대상인지 결정합니다.

7단계 - L4 전달: 로컬 대상 패킷은 ip_local_deliver()를 거쳐 Netfilter NF_INET_LOCAL_IN 훅을 통과한 후, IP 프로토콜 번호에 따라 tcp_v4_rcv()(TCP) 또는 udp_rcv()(UDP) 등 전송 계층 핸들러로 전달됩니다.

8단계 - 소켓 수신 큐: 전송 계층이 패킷을 해당 소켓의 sk_receive_queue에 삽입하고 sk->sk_data_ready() 콜백으로 대기 중인 프로세스(Process)를 깨웁니다. 유저 공간의 recv()/recvmsg()가 큐에서 데이터를 꺼내 사용자 버퍼로 복사합니다.

RX 경로 드롭 포인트: 각 단계에서 패킷이 드롭될 수 있습니다. RX Ring Full(NIC 단), netdev_budget 초과(NAPI), netdev_max_backlog 초과(per-CPU backlog), Netfilter DROP, 소켓 버퍼 초과(sk_rcvbuf) 등이 대표적입니다. /proc/net/softnet_statethtool -S로 드롭 위치를 특정할 수 있습니다.

net_rx_action() 내부 구현

NET_RX_SOFTIRQ가 트리거되면 net_rx_action()이 호출됩니다. 이 함수는 per-CPU softnet_datapoll_list에 등록된 NAPI 구조체를 순회하며, 각각의 poll() 콜백을 호출합니다. 전체 budget(기본 300)과 시간 제한(2 jiffies)이라는 두 가지 제약 조건을 적용하여, softirq가 CPU를 과도하게 점유하지 않도록 합니다.

/* net/core/dev.c — net_rx_action() 핵심 로직 (간략화) */
static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    unsigned long time_limit = jiffies + 2;  /* 최대 2 jiffies */
    int budget = READ_ONCE(netdev_budget);       /* 기본 300 */
    LIST_HEAD(list);
    LIST_HEAD(repoll);

    local_irq_disable();
    list_splice_init(&sd->poll_list, &list);
    local_irq_enable();

    for (;;) {
        struct napi_struct *n;

        if (list_empty(&list)) {
            if (!sd_has_rps_ipi_waiting(sd) &&
                list_empty(&repoll))
                return;
            break;
        }

        n = list_first_entry(&list, struct napi_struct, poll_list);
        budget -= napi_poll(n, &repoll);

        /* budget 소진 또는 시간 초과 시 중단 */
        if (unlikely(budget <= 0 ||
                     time_after_eq(jiffies, time_limit))) {
            sd->time_squeeze++;   /* softnet_stat 두 번째 열 */
            break;
        }
    }
    /* 미처리 NAPI를 poll_list에 복원, softirq 재스케줄 */
    local_irq_disable();
    list_splice_tail_init(&sd->poll_list, &list);
    list_splice_tail(&repoll, &list);
    if (!list_empty(&list))
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
    local_irq_enable();
}
코드 설명
  • 3-5행per-CPU softnet_data에서 시간 제한(2 jiffies)과 budget(기본 300)을 설정합니다. budget은 /proc/sys/net/core/netdev_budget으로 조정할 수 있습니다.
  • 9-10행인터럽트를 비활성화한 상태에서 poll_list를 로컬 list로 이동합니다. 인터럽트 핸들러가 동시에 poll_list를 수정하는 것을 방지합니다.
  • 19행napi_poll()을 호출하여 해당 NAPI 구조체의 poll() 콜백을 실행합니다. 반환값은 처리한 패킷 수입니다.
  • 22-25행budget이 0 이하이거나 시간이 초과되면 루프를 중단합니다. time_squeeze 카운터를 증가시켜 이 상황을 추적할 수 있습니다.
  • 28-32행미처리 NAPI가 남아 있으면 poll_list에 복원하고 NET_RX_SOFTIRQ를 재발생시킵니다. 이를 통해 남은 패킷이 다음 softirq 사이클에서 처리됩니다.

NAPI 폴링 루프 상세

드라이버의 poll() 콜백은 RX Ring Buffer에서 패킷을 꺼내 상위 계층으로 전달하는 핵심 루프입니다. budget 범위 내에서 가능한 많은 패킷을 처리하며, 모든 패킷을 소진하면 인터럽트를 재활성화합니다. 아래는 전형적인 드라이버 poll 함수의 구조입니다.

/* 전형적인 NIC 드라이버 NAPI poll 함수 */
static int driver_poll(struct napi_struct *napi, int budget)
{
    struct driver_ring *ring = container_of(napi, struct driver_ring, napi);
    int work_done = 0;

    while (work_done < budget) {
        struct sk_buff *skb;
        struct rx_desc *desc = &ring->desc[ring->next_to_clean];

        /* 디스크립터 소유권 확인 (DD 비트) */
        if (!(le32_to_cpu(desc->status) & RX_DESC_DONE))
            break;

        /* DMA 언맵 및 sk_buff 구성 */
        dma_sync_single_for_cpu(ring->dev, desc->dma_addr,
                                ring->buf_len, DMA_FROM_DEVICE);
        skb = ring->skb_array[ring->next_to_clean];
        skb_put(skb, desc->length);

        /* 프로토콜, 체크섬 오프로드 결과 설정 */
        skb->protocol = eth_type_trans(skb, ring->netdev);
        if (desc->status & RX_CSUM_VALID)
            skb->ip_summed = CHECKSUM_UNNECESSARY;

        /* GRO를 통해 상위 계층 전달 */
        napi_gro_receive(napi, skb);

        ring->next_to_clean = (ring->next_to_clean + 1) % ring->count;
        work_done++;
    }

    /* 새 버퍼 할당하여 RX Ring 보충 */
    driver_alloc_rx_buffers(ring);

    /* 모든 패킷 처리 완료 시 인터럽트 재활성화 */
    if (work_done < budget) {
        if (napi_complete_done(napi, work_done))
            driver_enable_irq(ring);
    }

    return work_done;
}
코드 설명
  • 7-13행budget 한도 내에서 RX 디스크립터의 DD(Descriptor Done) 비트를 확인하여 NIC가 패킷을 완료했는지 검사합니다. DD 비트가 없으면 더 이상 처리할 패킷이 없으므로 루프를 종료합니다.
  • 16-20행DMA 동기화 후 sk_buff에 실제 패킷 길이를 설정합니다. skb_put()으로 tail 포인터를 이동하여 유효 데이터 영역을 확정합니다.
  • 22-24행eth_type_trans()가 이더넷 헤더를 파싱하여 EtherType을 설정하고, NIC 체크섬 오프로드 결과를 ip_summed에 반영합니다. CHECKSUM_UNNECESSARY는 커널이 체크섬 재검증을 생략해도 됨을 의미합니다.
  • 27행napi_gro_receive()로 GRO 병합을 시도한 후 __netif_receive_skb()로 전달합니다.
  • 37-39행work_done이 budget보다 작으면 모든 패킷을 처리한 것이므로, napi_complete_done()으로 NAPI 상태를 해제하고 인터럽트를 재활성화합니다. busy polling이 활성화된 경우 napi_complete_done()이 false를 반환하여 인터럽트를 재활성화하지 않을 수 있습니다.

GRO 병합 메커니즘

GRO(Generic Receive Offload)는 동일 플로우의 연속 패킷을 하나의 큰 sk_buff로 병합하여 상위 스택의 처리 횟수를 줄이는 메커니즘입니다. napi_gro_receive()는 내부적으로 dev_gro_receive()를 호출하여 프로토콜별 GRO 콜백을 실행합니다.

/* net/core/gro.c — GRO 수신 처리 핵심 흐름 (간략화) */
gro_result_t dev_gro_receive(struct napi_struct *napi,
                             struct sk_buff *skb)
{
    struct list_head *gro_head = &napi->gro_hash[bucket].list;
    struct sk_buff *pp = NULL;

    /* 1. L2 프로토콜별 gro_receive 콜백 호출 */
    /*    IPv4 → inet_gro_receive() → tcp4_gro_receive() */
    /*    IPv6 → ipv6_gro_receive() → tcp6_gro_receive() */
    pp = call_gro_receive(ptype->callbacks.gro_receive, gro_head, skb);

    switch (ret) {
    case GRO_MERGED:
        /* skb가 기존 GRO 패킷에 병합됨 — skb 해제 */
        break;
    case GRO_MERGED_FREE:
        /* 병합 후 원본 skb 즉시 해제 */
        break;
    case GRO_HELD:
        /* skb를 GRO 리스트에 보관 (향후 병합 대기) */
        list_add(&skb->list, gro_head);
        napi->gro_hash[bucket].count++;
        break;
    case GRO_NORMAL:
        /* GRO 불가 — 일반 수신 경로로 전달 */
        gro_normal_one(napi, skb, 1);
        break;
    case GRO_CONSUMED:
        /* 프로토콜이 skb 소유권을 가져감 */
        break;
    }
    return ret;
}
코드 설명
  • 5행NAPI 구조체에 GRO 해시 테이블이 존재하며, 플로우의 해시값에 따라 버킷이 결정됩니다. 동일 플로우의 패킷들이 같은 버킷에 모여 병합 후보를 빠르게 찾을 수 있습니다.
  • 8-11행EtherType에 등록된 프로토콜별 gro_receive 콜백을 호출합니다. IPv4 TCP의 경우 inet_gro_receive()tcp4_gro_receive() 체인으로 처리됩니다.
  • 13-15행GRO_MERGED는 패킷이 기존 GRO 버퍼에 성공적으로 병합되었음을 의미합니다. TCP의 경우 시퀀스 번호가 연속이고 헤더가 호환되는 경우 병합됩니다.
  • 20-23행GRO_HELD는 병합 대상이 아직 없어 GRO 리스트에 보관하는 경우입니다. 다음 패킷이 도착하면 이 패킷과 병합을 시도합니다. gro_hash[].countMAX_GRO_SKBS(8)에 도달하면 가장 오래된 패킷을 플러시합니다.
  • 24-27행GRO_NORMAL은 GRO 병합이 불가능한 패킷(UDP 비-GRO, 단편 패킷 등)으로, 일반 수신 경로(__netif_receive_skb())로 전달됩니다.

GRO 병합이 성공하면 하나의 64KB sk_buff로 수십 개의 세그먼트를 전달할 수 있어, TCP 수신 경로의 함수 호출 횟수가 대폭 감소합니다. GRO 병합 조건은 다음과 같습니다:

조건설명불만족 시
동일 플로우src/dst IP, src/dst port, protocol이 동일GRO_NORMAL
연속 시퀀스TCP 시퀀스 번호가 이전 세그먼트에 연속GRO_NORMAL
호환 헤더IP/TCP 헤더 옵션이 동일 (timestamp 제외)GRO_NORMAL
동일 MACVLAN 태그, MAC 헤더가 동일GRO_NORMAL
PSH 플래그PSH가 설정된 세그먼트는 병합 후 즉시 플러시flush 트리거
최대 크기병합 후 총 크기가 64KB 이하flush 후 새 GRO 시작
NAPI 폴링과 GRO 병합의 전체 처리 흐름 NAPI 폴링 + GRO 병합 처리 흐름 Hard IRQ napi_schedule() NET_RX_SOFTIRQ net_rx_action() 드라이버 poll() 콜백 budget(64) 범위에서 RX 디스크립터 처리 DMA 언맵 → sk_buff 구성 → eth_type_trans napi_gro_receive() dev_gro_receive() → 프로토콜별 gro_receive() GRO_MERGED: 기존 패킷에 병합 GRO_NORMAL: 일반 수신 경로 전달 GRO_HELD: GRO 리스트 보관 __netif_receive_skb() → ip_rcv() / ipv6_rcv() work_done < budget: napi_complete_done() 인터럽트 재활성화 work_done == budget: NAPI 재스케줄
Hard IRQ에서 __netif_receive_skb()까지의 NAPI + GRO 처리 흐름

Busy Polling (SO_BUSY_POLL)

Busy Polling은 유저 공간의 poll()/epoll_wait()/select()가 반환 전에 직접 NAPI poll을 호출하는 메커니즘입니다. softirq 스케줄링 지연을 제거하여 수 마이크로초 수준의 레이턴시 개선을 달성할 수 있습니다. 금융 트레이딩, HPC 메시지 패싱 등 초저지연 환경에서 유용합니다.

/* include/net/busy_poll.h — sk_busy_loop() 핵심 로직 (간략화) */
static inline void sk_busy_loop(struct sock *sk, int nonblock)
{
    unsigned int napi_id = READ_ONCE(sk->sk_napi_id);
    unsigned long end_time = busy_loop_end_time(sk);
    int (*busy_poll)(struct napi_struct *napi, int budget);

    if (!napi_id)
        return;

    do {
        local_bh_disable();
        /* 소켓에 연결된 NAPI를 직접 poll */
        napi_busy_loop(napi_id, nonblock ? NULL : busy_loop_current_should_spin,
                       sk->sk_prefer_busy_poll ? busy_poll : NULL,
                       BUSY_POLL_BUDGET);
        local_bh_enable();
    } while (!nonblock && !sk_has_rx_data(sk) &&
              !busy_loop_timeout(end_time));
}
코드 설명
  • 4행sk_napi_id는 이 소켓의 마지막 수신 패킷을 처리한 NAPI 인스턴스의 ID입니다. RX 경로에서 자동으로 설정됩니다.
  • 5행busy_loop_end_time()SO_BUSY_POLL 소켓 옵션에 설정된 시간(마이크로초)을 기반으로 종료 시점을 계산합니다.
  • 13-16행napi_busy_loop()가 해당 NAPI의 poll 콜백을 BUSY_POLL_BUDGET(8)만큼 직접 호출합니다. softirq 컨텍스트가 아닌 프로세스 컨텍스트에서 실행되므로, 해당 CPU의 BH를 비활성화합니다.
  • 18-19행소켓에 수신 데이터가 도착하거나 타임아웃이 만료될 때까지 루프를 반복합니다.
/* Busy Polling 활성화: 유저 공간 설정 */
int busy_poll_usec = 50;  /* 50μs 동안 busy poll */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL,
           &busy_poll_usec, sizeof(busy_poll_usec));

/* SO_PREFER_BUSY_POLL: 드라이버 전용 busy poll 콜백 사용 */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL,
           &prefer, sizeof(prefer));
코드 설명
  • 2-3행SO_BUSY_POLL에 마이크로초 단위 값을 설정합니다. 0이면 비활성화, 양수이면 해당 시간 동안 busy poll을 수행합니다.
  • 7-9행SO_PREFER_BUSY_POLL은 드라이버가 전용 busy poll 콜백(ndo_busy_poll)을 제공할 때 이를 우선 사용하도록 합니다.
# 시스템 전역 busy poll 설정 (sysctl)
sysctl -w net.core.busy_poll=50       # poll/select/epoll 기본 busy poll 시간(μs)
sysctl -w net.core.busy_read=50       # read/recv 기본 busy poll 시간(μs)

# NAPI ID 기반 소켓 바인딩 확인
cat /proc/net/tcp | awk '{print $13}'  # napi_id 열 확인

RX 경로 성능 카운터 심층 분석

/proc/net/softnet_stat은 per-CPU 네트워크 소프트 인터럽트 통계를 16진수로 출력합니다. 각 행이 하나의 CPU에 대응하며, 패킷 드롭과 softirq 스케줄링 문제를 진단하는 핵심 도구입니다.

/* net/core/net-procfs.c — softnet_stat 출력 형식 */
/* include/linux/netdevice.h — softnet_data 구조체 */
struct softnet_data {
    struct list_head    poll_list;        /* 폴링 대기 NAPI 리스트 */
    struct sk_buff_head input_pkt_queue;  /* non-NAPI 또는 RPS 입력 큐 */
    struct sk_buff_head process_queue;    /* 처리 중인 패킷 큐 */

    unsigned int        processed;        /* 열 1: 총 처리 패킷 수 */
    unsigned int        time_squeeze;     /* 열 2: budget/시간 초과 횟수 */
    unsigned int        received_rps;     /* 열 9: RPS 수신 횟수 */
    unsigned int        dropped;          /* input_pkt_queue 오버플로우 */
    unsigned int        flow_limit_count; /* 열 10: flow limit 드롭 */
    unsigned int        cpu_collision;    /* 열 8: TX CPU 충돌 */
    unsigned int        backlog_len;      /* 현재 backlog 길이 */
};
코드 설명
  • 8행processed: /proc/net/softnet_stat의 첫 번째 열입니다. 해당 CPU에서 처리된 총 패킷 수를 나타냅니다.
  • 9행time_squeeze: 두 번째 열입니다. net_rx_action()이 budget 또는 시간 제한으로 중단된 횟수입니다. 이 값이 증가하면 netdev_budget 또는 netdev_budget_usecs를 늘려야 합니다.
  • 11행dropped: input_pkt_queuenetdev_max_backlog(기본 1000)을 초과하여 드롭된 패킷 수입니다. non-NAPI 드라이버 또는 RPS가 활성화된 경우에 발생합니다.
  • 13행cpu_collision: TX 경로에서 다른 CPU가 이미 qdisc 락을 보유하여 전송에 실패한 횟수입니다. 멀티큐 NIC에서 XPS를 설정하면 줄일 수 있습니다.
열 번호필드의미대응 조치
1processed처리된 총 패킷 수CPU 간 불균형 시 RPS/RSS 조정
2time_squeezebudget/시간 초과로 중단된 횟수netdev_budget 증가 (300→600)
3(0)미사용 (구 backlog 필드)
4-7(0)미사용
8cpu_collisionTX qdisc 락 충돌멀티큐 + XPS 설정
9received_rpsRPS IPI로 수신된 패킷RPS 정상 동작 확인
10flow_limit_countFlow Limit으로 드롭flow_limit_table_len 조정
11softnet_backlog_len현재 backlog 큐 길이실시간 큐 상태 확인
12indexCPU 번호
13droppedbacklog 오버플로우 드롭netdev_max_backlog 증가
# softnet_stat 실시간 모니터링
watch -n1 'cat /proc/net/softnet_stat'

# 특정 CPU의 time_squeeze 변화 추적
while true; do
  awk 'NR==1 {printf "CPU0 processed=%d time_squeeze=%d dropped=%d\n", \
    strtonum("0x"$1), strtonum("0x"$2), strtonum("0x"$3)}' \
    /proc/net/softnet_stat
  sleep 1
done

# budget 관련 sysctl 튜닝
sysctl -w net.core.netdev_budget=600            # 기본 300
sysctl -w net.core.netdev_budget_usecs=4000     # 기본 2000 (2ms)
sysctl -w net.core.netdev_max_backlog=2000      # 기본 1000

RPS/RFS/XPS 패킷 스티어링

멀티큐 NIC에서도 모든 큐가 효율적으로 사용되지 않거나, 단일 큐 NIC에서 멀티코어를 활용하기 위해 소프트웨어 패킷 스티어링이 필요합니다. RPS(Receive Packet Steering), RFS(Receive Flow Steering), XPS(Transmit Packet Steering)는 패킷 처리를 여러 CPU에 분산합니다.

/* net/core/dev.c — RPS 처리 (get_rps_cpu) 핵심 로직 (간략화) */
static int get_rps_cpu(struct net_device *dev,
                      struct sk_buff *skb,
                      struct rps_dev_flow **rflowp)
{
    const struct rps_sock_flow_table *sock_flow_table;
    struct netdev_rx_queue *rxqueue = dev->_rx;
    u32 flow_hash;
    int cpu = -1;

    /* 1. 패킷의 플로우 해시 계산 (5-tuple) */
    flow_hash = skb_get_hash(skb);
    if (!flow_hash)
        goto done;

    /* 2. RFS: 소켓이 마지막으로 실행된 CPU로 스티어링 */
    sock_flow_table = rcu_dereference(rps_sock_flow_table);
    if (sock_flow_table) {
        u32 ident = sock_flow_table->ents[flow_hash &
                    sock_flow_table->mask];
        cpu = ident & ~0x80000000U;  /* CPU 번호 추출 */
    }

    /* 3. RPS fallback: 해시 기반 CPU 선택 */
    if (cpu < 0) {
        u32 map_len = map->len;
        cpu = map->cpus[reciprocal_scale(flow_hash, map_len)];
    }

    return cpu;
}
코드 설명
  • 12행skb_get_hash()는 패킷의 5-tuple(소스/목적지 IP:포트, 프로토콜)에서 해시를 계산합니다. 동일 플로우의 패킷은 동일 해시 → 동일 CPU로 전달되어 캐시 효율성을 유지합니다.
  • 17-22행RFS가 활성화되어 있으면 rps_sock_flow_table에서 해당 플로우의 소켓이 마지막으로 실행된 CPU를 찾습니다. 이를 통해 수신 패킷이 소켓을 처리하는 CPU와 같은 CPU에서 처리되어 캐시 미스를 줄입니다.
  • 25-28행RFS 정보가 없으면 RPS fallback으로, CPU 맵에서 해시 기반으로 CPU를 선택합니다. CPU 맵은 /sys/class/net/<dev>/queues/rx-<N>/rps_cpus에서 설정합니다.
기능방향수준설정 방법동작
RSSRX하드웨어ethtool -XNIC 해시 → 하드웨어 큐 분배
RPSRX소프트웨어rps_cpus sysfs해시 → CPU IPI 전달
RFSRX소프트웨어rps_sock_flow_entries소켓 CPU 친화성 기반
XPSTX소프트웨어xps_cpus sysfsCPU → TX 큐 매핑
aRFSRX하드웨어ethtool -K ntuple onNIC 하드웨어 플로우 스티어링
# RPS 설정 (CPU 0-3에 분배)
echo f > /sys/class/net/eth0/queues/rx-0/rps_cpus

# RFS 활성화 (전역 플로우 테이블 크기)
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

# XPS 설정 (CPU 0 → TX 큐 0, CPU 1 → TX 큐 1)
echo 1 > /sys/class/net/eth0/queues/tx-0/xps_cpus
echo 2 > /sys/class/net/eth0/queues/tx-1/xps_cpus

# RSS 인다이렉션 테이블 확인
ethtool -x eth0

# RSS 해시 키 설정
ethtool -X eth0 equal 4  # 4개 큐에 균등 분배

패킷 송신 경로 (TX Path)

유저 공간의 send()/sendmsg() 호출부터 NIC가 패킷을 와이어에 전송할 때까지의 경로입니다. 수신 경로의 역순이지만, qdisc(큐잉 규칙)와 GSO(Generic Segmentation Offload)가 개입하는 점이 핵심 차이입니다.

패킷 송신 경로(TX Path): 유저 공간 send()에서 NIC 전송까지의 8단계 패킷 송신 경로 (TX Path) 상세 1. send() / sendmsg() / sendfile() (유저 공간) 2. sock_sendmsg() / inet_sendmsg() / tcp_sendmsg() or udp_sendmsg() 3. TCP: tcp_write_xmit() / sk_buff 세그먼트 생성 / UDP: udp_send_skb() 4. ip_queue_xmit() / ip_output() / Netfilter OUTPUT + POSTROUTING 5. __dev_queue_xmit() / TC qdisc: enqueue / dequeue / classify 6a. GSO: dev_hard_start_xmit() 세그먼테이션 6b. TSO: NIC 하드웨어 세그먼테이션 7. ndo_start_xmit() / TX Ring Buffer DMA / doorbell 8. TX Completion IRQ / dev_kfree_skb() / NAPI TX clean
유저 공간 send()에서 NIC 전송까지의 패킷 송신 경로

TX 경로의 핵심 처리 흐름

소켓 계층에서 전송 계층까지: 유저 공간의 send() 시스템 콜(System Call)은 sock_sendmsg()를 거쳐 프로토콜별 sendmsg() 구현으로 전달됩니다. TCP의 경우 tcp_sendmsg()가 유저 데이터를 소켓의 sk_write_queuesk_buff 형태로 복사하고, Nagle 알고리즘과 혼잡 제어(Congestion Control)를 거쳐 tcp_write_xmit()에서 실제 전송을 결정합니다. UDP는 udp_sendmsg()에서 즉시 IP 계층으로 전달합니다.

IP 계층 처리: ip_queue_xmit()에서 라우팅 결과(dst_entry)를 참조하여 소스 IP, 출력 인터페이스를 결정하고 IP 헤더를 추가합니다. Netfilter NF_INET_LOCAL_OUTNF_INET_POST_ROUTING 훅을 통과합니다. IP 단편화(Fragmentation)가 필요하면 ip_fragment()에서 MTU에 맞게 분할합니다.

TC/qdisc 계층: __dev_queue_xmit()에서 출력 디바이스의 qdisc에 패킷을 enqueue합니다. 기본 qdisc는 pfifo_fast 또는 fq_codel이며, htb, tbf 등으로 대역폭(Bandwidth) 제한과 트래픽 셰이핑을 수행할 수 있습니다. dequeue된 패킷은 dev_hard_start_xmit()으로 드라이버에 전달됩니다.

드라이버 전송: ndo_start_xmit() 콜백에서 DMA 매핑(Mapping) 후 TX Ring Buffer에 디스크립터를 기록하고, NIC의 doorbell 레지스터(Register)에 쓰기를 수행하여 전송을 시작합니다. TX 완료 인터럽트로 sk_buff를 해제합니다.

GSO vs TSO: GSO(Generic Segmentation Offload)는 커널 소프트웨어에서 대형 세그먼트를 분할하고, TSO(TCP Segmentation Offload)는 NIC 하드웨어에서 분할합니다. TSO를 지원하는 NIC에서는 최대 64KB 크기의 sk_buff를 드라이버까지 전달하여 CPU 부하를 줄입니다. ethtool -k로 오프로드 상태를 확인할 수 있습니다.

__dev_queue_xmit() 내부 구현

__dev_queue_xmit()은 IP 계층에서 내려온 sk_buff를 출력 디바이스의 qdisc에 전달하는 핵심 함수입니다. qdisc가 없거나(noqueue) 조건을 만족하면 qdisc를 우회하여 직접 전송하는 경로도 있습니다.

/* net/core/dev.c — __dev_queue_xmit() 핵심 로직 (간략화) */
static int __dev_queue_xmit(struct sk_buff *skb,
                            struct net_device *sb_dev)
{
    struct net_device *dev = skb->dev;
    struct netdev_queue *txq;
    struct Qdisc *q;

    /* 1. TC egress 처리 (tc filter/action) */
    skb = sch_handle_egress(skb, &rc, dev);
    if (!skb)
        goto out;

    /* 2. TX 큐 선택 (XPS 또는 해시 기반) */
    txq = netdev_core_pick_tx(dev, skb, sb_dev);
    q = rcu_dereference_bh(txq->qdisc);

    if (q->enqueue) {
        /* 3a. qdisc 경로: enqueue 후 dequeue/전송 */
        rc = __dev_xmit_skb(skb, q, dev, txq);
    } else {
        /* 3b. qdisc 우회 (noqueue): 직접 전송 */
        if (dev_xmit_recursion()) {
            net_crit_ratelimited("Recursion! dev=%s\n", dev->name);
            goto drop;
        }

        /* BQL 체크 후 직접 전송 */
        skb_get_tx_queue(skb, txq);
        PRANDOM_ADD_NOISE(skb, dev, txq, jiffies);
        rc = dev_hard_start_xmit(skb, dev, txq, NULL);
    }
    return rc;
}
코드 설명
  • 10행sch_handle_egress()는 TC egress 훅을 처리합니다. tc filter로 설정된 규칙이 있으면 이 시점에서 패킷을 분류/수정/드롭할 수 있습니다.
  • 15행netdev_core_pick_tx()는 멀티큐 NIC에서 전송할 TX 큐를 선택합니다. XPS(Transmit Packet Steering)가 설정되어 있으면 CPU-큐 매핑을 사용하고, 그렇지 않으면 skb 해시를 기반으로 선택합니다.
  • 18-20행qdisc에 enqueue 함수가 있으면 일반 qdisc 경로로 진입합니다. __dev_xmit_skb()는 qdisc 락을 획득하고, enqueue → dequeue → dev_hard_start_xmit() 순서로 처리합니다.
  • 21-31행loopback, veth 등 qdisc가 noqueue인 디바이스는 qdisc 처리를 건너뛰고 dev_hard_start_xmit()을 직접 호출합니다. 재귀 방지 검사가 포함되어 있습니다.

qdisc 처리 흐름

qdisc에 패킷이 enqueue되면, __qdisc_run()이 dequeue 루프를 실행하여 패킷을 드라이버로 전달합니다. 이 과정은 qdisc 락을 보유한 상태에서 수행되므로, 다른 CPU와의 경합이 발생할 수 있습니다.

/* net/sched/sch_generic.c — qdisc 실행 흐름 (간략화) */
void __qdisc_run(struct Qdisc *q)
{
    int quota = READ_ONCE(dev_tx_weight);  /* 기본 64 */
    int packets;

    while (qdisc_restart(q, &packets)) {
        quota -= packets;
        if (quota <= 0) {
            /* quota 소진 시 softirq로 연기 */
            __netif_schedule(q);
            break;
        }
    }
}

/* qdisc_restart() 내부 */
static inline bool qdisc_restart(struct Qdisc *q, int *packets)
{
    struct sk_buff *skb = dequeue_skb(q, packets);
    if (unlikely(!skb))
        return false;

    /* GSO 세그먼테이션이 필요하면 이 시점에서 수행 */
    if (sch_direct_xmit(skb, q, dev, txq, NULL, true))
        return true;   /* dequeue 계속 */

    return false;      /* 드라이버 큐 full — 중단 */
}
코드 설명
  • 4행dev_tx_weight는 한 번의 __qdisc_run() 호출에서 처리할 최대 패킷 수입니다. 기본값은 64이며, /proc/sys/net/core/dev_weight로 조정할 수 있습니다.
  • 7행qdisc_restart()는 qdisc에서 패킷을 dequeue하고 드라이버로 전송을 시도합니다. 성공하면 true를 반환하여 루프가 계속됩니다.
  • 10-12행quota가 소진되면 __netif_schedule()NET_TX_SOFTIRQ를 발생시켜 나머지 패킷을 softirq 컨텍스트에서 처리합니다.
  • 20행dequeue_skb()는 qdisc의 dequeue 콜백을 호출합니다. fq_codel의 경우 CoDel 알고리즘으로 큐 지연을 측정하고, 지연이 target(5ms)을 초과하면 패킷을 드롭합니다.
  • 24-25행sch_direct_xmit()dev_hard_start_xmit()을 호출하여 실제 전송을 수행합니다. 드라이버의 TX 큐가 가득 차면(NETDEV_TX_BUSY) false를 반환하고, qdisc에 패킷을 다시 requeue합니다.
TX qdisc 처리 아키텍처: __dev_queue_xmit()에서 NIC까지 TX qdisc 처리 아키텍처 ip_output() / ip6_output() __dev_queue_xmit(skb) sch_handle_egress() — TC egress 훅 netdev_core_pick_tx() — XPS/해시 큐 선택 qdisc 있음 noqueue qdisc enqueue → __qdisc_run → dequeue 직접 전송 (qdisc 우회) dev_hard_start_xmit() → ndo_start_xmit() BQL: netdev_tx_sent_queue()
__dev_queue_xmit()에서 NIC 드라이버까지의 TX 처리 아키텍처

GSO 세그먼테이션 내부

GSO(Generic Segmentation Offload)는 TCP/UDP 대형 패킷을 MSS 크기의 세그먼트로 분할하는 소프트웨어 메커니즘입니다. dev_hard_start_xmit()에서 NIC가 TSO를 지원하지 않으면 __skb_gso_segment()를 호출하여 분할합니다.

/* net/core/skbuff.c — GSO 세그먼테이션 핵심 경로 (간략화) */
struct sk_buff *__skb_gso_segment(struct sk_buff *skb,
                                  netdev_features_t features, bool tx_path)
{
    struct sk_buff *segs;

    /* skb_shared_info→gso_type에 따라 콜백 선택 */
    /* SKB_GSO_TCPV4 → tcp4_gso_segment() */
    /* SKB_GSO_TCPV6 → tcp6_gso_segment() */
    /* SKB_GSO_UDP_L4 → __udp_gso_segment() */
    segs = skb_mac_gso_segment(skb, features);

    return segs; /* 세그먼트 sk_buff 연결 리스트 */
}

/* TCP GSO 세그먼테이션 (net/ipv4/tcp_offload.c) */
struct sk_buff *tcp4_gso_segment(struct sk_buff *skb,
                                netdev_features_t features)
{
    /* gso_size = MSS, gso_segs = 원본 길이 / MSS */
    unsigned int mss = skb_shinfo(skb)->gso_size;

    /* skb_segment()로 실제 분할 수행 */
    /* 각 세그먼트에 TCP/IP 헤더 복사, 시퀀스 번호 조정 */
    /* 마지막 세그먼트에만 PSH 플래그 설정 */
    segs = tcp_gso_segment(skb, features);

    /* 각 세그먼트의 체크섬 재계산 */
    return segs;
}
코드 설명
  • 7-10행skb_shared_infogso_type 필드에 따라 프로토콜별 세그먼테이션 콜백이 선택됩니다. TCP, UDP, SCTP 등 각 프로토콜이 고유한 GSO 콜백을 등록합니다.
  • 11행skb_mac_gso_segment()는 MAC 계층부터 세그먼테이션을 시작하여, 각 계층의 GSO 콜백을 체인으로 호출합니다.
  • 21행gso_size는 MSS(Maximum Segment Size)에 해당하며, 각 세그먼트의 페이로드 크기를 결정합니다.
  • 26행tcp_gso_segment()가 실제 분할을 수행합니다. 각 세그먼트에 TCP 헤더를 복사하고, 시퀀스 번호를 mss씩 증가시키며, 마지막 세그먼트에만 PSH 플래그를 설정합니다.
GSO 타입 플래그프로토콜콜백비고
SKB_GSO_TCPV4TCP over IPv4tcp4_gso_segment()가장 일반적
SKB_GSO_TCPV6TCP over IPv6tcp6_gso_segment()IPv6 확장 헤더 처리
SKB_GSO_UDP_L4UDP (GSO)__udp_gso_segment()UDP GSO (커널 4.18+)
SKB_GSO_PARTIAL부분 GSO드라이버별NIC가 외부 헤더만 처리
SKB_GSO_GREGRE 터널gre_gso_segment()터널 내부 세그먼테이션
SKB_GSO_UDP_TUNNELVXLAN/Geneve터널별캡슐화 후 세그먼테이션

TX Completion과 BQL

TX Completion은 NIC가 패킷 전송을 완료한 후 DMA 매핑을 해제하고 sk_buff를 반환하는 과정입니다. BQL(Byte Queue Limits)은 드라이버의 TX 큐에 과도한 바이트가 적체되는 것을 방지하여 qdisc 계층의 지연을 최소화합니다.

/* TX Completion 처리 (NAPI TX clean) — 드라이버 패턴 */
static int driver_tx_clean(struct driver_ring *ring, int budget)
{
    unsigned int total_bytes = 0, total_packets = 0;

    while (total_packets < budget) {
        struct tx_desc *desc = &ring->desc[ring->next_to_clean];

        if (!(desc->status & TX_DESC_DONE))
            break;

        /* DMA 매핑 해제 */
        dma_unmap_single(ring->dev, desc->dma_addr,
                         desc->length, DMA_TO_DEVICE);

        /* sk_buff 해제 — 정상 전송이므로 consume_skb 사용 */
        total_bytes += ring->tx_buf[ring->next_to_clean].bytecount;
        dev_consume_skb_any(ring->tx_buf[ring->next_to_clean].skb);

        total_packets++;
        ring->next_to_clean = (ring->next_to_clean + 1) % ring->count;
    }

    /* BQL 완료 통지 — 큐에서 제거된 바이트 수 보고 */
    netdev_tx_completed_queue(ring->txq, total_packets, total_bytes);

    /* TX 큐가 멈췄으면 재개 */
    if (unlikely(netif_tx_queue_stopped(ring->txq)) &&
        driver_desc_unused(ring) >= TX_WAKE_THRESHOLD)
        netif_tx_wake_queue(ring->txq);

    return total_packets;
}
코드 설명
  • 9-10행TX 디스크립터의 DD(Descriptor Done) 비트를 확인합니다. NIC가 전송을 완료하면 이 비트를 설정합니다.
  • 13-14행dma_unmap_single()로 DMA 매핑을 해제합니다. 이 시점 이후에야 sk_buff의 데이터 영역을 안전하게 해제할 수 있습니다.
  • 18행dev_consume_skb_any()consume_skb()의 변형으로, 정상적인 패킷 소비를 나타냅니다. kfree_skb()와 달리 drop_monitor에 기록되지 않습니다.
  • 25행netdev_tx_completed_queue()는 BQL에 전송 완료된 바이트 수를 보고합니다. BQL은 이 정보를 사용하여 TX 큐의 최대 허용 바이트 수를 동적으로 조정합니다.
  • 28-30행TX 큐가 이전에 netif_tx_stop_queue()로 중단되었고, 충분한 디스크립터가 확보되면 큐를 재개합니다.
BQL 동작 원리: BQL은 드라이버가 netdev_tx_sent_queue()(전송 시)와 netdev_tx_completed_queue()(완료 시)를 호출하여 in-flight 바이트 수를 추적합니다. in-flight 바이트가 동적으로 계산된 limit를 초과하면 qdisc에 netif_tx_stop_queue()를 통지하여 추가 enqueue를 중단합니다. 이를 통해 드라이버의 TX 버퍼가 과도하게 차는 것(bufferbloat)을 방지하고, qdisc(fq_codel 등)가 효과적으로 작동할 수 있게 합니다. BQL 상태는 /sys/class/net/<dev>/queues/tx-<N>/byte_queue_limits/에서 확인할 수 있습니다.

패킷 수명주기 End-to-End 추적

실제 운영 환경에서 TCP 연결의 패킷 하나가 생성되고 소멸되기까지의 전체 여정을 추적합니다. 아래는 클라이언트가 send()로 데이터를 보내고 서버가 recv()로 받는 과정에서 커널 내부에서 발생하는 모든 주요 이벤트를 시간순으로 정리한 것입니다.

TCP 패킷 End-to-End 수명주기: 송신측 send()에서 수신측 recv()까지의 전체 경로 TCP 패킷 End-to-End 수명주기 송신측 (Sender) 수신측 (Receiver) 1. send(fd, buf, len) - 유저 공간 시스템 콜 2. tcp_sendmsg() - sk_write_queue에 데이터 복사 3. tcp_write_xmit() - cwnd/Nagle 확인, sk_buff 생성 4. ip_queue_xmit() - IP 헤더, 라우팅, Netfilter 5. __dev_queue_xmit() - qdisc enqueue/dequeue 6. ndo_start_xmit() - DMA, TX Ring, doorbell Wire / Network 7. NIC DMA - RX Ring Buffer 기록, IRQ 발생 8. NAPI poll() - GRO 병합, XDP 통과 9. ip_rcv() - IP 검증, FIB, Netfilter PREROUTING 10. tcp_v4_rcv() - 소켓 조회, ACK 생성 11. sk_receive_queue 삽입, sk_data_ready() 12. recv(fd, buf, len) - 유저 버퍼 복사, skb 해제 ACK 패킷 (역방향)
TCP 패킷의 전체 수명주기: 송신측 send()에서 수신측 recv()까지

패킷별 커널 내부 타임스탬프 추적

패킷의 지연을 정밀하게 분석하려면 각 단계의 타임스탬프를 기록해야 합니다. Linux 커널은 SO_TIMESTAMPING 소켓 옵션을 통해 소프트웨어/하드웨어 타임스탬프를 제공합니다.

/* SO_TIMESTAMPING으로 패킷별 정밀 타임스탬프 수집 */
int flags = SOF_TIMESTAMPING_TX_SOFTWARE |
           SOF_TIMESTAMPING_RX_SOFTWARE |
           SOF_TIMESTAMPING_SOFTWARE |
           SOF_TIMESTAMPING_TX_HARDWARE |
           SOF_TIMESTAMPING_RX_HARDWARE |
           SOF_TIMESTAMPING_RAW_HARDWARE;

setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &flags, sizeof(flags));

주요 추적 지점과 tracepoint

추적 지점tracepoint / kprobe측정 내용
sk_buff 할당skb:skb_copy_datagram_iovec유저 버퍼 복사 시점
qdisc enqueueqdisc:qdisc_enqueueTC 큐잉 지연
드라이버 전송net:net_dev_start_xmit드라이버 진입 시점
NIC 전송 완료net:net_dev_xmitHW 전송 완료
패킷 수신net:netif_receive_skb소프트웨어 수신 시점
패킷 드롭skb:kfree_skb드롭 위치와 원인
TCP 재전송tcp:tcp_retransmit_skb재전송 발생 시점
TCP 상태 전이sock:inet_sock_set_state소켓 상태 변경
# ftrace로 패킷 경로 추적
echo 1 > /sys/kernel/debug/tracing/events/net/netif_receive_skb/enable
echo 1 > /sys/kernel/debug/tracing/events/net/net_dev_start_xmit/enable
echo 1 > /sys/kernel/debug/tracing/events/skb/kfree_skb/enable
cat /sys/kernel/debug/tracing/trace_pipe

# bpftrace로 TCP 재전송 추적
bpftrace -e 'tracepoint:tcp:tcp_retransmit_skb {
    printf("retransmit: sport=%d dport=%d state=%d\n",
           args->sport, args->dport, args->state);
}'

# perf로 패킷 드롭 위치 추적
perf record -g -e skb:kfree_skb -- sleep 10
perf script | head -50

sk_buff 구조체

struct sk_buff는 네트워크 패킷을 표현하는 핵심 자료구조입니다. head/data/tail/end 4개 포인터로 버퍼를 관리하며, 각 프로토콜 계층이 skb_push()/skb_pull()로 헤더를 추가/제거합니다. 소속 소켓(sk), 네트워크 디바이스(dev), 프로토콜(protocol), 계층별 헤더 오프셋(Offset) 등 메타데이터를 포함합니다.

sk_buff 메모리 레이아웃: head/data/tail/end 포인터와 헤더 조작 함수 sk_buff 메모리 레이아웃 headroom 패킷 데이터 (L2 + L3 + L4 + Payload) tailroom head data tail end skb_push() (헤더 추가) skb_pull() (헤더 제거) skb_put() (데이터 추가) sk_buff 메타데이터: sk, dev, protocol, transport_header, network_header, mac_header cb[48] (프로토콜별 제어 블록), tstamp, mark, priority, skb_iif, hash ...
sk_buff 버퍼 포인터와 헤더 조작 함수

주요 sk_buff 조작 함수

함수동작사용 시점
alloc_skb(size, gfp)sk_buff + 데이터 버퍼 할당패킷 생성 시
skb_reserve(skb, len)data/tail을 len만큼 전진 (headroom 확보)드라이버 초기화, 헤더 공간 예약
skb_put(skb, len)tail을 len만큼 전진 (데이터 추가)패킷 데이터 기록
skb_push(skb, len)data를 len만큼 후퇴 (헤더 추가)L3/L2 헤더 추가 (송신)
skb_pull(skb, len)data를 len만큼 전진 (헤더 제거)L2/L3 헤더 파싱 (수신)
skb_clone(skb, gfp)메타데이터만 복제, 데이터 공유tcpdump, Netfilter 복제
skb_copy(skb, gfp)메타데이터 + 데이터 전체 복제데이터 수정이 필요한 경우
kfree_skb(skb)참조 카운트(Reference Count) 감소, 0이면 해제드롭, 처리 완료
consume_skb(skb)정상 소비로 해제 (drop_monitor 무시)정상 처리 완료
상세 문서: sk_buff의 메모리 레이아웃, 헤더 포인터 연산, 복제/참조 메커니즘, GSO/GRO 연동, 성능 패턴 등은 sk_buff 문서를 참고하세요.

sk_buff 할당 내부 구현

alloc_skb()는 네트워크 스택에서 가장 빈번하게 호출되는 할당 함수입니다. sk_buff 메타데이터 구조체와 데이터 버퍼를 별도로 할당하며, 캐시 효율성을 위해 SLAB/SLUB 할당자의 전용 캐시(skbuff_head_cache)를 사용합니다.

/* net/core/skbuff.c — alloc_skb() 내부 구현 (간략화) */
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
                            int flags, int node)
{
    struct sk_buff *skb;
    u8 *data;
    unsigned int osize;

    /* 1. sk_buff 메타데이터 할당 (전용 kmem_cache) */
    skb = kmem_cache_alloc_node(skbuff_head_cache,
                                gfp_mask & ~GFP_DMA, node);
    if (unlikely(!skb))
        return NULL;

    /* 2. 데이터 버퍼 할당 (size + skb_shared_info 크기) */
    size = SKB_DATA_ALIGN(size);
    size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
    data = kmalloc_reserve(size, gfp_mask, node, &osize);
    if (unlikely(!data))
        goto nodata;

    size = osize - SKB_DATA_ALIGN(sizeof(struct skb_shared_info));

    /* 3. sk_buff 필드 초기화 */
    skb->head = data;
    skb->data = data;
    skb_reset_tail_pointer(skb);
    skb->end = skb->tail + size;
    skb->mac_header = (typeof(skb->mac_header))~0U;
    skb->transport_header = (typeof(skb->transport_header))~0U;

    /* 4. skb_shared_info 초기화 (end 포인터 직후) */
    struct skb_shared_info *shinfo = skb_shinfo(skb);
    memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
    atomic_set(&shinfo->dataref, 1);

    skb_set_kcov_handle(skb, kcov_common_handle());
    return skb;
}
코드 설명
  • 10-11행skbuff_head_cachestruct sk_buff 전용 SLAB 캐시입니다. 네트워크 스택에서 가장 빈번한 할당 대상이므로, 전용 캐시를 사용하여 할당/해제 속도를 최적화합니다.
  • 16-18행데이터 버퍼 크기는 요청 크기에 skb_shared_info 크기를 더한 값으로 정렬합니다. skb_shared_info는 데이터 버퍼 끝에 위치하며, 프래그먼트 정보, GSO 메타데이터 등을 저장합니다.
  • 25-28행headdata를 버퍼 시작으로 설정하고, tail도 시작 위치로 리셋합니다. end는 데이터 영역의 끝(skb_shared_info 직전)을 가리킵니다.
  • 33-35행skb_shinfo(skb) 매크로는 skb->end 위치의 skb_shared_info를 반환합니다. dataref를 1로 초기화하여 참조 카운트를 설정합니다.

Paged Data와 skb_shared_info

skb_shared_info는 sk_buff의 데이터 버퍼 끝(end 포인터 위치)에 배치되는 메타데이터 구조체입니다. 선형 데이터 영역 외에 페이지 기반 데이터(paged data), GSO 정보, frag_list를 관리합니다.

/* include/linux/skbuff.h — skb_shared_info 구조체 */
struct skb_shared_info {
    __u8            flags;           /* SKBFL_* 플래그 */
    __u8            meta_len;        /* 메타데이터 길이 */
    __u8            nr_frags;        /* frags[] 사용 수 (최대 17) */
    __u8            tx_flags;        /* SKBTX_* 플래그 */
    unsigned short  gso_size;        /* GSO 세그먼트 크기 (MSS) */
    unsigned short  gso_segs;        /* GSO 세그먼트 수 */
    unsigned int    gso_type;        /* SKB_GSO_* 플래그 */
    struct sk_buff  *frag_list;      /* IP 단편/GRO 리스트 */
    struct skb_shared_hwtstamps hwtstamps; /* HW 타임스탬프 */
    atomic_t        dataref;         /* 데이터 참조 카운트 */
    unsigned int    xdp_frags_size;  /* XDP 프래그먼트 총 크기 */

    /* Paged data — 각 프래그먼트는 page + offset + size */
    skb_frag_t      frags[MAX_SKB_FRAGS]; /* 최대 17개 */
};

/* skb_frag_t — 페이지 프래그먼트 */
typedef struct skb_frag {
    struct {
        struct page *p;       /* 물리 페이지 */
    } bv_page;
    __u32   bv_offset;              /* 페이지 내 오프셋 */
    __u32   bv_len;                 /* 데이터 길이 */
} skb_frag_t;
코드 설명
  • 5행nr_frags는 현재 사용 중인 페이지 프래그먼트 수입니다. MAX_SKB_FRAGS는 기본 17로, 하나의 sk_buff가 최대 17개의 페이지 프래그먼트를 가질 수 있습니다.
  • 7-9행GSO 관련 필드들입니다. gso_size는 세그먼트당 페이로드 크기(MSS), gso_segs는 세그먼트 수, gso_type은 프로토콜별 GSO 타입입니다.
  • 10행frag_list는 IP 단편화로 생성된 프래그먼트 체인이나 GRO로 병합된 sk_buff 리스트를 가리킵니다.
  • 12행dataref는 데이터 버퍼의 참조 카운트입니다. skb_clone() 시 이 값이 증가하여 여러 sk_buff가 동일 데이터를 공유할 수 있습니다.
  • 16행frags[] 배열의 각 요소는 하나의 페이지 프래그먼트를 나타냅니다. DMA scatter-gather 전송에서 각 프래그먼트가 별도의 DMA 디스크립터에 매핑됩니다.

sk_buff 참조 카운트와 수명 관리

sk_buff에는 두 가지 참조 카운트가 존재합니다: skb->users(sk_buff 구조체 자체)와 skb_shared_info->dataref(데이터 버퍼)입니다. 이 이중 참조 모델은 메타데이터와 데이터의 독립적인 수명 관리를 가능하게 합니다.

/* sk_buff 참조 카운트 관리 함수들 */

/* skb_get() — sk_buff 참조 카운트 증가 */
static inline struct sk_buff *skb_get(struct sk_buff *skb)
{
    refcount_inc(&skb->users);
    return skb;
}

/* kfree_skb() — 드롭으로 인한 해제 (drop_monitor 추적) */
void kfree_skb_reason(struct sk_buff *skb,
                      enum skb_drop_reason reason)
{
    if (!skb_unref(skb))
        return;

    /* drop_reason 추적 포인트 발생 */
    trace_kfree_skb(skb, __builtin_return_address(0), reason);
    __kfree_skb(skb);
}

/* consume_skb() — 정상 소비로 해제 (drop_monitor 무시) */
void consume_skb(struct sk_buff *skb)
{
    if (!skb_unref(skb))
        return;

    /* consume 추적 포인트 발생 (drop으로 기록하지 않음) */
    trace_consume_skb(skb);
    __kfree_skb(skb);
}
코드 설명
  • 4-8행skb_get()은 sk_buff의 users 참조 카운트를 증가시킵니다. tcpdump(AF_PACKET)에서 패킷 복사본을 유지할 때, Netfilter에서 패킷을 검사할 때 등에 사용됩니다.
  • 11-20행kfree_skb_reason()은 패킷이 드롭된 경우에 사용됩니다. trace_kfree_skb 추적 포인트를 발생시켜 drop_monitor 도구나 perf로 드롭 원인과 위치를 추적할 수 있습니다.
  • 23-31행consume_skb()는 패킷이 정상적으로 처리 완료된 경우에 사용됩니다. trace_consume_skb를 발생시키며 drop_monitor에 기록되지 않습니다. TX 완료, recv() 복사 후 해제 등에서 사용합니다.
드롭 원인 추적: 커널 5.17부터 enum skb_drop_reason이 도입되어 드롭 원인을 세분화합니다. SKB_DROP_REASON_NOT_SPECIFIED, SKB_DROP_REASON_NO_SOCKET, SKB_DROP_REASON_TCP_OLD_ACK 등 수십 가지 원인을 구분할 수 있습니다. perf trace --event skb:kfree_skb로 추적하면 드롭 위치와 원인을 함께 확인할 수 있습니다.

Page Pool 프레임워크

Page Pool은 NIC 드라이버의 RX 경로에서 페이지 할당/해제 오버헤드를 줄이기 위한 페이지 재활용 프레임워크입니다. 페이지를 DMA 매핑 상태로 캐싱하여, 매 패킷마다 alloc_page()/dma_map_page()를 호출하는 비용을 제거합니다.

/* include/net/page_pool/types.h — Page Pool 생성 */
struct page_pool_params {
    unsigned int   flags;           /* PP_FLAG_* */
    unsigned int   order;           /* 페이지 order (보통 0) */
    unsigned int   pool_size;       /* 풀 크기 (Ring Buffer 크기) */
    int            nid;             /* NUMA 노드 */
    struct device  *dev;            /* DMA 매핑용 디바이스 */
    enum dma_data_direction dma_dir; /* DMA_FROM_DEVICE */
    unsigned int   max_len;         /* 최대 버퍼 길이 */
    unsigned int   offset;          /* headroom 오프셋 */
};

/* 드라이버 초기화 시 Page Pool 생성 */
struct page_pool *pool = page_pool_create(&params);

/* NAPI poll에서 페이지 할당 (DMA 매핑 포함) */
struct page *page = page_pool_dev_alloc_pages(pool);

/* 패킷 처리 완료 후 페이지 반환 (재활용) */
page_pool_put_full_page(pool, page, false);

/* sk_buff에서 Page Pool 페이지 사용 시 */
skb_mark_for_recycle(skb);  /* consume_skb 시 자동 재활용 */
코드 설명
  • 3행PP_FLAG_DMA_MAP은 페이지 할당 시 자동으로 DMA 매핑을 수행합니다. PP_FLAG_DMA_SYNC_DEV는 재활용 시 DMA 동기화도 자동 처리합니다.
  • 14행page_pool_create()는 per-CPU 캐시와 공유 풀을 초기화합니다. per-CPU 캐시는 락 없이 빠르게 할당/반환할 수 있습니다.
  • 17행page_pool_dev_alloc_pages()는 먼저 per-CPU 캐시에서 페이지를 찾고, 없으면 공유 풀, 최후에는 페이지 할당자(alloc_pages_node())를 사용합니다.
  • 20행드라이버가 직접 페이지를 반환하면 재활용 경로로 진입합니다. DMA 매핑이 유지되므로 다음 RX에서 재매핑이 불필요합니다.
  • 23행skb_mark_for_recycle()은 sk_buff가 consume_skb()로 해제될 때 내부 페이지를 Page Pool로 자동 반환하도록 표시합니다.
Page Pool 페이지 재활용 흐름: 할당부터 재활용까지 Page Pool 페이지 재활용 흐름 alloc_pages_node() (최후 수단) Page Pool per-CPU 캐시 + 공유 ring (DMA 매핑 유지) 할당 NAPI poll() DMA 수신 → sk_buff 구성 skb_mark_for_recycle(skb) 네트워크 스택 처리 ip_rcv → tcp_v4_rcv → sk_receive_queue 유저 recv() 데이터 복사 consume_skb() 페이지 재활용 (DMA 매핑 유지) Page Pool 성능 이점 alloc_pages() 호출 90%+ 감소 dma_map/unmap_page() 호출 제거 NUMA 친화 페이지 재사용 보장 통계 확인 /sys/kernel/debug/page_pool/ ethtool -S eth0 | grep page_pool alloc_fast, alloc_slow, recycle_cached, recycle_ring
Page Pool의 페이지 할당-사용-재활용 전체 흐름
sk_buff 메모리 레이아웃: 선형 데이터와 paged data, skb_shared_info의 배치 sk_buff 메모리 레이아웃 상세 struct sk_buff head → 버퍼 시작 data → 유효 데이터 시작 tail → 유효 데이터 끝 end → 버퍼 끝 users (refcount) sk, dev, protocol ... headroom (skb_reserve 공간) head 선형 데이터 (Linear Data) MAC 헤더 | IP 헤더 | TCP 헤더 | 페이로드(일부) data tail tailroom (skb_put 가능 공간) end struct skb_shared_info nr_frags, gso_size, gso_segs, gso_type dataref (atomic_t), frag_list frags[0..16] (skb_frag_t 배열) Page frag[0] Page frag[1] Page frag[N] Paged Data (scatter-gather) 선형 영역: skb->len - skb->data_len Paged 영역: skb->data_len (frags[]) 총 크기: skb->len = linear + paged
sk_buff의 선형 데이터, tailroom, skb_shared_info, Paged Data의 메모리 배치

소켓 API와 커널 내부 매핑

유저 공간의 BSD 소켓 API 호출이 커널 내부에서 어떤 함수 체인으로 변환되는지를 정리합니다. 이 매핑을 알면 네트워크 문제 디버깅(Debugging) 시 커널 소스의 어디를 확인해야 하는지 바로 파악할 수 있습니다.

소켓 API와 커널 내부 함수 매핑: 유저 공간 시스템 콜에서 프로토콜별 구현까지 소켓 API - 커널 내부 함수 매핑 유저 공간 API VFS / 소켓 계층 AF_INET 계층 프로토콜 구현 socket(AF_INET, SOCK_STREAM) __sock_create() inet_create() tcp_v4_init_sock() connect(fd, addr, len) inet_stream_connect() sk->prot->connect() tcp_v4_connect() send(fd, buf, len, 0) sock_sendmsg() inet_sendmsg() tcp_sendmsg() recv(fd, buf, len, 0) sock_recvmsg() inet_recvmsg() tcp_recvmsg() listen(fd, backlog) inet_listen() inet_csk_listen() accept(fd, addr, len) inet_accept() inet_csk_accept() setsockopt(SOL_TCP, ...) sock_common_setsockopt() tcp_setsockopt() close(fd) inet_release() tcp_close() UDP: udp_sendmsg(), udp_recvmsg() / SCTP: sctp_sendmsg(), sctp_recvmsg() / RAW: raw_sendmsg(), raw_recvmsg()
BSD 소켓 API에서 프로토콜별 커널 함수까지의 호출 체인

소켓 계층 (Socket Layer)

소켓은 유저스페이스 프로세스와 커널 네트워크 스택을 연결하는 인터페이스입니다. struct socket(VFS 인터페이스)과 struct 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 */
    /* ... */
};

소켓 생성 과정

유저 공간에서 socket(AF_INET, SOCK_STREAM, 0)을 호출하면 커널 내부에서 다음 과정이 진행됩니다:

  1. sys_socket()__sock_create(): struct socket 할당
  2. inet_create(): AF_INET 패밀리에 등록된 프로토콜(TCP)에서 struct sock 할당
  3. sock_map_fd(): VFS 파일 디스크립터(File Descriptor)에 매핑하여 read()/write()/poll() 가능하게 함
  4. 프로토콜별 초기화: tcp_v4_init_sock()에서 TCP 관련 필드(혼잡 제어, 재전송 타이머(Timer) 등) 초기화
/* net/socket.c — __sock_create() 핵심 로직 (간략화) */
int __sock_create(struct net *net, int family, int type,
                 int protocol, struct socket **res, int kern)
{
    struct socket *sock;
    const struct net_proto_family *pf;

    /* 1. socket 구조체 할당 (inode 포함) */
    sock = sock_alloc();
    if (!sock)
        return -ENFILE;

    sock->type = type;

    /* 2. 주소 패밀리별 핸들러 조회 */
    pf = rcu_dereference(net_families[family]);
    /* AF_INET → inet_family_ops */
    /* AF_UNIX → unix_family_ops */

    /* 3. 패밀리별 create 호출 */
    err = pf->create(net, sock, protocol, kern);
    /* inet_create(): struct sock 할당, 프로토콜 ops 연결 */

    /* 4. BPF cgroup 소켓 생성 훅 */
    err = security_socket_post_create(sock, family, type, protocol, kern);

    *res = sock;
    return 0;
}
코드 설명
  • 9행sock_alloc()은 sockfs에 inode를 생성하고, struct socket을 할당합니다. 소켓은 VFS에서 특수 파일로 취급되어 read()/write()/poll()이 가능합니다.
  • 16행net_families[] 배열에서 주소 패밀리(AF_INET, AF_UNIX 등)에 해당하는 핸들러를 찾습니다. 각 패밀리는 sock_register()로 등록됩니다.
  • 21행AF_INET의 경우 inet_create()가 호출되어 struct sock을 할당하고, 소켓 타입(SOCK_STREAM, SOCK_DGRAM)에 따라 TCP/UDP의 proto_ops를 연결합니다.
  • 24행LSM(Linux Security Module)과 BPF cgroup 소켓 프로그램이 소켓 생성을 검사합니다. 정책에 위반되면 소켓 생성이 거부됩니다.

소켓-VFS 연결 구조

소켓은 VFS의 파일 디스크립터를 통해 유저 공간에 노출됩니다. socket() 시스템 콜은 내부적으로 sock_alloc()으로 inode를 생성하고, sock_map_fd()로 파일 디스크립터에 매핑합니다. 이 구조 덕분에 select()/poll()/epoll()로 소켓을 다른 파일과 함께 멀티플렉싱할 수 있습니다.

/* VFS → 소켓 연결 체인 */
/* fd → struct file → file->private_data → struct socket */
/* struct socket → socket->sk → struct sock */
/* struct sock → (struct tcp_sock 또는 struct udp_sock) */

/* 소켓 파일 연산: 소켓도 파일처럼 동작합니다 */
static const struct file_operations socket_file_ops = {
    .owner      = THIS_MODULE,
    .llseek     = no_llseek,       /* seek 미지원 */
    .read_iter  = sock_read_iter,  /* read() → recvmsg() */
    .write_iter = sock_write_iter, /* write() → sendmsg() */
    .poll       = sock_poll,       /* poll/epoll 지원 */
    .unlocked_ioctl = sock_ioctl,  /* ioctl 지원 */
    .mmap       = sock_mmap,       /* mmap (AF_PACKET 등) */
    .release    = sock_close,      /* close() → 소켓 정리 */
    .fasync     = sock_fasync,     /* SIGIO 비동기 알림 */
    .sendpage   = sock_sendpage,   /* sendfile/splice 지원 */
    .splice_write = generic_splice_sendpage,
    .splice_read  = sock_splice_read,
};
코드 설명
  • 1-4행유저 공간의 fd는 VFS struct file을 가리키고, private_datastruct socket에 접근합니다. socket->sk로 프로토콜별 struct sock(TCP의 경우 struct tcp_sock)에 접근합니다.
  • 10-11행read()/write() 시스템 콜이 소켓에 사용되면 내부적으로 recvmsg()/sendmsg()로 변환됩니다. 이 덕분에 소켓을 파이프나 파일과 동일한 API로 다룰 수 있습니다.
  • 12행sock_poll()은 소켓의 읽기/쓰기 가능 상태를 확인합니다. epoll에서 EPOLLIN/EPOLLOUT 이벤트를 감시할 때 이 함수가 호출됩니다.
  • 17행sock_sendpage()sendfile() 시스템 콜에서 사용됩니다. 파일의 페이지를 소켓의 sk_buff에 직접 매핑하여 제로카피 전송을 수행합니다.

주요 소켓 주소 패밀리

주소 패밀리용도소켓 타입핵심 소스
AF_INETIPv4 네트워킹STREAM, DGRAM, RAWnet/ipv4/af_inet.c
AF_INET6IPv6 네트워킹STREAM, DGRAM, RAWnet/ipv6/af_inet6.c
AF_UNIX로컬 IPCSTREAM, DGRAM, SEQPACKETnet/unix/af_unix.c
AF_PACKET원시 패킷 캡처RAW, DGRAMnet/packet/af_packet.c
AF_NETLINK커널-유저 통신RAW, DGRAMnet/netlink/af_netlink.c
AF_XDP고성능 패킷 처리RAWnet/xdp/xsk.c

소켓 메모리 관리

소켓은 송신/수신 버퍼 크기를 sk_sndbuf/sk_rcvbuf로 제한하며, 실제 사용량을 sk_wmem_alloc/sk_rmem_alloc으로 추적합니다. TCP의 경우 전역 메모리 압력(memory pressure) 메커니즘이 모든 TCP 소켓의 메모리 할당을 조절합니다.

/* net/core/sock.c — 소켓 메모리 회계 핵심 로직 */

/* 수신 방향: sk_buff를 수신 큐에 추가할 때 */
int __sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
    unsigned long flags;
    int err = 0;

    /* sk_rmem_alloc + skb->truesize가 sk_rcvbuf 초과 시 드롭 */
    if (atomic_read(&sk->sk_rmem_alloc) + skb->truesize >=
        (unsigned int)READ_ONCE(sk->sk_rcvbuf)) {
        err = -ENOMEM;
        goto out;
    }

    /* skb->truesize만큼 수신 메모리 사용량 증가 */
    skb_set_owner_r(skb, sk);
    __skb_queue_tail(&sk->sk_receive_queue, skb);
    sk->sk_data_ready(sk);  /* 대기 프로세스 깨움 */
out:
    return err;
}

/* TCP 전역 메모리 압력 (sysctl tcp_mem) */
/* tcp_mem[0]: 정상 — 모든 소켓 자유롭게 할당 */
/* tcp_mem[1]: 압력 — 새 할당 제한 시작 */
/* tcp_mem[2]: 한계 — 새 할당 거부 (OOM) */
long sysctl_tcp_mem[3];  /* 페이지 단위 */
코드 설명
  • 10-11행현재 수신 메모리 사용량(sk_rmem_alloc)에 새 sk_buff의 truesize(메타데이터 + 데이터 포함 실제 크기)를 더한 값이 수신 버퍼 한도(sk_rcvbuf)를 초과하면 패킷을 드롭합니다.
  • 17행skb_set_owner_r()은 sk_buff의 소유권을 소켓에 할당하고, sk_rmem_allocskb->truesize만큼 증가시킵니다.
  • 19행sk_data_ready() 콜백은 기본적으로 sock_def_readable()로, sk_wq에 대기 중인 프로세스를 깨웁니다. epoll 환경에서는 ep_poll_callback()이 호출됩니다.
  • 24-27행TCP 전역 메모리 한도는 sysctl net.ipv4.tcp_mem으로 설정합니다. 시스템의 총 TCP 소켓 메모리 사용량이 이 임계값을 초과하면 memory pressure 상태에 진입합니다.
# 소켓 메모리 관련 sysctl 확인
sysctl net.ipv4.tcp_mem              # 전역 TCP 메모리 한도 (페이지)
sysctl net.ipv4.tcp_rmem             # 소켓당 수신 버퍼 [min default max]
sysctl net.ipv4.tcp_wmem             # 소켓당 송신 버퍼 [min default max]
sysctl net.core.rmem_max             # SO_RCVBUF 최대값
sysctl net.core.wmem_max             # SO_SNDBUF 최대값

# 현재 TCP 메모리 사용량 확인
cat /proc/net/sockstat | grep TCP    # TCP: inuse, orphan, tw, alloc, mem

TCP 자동 버퍼 튜닝 (Autotuning)

Linux TCP는 수신/송신 버퍼 크기를 연결의 RTT와 대역폭에 따라 자동으로 조정합니다. tcp_rmem/tcp_wmem의 min/default/max 값이 자동 튜닝의 범위를 결정합니다.

/* net/ipv4/tcp_input.c — TCP 자동 수신 버퍼 조정 (간략화) */
void tcp_rcv_space_adjust(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int time, space;

    /* RTT 동안 수신된 데이터량으로 필요 버퍼 추정 */
    time = tcp_stamp_us_delta(tp->tcp_mstamp, tp->rcvq_space.time);
    if (time < (tp->rcv_rtt_est.rtt_us >> 3))
        return;

    /* BDP (Bandwidth-Delay Product) 계산 */
    space = 2 * (tp->copied_seq - tp->rcvq_space.seq);
    space = max(tp->rcvq_space.space, space);

    /* sk_rcvbuf 조정 (tcp_rmem[2]가 상한) */
    if (space > READ_ONCE(sk->sk_rcvbuf)) {
        int new_rcvbuf = min(space, READ_ONCE(
            sock_net(sk)->ipv4.sysctl_tcp_rmem[2]));
        WRITE_ONCE(sk->sk_rcvbuf, new_rcvbuf);
    }

    /* 수신 윈도우 스케일링도 같이 조정 */
    tp->rcvq_space.space = space;
    tp->rcvq_space.seq = tp->copied_seq;
    tp->rcvq_space.time = tp->tcp_mstamp;
}
코드 설명
  • 8-10행RTT(Round Trip Time)의 1/8 시간이 경과해야 조정을 시도합니다. 너무 자주 조정하면 오버헤드가 커집니다.
  • 13행BDP(Bandwidth-Delay Product)를 추정합니다. RTT 동안 수신된 데이터량의 2배를 필요 버퍼 크기로 계산합니다. 이를 통해 대역폭이 높거나 RTT가 긴 연결에서 자동으로 버퍼를 확대합니다.
  • 17-20행sk_rcvbuf를 추정 BDP까지 확대하되, tcp_rmem[2](max) 값을 초과하지 않습니다. 전역 메모리 압력(memory pressure) 상태에서는 확대가 제한됩니다.
cgroup 네트워크 메모리 제어: cgroup v2에서는 memory.max으로 컨테이너의 전체 메모리를 제한할 때 소켓 버퍼 메모리도 포함됩니다. memory.stat에서 sock 필드가 소켓 메모리 사용량을 보여줍니다. TCP 자동 튜닝은 cgroup 메모리 한도를 인식하여, 한도에 가까우면 버퍼 확대를 자제합니다. memory.eventssock_max 이벤트로 소켓 메모리 한도 도달을 모니터링할 수 있습니다.
# TCP 자동 튜닝 확인
sysctl net.ipv4.tcp_moderate_rcvbuf    # 1 = 자동 튜닝 활성 (기본)

# 특정 연결의 현재 버퍼 크기 확인
ss -tim dst 10.0.0.1:443
# 출력: skmem:(r0,rb6291456,t0,tb87380,...)
# rb = 현재 수신 버퍼 크기 (자동 조정됨)
# tb = 현재 송신 버퍼 크기

# 자동 튜닝 비활성화 (특정 소켓에서)
# setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
# SO_RCVBUF를 명시적으로 설정하면 해당 소켓의 자동 튜닝이 비활성화됩니다

# cgroup v2 소켓 메모리 확인
cat /sys/fs/cgroup/system.slice/docker-<ID>.scope/memory.stat | grep sock
# sock 12345678  (소켓 버퍼 사용량, 바이트)

SO_REUSEPORT와 eBPF 소켓 선택

SO_REUSEPORT는 여러 소켓이 동일한 주소:포트에 바인드할 수 있게 하여, 수신 연결을 여러 프로세스/스레드에 분산합니다. 기본적으로 해시 기반으로 소켓을 선택하며, BPF 프로그램을 부착하여 커스텀 선택 로직을 구현할 수 있습니다.

/* SO_REUSEPORT 기본 사용법 */
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
bind(fd, addr, addrlen);
listen(fd, backlog);

/* BPF 소켓 선택 프로그램 부착 */
/* net/core/sock_reuseport.c — reuseport_select_sock() */
struct sock *reuseport_select_sock(struct sock *sk,
    u32 hash, struct sk_buff *skb, int hdr_len)
{
    struct sock_reuseport *reuse = rcu_dereference(sk->sk_reuseport_cb);
    struct bpf_prog *prog;

    prog = rcu_dereference(reuse->prog);
    if (prog) {
        /* BPF 프로그램이 소켓 인덱스를 반환 */
        u32 index = bpf_prog_run_save_cb(prog, skb);
        sk2 = reuse->socks[index];
    } else {
        /* 기본: 해시 기반 소켓 선택 */
        sk2 = reuse->socks[reciprocal_scale(hash, reuse->num_socks)];
    }
    return sk2;
}
코드 설명
  • 1-5행여러 프로세스가 동일한 주소:포트에 SO_REUSEPORT를 설정하고 바인드하면, 커널이 수신 패킷을 자동으로 분산합니다.
  • 15-19행BPF 프로그램이 부착되어 있으면, 프로그램이 패킷 내용(소스 IP, 쿠키 등)을 검사하여 소켓 인덱스를 반환합니다. 이를 통해 특정 클라이언트의 연결을 특정 워커에 고정(session affinity)할 수 있습니다.
  • 22행BPF 프로그램이 없으면 패킷의 4-tuple 해시를 사용하여 소켓을 선택합니다. reciprocal_scale()은 나눗셈 없이 해시를 소켓 수 범위로 스케일합니다.

epoll과 소켓 통합

epoll은 소켓의 이벤트(수신 데이터, 연결 완료, 쓰기 가능 등)를 효율적으로 감시하는 I/O 멀티플렉싱 메커니즘입니다. 소켓의 sk_data_ready, sk_write_space 등의 콜백이 epoll의 대기 큐와 연결됩니다.

/* fs/eventpoll.c + net/core/sock.c — epoll-소켓 연동 경로 */

/* 1. epoll_ctl(EPOLL_CTL_ADD) 시 */
/*    → ep_insert() → ep_item_poll() → sock_poll() */
/*    → init_waitqueue_func_entry(&ep_entry->wait, ep_poll_callback) */
/*    → add_wait_queue(sk_sleep(sk), &ep_entry->wait) */

/* 2. 패킷 수신 시 콜백 체인 */
/*    tcp_v4_rcv() → tcp_data_queue() */
/*    → sk->sk_data_ready(sk) */
/*    → sock_def_readable() */
/*    → wake_up_interruptible_sync_poll(sk_wq, EPOLLIN) */
/*    → ep_poll_callback() */
/*    → ep를 rdllist에 추가 → epoll_wait() 반환 */

/* 3. Edge-Triggered vs Level-Triggered */
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;   /* Edge-Triggered */
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);

/* ET 모드에서는 데이터를 모두 읽을 때까지 반복해야 합니다 */
/* 그렇지 않으면 새 데이터 도착까지 알림을 받지 못합니다 */
코드 설명
  • 3-6행epoll_ctl(EPOLL_CTL_ADD)는 소켓의 대기 큐에 ep_poll_callback을 등록합니다. 이후 소켓 상태 변경 시 이 콜백이 호출됩니다.
  • 8-14행패킷이 TCP 수신 큐에 삽입되면 sk_data_readysock_def_readable()ep_poll_callback() 체인으로 epoll에 통지됩니다. ep_poll_callback()은 해당 epoll 항목을 ready list에 추가하고, epoll_wait()에서 대기 중인 프로세스를 깨웁니다.
  • 17-19행EPOLLET(Edge-Triggered) 모드에서는 상태가 변경될 때만 알림을 받습니다. Level-Triggered(기본)에서는 조건이 충족되는 동안 계속 알림을 받습니다.

소켓 해시 테이블 조회

수신 패킷이 전송 계층에 도달하면, 4-tuple(소스 IP/포트, 목적지 IP/포트) 해시를 기반으로 대상 소켓을 찾습니다. TCP는 inet_hashinfo 해시 테이블에서, UDP는 udp_table에서 조회합니다.

/* net/ipv4/inet_hashtables.c — TCP 소켓 조회 (간략화) */
struct sock *__inet_lookup_established(
    struct net *net,
    struct inet_hashinfo *hashinfo,
    const __be32 saddr, const __be16 sport,
    const __be32 daddr, const u16 hnum,
    const int dif, const int sdif)
{
    /* 1. 4-tuple 해시 계산 */
    unsigned int hash = inet_ehashfn(net, daddr, hnum,
                                      saddr, sport);
    unsigned int slot = hash & hashinfo->ehash_mask;

    /* 2. 해시 버킷에서 ESTABLISHED 소켓 검색 */
    struct inet_ehash_bucket *head = &hashinfo->ehash[slot];
    sk_for_each_rcu(sk, &head->chain) {
        if (sk->sk_hash != hash)
            continue;
        if (inet_match(net, sk, saddr, daddr, sport, hnum, dif, sdif))
            return sk;
    }

    /* 3. 없으면 LISTEN 해시에서 검색 */
    return __inet_lookup_listener(net, hashinfo, skb,
                                  doff, saddr, sport, daddr, hnum, dif, sdif);
}
코드 설명
  • 10-12행inet_ehashfn()은 4-tuple을 해시합니다. Jenkins 해시 또는 SipHash를 사용하여 해시 충돌 공격을 방지합니다. 해시 시드는 부팅 시 랜덤으로 생성됩니다.
  • 15-21행ESTABLISHED 해시 테이블(ehash)에서 4-tuple이 정확히 일치하는 소켓을 찾습니다. RCU 보호 하에 조회하므로 락 없이 빠르게 검색할 수 있습니다.
  • 24-26행ESTABLISHED 소켓이 없으면 LISTEN 해시 테이블에서 목적지 포트로 리스닝 소켓을 찾습니다. SO_REUSEPORT가 설정된 경우 여러 리스닝 소켓 중 하나를 선택합니다.
TCP 소켓 해시 테이블 조회 구조: ESTABLISHED hash와 LISTEN hash TCP 소켓 해시 테이블 (inet_hashinfo) 구조 수신 패킷 (4-tuple) inet_ehashfn() SipHash(saddr,daddr,sport,dport) ehash (ESTABLISHED 해시 테이블) bucket[0] bucket[1] bucket[N-1] 체인: sock → sock → sock (RCU 보호) 크기: ehash_mask + 1 (보통 수만~수십만) 미발견 lhash2 (LISTEN 해시 테이블) port 80 port 443 port 8080 목적지 포트 기반 해시 (dport 전용) SO_REUSEPORT: 동일 포트 다중 소켓 Early Demux 최적화 ip_rcv_finish()에서 먼저 소켓 조회 → dst_entry 캐시 재사용 → FIB 생략 조회 결과 활용 발견: tcp_v4_do_rcv(sk, skb) 미발견: tcp_v4_send_reset() + 드롭
TCP 소켓 해시 테이블 조회: ESTABLISHED hash → LISTEN hash 순서
# TCP 해시 테이블 크기 확인
dmesg | grep "TCP established hash"
# TCP established hash table entries: 524288 (order: 10, 4194304 bytes)

# 현재 소켓 통계
ss -s                                 # 전체 소켓 요약
cat /proc/net/sockstat                # TCP/UDP 소켓 상세 통계
cat /proc/net/sockstat6               # IPv6 소켓 통계

# LISTEN 소켓의 accept queue 상태
ss -lnt | awk '{print $2, $3, $4}'    # Recv-Q Send-Q Local Address
# Recv-Q = 현재 accept queue 대기 수
# Send-Q = accept queue 최대 크기 (backlog)

네트워크 계층 (IP)

네트워크 계층은 패킷의 소스/목적지 주소 관리, 라우팅, 단편화/재조립을 담당합니다. Linux 커널은 IPv4(net/ipv4/)와 IPv6(net/ipv6/)를 독립적으로 구현하며, 듀얼 스택으로 동시 운영합니다.

IPv4 수신 처리 흐름

/* 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 net *net = dev_net(dev);

    /* 1. IP 헤더 길이, 버전, 체크섬 검증 */
    /* 2. pskb_may_pull()로 헤더 접근 보장 */

    /* 3. Netfilter NF_INET_PRE_ROUTING 훅 통과 */
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
                  net, NULL, skb, dev, NULL,
                  ip_rcv_finish);
}

/* ip_rcv_finish → ip_route_input_noref()로 FIB 조회 */
/* 결과에 따라: */
/*   로컬 대상 → ip_local_deliver() → L4 전달 */
/*   포워딩   → ip_forward() → ip_output() */
/*   멀티캐스트 → ip_mr_input() */

IP 단편화와 재조립

MTU(Maximum Transmission Unit)보다 큰 패킷은 단편화가 필요합니다. 송신 측에서 ip_fragment()가 수행하며, 수신 측에서 ip_defrag()가 단편을 재조립합니다. DF(Don't Fragment) 비트가 설정된 경우 단편화 대신 ICMP "Fragmentation Needed" 메시지를 반환합니다. Path MTU Discovery는 이 메커니즘을 활용합니다.

항목IPv4IPv6
주소 길이32비트 (4바이트)128비트 (16바이트)
헤더 크기가변 (20~60바이트)고정 40바이트 + 확장 헤더
단편화라우터/호스트 모두 가능소스 호스트만 가능
체크섬(Checksum)헤더 체크섬 포함헤더 체크섬 없음 (L2/L4 의존)
브로드캐스트지원미지원 (멀티캐스트로 대체)
이웃 탐색ARPNDP (ICMPv6 기반)
기본 MTU576바이트 (권장 최소)1280바이트 (필수 최소)
커널 소스net/ipv4/net/ipv6/
상세 문서: IPv4/IPv6 프로토콜 처리, IP 옵션, 단편화/재조립, 멀티캐스트, PMTU Discovery의 상세 내용은 IP (IPv4/IPv6) 문서를 참고하세요.

이웃 서브시스템 (Neighbour Subsystem)

이웃 서브시스템은 L3 주소(IP)를 L2 주소(MAC)로 변환하는 프레임워크입니다. IPv4에서는 ARP, IPv6에서는 NDP(Neighbor Discovery Protocol)를 사용합니다. struct neighbour는 이웃 항목을 나타내며, 상태 머신을 통해 주소 해석의 진행 상황을 관리합니다.

/* include/net/neighbour.h — struct neighbour 핵심 필드 */
struct neighbour {
    struct neighbour       *next;           /* 해시 체인 */
    struct neigh_table     *tbl;            /* ARP 또는 NDP 테이블 */
    struct neigh_parms     *parms;          /* 인터페이스별 파라미터 */
    unsigned long          updated;         /* 마지막 업데이트 시각 */
    rwlock_t               lock;
    refcount_t             refcnt;
    unsigned int           arp_queue_len_bytes;
    struct sk_buff_head    arp_queue;       /* 해석 대기 패킷 큐 */
    struct timer_list      timer;           /* 상태 전이 타이머 */
    unsigned long          used;            /* 마지막 사용 시각 */
    atomic_t               probes;          /* 보낸 프로브 수 */
    u8                     nud_state;       /* NUD_* 상태 */
    u8                     type;
    u8                     dead;
    u8                     protocol;
    u8                     ha[ALIGN(MAX_ADDR_LEN, sizeof(unsigned long))];
                                             /* 하드웨어 주소 (MAC) */
    struct net_device      *dev;            /* 출력 디바이스 */
    const struct neigh_ops *ops;            /* 출력 콜백 */
    u8                     primary_key[];   /* L3 주소 (IP) */
};
코드 설명
  • 10행arp_queue는 MAC 주소가 아직 해석되지 않은 상태에서 전송을 시도하는 패킷을 임시로 저장하는 큐입니다. 주소 해석이 완료되면 대기 중인 패킷을 모두 전송합니다.
  • 14행nud_state는 NUD(Neighbor Unreachability Detection) 상태입니다. NUD_INCOMPLETE, NUD_REACHABLE, NUD_STALE, NUD_DELAY, NUD_PROBE, NUD_FAILED 등의 값을 가집니다.
  • 18-19행ha는 해석된 하드웨어 주소(MAC 주소)입니다. ARP 응답이나 NDP Neighbor Advertisement를 수신하면 이 필드가 설정됩니다.
ARP/NDP 이웃 상태 머신: INCOMPLETE에서 FAILED까지의 상태 전이 ARP/NDP 이웃 상태 머신 (NUD State Machine) NONE INCOMPLETE REACHABLE STALE DELAY PROBE FAILED 요청 전송 응답 수신 타임아웃 트래픽 발생 5초 후 프로브 실패 응답 수신 최대 재시도 초과 REACHABLE 기본 타임아웃: 30초 (base_reachable_time_ms) | DELAY → PROBE: 5초 (delay_first_probe_time) 최대 유니캐스트 프로브: 3회 (ucast_solicit) | 최대 멀티캐스트 프로브: 3회 (mcast_solicit)
ARP/NDP 이웃 상태 머신 전이 다이어그램
/* net/ipv4/arp.c — arp_process() 핵심 로직 (간략화) */
static int arp_process(struct net *net, struct sock *sk,
                      struct sk_buff *skb)
{
    struct arphdr *arp = arp_hdr(skb);
    unsigned char *sha, *tha;   /* 소스/타깃 하드웨어 주소 */
    __be32 sip, tip;            /* 소스/타깃 IP 주소 */

    /* ARP 헤더에서 주소 추출 */
    arp_ptr = (unsigned char *)(arp + 1);
    sha = arp_ptr;              /* Sender Hardware Address */
    memcpy(&sip, arp_ptr + dev->addr_len, 4);  /* Sender IP */
    tha = arp_ptr + dev->addr_len + 4;          /* Target HA */
    memcpy(&tip, arp_ptr + 2 * dev->addr_len + 4, 4);

    if (arp->ar_op == htons(ARPOP_REQUEST)) {
        /* ARP Request: 대상 IP가 로컬이면 ARP Reply 전송 */
        if (inet_addr_type(net, tip) == RTN_LOCAL) {
            arp_send_dst(ARPOP_REPLY, ETH_P_ARP, sip, dev,
                         tip, sha, dev->dev_addr, sha, reply_dst);
        }
        /* Sender의 MAC-IP 매핑을 이웃 캐시에 갱신 */
        neigh_event_ns(&arp_tbl, sha, &sip, dev);
    } else if (arp->ar_op == htons(ARPOP_REPLY)) {
        /* ARP Reply: Sender의 MAC-IP 매핑 갱신 */
        n = __neigh_lookup(&arp_tbl, &sip, dev, 0);
        if (n) {
            neigh_update(n, sha, NUD_REACHABLE, ...);
            /* arp_queue의 대기 패킷 전송 */
        }
    }
}
코드 설명
  • 10-14행ARP 패킷에서 Sender/Target의 하드웨어 주소(MAC)와 프로토콜 주소(IP)를 추출합니다. ARP 헤더 직후에 이 주소들이 순서대로 배치됩니다.
  • 16-21행ARP Request를 수신하면, 대상 IP가 로컬 인터페이스에 할당된 IP인지 확인하고, 맞으면 ARP Reply를 전송합니다. 프록시 ARP가 활성화된 경우 다른 서브넷의 IP에도 응답합니다.
  • 23행neigh_event_ns()는 ARP Request의 Sender 정보로 이웃 캐시를 갱신합니다. 이를 통해 요청자의 MAC 주소를 학습합니다.
  • 26-29행ARP Reply를 수신하면 이웃 항목을 NUD_REACHABLE 상태로 업데이트합니다. arp_queue에 대기 중인 패킷들이 있으면 이 시점에 모두 전송됩니다.
/* net/core/neighbour.c — neigh_resolve_output() TX 경로 */
/* IP → 이웃 → dev_queue_xmit 출력 흐름 */
int neigh_resolve_output(struct neighbour *neigh,
                        struct sk_buff *skb)
{
    if (!neigh_event_send(neigh, skb)) {
        /* MAC 주소 해석 완료 → L2 헤더 추가 후 전송 */
        rc = dev_hard_header(skb, dev, ntohs(skb->protocol),
                             neigh->ha, NULL, skb->len);
        if (rc >= 0)
            rc = dev_queue_xmit(skb);
    }
    /* neigh_event_send()가 1을 반환하면: */
    /* ARP/NDP 요청 전송 후 skb를 arp_queue에 대기 */
    return rc;
}
코드 설명
  • 6행neigh_event_send()는 이웃 항목의 상태를 확인합니다. MAC 주소가 해석된 상태(REACHABLE/STALE)이면 0을 반환하고, 미해석 상태이면 ARP/NDP 요청을 전송하고 1을 반환합니다.
  • 8-9행dev_hard_header()neigh->ha(해석된 MAC 주소)를 사용하여 이더넷 헤더를 추가합니다.
  • 13-14행MAC 주소가 아직 없으면 패킷은 arp_queue에 대기합니다. ARP Reply가 도착하면 대기 중인 패킷이 모두 전송됩니다.
# ARP/이웃 캐시 관리 명령어
ip neigh show                          # ARP 캐시 전체 표시
ip neigh show dev eth0                 # eth0의 이웃 항목만
ip neigh add 192.168.1.1 lladdr aa:bb:cc:dd:ee:ff dev eth0  # 정적 항목 추가
ip neigh flush dev eth0                # eth0의 캐시 초기화

# 이웃 파라미터 조정
sysctl -w net.ipv4.neigh.eth0.base_reachable_time_ms=30000  # REACHABLE 타임아웃
sysctl -w net.ipv4.neigh.eth0.gc_stale_time=60              # STALE GC 시간
sysctl -w net.ipv4.neigh.default.gc_thresh3=4096            # 최대 이웃 항목 수

# IPv6 이웃 탐색
ip -6 neigh show                       # NDP 캐시

ip_rcv_finish() 내부 상세

ip_rcv_finish()는 Netfilter PRE_ROUTING 훅을 통과한 패킷의 라우팅 결정과 목적지 캐시 설정을 담당합니다. Early Demux 최적화를 통해 TCP/UDP 소켓이 이미 결정된 경우 FIB 조회를 생략할 수 있습니다.

/* net/ipv4/ip_input.c — ip_rcv_finish() 핵심 로직 (간략화) */
static int ip_rcv_finish(struct net *net, struct sock *sk,
                         struct sk_buff *skb)
{
    struct net_device *dev = skb->dev;
    int err;

    /* 1. Early Demux — 소켓 캐시에서 dst_entry 획득 */
    if (READ_ONCE(net->ipv4.sysctl_ip_early_demux) &&
        !skb_dst(skb) && !skb->sk) {
        int protocol = ip_hdr(skb)->protocol;

        /* TCP/UDP의 early_demux 콜백 호출 */
        /* 4-tuple로 소켓을 미리 찾아 dst_entry 캐시 사용 */
        ipprot = rcu_dereference(inet_protos[protocol]);
        if (ipprot && ipprot->early_demux)
            ipprot->early_demux(skb);
    }

    /* 2. dst_entry가 없으면 FIB 조회 수행 */
    if (!skb_valid_dst(skb)) {
        err = ip_route_input_noref(skb, ip_hdr(skb)->daddr,
                                   ip_hdr(skb)->saddr,
                                   ip_hdr(skb)->tos, dev);
        if (unlikely(err))
            goto drop;
    }

    /* 3. dst_entry의 input 함수 호출 */
    /*    로컬: ip_local_deliver() */
    /*    포워딩: ip_forward() */
    return dst_input(skb);
}
코드 설명
  • 9-17행Early Demux는 IP 처리 초기에 TCP/UDP 소켓을 미리 찾아서, 해당 소켓의 dst_entry 캐시를 재사용하는 최적화입니다. sysctl_ip_early_demux가 활성화(기본)되어 있으면 FIB 조회를 생략하여 패킷당 수백 ns를 절약할 수 있습니다.
  • 21-26행Early Demux가 실패하거나 비활성화된 경우 ip_route_input_noref()로 FIB(Forwarding Information Base) 조회를 수행합니다. FIB 조회 결과에 따라 dst_entryinput 함수가 ip_local_deliver(로컬) 또는 ip_forward(포워딩)로 설정됩니다.
  • 32행dst_input(skb)skb_dst(skb)->input(skb)를 호출하여 패킷을 다음 처리 단계로 전달합니다.

IP 단편화/재조립 내부

IP 단편화는 패킷이 출력 인터페이스의 MTU를 초과할 때 ip_do_fragment()에서 수행됩니다. 재조립은 수신 측에서 ip_defrag()가 단편을 수집하여 원본 패킷을 복원합니다.

/* net/ipv4/ip_output.c — ip_do_fragment() 핵심 로직 (간략화) */
int ip_do_fragment(struct net *net, struct sock *sk,
                   struct sk_buff *skb,
                   int (*output)(struct net *, struct sock *,
                                struct sk_buff *))
{
    unsigned int mtu = ip_skb_dst_mtu(sk, skb);
    unsigned int hlen = ip_hdrlen(skb);
    unsigned int left = skb->len - hlen;   /* 페이로드 길이 */
    unsigned int len = (mtu - hlen) & ~7;  /* 8바이트 정렬 */
    unsigned int offset = (ntohs(ip_hdr(skb)->frag_off) &
                           IP_OFFSET) << 3;

    /* DF 비트 설정 시 ICMP "Fragmentation Needed" 반환 */
    if (unlikely((ip_hdr(skb)->frag_off & htons(IP_DF)) &&
                 !skb->ignore_df)) {
        icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
                  htonl(mtu));
        goto fail;
    }

    /* 각 단편에 대해: */
    while (left > 0) {
        /* 단편 sk_buff 생성 */
        /* IP 헤더 복사, frag_off 설정, MF 비트 설정 */
        /* output() 콜백으로 전송 */
        left -= len;
        offset += len;
    }
}
코드 설명
  • 7-10행MTU에서 IP 헤더 길이를 빼고 8바이트 경계로 정렬하여 각 단편의 최대 페이로드 크기를 결정합니다. IP 단편의 오프셋은 8바이트 단위이므로 페이로드 크기도 8의 배수여야 합니다.
  • 15-20행DF(Don't Fragment) 비트가 설정된 경우 단편화 대신 ICMP "Fragmentation Needed" 메시지를 소스 호스트에 반환합니다. Path MTU Discovery는 이 메커니즘을 활용하여 경로의 최소 MTU를 탐색합니다.
  • 23-29행각 단편에 원본 IP 헤더를 복사하고, frag_off 필드에 오프셋과 MF(More Fragments) 비트를 설정합니다. 마지막 단편에는 MF 비트가 설정되지 않습니다.
/* net/ipv4/ip_fragment.c — ip_defrag() 재조립 핵심 (간략화) */
int ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
    struct ipq *qp;

    /* IP ID + 소스/목적지 IP로 재조립 큐 검색/생성 */
    qp = ip_find(net, ip_hdr(skb), user, vif);
    if (!qp)
        goto drop;

    /* 단편을 큐에 삽입 (오프셋 순서 유지) */
    ip_frag_queue(qp, skb);

    /* 모든 단편이 도착했는지 확인 */
    if (ip_frag_reasm(qp, skb, prev_tail, net)) {
        /* 재조립 완료 — 원본 패킷 복원 */
        return 0;
    }
    return -EINPROGRESS;  /* 아직 단편 대기 중 */
}
코드 설명
  • 7행ip_find()는 IP Identification, 소스/목적지 IP, 프로토콜을 키로 해시 테이블에서 재조립 큐를 찾거나 새로 생성합니다.
  • 12행ip_frag_queue()는 단편을 오프셋 순서대로 큐에 삽입합니다. 겹치는 단편은 탐지하여 처리하며, 보안을 위해 과도한 단편 수에 대한 제한이 있습니다.
  • 15행ip_frag_reasm()은 모든 단편이 도착했는지(첫 단편의 오프셋 0 + 마지막 단편의 MF=0 + 전체 길이 일치) 확인하고, 성공하면 하나의 sk_buff로 재조립합니다.
단편화 공격 방어: 커널은 재조립 큐에 다양한 제한을 적용합니다. net.ipv4.ipfrag_max_dist(기본 64)로 비순차 단편 수를 제한하고, ipfrag_time(기본 30초)으로 타임아웃을 설정하며, ipfrag_high_thresh/ipfrag_low_thresh로 전체 메모리 사용량을 제한합니다. 메모리 임계값 초과 시 ip_evictor()가 가장 오래된 큐를 강제로 제거합니다.
# IP 단편화 관련 sysctl 설정
sysctl net.ipv4.ipfrag_time              # 재조립 타임아웃 (기본 30초)
sysctl net.ipv4.ipfrag_high_thresh       # 메모리 상한 (기본 4MB)
sysctl net.ipv4.ipfrag_low_thresh        # 메모리 하한 (기본 3MB)
sysctl net.ipv4.ipfrag_max_dist          # 비순차 단편 최대 수 (기본 64)

# 단편화 통계 확인
cat /proc/net/snmp | grep -A1 Ip
# ReasmTimeout ReasmReqds ReasmOKs ReasmFails
# FragOKs FragFails FragCreates

nstat -az | grep -i frag
# IpReasmReqds — 재조립 요청 수
# IpReasmOKs — 재조립 성공 수
# IpReasmFails — 재조립 실패 수
# IpFragOKs — 단편화 성공 수
# IpFragFails — 단편화 실패 수 (DF 비트 등)

# PMTU Discovery 관련
sysctl net.ipv4.ip_no_pmtu_disc          # 0: PMTUD 활성 (기본)
sysctl net.ipv4.tcp_mtu_probing          # 0: 비활성, 1: ICMP 차단 시만, 2: 항상

# 경로 MTU 캐시 확인
ip route get 10.0.0.1                    # mtu 필드 확인
ip route show cache                      # PMTU 캐시된 경로

IP 옵션과 확장 헤더

IPv4 옵션과 IPv6 확장 헤더는 패킷에 추가 정보를 전달하는 메커니즘입니다. 보안과 성능에 영향을 미치므로, 커널은 각 옵션을 엄격하게 검증합니다.

IPv4 옵션타입용도보안 고려
Record Route7경로 기록정보 유출 위험, 일반적으로 차단
Timestamp68홉별 시간 기록정보 유출 위험
Loose Source Route131경유 라우터 지정보안 위험 — rp_filter로 차단 권장
Strict Source Route137정확한 경로 지정보안 위험 — 차단 권장
Router Alert148라우터 특별 처리 요청IGMP/RSVP에서 사용
IPv6 확장 헤더Next Header 값용도처리
Hop-by-Hop0모든 홉에서 처리Router Alert, Jumbo Payload
Routing43SRv6 세그먼트 라우팅SRv6 인프라에서 사용
Fragment44단편화 정보소스만 단편화 가능
Destination60목적지 옵션MIPv6 등
Authentication (AH)51무결성 검증IPsec
ESP50암호화IPsec
# Source Routing 차단 (보안 강화)
sysctl -w net.ipv4.conf.all.accept_source_route=0
sysctl -w net.ipv6.conf.all.accept_source_route=0

# Reverse Path Filtering (스푸핑 방어)
sysctl -w net.ipv4.conf.all.rp_filter=1     # strict mode
sysctl -w net.ipv4.conf.all.rp_filter=2     # loose mode (멀티호밍 시)

# ICMP 리다이렉트 차단
sysctl -w net.ipv4.conf.all.accept_redirects=0
sysctl -w net.ipv4.conf.all.send_redirects=0
sysctl -w net.ipv6.conf.all.accept_redirects=0

전송 계층 (Transport Layer)

전송 계층은 종단 간 데이터 전달의 신뢰성, 흐름 제어(Flow Control), 멀티플렉싱을 담당합니다. Linux 커널은 struct proto 인터페이스를 통해 다양한 전송 프로토콜을 플러그인 방식으로 지원합니다.

TCP 개요

Linux TCP 구현은 RFC 793 상태 머신(CLOSED, LISTEN, SYN_SENT, SYN_RECEIVED, ESTABLISHED, FIN_WAIT_1, FIN_WAIT_2, CLOSE_WAIT, CLOSING, LAST_ACK, TIME_WAIT)을 정밀하게 구현합니다. 플러그인 방식의 혼잡 제어(CUBIC 기본, BBR, Reno, DCTCP 등), kTLS, Zero-Copy sendfile, RACK/TLP 재전송을 지원합니다.

/* 혼잡 제어 알고리즘 등록 인터페이스 */
struct tcp_congestion_ops {
    struct list_head  list;
    u32               key;
    u32               flags;

    /* 혼잡 이벤트 콜백 */
    void (*init)(struct sock *sk);
    void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
    u32  (*ssthresh)(struct sock *sk);
    void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
    void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
    /* ... */

    char              name[TCP_CA_NAME_MAX];
    struct module     *owner;
};
혼잡 제어감지 방식특징사용 시나리오
CUBIC손실 기반3차 함수 cwnd 증가, 빠른 수렴기본값, 범용 환경
BBR대역폭/RTT 모델BtlBw x RTprop 기반, 버퍼 최소화고지연, 패킷 손실 환경
BBRv2모델 기반 + ECN공정성(Fairness) 개선, 큐잉 감소BBR 후속, 범용
DCTCPECN 기반ECN 비율에 비례하여 cwnd 조절데이터센터 내부
Reno손실 기반AIMD(가산 증가/승산 감소)레거시, 교육용
Vegas지연 기반RTT 변화 감지저지연 요구 환경
# TCP 혼잡 제어 알고리즘 관리
sysctl net.ipv4.tcp_congestion_control           # 현재 기본 알고리즘
sysctl net.ipv4.tcp_available_congestion_control  # 사용 가능한 알고리즘
sysctl net.ipv4.tcp_allowed_congestion_control    # 비root 사용 허용

# BBR 활성화
sysctl -w net.core.default_qdisc=fq              # BBR은 fq qdisc 필요
sysctl -w net.ipv4.tcp_congestion_control=bbr

# 소켓별 혼잡 제어 변경 (프로그래밍)
# setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bbr", 3);

# TCP 상태별 소켓 수 확인
ss -s | grep TCP
cat /proc/net/sockstat | grep TCP

# 특정 연결의 TCP 내부 상태 확인
ss -ti dst 10.0.0.1:443
# 출력: cubic wscale:7,7 rto:204 rtt:1.234/0.567
# cwnd:10 ssthresh:7 bytes_sent:12345 retrans:0/0

TCP 윈도우 관리

TCP는 수신 윈도우(rwnd)와 혼잡 윈도우(cwnd)의 최솟값으로 전송 속도를 결정합니다. 수신 윈도우는 수신자의 버퍼 가용 공간을 나타내고, 혼잡 윈도우는 네트워크의 가용 용량을 추정합니다.

/* TCP 전송 가능량 결정 */
/* effective_window = min(cwnd, rwnd) - in_flight */

/* net/ipv4/tcp_output.c — tcp_write_xmit() 핵심 로직 (간략화) */
static bool tcp_write_xmit(struct sock *sk,
                           unsigned int mss_now, int nonagle,
                           int push_one, gfp_t gfp)
{
    struct tcp_sock *tp = tcp_sk(sk);
    unsigned int cwnd_quota;

    while ((skb = tcp_send_head(sk))) {
        cwnd_quota = tcp_cwnd_test(tp, skb);
        if (!cwnd_quota) {
            /* cwnd 초과 — 전송 중단 */
            tcp_chrono_start(sk, TCP_CHRONO_RWND_LIMITED);
            break;
        }

        /* Nagle 알고리즘 확인 */
        if (unlikely(!tcp_nagle_test(tp, skb, mss_now, nonagle)))
            break;

        /* 수신 윈도우 확인 */
        if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now)))
            break;

        /* TSO/GSO 세그먼트 수 결정 */
        tso_segs = tcp_init_tso_segs(skb, mss_now);

        /* 전송 */
        tcp_transmit_skb(sk, skb, 1, gfp);
        tcp_event_new_data_sent(sk, skb);
    }
    return !tp->packets_out;
}
코드 설명
  • 12-17행tcp_cwnd_test()tp->snd_cwnd에서 현재 in-flight 패킷 수를 빼서 전송 가능한 세그먼트 수를 계산합니다. cwnd를 초과하면 전송을 중단합니다.
  • 20-21행Nagle 알고리즘은 작은 세그먼트의 불필요한 전송을 방지합니다. MSS 미만의 데이터가 있고 ACK 미확인 데이터가 존재하면 전송을 지연합니다. TCP_NODELAY 옵션으로 비활성화할 수 있습니다.
  • 24-25행tcp_snd_wnd_test()는 수신자가 광고한 수신 윈도우(rwnd) 범위 내인지 확인합니다. 수신자의 버퍼가 가득 차면(rwnd=0) Zero Window Probe를 시작합니다.
  • 31행tcp_transmit_skb()가 TCP/IP 헤더를 추가하고 IP 계층으로 전달합니다. 재전송 큐에 sk_buff 복사본을 유지합니다.

UDP 개요

UDP는 비연결형 프로토콜로, 상태 머신이나 혼잡 제어 없이 패킷을 전달합니다. udp_sendmsg()에서 IP 계층으로 즉시 전달하고, udp_rcv()에서 포트 번호 기반으로 소켓에 전달합니다. UDP-GRO를 지원하여 수신 성능을 개선하며, QUIC(UDP 기반 HTTP/3) 프로토콜의 기반이 됩니다.

kTLS (Kernel TLS)

kTLS는 TLS 레코드 계층의 암호화/복호화를 커널 공간에서 수행하는 기능입니다. 유저 공간의 TLS 라이브러리(OpenSSL 등)가 핸드셰이크를 완료한 후, 세션 키를 커널에 전달하면 이후의 데이터 전송은 커널 TCP 계층에서 직접 암호화됩니다. sendfile()과 결합하면 TLS 정적 파일 서빙에서 유저 공간 복사를 완전히 제거할 수 있습니다.

/* kTLS 설정 — 유저 공간 (OpenSSL 핸드셰이크 후) */

/* 1. ULP(Upper Layer Protocol)로 TLS 설정 */
setsockopt(fd, SOL_TCP, TCP_ULP, "tls", sizeof("tls"));

/* 2. TX 방향 암호화 키 설정 */
struct tls12_crypto_info_aes_gcm_128 crypto_info;
crypto_info.info.version = TLS_1_2_VERSION;
crypto_info.info.cipher_type = TLS_CIPHER_AES_GCM_128;
memcpy(crypto_info.iv, iv, TLS_CIPHER_AES_GCM_128_IV_SIZE);
memcpy(crypto_info.key, key, TLS_CIPHER_AES_GCM_128_KEY_SIZE);
memcpy(crypto_info.salt, salt, TLS_CIPHER_AES_GCM_128_SALT_SIZE);
memcpy(crypto_info.rec_seq, rec_seq, TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE);

setsockopt(fd, SOL_TLS, TLS_TX, &crypto_info, sizeof(crypto_info));

/* 3. RX 방향 복호화 키 설정 (선택적) */
setsockopt(fd, SOL_TLS, TLS_RX, &crypto_info_rx, sizeof(crypto_info_rx));

/* 이후 send()/sendfile()은 커널에서 자동 암호화 */
sendfile(fd, file_fd, NULL, file_size);
/* 파일 → 페이지 캐시 → 커널 TLS 암호화 → NIC */
/* 유저 공간 복사 0회! */
코드 설명
  • 4행TCP_ULP로 TLS를 설정하면, TCP 소켓에 kTLS 계층이 삽입됩니다. struct tls_context가 생성되어 암호화 컨텍스트를 관리합니다.
  • 7-14행TLS 핸드셰이크에서 협상된 암호화 키, IV(Initialization Vector), salt, 레코드 시퀀스 번호를 커널에 전달합니다. 커널은 이 정보로 AES-GCM 암호화를 수행합니다.
  • 16행TLS_TX를 설정하면 이후의 모든 send()/sendfile() 데이터가 커널 내부에서 TLS 레코드로 암호화됩니다.
  • 21-23행kTLS + sendfile() 조합은 파일 데이터를 유저 공간으로 복사하지 않고 커널에서 직접 암호화하여 전송합니다. Nginx 등의 웹 서버에서 정적 파일 서빙 시 CPU 사용률을 30~50% 절감할 수 있습니다.
kTLS 모드암호화 위치성능지원 환경
SW kTLS커널 CPU유저 복사 제거모든 NIC
HW kTLSNIC 하드웨어CPU 암호화 비용 제거Mellanox CX-6+, Intel E810
# kTLS 활성화 확인
grep TLS /boot/config-$(uname -r)     # CONFIG_TLS=m
modprobe tls                           # 모듈 로드

# kTLS 통계 확인
cat /proc/net/tls_stat
# TlsCurrTxSw   TlsCurrRxSw   TlsCurrTxDevice   TlsCurrRxDevice
# TlsTxSw       TlsRxSw       TlsTxDevice        TlsRxDevice

# NIC kTLS 하드웨어 오프로드 확인
ethtool -k eth0 | grep tls
# tls-hw-tx-offload: on
# tls-hw-rx-offload: on

SCTP 개요

SCTP(Stream Control Transmission Protocol)는 멀티스트리밍과 멀티호밍을 지원하는 전송 프로토콜입니다. 하나의 연결(association) 내에 여러 스트림을 가질 수 있어 Head-of-Line Blocking을 방지하고, 여러 IP 주소를 동시에 사용하여 경로 장애에 대한 복원력을 제공합니다.

상세 문서: TCP 상태 머신, 혼잡 제어 내부, kTLS, 재전송/SACK은 TCP 프로토콜를, UDP 상세는 UDP를, SCTP 상세는 SCTP 문서를 참고하세요.

TCP 수신 경로 내부 (tcp_v4_rcv)

tcp_v4_rcv()는 IP 계층에서 TCP 패킷을 수신하는 진입점입니다. 소켓 조회, 시퀀스 번호 검증, ACK 처리, 데이터 큐잉을 수행합니다. 성능을 위해 Fast Path(헤더 예측)와 Slow Path를 구분합니다.

/* net/ipv4/tcp_ipv4.c — tcp_v4_rcv() 핵심 로직 (간략화) */
int tcp_v4_rcv(struct sk_buff *skb)
{
    const struct tcphdr *th = tcp_hdr(skb);
    struct sock *sk;

    /* 1. TCP 체크섬 검증 */
    if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))
        goto csum_error;

    /* 2. 소켓 조회 (4-tuple 해시) */
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th),
                           th->source, th->dest, sdif, &refcounted);
    if (!sk)
        goto no_tcp_socket;

    /* 3. TIME_WAIT 상태 처리 */
    if (sk->sk_state == TCP_TIME_WAIT)
        goto do_time_wait;

    /* 4. 소켓 락 획득 시도 */
    if (!sock_owned_by_user(sk)) {
        /* Fast path: 소켓 락 미보유 → 즉시 처리 */
        ret = tcp_v4_do_rcv(sk, skb);
    } else {
        /* Slow path: 유저 프로세스가 락 보유 중 */
        /* backlog 큐에 추가, release_sock() 시 처리 */
        if (!tcp_add_backlog(sk, skb))
            goto discard_and_relse;
    }
    return ret;
}
코드 설명
  • 8-9행TCP 체크섬을 검증합니다. NIC가 체크섬 오프로드를 수행한 경우(CHECKSUM_UNNECESSARY) 이 검증은 생략됩니다.
  • 12-15행__inet_lookup_skb()로 4-tuple(소스/목적지 IP:포트)에 해당하는 소켓을 찾습니다. ESTABLISHED 해시 → LISTEN 해시 순서로 검색합니다.
  • 22-24행소켓 락이 비어 있으면 tcp_v4_do_rcv()로 즉시 처리합니다. 이 경로에서 헤더 예측(header prediction)이 활성화되어 있으면 Fast Path로 ACK 처리와 데이터 큐잉을 최적화합니다.
  • 25-29행유저 프로세스가 소켓 락을 보유 중이면(예: recv() 실행 중) backlog 큐에 패킷을 추가합니다. 유저 프로세스가 release_sock()을 호출하면 backlog 큐의 패킷이 일괄 처리됩니다.
/* net/ipv4/tcp_input.c — TCP Fast Path (Header Prediction) */
int tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* 헤더 예측: 예상 시퀀스, 예상 ACK, 단순 데이터 패킷 */
    if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
        TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {

        /* ★ Fast Path — 99%+ 패킷이 이 경로 */

        /* 순수 ACK (데이터 없음) */
        if (skb->len == tcp_header_len) {
            tcp_ack(sk, skb, 0);
            __kfree_skb(skb);
            return;
        }

        /* 데이터 패킷 — 소켓 수신 큐에 직접 추가 */
        eaten = tcp_queue_rcv(sk, skb, &fragstolen);
        tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
        tcp_event_data_recv(sk, skb);
        __tcp_ack_snd_check(sk, 0);  /* 지연 ACK 확인 */
        return;
    }

    /* ★ Slow Path — OOO, 재전송, FIN 등 */
    tcp_data_queue(sk, skb);
}
코드 설명
  • 7-8행Header Prediction은 TCP 플래그와 시퀀스 번호가 예상값과 일치하는지 검사합니다. 대부분의 데이터 전송에서 패킷은 순서대로 도착하므로, 이 조건을 만족하는 비율이 99% 이상입니다.
  • 13-16행데이터가 없는 순수 ACK 패킷은 tcp_ack()으로 혼잡 창 업데이트와 재전송 타이머 관리만 수행합니다.
  • 20-24행순서가 맞는 데이터 패킷은 tcp_queue_rcv()로 소켓 수신 큐에 직접 추가합니다. 지연 ACK 로직을 확인하여 즉시 ACK 또는 타이머 기반 지연 ACK를 결정합니다.
  • 28행Fast Path 조건이 불만족하면 Slow Path(tcp_data_queue())로 진입합니다. Out-of-Order 패킷 관리, SACK 처리, 재전송 탐지 등 복잡한 로직이 여기에 있습니다.

TCP 타이머 시스템

TCP는 신뢰성 있는 전송을 위해 여러 타이머를 운영합니다. 각 타이머는 특정 상태에서 활성화되며, 만료 시 재전송, 연결 종료, 또는 keepalive 프로브를 수행합니다.

타이머용도기본 설정관련 sysctl
재전송 타이머ACK 미수신 세그먼트 재전송RTO 기반 (200ms~120s)tcp_retries1, tcp_retries2
지연 ACK 타이머ACK 전송 지연 (피기백 기회)40~200mstcp_delack_min
Keepalive 타이머유휴 연결 생존 확인7200초 (2시간)tcp_keepalive_time, _intvl, _probes
TIME_WAIT 타이머이전 연결 패킷 소멸 대기60초 (2*MSL)tcp_tw_timeout
FIN_WAIT2 타이머orphan FIN_WAIT2 소켓 정리60초tcp_fin_timeout
TLP 타이머Tail Loss Probe 전송2*SRTT 또는 최소 10ms
Zero Window 프로브수신 창 0 시 프로빙지수 백오프
/* net/ipv4/tcp_timer.c — TCP 타이머 콜백 등록 */
void tcp_init_xmit_timers(struct sock *sk)
{
    /* 재전송/TLP 타이머 */
    inet_csk_init_xmit_timers(sk,
        &tcp_write_timer,     /* 재전송 타이머 만료 시 */
        &tcp_delack_timer,    /* 지연 ACK 타이머 만료 시 */
        &tcp_keepalive_timer); /* Keepalive 타이머 만료 시 */
}

/* tcp_write_timer() → tcp_retransmit_timer() */
void tcp_retransmit_timer(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* RTO 만료: 가장 오래된 미확인 세그먼트 재전송 */
    if (tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1))
        return;

    /* RTO 지수 백오프 (최대 120초) */
    inet_csk(sk)->icsk_rto = min(inet_csk(sk)->icsk_rto << 1,
                                   TCP_RTO_MAX);

    /* 최대 재시도 횟수 초과 시 연결 중단 */
    if (tcp_out_of_resources(sk, true))
        return;

    tcp_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                         inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
}
코드 설명
  • 5-8행소켓 초기화 시 세 가지 핵심 타이머의 만료 콜백을 등록합니다. 각 타이머는 독립적으로 동작하며, inet_csk(INET connection socket) 프레임워크가 관리합니다.
  • 17행tcp_retransmit_skb()가 재전송 큐(tcp_rtx_queue)의 첫 번째(가장 오래된) 미확인 세그먼트를 재전송합니다.
  • 21-22행RTO(Retransmission Timeout)를 2배로 증가시킵니다(지수 백오프). 최대값은 TCP_RTO_MAX(120초)입니다. 초기 RTO는 1초이며, RTT 측정 후 SRTT 기반으로 동적 계산됩니다.
  • 25-26행재시도 횟수가 tcp_retries2(기본 15)를 초과하면 연결을 포기하고 소켓을 닫습니다.
TCP 연결 수립/해제: SYN Queue, Accept Queue, 3-way/4-way Handshake TCP 연결 수립: SYN Queue / Accept Queue 구조 클라이언트 서버 (LISTEN) SYN (seq=x) SYN_SENT SYN Queue (half-open) struct request_sock 생성 max: tcp_max_syn_backlog SYN_RECV SYN+ACK (seq=y, ack=x+1) ACK (ack=y+1) ESTABLISHED Accept Queue (established) full struct sock 생성 max: min(backlog, somaxconn) ESTABLISHED accept() 유저 프로세스: accept() 반환 → fd SYN Queue 오버플로우 시: SYN Cookie 활성화 request_sock 할당 없이 ISN에 상태 인코딩 DATA ACK TCP Fast Open (TFO): SYN + Cookie + DATA → 0-RTT sysctl tcp_fastopen = 3
TCP 3-way Handshake와 SYN Queue / Accept Queue 구조

TCP 제로카피 메커니즘

TCP에서 제로카피(Zero-Copy)는 유저 공간 버퍼의 데이터를 커널 버퍼로 복사하지 않고 직접 NIC에 전달하는 기법입니다. MSG_ZEROCOPY(송신), sendfile()(파일→소켓), TCP receive zero-copy(수신) 등 여러 방식이 있습니다.

/* MSG_ZEROCOPY 송신 — 유저 공간 */
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one));

/* 제로카피 전송 */
send(fd, buf, len, MSG_ZEROCOPY);

/* 완료 통지 수신 (errqueue에서) */
struct msghdr msg = {};
struct sock_extended_err *serr;
char cmsg_buf[64];

msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);

/* MSG_ERRQUEUE에서 완료 통지 읽기 */
recvmsg(fd, &msg, MSG_ERRQUEUE);

/* serr->ee_origin == SO_EE_ORIGIN_ZEROCOPY */
/* serr->ee_data = 완료된 전송의 카운터 */
/* 이 시점 이후 buf를 안전하게 재사용할 수 있습니다 */
코드 설명
  • 2행SO_ZEROCOPY를 설정하면 소켓이 제로카피 전송을 지원합니다. 내부적으로 sk->sk_socket->file->f_opSOCK_ZEROCOPY 플래그가 설정됩니다.
  • 5행MSG_ZEROCOPY 플래그로 전송하면, 커널이 유저 버퍼를 get_user_pages()로 고정(pin)하여 DMA 전송에 직접 사용합니다. 데이터 복사가 발생하지 않습니다.
  • 16행NIC가 DMA 전송을 완료하면 error queue(MSG_ERRQUEUE)를 통해 완료 통지를 보냅니다. 유저 공간은 이 통지를 받은 후에야 버퍼를 수정하거나 해제할 수 있습니다.
/* sendfile() — 커널 내부 경로 */
/* sys_sendfile() → do_sendfile() → do_splice_direct() */
/*   → splice_direct_to_actor() → pipe + tcp_splice_read/write */

/* sendfile(out_fd, in_fd, offset, count) */
/* 파일 → 파이프 → 소켓 전송 (유저 공간 복사 없음) */
/* 페이지 캐시의 페이지를 sk_buff의 frags[]에 직접 매핑 */

/* 커널 내부: tcp_sendpage() 또는 tcp_sendmsg_locked() */
/* skb_fill_page_desc()로 파일 페이지를 skb에 추가 */
skb_fill_page_desc(skb, i, page, offset, len);
/* → NIC DMA scatter-gather로 직접 전송 */
코드 설명
  • 2-3행sendfile()은 파일 시스템의 페이지 캐시에서 페이지를 직접 소켓의 sk_buff에 매핑합니다. 유저 공간으로의 데이터 복사가 완전히 제거됩니다.
  • 11행skb_fill_page_desc()는 파일의 페이지를 sk_buff의 frags[] 배열에 추가합니다. NIC가 scatter-gather DMA를 지원하면 여러 페이지를 하나의 전송으로 처리할 수 있습니다.

TCP 연결 관리 내부

TCP 연결 수립은 SYN Queue(half-open)와 Accept Queue(established)의 이중 큐 구조로 관리됩니다. SYN Flood 공격 방어를 위한 SYN Cookie 메커니즘과, 지연을 줄이기 위한 TCP Fast Open도 구현되어 있습니다.

/* TCP 연결 수립 큐 구조 */

/* SYN Queue (half-open 연결): request_sock */
/* 클라이언트 SYN → request_sock 생성 → SYN+ACK 전송 */
/* 최대 크기: sysctl tcp_max_syn_backlog (기본 256) */

/* Accept Queue (완전 연결): */
/* 클라이언트 ACK → full sock 생성 → accept queue 삽입 */
/* 최대 크기: listen(fd, backlog)의 backlog 인수 */
/* 또는 sysctl net.core.somaxconn (기본 4096) */

/* SYN Cookie 메커니즘 */
/* SYN Queue 오버플로우 시 활성화 (tcp_syncookies=1) */
u32 __cookie_v4_init_sequence(const struct iphdr *iph,
                              const struct tcphdr *th,
                              u16 *mssp)
{
    /* ISN에 MSS, 타임스탬프, 비밀 키를 인코딩 */
    /* SYN+ACK의 시퀀스 번호 자체에 연결 정보 저장 */
    /* → request_sock 할당 불필요 → 메모리 고갈 방지 */
    return secure_tcp_syn_cookie(saddr, daddr, sport, dport,
                                 sseq, data);
}

/* TCP Fast Open (TFO) */
/* 첫 연결: 쿠키 요청 (TCP option) */
/* 이후 연결: SYN + 쿠키 + 데이터 → 0-RTT 핸드셰이크 */
/* sysctl: net.ipv4.tcp_fastopen = 3 (클라이언트+서버) */
코드 설명
  • 3-5행SYN Queue는 SYN을 수신했지만 아직 3-way 핸드셰이크가 완료되지 않은 half-open 연결을 저장합니다. request_sock 구조체로 표현되며, tcp_max_syn_backlog로 크기를 제한합니다.
  • 7-10행Accept Queue는 핸드셰이크가 완료된 연결을 accept()가 가져갈 때까지 보관합니다. 크기는 listen()의 backlog 인수와 somaxconn 중 작은 값으로 제한됩니다.
  • 12-22행SYN Cookie는 SYN Queue가 가득 찼을 때 활성화됩니다. request_sock을 할당하지 않고, SYN+ACK의 ISN(Initial Sequence Number)에 연결 정보(MSS, 타임스탬프)를 암호학적으로 인코딩합니다. 클라이언트의 ACK에서 이 정보를 복원하여 연결을 생성합니다.
  • 25-28행TCP Fast Open은 이전에 발급받은 쿠키를 SYN 패킷에 포함하여, 서버가 SYN+ACK를 보내기 전에 이미 데이터를 처리할 수 있게 합니다. HTTP 요청 등에서 1-RTT를 절약합니다.

UDP 내부 상세

UDP는 TCP에 비해 단순하지만, 고성능 처리를 위한 다양한 최적화가 구현되어 있습니다. VXLAN, WireGuard 등의 터널링 프로토콜이 UDP 캡슐화를 사용하므로, UDP 수신 경로의 성능은 네트워크 가상화에 직접적인 영향을 미칩니다.

/* net/ipv4/udp.c — __udp4_lib_rcv() 핵심 로직 (간략화) */
int __udp4_lib_rcv(struct sk_buff *skb,
                   struct udp_table *udptable, int proto)
{
    struct sock *sk;
    const struct udphdr *uh = udp_hdr(skb);

    /* 1. UDP 체크섬 검증 */
    if (udp4_csum_init(skb, uh, proto))
        goto csum_error;

    /* 2. 소켓 조회 (dest port 해시) */
    sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);

    if (sk) {
        /* 3a. UDP 캡슐화 (VXLAN, WireGuard 등) */
        if (udp_sk(sk)->encap_type) {
            int ret = udp_sk(sk)->encap_rcv(sk, skb);
            if (ret <= 0)
                return -ret;
            /* ret > 0: 캡슐화 처리 안 함, 일반 UDP 계속 */
        }

        /* 3b. 일반 UDP 수신 */
        return udp_unicast_rcv_skb(sk, skb, uh);
    }

    /* 4. 소켓 없음 → ICMP Port Unreachable */
    icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);
    goto drop;
}
코드 설명
  • 13행UDP 소켓 조회는 목적지 포트를 기반으로 udp_table 해시 테이블에서 수행합니다. 연결 개념이 없으므로 포트 번호만으로 소켓을 찾습니다.
  • 17-21행UDP 캡슐화가 설정된 소켓(VXLAN, WireGuard, GTP 등)은 encap_rcv 콜백으로 내부 패킷을 디캡슐화합니다. 이 콜백이 0을 반환하면 패킷 처리가 완료된 것이며, 양수를 반환하면 일반 UDP 수신으로 계속합니다.
  • 29행매칭되는 소켓이 없으면 ICMP "Port Unreachable" 메시지를 전송합니다. UDP 스캔 공격 시 이 ICMP 응답이 대량 생성될 수 있으므로, icmp_ratelimit sysctl로 속도를 제한합니다.

UDP-GRO와 UDP GSO

UDP-GRO는 NIC 또는 GRO 계층에서 동일 플로우의 연속 UDP 패킷을 하나의 큰 sk_buff로 병합합니다. UDP GSO는 반대로 대형 UDP 데이터그램을 세그먼트로 분할하여 전송합니다. 두 기능 모두 QUIC, WireGuard 등 UDP 기반 프로토콜의 성능을 크게 향상시킵니다.

/* UDP GSO 송신 — 유저 공간 설정 */
int gso_size = 1472;  /* 세그먼트 크기 (MTU - IP/UDP 헤더) */
setsockopt(fd, IPPROTO_UDP, UDP_SEGMENT,
           &gso_size, sizeof(gso_size));

/* 대형 데이터그램 전송 (최대 64KB) */
/* 커널이 자동으로 gso_size 단위로 분할 */
send(fd, large_buf, 65507, 0);

/* 또는 sendmsg로 세그먼트 크기 지정 */
struct msghdr msg = {};
char cmsg_buf[CMSG_SPACE(sizeof(uint16_t))];
msg.msg_control = cmsg_buf;
msg.msg_controllen = sizeof(cmsg_buf);

struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
cm->cmsg_level = SOL_UDP;
cm->cmsg_type = UDP_SEGMENT;
cm->cmsg_len = CMSG_LEN(sizeof(uint16_t));
*(uint16_t *)CMSG_DATA(cm) = gso_size;

sendmsg(fd, &msg, 0);
코드 설명
  • 2-4행UDP_SEGMENT 소켓 옵션으로 GSO 세그먼트 크기를 설정합니다. 일반적으로 MTU에서 IP(20) + UDP(8) 헤더를 뺀 1472바이트를 사용합니다.
  • 8행64KB 근처의 대형 데이터를 한 번에 전송하면, 커널이 __udp_gso_segment()에서 자동으로 분할합니다. 시스템 콜 횟수가 1/44로 줄어들어 PPS가 크게 향상됩니다.
  • 16-21행sendmsg()의 ancillary data로 세그먼트 크기를 패킷별로 다르게 지정할 수 있습니다. QUIC에서 패킷 크기가 가변인 경우 유용합니다.
/* UDP-GRO 수신 — 유저 공간 설정 */
int val = 1;
setsockopt(fd, IPPROTO_UDP, UDP_GRO, &val, sizeof(val));

/* recvmsg()에서 GRO된 대형 데이터그램 수신 */
/* cmsg로 각 세그먼트의 크기 정보 전달 */
struct cmsghdr *cm;
for (cm = CMSG_FIRSTHDR(&msg); cm; cm = CMSG_NXTHDR(&msg, cm)) {
    if (cm->cmsg_type == UDP_GRO) {
        uint16_t gro_size = *(uint16_t *)CMSG_DATA(cm);
        /* gro_size 단위로 수신 데이터를 분리 처리 */
    }
}
코드 설명
  • 2-3행UDP_GRO 소켓 옵션을 활성화하면 GRO로 병합된 대형 UDP 데이터그램을 한 번의 recvmsg()로 수신할 수 있습니다.
  • 9-12행GRO된 데이터의 원래 세그먼트 크기가 ancillary data(cmsg)로 전달됩니다. 유저 공간은 이 크기를 기반으로 개별 데이터그램 경계를 파악합니다.

UDP 송신 코크 모드 (Cork Mode)

UDP 코크 모드(UDP_CORK)는 여러 send() 호출의 데이터를 하나의 UDP 데이터그램으로 결합하여 전송합니다. 애플리케이션이 여러 조각의 데이터를 순차적으로 기록한 후 하나의 패킷으로 전송해야 할 때 유용합니다.

/* UDP Cork 모드 사용 */
int cork = 1;
setsockopt(fd, IPPROTO_UDP, UDP_CORK, &cork, sizeof(cork));

/* 여러 write()가 하나의 UDP 데이터그램에 축적 */
send(fd, header, header_len, 0);   /* 아직 전송 안 됨 */
send(fd, payload, payload_len, 0); /* 아직 전송 안 됨 */
send(fd, trailer, trailer_len, 0); /* 아직 전송 안 됨 */

/* Cork 해제 시 축적된 데이터를 하나의 데이터그램으로 전송 */
cork = 0;
setsockopt(fd, IPPROTO_UDP, UDP_CORK, &cork, sizeof(cork));
/* 이 시점에 header+payload+trailer가 하나의 UDP 패킷으로 전송 */
코드 설명
  • 2-3행UDP_CORK를 1로 설정하면 이후의 send() 데이터가 소켓의 pending 버퍼에 축적됩니다.
  • 6-8행여러 번의 send() 호출에도 불구하고, 데이터가 즉시 전송되지 않고 sk_write_queue에 축적됩니다.
  • 11-12행UDP_CORK를 0으로 해제하면 udp_push_pending_frames()가 호출되어 축적된 모든 데이터가 하나의 UDP 데이터그램으로 전송됩니다.

프로토콜 등록 메커니즘

Linux 커널의 네트워크 스택은 프로토콜 독립적인 프레임워크로 설계되어, 새로운 네트워크/전송 프로토콜을 모듈로 등록할 수 있습니다. 이 구조 덕분에 IPv4, IPv6, TCP, UDP, SCTP 등이 동일한 인터페이스를 통해 동작합니다.

L3 프로토콜 등록: packet_type

L2(이더넷 등) 위에서 동작하는 L3 프로토콜은 struct packet_typedev_add_pack()으로 등록합니다. 패킷이 __netif_receive_skb()에 도달하면 skb->protocol(EtherType)에 따라 등록된 핸들러가 호출됩니다.

/* net/ipv4/af_inet.c — IPv4 프로토콜 등록 */
static struct packet_type ip_packet_type __read_mostly = {
    .type = cpu_to_be16(ETH_P_IP),   /* EtherType 0x0800 */
    .func = ip_rcv,                   /* 수신 핸들러 */
};

/* 초기화 시 등록 */
dev_add_pack(&ip_packet_type);

/* IPv6의 경우: ETH_P_IPV6 (0x86DD) → ipv6_rcv() */
/* ARP의 경우: ETH_P_ARP (0x0806) → arp_rcv() */

L4 프로토콜 등록: net_protocol

IP 계층 위에서 동작하는 L4 프로토콜은 struct net_protocolinet_add_protocol()로 등록합니다. IP 헤더의 프로토콜 번호에 따라 해당 핸들러가 호출됩니다.

/* net/ipv4/af_inet.c — TCP/UDP/ICMP 프로토콜 등록 */
static const struct net_protocol tcp_protocol = {
    .handler     = tcp_v4_rcv,       /* TCP 수신 핸들러 */
    .err_handler = tcp_v4_err,       /* ICMP 에러 핸들러 */
    .no_policy   = 1,
};

static const struct net_protocol udp_protocol = {
    .handler     = udp_rcv,          /* UDP 수신 핸들러 */
    .err_handler = udp_err,
    .no_policy   = 1,
};

/* IP 프로토콜 번호로 등록 */
inet_add_protocol(&tcp_protocol, IPPROTO_TCP);   /* 프로토콜 6 */
inet_add_protocol(&udp_protocol, IPPROTO_UDP);   /* 프로토콜 17 */
inet_add_protocol(&icmp_protocol, IPPROTO_ICMP); /* 프로토콜 1 */

소켓 프로토콜 등록: struct proto

소켓 계층에서 사용할 프로토콜은 struct protoproto_register()로 등록합니다. 이 구조체는 connect(), sendmsg(), recvmsg() 등의 프로토콜별 구현을 담습니다.

/* net/ipv4/tcp_ipv4.c — TCP 프로토콜 오퍼레이션 */
struct proto tcp_prot = {
    .name           = "TCP",
    .owner          = THIS_MODULE,
    .close          = tcp_close,
    .connect        = tcp_v4_connect,
    .disconnect     = tcp_disconnect,
    .accept         = inet_csk_accept,
    .sendmsg        = tcp_sendmsg,
    .recvmsg        = tcp_recvmsg,
    .setsockopt     = tcp_setsockopt,
    .getsockopt     = tcp_getsockopt,
    .hash           = inet_hash,
    .unhash         = inet_unhash,
    .obj_size       = sizeof(struct tcp_sock),
    /* ... */
};
EXPORT_SYMBOL(tcp_prot);

주소 패밀리 등록: inet_protosw

소켓 타입(SOCK_STREAM, SOCK_DGRAM 등)과 프로토콜의 매핑은 struct inet_protosw 배열로 정의됩니다. socket(AF_INET, SOCK_STREAM, 0) 호출 시 이 배열에서 적절한 프로토콜을 찾습니다.

/* net/ipv4/af_inet.c — AF_INET 프로토콜 스위치 */
static struct inet_protosw inetsw_array[] = {
    {
        .type     = SOCK_STREAM,       /* 스트림 소켓 */
        .protocol = IPPROTO_TCP,       /* → TCP */
        .prot     = &tcp_prot,         /* struct proto */
        .ops      = &inet_stream_ops,  /* struct proto_ops */
    },
    {
        .type     = SOCK_DGRAM,        /* 데이터그램 소켓 */
        .protocol = IPPROTO_UDP,       /* → UDP */
        .prot     = &udp_prot,
        .ops      = &inet_dgram_ops,
    },
    {
        .type     = SOCK_RAW,          /* RAW 소켓 */
        .protocol = IPPROTO_IP,        /* → RAW */
        .prot     = &raw_prot,
        .ops      = &inet_sockraw_ops,
    },
};
프로토콜 등록 계층 요약: L2 디멀티플렉싱(packet_type) → L3 디멀티플렉싱(net_protocol) → 소켓 프로토콜(struct proto) → 주소 패밀리 스위치(inet_protosw). 이 4단계 구조 덕분에 새로운 프로토콜을 기존 코드 수정 없이 모듈로 추가할 수 있습니다.

Netfilter 프레임워크

Netfilter는 커널의 패킷 필터링 프레임워크입니다. 네트워크 스택의 5개 훅 포인트에 콜백을 등록하여 패킷을 검사, 수정, 차단합니다. iptables/nftables, conntrack, NAT의 백엔드입니다.

Netfilter 5개 훅 포인트: PREROUTING, INPUT, FORWARD, OUTPUT, POSTROUTING Netfilter 훅 포인트 (IPv4) Packet In PREROUTING Routing Decision INPUT Local Process FORWARD POSTROUTING OUTPUT NF_ACCEPT: 다음 훅으로 진행 | NF_DROP: 패킷 폐기 | NF_QUEUE: 유저 공간으로 전달 | NF_STOLEN: 모듈이 패킷 소유권 인수
Netfilter 5개 훅 포인트와 패킷 경로

Netfilter 훅별 역할

위치주요 용도nftables 체인
NF_INET_PRE_ROUTINGIP 수신 직후, 라우팅 전DNAT, conntrack 생성prerouting
NF_INET_LOCAL_IN라우팅 후, 로컬 대상입력 필터링, Rate Limitinginput
NF_INET_FORWARD라우팅 후, 포워딩 대상포워딩 필터링forward
NF_INET_LOCAL_OUT로컬 송신, 라우팅 전출력 필터링, 로컬 DNAToutput
NF_INET_POST_ROUTING라우팅 후, 인터페이스 출력 전SNAT/Masqueradepostrouting

conntrack (연결 추적(Connection Tracking))

Netfilter의 연결 추적 시스템(nf_conntrack)은 모든 연결의 상태를 추적하여 stateful 방화벽(Firewall)과 NAT를 가능하게 합니다. 각 연결은 struct nf_conn으로 표현되며, 해시 테이블(Hash Table)에 저장됩니다. 연결 상태는 NEW, ESTABLISHED, RELATED, INVALID로 분류됩니다.

# conntrack 상태 확인
conntrack -L                          # 전체 연결 목록
conntrack -C                          # 현재 추적 중인 연결 수
cat /proc/sys/net/netfilter/nf_conntrack_max  # 최대 추적 수
cat /proc/sys/net/netfilter/nf_conntrack_count # 현재 추적 수
conntrack 성능 영향: conntrack은 패킷당 해시(Hash) 조회를 수행하므로 고속 네트워크에서 병목(Bottleneck)이 될 수 있습니다. nf_conntrack_max 도달 시 새 연결이 거부되고, 해시 충돌이 많으면 지연이 증가합니다. Netfilter Flowtable(nf_flow_table)을 사용하면 ESTABLISHED 연결의 패킷을 빠른 경로로 처리하여 conntrack 오버헤드를 줄일 수 있습니다.
상세 문서: Netfilter 훅 체계, nftables/iptables 아키텍처, conntrack 내부, NFQUEUE, 방화벽 규칙 최적화는 Netfilter 프레임워크 문서를 참고하세요.

Netfilter 훅 등록 내부

Netfilter 훅 함수는 nf_register_net_hook()으로 등록하며, 각 훅 포인트에 우선순위(priority) 기반으로 정렬됩니다. 패킷이 훅 포인트를 통과할 때 nf_hook_slow()가 등록된 함수들을 순서대로 호출합니다.

/* include/linux/netfilter.h — 훅 등록 구조체 */
struct nf_hook_ops {
    nf_hookfn           *hook;       /* 훅 콜백 함수 */
    struct net_device   *dev;        /* 디바이스 필터 (선택) */
    int                 hooknum;     /* NF_INET_* 훅 포인트 */
    int                 priority;    /* 실행 우선순위 */
};

/* net/netfilter/core.c — nf_hook_slow() 처리 루프 (간략화) */
int nf_hook_slow(struct sk_buff *skb,
                struct nf_hook_state *state,
                const struct nf_hook_entries *e,
                unsigned int s)
{
    unsigned int verdict;

    for (; s < e->num_hook_entries; s++) {
        verdict = nf_hook_entry_hookfn(&e->hooks[s], skb, state);
        switch (verdict & NF_VERDICT_MASK) {
        case NF_ACCEPT:
            break;              /* 다음 훅으로 계속 */
        case NF_DROP:
            kfree_skb(skb);
            return NF_DROP;     /* 패킷 폐기 */
        case NF_STOLEN:
            return NF_STOLEN;   /* 모듈이 소유권 가져감 */
        case NF_QUEUE:
            /* NFQUEUE로 유저 공간 전달 */
            return nf_queue(skb, state, s, verdict);
        case NF_REPEAT:
            s--;                   /* 현재 훅 재실행 */
            break;
        }
    }
    return 1;  /* 모든 훅 통과 */
}
코드 설명
  • 3행hook 콜백 함수는 NF_ACCEPT, NF_DROP, NF_STOLEN, NF_QUEUE, NF_REPEAT 중 하나를 반환합니다.
  • 6행priority는 동일 훅 포인트에 등록된 여러 훅 함수의 실행 순서를 결정합니다. conntrack은 NF_IP_PRI_CONNTRACK(-200)으로 대부분의 필터보다 먼저 실행됩니다.
  • 17-18행등록된 훅 엔트리를 순서대로 순회하며 콜백을 호출합니다. NF_ACCEPT이면 다음 훅으로 진행하고, NF_DROP이면 즉시 패킷을 폐기합니다.
  • 27-29행NF_QUEUE는 패킷을 nfnetlink_queue를 통해 유저 공간(예: Suricata IPS)으로 전달합니다. 유저 공간이 판정(verdict)을 내리면 패킷 처리가 재개됩니다.

nf_conntrack 내부 구현

연결 추적은 패킷의 5-tuple(프로토콜, 소스/목적지 IP:포트)을 기반으로 해시 테이블에서 연결 항목(struct nf_conn)을 조회합니다. NAT 변환 시 원본 방향(original)과 응답 방향(reply)의 tuple 쌍이 저장됩니다.

/* net/netfilter/nf_conntrack_core.c — 연결 추적 핵심 (간략화) */
unsigned int nf_conntrack_in(struct sk_buff *skb,
                             const struct nf_hook_state *state)
{
    struct nf_conn *ct;
    enum ip_conntrack_info ctinfo;
    struct nf_conntrack_tuple tuple;

    /* 1. 패킷에서 tuple 추출 (프로토콜, IP, 포트) */
    if (!nf_ct_get_tuple(skb, &tuple))
        return NF_ACCEPT;

    /* 2. 해시 테이블에서 기존 연결 조회 */
    u32 hash = hash_conntrack_raw(&tuple, zone);
    ct = __nf_conntrack_find_get(net, zone, &tuple, hash);

    if (!ct) {
        /* 3. 새 연결: nf_conn 할당, NEW 상태 */
        ct = init_conntrack(net, tmpl, &tuple, skb, dataoff, hash);
        if (!ct || IS_ERR(ct))
            return NF_DROP;
    }

    /* 4. 프로토콜별 추적 업데이트 */
    /* TCP: 상태 머신 전이 (SYN_SENT→ESTABLISHED 등) */
    /* UDP: 타임아웃 갱신 */
    ret = nf_conntrack_handle_packet(ct, skb, dataoff, ctinfo, state);

    /* 5. skb에 conntrack 정보 연결 */
    nf_ct_set(skb, ct, ctinfo);
    return ret;
}
코드 설명
  • 10-11행nf_ct_get_tuple()은 패킷에서 프로토콜, 소스/목적지 IP, 포트를 추출하여 nf_conntrack_tuple을 구성합니다.
  • 14-15행tuple의 해시로 conntrack 해시 테이블을 조회합니다. 해시 테이블 크기는 nf_conntrack_buckets로 설정하며, 기본값은 nf_conntrack_max/4입니다.
  • 19행기존 연결이 없으면 init_conntrack()으로 새 nf_conn을 할당합니다. 전체 연결 수가 nf_conntrack_max에 도달하면 early_drop()으로 가장 오래된 연결을 제거합니다.
  • 30행nf_ct_set()으로 sk_buff에 conntrack 정보를 연결합니다. 이후 Netfilter 규칙에서 ct state established 등의 조건 매칭에 사용됩니다.
# conntrack 튜닝 명령어

# 최대 추적 수 증가 (기본 65536)
sysctl -w net.netfilter.nf_conntrack_max=262144

# 해시 버킷 수 조정 (모듈 로드 시)
# modprobe nf_conntrack hashsize=65536
echo 65536 > /sys/module/nf_conntrack/parameters/hashsize

# 타임아웃 최적화
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=86400
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
sysctl -w net.netfilter.nf_conntrack_udp_timeout=30
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=120

# conntrack 이벤트 모니터링
conntrack -E                          # 실시간 이벤트 감시
conntrack -E -e NEW,DESTROY           # 연결 생성/삭제만

# conntrack 통계
cat /proc/net/stat/nf_conntrack       # per-CPU 통계
# 열: searched found new invalid ignore delete ...
# drop early_drop error search_restart

nftables vs iptables 아키텍처

nftables는 iptables의 후속으로, 바이트코드 VM 기반의 유연한 패킷 분류 엔진을 제공합니다. iptables의 테이블/체인/규칙 구조를 유지하면서도, set 인프라(해시/RB-tree/bitmap)와 verdict map으로 성능과 표현력을 개선합니다.

특성iptablesnftables
커널 모듈x_tables + match/target 모듈nf_tables 단일 모듈
규칙 평가선형 순차 매칭바이트코드 VM + set 조회
규칙 업데이트전체 체인 원자적 교체개별 규칙 원자적 추가/삭제
프로토콜 지원IPv4/IPv6/ARP 별도inet 패밀리로 통합 가능
Set 타입ipset (별도 모듈)내장 (hash, rbtree, bitmap)
Verdict Map미지원Set 요소에 verdict 연결
커널 인터페이스setsockopt/getsockoptnetlink (nfnetlink)
성능 (대규모 규칙)O(n) 선형O(1) 해시 set 사용 시

nftables 바이트코드 VM 명령어 예시

nft add rule inet filter input tcp dport 80 accept 의 내부 표현:

명령어 1: meta load l4proto → reg1
  [ meta load l4proto => reg 1 ]
명령어 2: cmp eq reg1 0x06 (TCP)
  [ cmp eq reg 1 0x00000006 ]
명령어 3: payload load 2b @ transport header + 2 → reg1
  [ payload load 2b @ transport header + 2 => reg 1 ]
명령어 4: cmp eq reg1 0x0050 (포트 80)
  [ cmp eq reg 1 0x00005000 ]
명령어 5: immediate verdict accept
  [ immediate reg 0 accept ]

Set을 사용한 O(1) 조회 예시:

nft add set inet filter allowed_ports { type inet_service; }
nft add element inet filter allowed_ports { 80, 443, 8080 }
nft add rule inet filter input tcp dport @allowed_ports accept
→ lookup 명령이 해시 테이블에서 O(1) 조회
코드 설명
  • 4-13행nftables VM은 레지스터 기반 바이트코드를 실행합니다. 각 명령이 패킷 필드를 레지스터에 로드하고, 비교하고, verdict를 결정합니다. iptables의 match/target 함수 호출보다 오버헤드가 적습니다.
  • 16-19행Set을 사용하면 수만 개의 IP/포트를 하나의 규칙으로 처리할 수 있습니다. 내부적으로 해시 테이블, RB-tree, 또는 비트맵을 사용하여 O(1) 또는 O(log n) 조회를 수행합니다. iptables에서는 규칙 수만큼 O(n) 순차 매칭이 필요했습니다.

Flowtable 가속 경로

Netfilter Flowtable은 ESTABLISHED 연결의 패킷을 Netfilter 훅을 우회하여 빠르게 전달하는 가속 메커니즘입니다. conntrack이 연결을 추적한 후, Flowtable에 등록된 플로우는 PRE_ROUTING 단계에서 바로 출력 인터페이스로 전달됩니다.

/* net/netfilter/nf_flow_table_core.c — Flowtable 동작 원리 */

/* 1. nftables에서 flowtable 정의 */
/*    nft add flowtable inet filter ft { */
/*        hook ingress priority 0; */
/*        devices = { eth0, eth1 }; */
/*    } */

/* 2. forward 체인에서 flow offload 규칙 추가 */
/*    nft add rule inet filter forward */
/*        ct state established flow add @ft */

/* 3. 커널 내부: flow 엔트리 조회 및 fast path */
unsigned int nf_flow_offload_ip_hook(void *priv,
                                     struct sk_buff *skb,
                                     const struct nf_hook_state *state)
{
    struct flow_offload_tuple_rhash *tuplehash;
    struct nf_flowtable *flow_table = priv;

    /* flow 테이블에서 조회 (rhashtable) */
    tuplehash = flow_offload_lookup(flow_table, &tuple);
    if (!tuplehash)
        return NF_ACCEPT;  /* 미등록: 일반 경로 */

    /* Fast path: IP TTL 감소, MAC 헤더 교체 */
    ip_decrease_ttl(ip_hdr(skb));
    nf_flow_nat_ip(flow, skb, thoff, dir);  /* NAT 적용 */
    skb->dev = outdev;
    neigh_xmit(NEIGH_ARP_TABLE, outdev, &nexthop, skb);
    return NF_STOLEN;  /* Netfilter 훅 체인 우회 */
}
코드 설명
  • 3-7행nftables에서 flowtable을 정의할 때, 가속을 적용할 디바이스 목록을 지정합니다. ingress 훅에 등록되어 PRE_ROUTING 이전에 실행됩니다.
  • 22행flow_offload_lookup()은 rhashtable(resizable hash table)에서 플로우를 O(1)으로 조회합니다.
  • 27-30행Fast path에서는 IP 라우팅, Netfilter 훅 체인, conntrack 업데이트를 모두 건너뛰고, TTL 감소와 NAT 변환만 수행한 후 neigh_xmit()으로 직접 출력합니다.
  • 31행NF_STOLEN을 반환하여 이후의 Netfilter 훅 처리를 완전히 우회합니다. 이를 통해 포워딩 성능을 3~5배 향상시킬 수 있습니다.
하드웨어 플로우 오프로드: Flowtable은 NF_FLOW_HW_OFFLOAD 플래그로 NIC 하드웨어까지 오프로드할 수 있습니다. tc flower 규칙으로 변환되어 NIC의 TCAM/eSwitch에 설치됩니다. Mellanox ConnectX-5+, Marvell Prestera 등이 지원하며, nft add flowtable ... flags offload로 활성화합니다.

네트워크 디바이스와 NAPI

struct net_device는 네트워크 인터페이스(eth0, wlan0 등)를 나타냅니다. net_device_ops로 드라이버 콜백을 등록하고, NAPI를 통해 고속 패킷 처리를 수행합니다.

net_device 핵심 필드

struct net_device {
    char                       name[IFNAMSIZ];    /* 인터페이스 이름 */
    unsigned int               mtu;               /* 최대 전송 단위 */
    unsigned int               flags;             /* IFF_UP, IFF_PROMISC 등 */
    unsigned char              dev_addr[MAX_ADDR_LEN]; /* MAC 주소 */

    const struct net_device_ops  *netdev_ops;     /* 드라이버 콜백 */
    const struct ethtool_ops    *ethtool_ops;    /* ethtool 콜백 */

    struct netdev_rx_queue      *_rx;             /* RX 큐 배열 */
    unsigned int               num_rx_queues;    /* RX 큐 수 */
    struct netdev_queue         *_tx;             /* TX 큐 배열 */
    unsigned int               num_tx_queues;    /* TX 큐 수 */

    struct Qdisc               *qdisc;           /* 기본 qdisc */
    /* ... */
};

NAPI 동작 흐름

NAPI(New API)는 인터럽트와 폴링을 혼합하여 고부하 네트워크에서 인터럽트 폭풍을 방지하는 수신 경로 프레임워크입니다. 핵심 동작은 다음과 같습니다:

  1. 패킷 도착 / IRQ 발생 — NIC가 패킷을 수신하면 하드웨어 인터럽트를 발생시킵니다.
  2. 폴링 모드 전환 — 드라이버 IRQ 핸들러가 napi_schedule()를 호출하고 인터럽트를 비활성화합니다.
  3. 배치 처리NET_RX_SOFTIRQ에서 poll() 콜백이 budget(기본 64)만큼 패킷을 배치로 처리합니다.
  4. 인터럽트 복귀 — 큐가 비면 napi_complete_done()으로 인터럽트를 재활성화합니다.
링크 속도최소 프레임(64B) PPS인터럽트 방식NAPI 배치
1 Gbps약 1,488,000CPU 포화 위험안정적 처리
10 Gbps약 14,880,000불가 (인터럽트만으로 CPU 전부 소모)멀티큐 + NAPI 필수
25 Gbps약 37,200,000불가NAPI + XDP 권장
100 Gbps약 148,800,000불가NAPI + 멀티큐 + XDP 필수
상세 문서: net_device_ops, NAPI, RX/TX 링, ethtool, phylink, XDP, 가상 netdev(TUN/TAP) 등은 Network Device 드라이버 문서를, NAPI 폴링 메커니즘의 상세 내용은 NAPI 문서를, GSO/GRO 오프로드는 GSO/GRO 문서를 참고하세요.

net_device 등록 과정

register_netdev()는 네트워크 디바이스를 커널에 등록하는 함수입니다. RTNL(Routing Netlink) 락을 획득한 상태에서 register_netdevice()를 호출하며, 디바이스 이름 할당, sysfs 등록, 알림(notification) 발송 등을 수행합니다.

/* net/core/dev.c — register_netdevice() 핵심 단계 (간략화) */
int register_netdevice(struct net_device *dev)
{
    struct net *net = dev_net(dev);

    /* 1. ndo_init() 콜백 호출 (드라이버 초기화) */
    if (dev->netdev_ops->ndo_init) {
        ret = dev->netdev_ops->ndo_init(dev);
        if (ret)
            goto err;
    }

    /* 2. 디바이스 이름이 %d 패턴이면 자동 할당 */
    if (strchr(dev->name, '%'))
        dev_alloc_name(dev, dev->name);

    /* 3. 네트워크 네임스페이스의 디바이스 리스트에 추가 */
    list_netdevice(dev);

    /* 4. sysfs/procfs 등록 */
    netdev_register_kobject(dev);

    /* 5. NETDEV_REGISTER 알림 발송 */
    call_netdevice_notifiers(NETDEV_REGISTER, dev);

    /* 6. default qdisc 할당 */
    dev_init_scheduler(dev);

    /* 7. 디바이스 상태를 REGISTERED로 변경 */
    dev->reg_state = NETREG_REGISTERED;
    return 0;
}
코드 설명
  • 7-10행ndo_init() 콜백은 드라이버가 하드웨어 초기화를 수행하는 시점입니다. DMA 할당, Ring Buffer 설정, NAPI 등록 등이 이루어집니다.
  • 18행list_netdevice()는 디바이스를 네트워크 네임스페이스의 디바이스 리스트에 추가합니다. ip link show로 확인할 수 있는 인터페이스 목록이 이 리스트입니다.
  • 24행NETDEV_REGISTER 알림은 notifier chain을 통해 라우팅, Netfilter, bonding 등 관심 서브시스템에 새 디바이스 등록을 통지합니다.
  • 27행dev_init_scheduler()는 기본 qdisc(보통 pfifo_fast 또는 fq_codel)를 할당합니다. sysctl net.core.default_qdisc에 의해 결정됩니다.
RTNL 락 주의사항: register_netdev()를 포함한 대부분의 네트워크 설정 작업은 RTNL(Routing Netlink) 뮤텍스를 보유한 상태에서 수행됩니다. 이 락은 전역적이며, ip link, ifconfig, 디바이스 등록/해제, 라우팅 테이블 변경 등 모든 네트워크 설정 변경이 직렬화됩니다. 따라서 대량의 네트워크 디바이스(컨테이너 수천 개)를 동시에 생성/삭제하면 RTNL 경합이 병목이 될 수 있습니다. 커널 5.13+에서는 RTNL 락을 세분화하는 작업이 진행 중입니다.
# 네트워크 디바이스 정보 확인
ip -d link show eth0                   # 상세 디바이스 정보
ethtool -i eth0                        # 드라이버/펌웨어 정보
ethtool -l eth0                        # RX/TX 큐 수
ethtool -g eth0                        # Ring Buffer 크기
ethtool -k eth0                        # 오프로드 기능 목록

# 인터럽트 상태 확인
cat /proc/interrupts | grep eth0       # per-CPU 인터럽트 분배
cat /proc/irq/<IRQ>/smp_affinity       # IRQ CPU 친화성

# NAPI 상태 확인
cat /sys/class/net/eth0/napi_defer_hard_irqs  # IRQ 지연 횟수
cat /sys/class/net/eth0/gro_flush_timeout     # GRO 플러시 타임아웃

인터럽트 코얼레싱 (Interrupt Coalescing)

인터럽트 코얼레싱은 여러 패킷을 모아서 하나의 인터럽트로 처리하여 CPU 오버헤드를 줄이는 NIC 하드웨어 기능입니다. ethtool -C로 설정하며, 지연(latency)과 처리량(throughput)의 트레이드오프를 조절합니다.

# 인터럽트 코얼레싱 현재 설정 확인
ethtool -c eth0

# 설정 변경
# rx-usecs: 인터럽트 발생까지 최대 대기 시간 (μs)
# rx-frames: 인터럽트 발생까지 최대 패킷 수
ethtool -C eth0 rx-usecs 50 rx-frames 64
ethtool -C eth0 tx-usecs 50 tx-frames 64

# 적응형 코얼레싱 (트래픽 패턴에 따라 자동 조정)
ethtool -C eth0 adaptive-rx on adaptive-tx on

# 초저지연 설정 (금융 트레이딩 등)
ethtool -C eth0 rx-usecs 0 rx-frames 1
ethtool -C eth0 tx-usecs 0 tx-frames 1
설정지연처리량CPU 사용률사용 시나리오
rx-usecs=0, rx-frames=1최소 (~1μs)낮음매우 높음초저지연 (HFT)
rx-usecs=50, rx-frames=64보통 (~50μs)높음보통범용 서버
rx-usecs=250, rx-frames=256높음 (~250μs)매우 높음낮음대량 전송 (백업 등)
adaptive-rx on동적동적동적혼합 워크로드

드라이버 모델: ndo_* 콜백

struct net_device_ops는 드라이버가 구현해야 하는 콜백 함수 집합입니다. 패킷 전송, 인터페이스 설정, 멀티캐스트 필터, TC 오프로드 등 모든 디바이스 동작이 이 인터페이스를 통해 추상화됩니다.

/* include/linux/netdevice.h — net_device_ops 핵심 콜백 */
struct net_device_ops {
    /* 인터페이스 관리 */
    int  (*ndo_open)(struct net_device *dev);
    int  (*ndo_stop)(struct net_device *dev);

    /* 패킷 전송 (TX 핵심 경로) */
    netdev_tx_t (*ndo_start_xmit)(struct sk_buff *skb,
                                  struct net_device *dev);

    /* TX 큐 선택 (멀티큐) */
    u16  (*ndo_select_queue)(struct net_device *dev,
                            struct sk_buff *skb,
                            struct net_device *sb_dev);

    /* MAC 주소/멀티캐스트 필터 */
    void (*ndo_set_rx_mode)(struct net_device *dev);
    int  (*ndo_set_mac_address)(struct net_device *dev, void *addr);

    /* MTU 변경 */
    int  (*ndo_change_mtu)(struct net_device *dev, int new_mtu);

    /* TC 하드웨어 오프로드 */
    int  (*ndo_setup_tc)(struct net_device *dev,
                        enum tc_setup_type type,
                        void *type_data);

    /* XDP 프로그램 부착 */
    int  (*ndo_bpf)(struct net_device *dev,
                   struct netdev_bpf *bpf);

    /* VLAN 필터 */
    int  (*ndo_vlan_rx_add_vid)(struct net_device *dev,
                               __be16 proto, u16 vid);
    /* ... 50개 이상의 콜백 */
};
코드 설명
  • 4-5행ndo_openip link set dev eth0 up 시 호출됩니다. Ring Buffer 할당, NAPI 활성화, 인터럽트 등록, PHY 링크 시작 등을 수행합니다. ndo_stop은 인터페이스 다운 시 역순으로 정리합니다.
  • 8-9행ndo_start_xmit은 TX 경로의 최종 단계로, 드라이버가 DMA 디스크립터를 설정하고 NIC에 전송을 지시합니다. NETDEV_TX_OK 또는 NETDEV_TX_BUSY를 반환합니다.
  • 17행ndo_set_rx_mode는 유니캐스트/멀티캐스트 주소 필터와 promiscuous 모드를 NIC 하드웨어에 설정합니다. ip maddr이나 tcpdump 시작 시 호출됩니다.
  • 24-26행ndo_setup_tc는 TC 규칙의 하드웨어 오프로드를 처리합니다. tc flower 필터, mqprio qdisc 등을 NIC의 eSwitch/TCAM에 설치합니다.
  • 29-30행ndo_bpf는 XDP 프로그램의 부착/분리를 처리합니다. XDP_SETUP_PROG 명령으로 프로그램을 설치하고, 드라이버가 RX 경로에 XDP 실행 지점을 설정합니다.

주요 자료구조 관계도

Linux 네트워크 스택의 핵심 자료구조들은 서로 긴밀하게 연결되어 있습니다. 아래 다이어그램은 패킷 처리에 관여하는 주요 구조체 간의 관계를 보여줍니다.

Linux 네트워크 스택 주요 자료구조 관계도: socket, sock, sk_buff, net_device 등의 연결 구조 네트워크 스택 주요 자료구조 관계도 struct file VFS 파일 객체 struct socket state, type, ops private_data struct sock (sk) sk_receive_queue sk_write_queue sk struct tcp_sock snd_cwnd, srtt retransmit_timer embed struct sk_buff head / data / tail / end sk, dev, protocol, cb[48] sk_receive/write_queue struct net_device name, mtu, flags netdev_ops, _rx, _tx dev struct dst_entry output(), input(), dev _skb_refdst struct napi_struct poll(), weight, budget struct Qdisc enqueue(), dequeue() qdisc struct net 네트워크 네임스페이스 nd_net struct proto connect, sendmsg, recvmsg struct proto_ops inet_stream_ops sk_prot ops struct nf_conn conntrack 연결 추적 _nfct 직접 참조 간접/내장 참조
네트워크 스택 핵심 자료구조 간의 참조 관계
자료구조 크기: struct sk_buff는 약 232바이트(64비트 시스템), struct sock은 약 760바이트, struct tcp_sock은 약 2,400바이트입니다. 고성능 서버에서 수십만 연결을 처리할 때 이 구조체들의 메모리 사용량이 상당할 수 있습니다. slabtop이나 /proc/slabinfo에서 TCPv6, sock_inode_cache, skbuff_head_cache 등으로 확인할 수 있습니다.

주요 자료구조 메모리 사용량 추적

# SLAB 캐시에서 네트워크 관련 객체 확인
slabtop -s c | head -20

# 개별 SLAB 캐시 상세 확인
cat /proc/slabinfo | grep -E "^(sk_buff|tcp_sock|sock_inode|request_sock)"

# 소켓 메모리 사용량 요약
cat /proc/net/sockstat
# sockets: used 1234
# TCP: inuse 567 orphan 0 tw 89 alloc 600 mem 123
# UDP: inuse 12 mem 3

# TCP 메모리 압력 상태 확인
cat /proc/sys/net/ipv4/tcp_mem
# 세 값: low(정상) pressure(경고) high(제한) — 페이지 단위

# 네트워크 버퍼 메모리 사용량 (바이트)
cat /proc/sys/net/core/optmem_max
cat /proc/sys/net/core/rmem_default
cat /proc/sys/net/core/wmem_default

자료구조 상속 계층

네트워크 자료구조는 C 언어의 구조체 내포(embedding)를 통해 상속 패턴을 구현합니다:

구조체상위 구조체추가 필드크기 (대략)
sock_common(기반)해시, 바인드 포트, 패밀리~120B
socksock_common 내포큐, 버퍼, 콜백, 타이머~760B
inet_socksock 내포IP 주소, 포트, TTL, TOS~900B
inet_connection_sockinet_sock 내포accept 큐, 재전송 타이머, PMTU~1,200B
tcp_sockinet_connection_sock 내포cwnd, srtt, SACK, 혼잡 제어~2,400B
udp_sockinet_sock 내포pending, corkflag, encap~950B

이 패턴 덕분에 struct sock *sk 포인터를 tcp_sk(sk), inet_sk(sk) 등의 매크로(Macro)로 캐스팅하여 프로토콜별 필드에 접근할 수 있습니다. 커널의 네트워크 코드에서 가장 빈번하게 사용되는 패턴입니다.

/* 소켓 캐스팅 매크로 */
struct tcp_sock *tp = tcp_sk(sk);    /* sock → tcp_sock */
struct inet_sock *inet = inet_sk(sk); /* sock → inet_sock */
struct inet_connection_sock *icsk = inet_csk(sk);

/* TCP 혼잡 윈도우 접근 */
u32 cwnd = tp->snd_cwnd;
u32 srtt = tp->srtt_us >> 3;  /* 마이크로초 단위 */

/* IP 주소 접근 */
__be32 saddr = inet->inet_saddr;
__be32 daddr = inet->inet_daddr;
__be16 sport = inet->inet_sport;

라우팅 서브시스템

Linux 라우팅은 FIB(Forwarding Information Base)를 기반으로 Longest Prefix Match(LPM) 조회를 수행합니다. FIB는 내부적으로 LC-trie(Level-Compressed trie) 자료구조를 사용하여 대규모 라우팅 테이블에서도 효율적인 조회를 보장합니다.

라우팅 조회 과정

  1. FIB 조회fib_lookup()에서 목적지 IP에 대한 LPM(Longest Prefix Match) 조회를 수행합니다.
  2. dst_entry 생성 — 조회 결과로 struct dst_entry를 생성하여 sk_buff에 연결합니다. 이 구조체는 출력 인터페이스, 다음 홉, MTU 정보를 포함합니다.
  3. 이웃 조회 — 다음 홉의 L2 주소(MAC)를 ARP/NDP 캐시에서 조회합니다. 캐시 미스 시 ARP 요청을 보내고 패킷을 neigh->arp_queue에 대기시킵니다.

라우팅 테이블 구조

테이블ID용도조회 순서
local255로컬 주소, 브로드캐스트1순위 (항상 먼저 조회)
main254일반 라우팅 엔트리2순위
default253기본 규칙용3순위
사용자 정의1~252Policy Routingip rule로 조회 순서 지정
# 라우팅 테이블 확인
ip route show table main           # 기본 라우팅 테이블
ip route show table local          # 로컬 주소 테이블
ip rule list                       # Policy Routing 규칙

# 특정 목적지에 대한 라우팅 결정 확인
ip route get 8.8.8.8               # FIB 조회 결과
ip route get 8.8.8.8 from 10.0.0.1 iif eth0  # 소스/인터페이스 지정
상세 문서: FIB 내부 구조(LC-trie), Policy Routing, ECMP/Multipath, VRF, IPv6 라우팅, Netfilter 상호작용, SRv6 등의 상세 내용은 라우팅 (Routing Subsystem) 문서를 참고하세요.

FIB 조회 내부 구현

fib_lookup()은 Policy Routing 규칙(ip rule)을 순회하며, 매칭되는 규칙의 라우팅 테이블에서 LPM 조회를 수행합니다. 내부적으로 LC-trie(Level-Compressed Trie) 자료구조를 사용하여 대규모 BGP 라우팅 테이블(수십만 엔트리)에서도 효율적으로 조회합니다.

/* net/ipv4/fib_rules.c — fib_lookup() 핵심 로직 (간략화) */
int fib_lookup(struct net *net,
              const struct flowi4 *flp,
              struct fib_result *res, unsigned int flags)
{
    /* Policy Routing 비활성 시 (대부분의 서버) */
    /* main + local 테이블만 직접 조회 (빠른 경로) */
    if (!net->ipv4.fib_has_custom_rules) {
        res->tclassid = 0;

        /* 1. local 테이블 조회 (로컬 주소, 브로드캐스트) */
        err = fib_table_lookup(local_table, flp, res, flags);
        if (err != -EAGAIN)
            return err;

        /* 2. main 테이블 조회 */
        err = fib_table_lookup(main_table, flp, res, flags);
        if (err != -EAGAIN)
            return err;

        return -ENETUNREACH;
    }

    /* Policy Routing 활성 시: 규칙 순회 */
    return __fib_lookup(net, flp, res, flags);
}

/* net/ipv4/fib_trie.c — fib_table_lookup() LC-trie 조회 */
int fib_table_lookup(struct fib_table *tb,
                    const struct flowi4 *flp,
                    struct fib_result *res, int fib_flags)
{
    struct trie *t = (struct trie *)tb->tb_data;
    t_key key = ntohl(flp->daddr);  /* 목적지 IP */
    struct key_vector *n, *pn;
    t_key cindex;

    /* Trie 루트에서 시작하여 목적지 IP 비트를 따라 탐색 */
    n = get_child_rcu(t->kv, key);
    while (n) {
        /* Leaf 노드: 프리픽스 매칭 확인 */
        /* Internal 노드: 다음 비트로 자식 탐색 */
        /* Longest Prefix Match를 위해 백트래킹 */
        /* ... */
    }
    /* fib_result에 nexthop, 출력 디바이스, 경로 타입 설정 */
    return err;
}
코드 설명
  • 8행Policy Routing이 비활성(기본)인 경우 최적화된 경로로 진입합니다. fib_has_custom_rules는 사용자 정의 ip rule이 추가되었을 때만 true가 됩니다.
  • 12행local 테이블을 먼저 조회합니다. 로컬에 할당된 IP 주소나 브로드캐스트 주소에 대한 매칭이 여기서 이루어집니다.
  • 17행local 테이블에서 매칭이 없으면 main 테이블에서 실제 라우팅 조회를 수행합니다.
  • 34행목적지 IP를 호스트 바이트 순서로 변환하여 Trie 키로 사용합니다. Trie는 IP 주소의 비트를 상위부터 탐색합니다.
  • 39행LC-trie는 일반 binary trie보다 레벨을 압축하여 조회 깊이를 줄입니다. 인터넷 전체 라우팅 테이블(약 90만 엔트리)에서도 평균 4-6단계 조회로 완료됩니다.

dst_entry 캐시와 라우팅 캐시

dst_entry는 라우팅 결정의 결과를 캐싱하는 구조체입니다. 커널 3.6부터 전역 라우팅 캐시(rt_hash_table)가 제거되고, 소켓별/FIB 엔트리별 캐시로 대체되었습니다. Early Demux에서 소켓의 dst_entry를 재사용하는 것이 현재의 주된 캐싱 전략입니다.

/* include/net/dst.h — dst_entry 핵심 필드 */
struct dst_entry {
    struct net_device  *dev;          /* 출력 디바이스 */
    struct dst_ops     *ops;          /* IPv4/IPv6 ops */
    unsigned long      expires;       /* 만료 시간 */
    int                (*input)(struct sk_buff *skb);
    int                (*output)(struct net *net,
                                 struct sock *sk,
                                 struct sk_buff *skb);
    unsigned int       flags;         /* DST_* 플래그 */
    int                error;
    struct neighbour  *_neighbour;    /* 다음 홉 이웃 */
    /* ... */
};

/* 수신 패킷: input 콜백 사용 */
/*   로컬 대상: input = ip_local_deliver */
/*   포워딩:   input = ip_forward */

/* 송신 패킷: output 콜백 사용 */
/*   유니캐스트: output = ip_output */
/*   멀티캐스트: output = ip_mc_output */
코드 설명
  • 3행dev는 패킷의 출력 네트워크 인터페이스입니다. FIB 조회 결과로 결정됩니다.
  • 6-9행inputoutput 함수 포인터는 패킷의 다음 처리 단계를 결정합니다. 수신 패킷은 dst_input(skb)으로, 송신 패킷은 dst_output(skb)으로 호출됩니다.
  • 12행_neighbour는 다음 홉의 이웃 항목(ARP 캐시 엔트리)을 가리킵니다. L2 헤더 추가 시 이 이웃의 MAC 주소를 사용합니다.
# 라우팅 캐시/FIB 통계 확인
cat /proc/net/fib_trie                 # FIB trie 구조 (상세)
cat /proc/net/fib_triestat             # FIB trie 통계
# Aver depth:     2.52
# Max depth:      7
# Leaves:         834
# Internal nodes: 312

# rt_cache 통계 (레거시, 참고용)
cat /proc/net/stat/rt_cache

# ECMP (Equal-Cost Multi-Path) 설정
ip route add 10.0.0.0/8 \
  nexthop via 192.168.1.1 weight 1 \
  nexthop via 192.168.1.2 weight 1

# ECMP 해시 기반 선택 (L3 또는 L3+L4)
sysctl net.ipv4.fib_multipath_hash_policy  # 0=L3, 1=L3+L4, 2=L3+L4+src

Policy Routing과 ip rule

Policy Routing은 소스 IP, 마킹(fwmark), 인터페이스 등 다양한 조건에 따라 서로 다른 라우팅 테이블을 적용하는 기능입니다. ip rule 명령으로 규칙을 관리하며, 규칙은 우선순위(priority) 순서로 평가됩니다.

# Policy Routing 기본 규칙 확인
ip rule list
# 0:       from all lookup local        → 로컬 주소 매칭
# 32766:   from all lookup main         → 기본 라우팅
# 32767:   from all lookup default      → 기본값

# 소스 IP 기반 라우팅 (멀티호밍)
ip rule add from 10.0.1.0/24 table 100 prio 100
ip route add default via 192.168.1.1 table 100

# fwmark 기반 라우팅 (Netfilter 연동)
iptables -t mangle -A OUTPUT -p tcp --dport 80 -j MARK --set-mark 1
ip rule add fwmark 1 table 200 prio 200
ip route add default via 10.0.0.1 table 200

# 인터페이스 기반 라우팅
ip rule add iif eth1 table 300 prio 300

# VRF (Virtual Routing and Forwarding) 설정
ip link add vrf-red type vrf table 10
ip link set dev vrf-red up
ip link set dev eth1 master vrf-red
# eth1의 모든 트래픽이 table 10으로 라우팅됩니다

# 라우팅 결정 디버깅
ip route get 8.8.8.8 from 10.0.1.5    # 소스 IP 지정 조회
ip route get 8.8.8.8 mark 1           # fwmark 지정 조회
ip route get fibmatch 10.0.0.0/24     # 정확한 FIB 매칭 조회

IPv6 라우팅 특이사항

IPv6 라우팅은 IPv4와 유사하지만 몇 가지 중요한 차이가 있습니다. IPv6는 확장 헤더를 사용하며, NDP(Neighbor Discovery Protocol)로 ARP를 대체하고, Router Advertisement(RA)를 통한 자동 주소 설정(SLAAC)을 지원합니다.

항목IPv4IPv6
라우팅 테이블fib_table (LC-trie)fib6_table (radix tree)
이웃 탐색ARP (이더넷 브로드캐스트)NDP (ICMPv6 멀티캐스트)
주소 자동 설정DHCPSLAAC + DHCPv6
단편화라우터/호스트소스 호스트만 (PMTUD 필수)
FIB 조회 함수fib_lookup()fib6_lookup()
라우팅 캐시per-socket dst_entryper-socket + exception table
멀티패스nexthop weightnexthop weight + source hash
# IPv6 라우팅 확인
ip -6 route show                       # IPv6 라우팅 테이블
ip -6 route get 2001:db8::1            # IPv6 경로 조회
ip -6 neigh show                       # NDP 이웃 캐시

# Router Advertisement 확인
radvdump                               # RA 패킷 덤프

# IPv6 관련 sysctl
sysctl net.ipv6.conf.all.forwarding    # IPv6 포워딩 (라우터 모드)
sysctl net.ipv6.conf.all.accept_ra     # RA 수락 (SLAAC)
sysctl net.ipv6.conf.all.use_tempaddr  # Privacy Extensions

TC (Traffic Control)와 qdisc

패킷 스케줄링과 트래픽 셰이핑을 담당합니다. 각 네트워크 디바이스에 qdisc(큐잉 규칙)가 연결됩니다. TC는 3개 구성 요소로 이루어집니다: qdisc(큐잉 규칙), class(계층적 분류), filter(패킷 분류기).

주요 qdisc 유형

qdisc유형동작용도
pfifo_fastClassless3-band 우선순위(Priority) FIFO레거시 기본값
fq_codelClasslessFair Queuing + CoDel AQM현재 기본값, 버퍼블로트 방지
htbClassfulHierarchical Token Bucket대역폭 보장/제한
tbfClasslessToken Bucket Filter단순 속도 제한
netemClassless네트워크 에뮬레이션지연/손실/중복 테스트
mqprioClassful멀티큐 우선순위 매핑하드웨어 큐 연동
clsactClassfulingress/egress eBPF 연결점TC BPF 프로그램 부착
ingress특수수신 경로 필터링수신 패킷 분류/리다이렉트
# TC 기본 사용 예제
tc qdisc show dev eth0                       # 현재 qdisc 확인
tc qdisc add dev eth0 root fq_codel          # fq_codel 설정
tc qdisc add dev eth0 root netem delay 100ms # 100ms 지연 추가

# HTB로 대역폭 제한
tc qdisc add dev eth0 root handle 1: htb default 10
tc class add dev eth0 parent 1: classid 1:10 htb rate 100mbit ceil 200mbit
tc class add dev eth0 parent 1: classid 1:20 htb rate 50mbit ceil 100mbit

# TC BPF 프로그램 부착
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj prog.o sec classifier
상세 문서: TC 아키텍처, qdisc 유형별 설정, classifier/filter, 대역폭 제한과 트래픽 셰이핑은 TC (Traffic Control) 문서를 참고하세요.

fq_codel 내부 동작

fq_codel은 Fair Queuing과 CoDel(Controlled Delay) AQM(Active Queue Management)을 결합한 qdisc로, 현재 Linux의 기본 qdisc입니다. 각 플로우에 별도의 큐를 할당하여 공정성을 보장하고, CoDel 알고리즘으로 큐잉 지연이 target(기본 5ms)을 초과하면 패킷을 드롭하여 버퍼블로트(Bufferbloat)를 방지합니다.

/* net/sched/sch_fq_codel.c — fq_codel 핵심 구조 (간략화) */
struct fq_codel_sched_data {
    u32             flows_cnt;     /* 플로우 큐 수 (기본 1024) */
    u32             quantum;       /* DRR 양자 (기본 1514) */
    u32             drop_batch_size; /* 한번에 드롭할 패킷 수 */
    struct codel_params cparams;   /* CoDel 파라미터 */
    struct fq_codel_flow *flows;   /* 플로우 큐 배열 */
    struct list_head  new_flows;   /* 새 플로우 리스트 */
    struct list_head  old_flows;   /* 기존 플로우 리스트 */
};

/* CoDel 파라미터 */
/* target: 허용 큐잉 지연 (기본 5ms) */
/* interval: 판단 주기 (기본 100ms) */
/* 큐잉 지연이 target을 interval 동안 */
/* 지속적으로 초과하면 드롭 시작 */

/* 패킷 분류: skb_get_hash()로 플로우 해시 계산 */
/* → hash % flows_cnt로 플로우 큐 결정 */
/* Deficit Round Robin으로 플로우 간 공정 dequeue */
코드 설명
  • 3행flows_cnt는 플로우 큐의 수입니다. 기본 1024개의 큐를 사용하여 최대 1024개의 플로우를 독립적으로 관리합니다. tc qdisc ... fq_codel flows 4096으로 변경할 수 있습니다.
  • 4행quantum은 Deficit Round Robin에서 각 플로우가 한 라운드에 전송할 수 있는 바이트 수입니다. MTU + 이더넷 헤더 크기로 설정됩니다.
  • 8-9행new_flowsold_flows는 DRR 스케줄러의 두 리스트입니다. 새 플로우는 new_flows에서 우선 처리되어 짧은 플로우(HTTP 요청 등)의 지연을 최소화합니다.
  • 13-16행CoDel은 큐잉 지연을 측정하여 버퍼블로트를 감지합니다. 패킷이 큐에 머문 시간이 target(5ms)을 interval(100ms) 동안 지속적으로 초과하면 드롭 빈도를 점진적으로 증가시킵니다.
# fq_codel 상세 통계 확인
tc -s qdisc show dev eth0

# 전형적인 출력:
# qdisc fq_codel 0: root refcnt 2 limit 10240p flows 1024
#   quantum 1514 target 5ms interval 100ms memory_limit 32Mb
#   ecn_mark 0 ce_threshold 0us
#  Sent 123456789 bytes 987654 pkt (dropped 12, overlimits 34 requeues 5)
#  backlog 0b 0p requeues 5
#   maxpacket 1514 drop_overlimit 0 new_flow_count 456
#   ecn_mark 0 new_flows_len 0 old_flows_len 2

# fq_codel 파라미터 조정
tc qdisc replace dev eth0 root fq_codel \
  target 1ms \       # 저지연 환경: 1ms 타깃
  interval 50ms \    # 판단 주기 단축
  flows 4096 \       # 플로우 큐 수 증가
  quantum 1514       # DRR 양자

멀티코어 패킷 분산

고속 네트워크에서 단일 CPU로는 패킷 처리량(Throughput)이 부족합니다. Linux 커널은 여러 메커니즘으로 패킷 처리를 여러 CPU에 분산합니다.

메커니즘계층동작설정 방법
RSS (Receive Side Scaling)NIC 하드웨어NIC가 플로우 해시로 RX 큐를 선택, 각 큐에 별도 IRQ/CPU 할당ethtool -L, ethtool -X
RPS (Receive Packet Steering)커널 소프트웨어소프트웨어 플로우 해시로 타겟 CPU 선택 (RSS 미지원 NIC용)/sys/class/net/*/queues/rx-*/rps_cpus
RFS (Receive Flow Steering)커널 소프트웨어패킷을 소켓 소유 CPU로 전달 (캐시 지역성 최적화)/proc/sys/net/core/rps_sock_flow_entries
XPS (Transmit Packet Steering)커널 소프트웨어송신 CPU와 TX 큐의 매핑 최적화/sys/class/net/*/queues/tx-*/xps_cpus
aRFS (Accelerated RFS)NIC 하드웨어NIC 하드웨어에서 RFS 규칙 적용ethtool -K rx-flow-hash
per-CPU backlog: RPS/RFS가 타겟 CPU를 결정하면 enqueue_to_backlog()로 해당 CPU의 softnet_data.input_pkt_queue에 패킷을 삽입하고 IPI(Inter-Processor Interrupt)로 타겟 CPU를 깨웁니다. 이 큐의 크기는 /proc/sys/net/core/netdev_max_backlog(기본 1000)으로 제한되며, 초과 시 패킷이 드롭됩니다.
# RSS 설정: 큐 수 확인/변경
ethtool -l eth0                           # 채널(큐) 수 확인
ethtool -L eth0 combined 8                # 8개 결합 큐로 설정

# IRQ-CPU 매핑 확인
cat /proc/interrupts | grep eth0          # NIC 인터럽트 분포

# RPS 활성화 (예: 8코어 시스템에서 CPU 0-7)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus

# softnet 통계 확인 (드롭 모니터링)
cat /proc/net/softnet_stat
상세 문서: RSS/RPS/RFS/XPS 설정, per-CPU backlog, flow dissector, softnet_stat 분석, 패킷 드롭 디버깅은 네트워크 패킷 흐름 & 디버깅 문서를 참고하세요.

Flow Dissector

Flow Dissector는 패킷의 L3/L4 헤더를 파싱하여 플로우 키(5-tuple)를 추출하는 커널 프레임워크입니다. RSS, RPS, fq_codel, TC 등 플로우 기반 처리를 수행하는 모든 서브시스템이 이 프레임워크를 사용합니다.

/* net/core/flow_dissector.c — Flow Dissector 핵심 구조 */
struct flow_keys {
    struct flow_dissector_key_control control;
    struct flow_dissector_key_basic   basic;    /* n_proto, ip_proto */
    struct flow_dissector_key_ipv4_addrs addrs;  /* src, dst IP */
    struct flow_dissector_key_ports  ports;    /* src, dst port */
    struct flow_dissector_key_tags   tags;     /* VLAN ID 등 */
    struct flow_dissector_key_vlan   vlan;     /* VLAN 정보 */
    struct flow_dissector_key_keyid  keyid;    /* 터널 키 */
};

/* 패킷에서 플로우 키 추출 */
u32 skb_get_hash(struct sk_buff *skb)
{
    if (!skb->l4_hash && !skb->sw_hash) {
        /* NIC가 RSS 해시를 제공하지 않으면 소프트웨어 해시 계산 */
        __skb_get_hash(skb);
        /* → skb_flow_dissect_flow_keys() */
        /* → __skb_flow_dissect() — 프로토콜별 파싱 */
        /* → flow_hash_from_keys() — jhash2 기반 해시 */
    }
    return skb->hash;
}
코드 설명
  • 2-10행flow_keys 구조체는 패킷의 플로우 식별 정보를 포함합니다. L3 주소(IPv4/IPv6), L4 포트, VLAN 태그, 터널 키 등을 추출합니다.
  • 16-21행NIC가 RSS 해시를 제공하면(l4_hash 또는 하드웨어 해시) 이를 사용하고, 제공하지 않으면 소프트웨어에서 패킷 헤더를 파싱하여 해시를 계산합니다.

IRQ 친화성(Affinity) 최적화

고성능 네트워크에서는 NIC 인터럽트를 적절한 CPU에 분배하는 것이 중요합니다. 각 RX/TX 큐의 IRQ를 특정 CPU에 고정하고, NAPI 처리와 유저 프로세스가 같은 CPU에서 실행되도록 구성합니다.

# irqbalance 중지 (수동 IRQ 관리 시)
systemctl stop irqbalance

# NIC IRQ 번호 확인
grep eth0 /proc/interrupts | awk '{print $1, $NF}'

# IRQ별 CPU 친화성 설정
# eth0-rx-0 → CPU 0, eth0-rx-1 → CPU 1, ...
echo 1 > /proc/irq/<IRQ0>/smp_affinity   # CPU 0
echo 2 > /proc/irq/<IRQ1>/smp_affinity   # CPU 1
echo 4 > /proc/irq/<IRQ2>/smp_affinity   # CPU 2
echo 8 > /proc/irq/<IRQ3>/smp_affinity   # CPU 3

# 자동화 스크립트 (set_irq_affinity 도구 사용)
# Intel 드라이버 패키지에 포함된 스크립트:
/usr/local/bin/set_irq_affinity.sh eth0

# NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# NIC가 연결된 NUMA 노드의 CPU에 IRQ를 할당하면
# 메모리 접근 지연이 줄어 성능이 향상됩니다

# 처리 확인
watch -n1 'cat /proc/interrupts | grep eth0'
# 각 CPU의 인터럽트 카운트가 균등한지 확인
NUMA 친화성: NIC와 같은 NUMA 노드의 CPU에 IRQ를 할당하면 메모리 접근 지연이 줄어 패킷 처리 성능이 10~20% 향상될 수 있습니다. numactl --hardware로 NUMA 토폴로지를 확인하고, NIC의 NUMA 노드는 cat /sys/class/net/eth0/device/numa_node로 확인합니다. 잘못된 NUMA 노드에 IRQ를 할당하면 원격 메모리 접근(remote memory access)으로 인해 PPS가 크게 저하될 수 있습니다.

네트워크 오프로드 기술

CPU 부하를 줄이기 위해 네트워크 처리의 일부를 NIC 하드웨어 또는 커널 소프트웨어 계층에서 최적화합니다. 오프로드 기능은 ethtool -k로 확인하고 ethtool -K로 제어합니다.

오프로드방향동작성능 영향
TSO (TCP Segmentation Offload)TXNIC 하드웨어에서 TCP 세그먼트 분할CPU 사용률 크게 감소
GSO (Generic Segmentation Offload)TX커널에서 대형 세그먼트를 지연 분할qdisc까지 단일 skb 전달
GRO (Generic Receive Offload)RX동일 플로우 패킷을 병합하여 상위 계층 호출 횟수 감소CPU 사용률 20-30% 감소
LRO (Large Receive Offload)RXNIC 하드웨어에서 패킷 병합라우터/포워딩에서 문제 가능
체크섬 오프로드TX/RXNIC에서 L3/L4 체크섬 계산/검증CPU 부하 감소
Scatter/GatherTX비연속 메모리를 DMA로 직접 전송메모리 복사 제거
# 오프로드 상태 확인
ethtool -k eth0 | grep -E "tcp-segmentation|generic-segmentation|generic-receive|checksum"

# 오프로드 제어
ethtool -K eth0 tso on              # TSO 활성화
ethtool -K eth0 gro on              # GRO 활성화
ethtool -K eth0 gso on              # GSO 활성화
ethtool -K eth0 tx-checksum-ipv4 on # TX 체크섬 오프로드
상세 문서: GSO/GRO의 내부 동작, 병합 조건, 드라이버 연동은 GSO/GRO 네트워크 오프로드 문서를, XDP/BPF 기반 고성능 패킷 처리는 BPF/eBPF/XDP 문서를 참고하세요.

체크섬 오프로드 내부 동작

체크섬 오프로드는 NIC 하드웨어에서 IP/TCP/UDP 체크섬을 계산(TX) 또는 검증(RX)하여 CPU 부하를 줄입니다. skb->ip_summed 필드가 체크섬 상태를 나타냅니다.

ip_summed 값방향의미커널 동작
CHECKSUM_NONERX체크섬 미검증커널이 소프트웨어로 검증
CHECKSUM_UNNECESSARYRXNIC가 체크섬 검증 완료커널이 검증 생략
CHECKSUM_COMPLETERXNIC가 부분 체크섬 제공커널이 나머지 검증
CHECKSUM_PARTIALTX커널이 부분 계산, NIC가 완성NIC가 최종 체크섬 삽입

TX 체크섬 오프로드 — 커널 내부

TCP/UDP 송신 시 의사 헤더 체크섬만 계산하며, ip_summed = CHECKSUM_PARTIAL을 설정합니다.

NIC 드라이버가 TX 디스크립터에 체크섬 오프로드 플래그를 설정하면, NIC 하드웨어가 csum_start부터 패킷 끝까지 체크섬을 계산하고 csum_offset 위치에 결과를 삽입합니다.

NIC가 체크섬 오프로드를 지원하지 않으면: dev_hard_start_xmit()skb_csum_hwoffload_help()skb_checksum_help()로 소프트웨어 체크섬을 계산합니다.

코드 설명
  • 3-6행TX 체크섬 오프로드에서는 커널이 의사 헤더(pseudo header) 체크섬만 계산하고, 나머지는 NIC에 위임합니다. csum_startcsum_offset으로 NIC에 계산 범위와 삽입 위치를 알려줍니다.
  • 12-14행NIC가 체크섬 오프로드를 지원하지 않으면 skb_checksum_help()가 소프트웨어에서 체크섬을 완전히 계산합니다. 이는 가상 NIC(veth, bridge)에서 자주 발생합니다.

XDP와 네트워크 스택 우회

XDP(eXpress Data Path)는 드라이버 레벨에서 eBPF 프로그램을 실행하여 sk_buff 할당 전에 패킷을 처리합니다. DDoS 방어, 패킷 필터링, 로드 밸런싱 등에서 10Mpps 이상의 성능을 달성할 수 있습니다.

/* XDP 프로그램의 반환값 (verdict) */
enum xdp_action {
    XDP_ABORTED = 0,    /* 오류 발생 — 패킷 드롭 + 추적 */
    XDP_DROP,            /* 패킷 드롭 (가장 빠름) */
    XDP_PASS,            /* 일반 네트워크 스택으로 전달 */
    XDP_TX,              /* 수신 인터페이스로 재전송 */
    XDP_REDIRECT,        /* 다른 인터페이스/AF_XDP로 리다이렉트 */
};

/* XDP 프로그램 예시: 특정 IP 드롭 (DDoS 방어) */
SEC("xdp")
int xdp_firewall(struct xdp_md *ctx)
{
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != bpf_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    /* BPF 맵에서 차단 IP 조회 */
    if (bpf_map_lookup_elem(&blocked_ips, &ip->saddr))
        return XDP_DROP;  /* sk_buff 할당 전 드롭 */

    return XDP_PASS;
}
코드 설명
  • 2-8행XDP verdict는 패킷의 처리 방향을 결정합니다. XDP_DROP은 sk_buff를 할당하지 않고 즉시 드롭하므로 가장 빠르며, DDoS 방어에 이상적입니다.
  • 14-15행XDP 프로그램은 xdp_md 구조체로 패킷 데이터에 접근합니다. datadata_end 포인터 사이가 패킷 데이터이며, 경계 검사가 필수입니다.
  • 29-30행BPF 맵(해시 테이블)에서 소스 IP를 조회하여 차단 여부를 결정합니다. 맵 조회는 O(1)이므로 수백만 개의 IP를 차단해도 성능 저하가 없습니다.
# XDP 프로그램 부착
ip link set dev eth0 xdpgeneric obj prog.o sec xdp  # 소프트웨어 XDP
ip link set dev eth0 xdp obj prog.o sec xdp         # 드라이버 XDP (네이티브)

# XDP 프로그램 상태 확인
ip link show eth0 | grep xdp

# XDP 프로그램 제거
ip link set dev eth0 xdp off

# XDP 통계 확인
bpftool prog show
bpftool map dump id <MAP_ID>

네트워크 네임스페이스와 가상 디바이스

네트워크 네임스페이스(struct net)는 네트워크 스택 전체를 격리하는 커널 기능입니다. 각 네임스페이스는 독립적인 네트워크 인터페이스, 라우팅 테이블, Netfilter 규칙, 소켓 공간을 가집니다. 컨테이너(Docker, Kubernetes)의 네트워크 격리 기반이 됩니다.

네트워크 네임스페이스 내부 구조

커널 내부에서 모든 네트워크 관련 함수는 struct net *net 매개변수를 통해 현재 네임스페이스를 전달받습니다. 예를 들어 라우팅 조회 시 fib_lookup(net, ...)으로 해당 네임스페이스의 라우팅 테이블만 조회합니다.

/* include/net/net_namespace.h — 네트워크 네임스페이스 핵심 필드 */
struct net {
    struct list_head       list;           /* 전역 네임스페이스 리스트 */
    struct ns_common       ns;             /* 공통 네임스페이스 정보 */
    struct net_device      *loopback_dev;  /* lo 인터페이스 */
    struct hlist_head      *dev_name_head; /* 이름별 디바이스 해시 */
    struct hlist_head      *dev_index_head;/* 인덱스별 디바이스 해시 */
    struct netns_ipv4     ipv4;           /* IPv4 전용 데이터 */
    struct netns_ipv6     ipv6;           /* IPv6 전용 데이터 */
    struct netns_nf       nf;             /* Netfilter 데이터 */
    struct netns_ct       ct;             /* conntrack 데이터 */
    struct sock           *rtnl;          /* rtnetlink 소켓 */
    /* ... */
};
# 네트워크 네임스페이스 생성 및 관리
ip netns add ns1                    # 네임스페이스 생성
ip netns list                       # 네임스페이스 목록
ip netns exec ns1 ip addr show      # 네임스페이스 내에서 명령 실행

# veth 페어로 네임스페이스 연결
ip link add veth0 type veth peer name veth1
ip link set veth1 netns ns1
ip addr add 10.0.0.1/24 dev veth0
ip netns exec ns1 ip addr add 10.0.0.2/24 dev veth1
ip link set veth0 up
ip netns exec ns1 ip link set veth1 up

가상 네트워크 디바이스 유형

Linux 커널은 다양한 가상 네트워크 디바이스를 제공합니다. 각 유형은 특정 네트워크 아키텍처 패턴에 맞게 설계되었습니다.

디바이스 유형용도동작 원리주요 사용 사례
veth네임스페이스 연결두 끝이 서로 연결된 가상 이더넷 쌍Docker 컨테이너, K8s Pod
bridgeL2 스위칭소프트웨어 이더넷 브릿지, MAC 학습VM 네트워킹, 컨테이너 브릿지
TUNL3 터널유저 공간과 IP 패킷 교환VPN (OpenVPN, WireGuard)
TAPL2 터널유저 공간과 이더넷 프레임 교환QEMU/KVM 네트워킹
MACVLANMAC 기반 가상화하나의 물리 NIC에 여러 MAC 주소컨테이너 직접 네트워킹
IPVLANIP 기반 가상화동일 MAC, 다른 IP로 L3 격리MAC 주소 제한 환경
VXLAN오버레이(Overlay) 네트워크UDP 캡슐화(Encapsulation)로 L2 over L3 터널K8s Flannel, 멀티테넌트
GENEVE범용 오버레이확장 가능한 UDP 캡슐화OVN, 차세대 오버레이
bondNIC 결합여러 물리 NIC를 하나로 결합고가용성, 대역폭 집계
dummy가상 인터페이스항상 UP 상태인 가상 NICloopback 대체, 라우팅 앵커
VRFL3 도메인 격리라우팅 테이블 격리 (L3 master)멀티테넌트 라우팅

가상 디바이스 생성 명령 예시

# veth 쌍 생성
ip link add veth0 type veth peer name veth1

# Linux Bridge 생성 및 포트 추가
ip link add br0 type bridge
ip link set eth0 master br0
ip link set veth0 master br0
ip link set br0 up

# MACVLAN 생성 (bridge 모드)
ip link add macvlan0 link eth0 type macvlan mode bridge
ip addr add 10.0.0.100/24 dev macvlan0
ip link set macvlan0 up

# IPVLAN 생성 (L3 모드)
ip link add ipvlan0 link eth0 type ipvlan mode l3
ip addr add 10.0.0.101/24 dev ipvlan0
ip link set ipvlan0 up

# VXLAN 생성 (멀티캐스트 기반)
ip link add vxlan100 type vxlan id 100 \
   group 239.1.1.1 dev eth0 dstport 4789
ip link set vxlan100 up

# GENEVE 생성
ip link add geneve100 type geneve id 100 \
   remote 10.0.0.2 dstport 6081
ip link set geneve100 up

# TUN 디바이스 생성
ip tuntap add name tun0 mode tun
ip addr add 10.1.0.1/24 dev tun0
ip link set tun0 up

# Bond 생성 (802.3ad LACP)
ip link add bond0 type bond mode 802.3ad
ip link set eth0 master bond0
ip link set eth1 master bond0
ip link set bond0 up

# VRF 생성
ip link add vrf-red type vrf table 100
ip link set vrf-red up
ip link set eth2 master vrf-red
컨테이너 네트워킹 패턴: Docker 기본 네트워크는 veth 쌍 + bridge(docker0) 구조입니다. 컨테이너의 eth0은 veth의 한쪽 끝이고, 다른 쪽은 호스트의 docker0 브릿지에 연결됩니다. Kubernetes는 CNI 플러그인에 따라 다양한 가상 디바이스 조합을 사용합니다(Calico: IPIP/VXLAN, Cilium: veth + eBPF, Flannel: VXLAN).

컨테이너 네트워킹 아키텍처 비교

CNI 플러그인데이터 경로오버레이정책 엔진(Policy Engine)성능 특성
Docker Bridgeveth + bridge없음 (호스트 로컬)iptables기준선
Flannelveth + bridge + VXLANVXLAN외부 의존오버레이 오버헤드
Calicoveth + 라우팅IPIP/VXLAN (선택)iptables/eBPFL3 직접 라우팅으로 높은 성능
Ciliumveth + eBPFVXLAN/GENEVE (선택)eBPFkube-proxy 대체, 최고 성능
AntreaOVSGENEVEOVS + eBPFOVS 기반 유연한 정책
SR-IOVVF 직접 할당없음하드웨어최소 지연, 최대 대역폭
상세 문서: 네트워크 네임스페이스는 네트워크 네임스페이스 문서를, VXLAN/GENEVE 오버레이는 VXLAN/GENEVE 오버레이 문서를 참고하세요.

네트워크 네임스페이스 내부 구현

네트워크 네임스페이스(struct net)의 내부 구현을 이해하면 컨테이너 네트워킹의 성능 특성과 제약을 파악할 수 있습니다. 이 섹션에서는 네임스페이스 생성/삭제 과정과 프로토콜 스택 격리 메커니즘을 설명합니다.

네임스페이스 생성 과정

clone(CLONE_NEWNET) 또는 unshare(CLONE_NEWNET) 시스템 콜로 새 네트워크 네임스페이스를 생성하면 커널 내부에서 다음 과정이 진행됩니다:

  1. struct net 할당copy_net_ns()가 새 struct net을 할당합니다. 각 네임스페이스 서브시스템(IPv4, IPv6, Netfilter 등)의 pernet_operations가 초기화됩니다.
  2. loopback 디바이스 생성loopback_net_init()이 새 네임스페이스 전용 lo 인터페이스를 생성합니다. 이것이 네임스페이스의 최초 네트워크 인터페이스입니다.
  3. 프로토콜별 초기화 — IPv4 라우팅 테이블(fib_net_init()), Netfilter 상태(nf_conntrack_pernet_init()), proc 파일시스템(Filesystem) 엔트리 등이 네임스페이스별로 초기화됩니다.
  4. sysctl 복사 — 네트워크 관련 sysctl 변수가 네임스페이스별로 독립 복사됩니다. /proc/sys/net/ 아래의 모든 항목이 네임스페이스별로 격리됩니다.
/* net/core/net_namespace.c — 네임스페이스 생성 핵심 */
static struct net *setup_net(struct net *net, struct user_namespace *user_ns)
{
    /* 참조 카운트 초기화 */
    refcount_set(&net->ns.count, 1);

    /* 모든 pernet_operations의 init 콜백 호출 */
    list_for_each_entry(ops, &pernet_list, list) {
        if (ops->init) {
            error = ops->init(net);
            if (error < 0)
                goto out_undo;
        }
    }
    /* ... */
}

pernet_operations 등록 메커니즘

각 네트워크 서브시스템은 struct pernet_operations를 등록하여 네임스페이스 생성/삭제 시 호출되는 콜백을 정의합니다. 이 메커니즘 덕분에 새로운 네트워크 기능을 추가해도 네임스페이스 격리가 자동으로 적용됩니다.

/* 서브시스템별 pernet_operations 등록 예시 */
static struct pernet_operations ipv4_net_ops = {
    .init = ipv4_net_init,
    .exit = ipv4_net_exit,
};

/* 초기화 시 등록 */
register_pernet_subsys(&ipv4_net_ops);

/* 네임스페이스 생성 시 ipv4_net_init() 자동 호출 */
/* 네임스페이스 삭제 시 ipv4_net_exit() 자동 호출 */

네임스페이스 간 디바이스 이동

ip link set dev eth0 netns ns1 명령은 dev_change_net_namespace() 함수를 호출합니다. 이 과정에서 디바이스의 nd_net 포인터가 대상 네임스페이스로 변경되고, 기존 네임스페이스의 디바이스 리스트에서 제거된 후 대상 네임스페이스의 리스트에 삽입됩니다. 디바이스의 모든 qdisc, 소켓 연결, Netfilter 상태가 재초기화됩니다.

네임스페이스 성능 고려사항: 네트워크 네임스페이스는 격리를 제공하지만, veth 쌍을 통한 패킷 전달은 추가 sk_buff 복사와 softirq 처리를 수반합니다. 고성능 컨테이너 네트워킹에서는 MACVLAN, IPVLAN, 또는 SRIOV VF를 사용하여 veth 오버헤드를 제거하는 것이 권장됩니다. eBPF 기반 네트워킹(Cilium 등)은 veth에서도 TC BPF로 경로를 단축할 수 있습니다.

네임스페이스에서 격리되는 자원

자원격리 수준공유 여부커널 구현
네트워크 인터페이스완전 격리네임스페이스 전용dev_net(dev)
라우팅 테이블완전 격리독립 FIBnet->ipv4.fib_table_hash
Netfilter 규칙완전 격리독립 nftables/iptablesnet->nf
conntrack 테이블완전 격리독립 해시 테이블net->ct
소켓 공간완전 격리포트 충돌 없음sock_net(sk)
sysctl 변수완전 격리독립 값netns_ipv4
/proc/net완전 격리네임스페이스별 통계proc_net_init()
ARP/NDP 캐시완전 격리독립 이웃 테이블neigh_table

가상 네트워크 디바이스 유형

Linux는 다양한 가상 네트워크 디바이스를 제공하여 네트워크 토폴로지를 소프트웨어로 구성할 수 있습니다. 각 디바이스 유형은 고유한 성능 특성과 사용 시나리오를 가집니다.

디바이스 유형용도패킷 경로성능 특성사용 예
veth네임스페이스 간 연결한쪽 전송 → 다른 쪽 수신추가 softirq, sk_buff 복사Docker 기본 네트워킹
bridgeL2 스위칭FDB 조회 → 포트 포워딩멀티 포트 시 해시 조회VM/컨테이너 브릿지
macvlanMAC 기반 가상 인터페이스MAC 주소로 직접 분배veth보다 낮은 오버헤드경량 컨테이너
ipvlanIP 기반 가상 인터페이스IP 주소로 직접 분배MAC 학습 불필요동일 MAC 공유 시
vxlanL2 over UDP 터널UDP 캡슐화/디캡슐화캡슐화 오버헤드오버레이 네트워크
geneve범용 터널 (VXLAN 후속)UDP 캡슐화, 가변 옵션캡슐화 오버헤드OVN, Cilium
wireguardVPN 터널암호화 + UDP 캡슐화암호화 CPU 비용사이트간 VPN
tun/tap유저 공간 네트워킹유저 ↔ 커널 패킷 전달유저/커널 전환 비용OpenVPN, QEMU
bond링크 집합해시/라운드로빈 분배분배 오버헤드 최소고가용성, 대역폭 합산
team링크 집합 (bond 대체)Netlink 기반 설정bond과 유사유연한 정책 설정
/* drivers/net/veth.c — veth 패킷 전달 핵심 (간략화) */
static netdev_tx_t veth_xmit(struct sk_buff *skb,
                              struct net_device *dev)
{
    struct veth_priv *priv = netdev_priv(dev);
    struct net_device *rcv = rcu_dereference(priv->peer);

    if (unlikely(!rcv)) {
        kfree_skb(skb);
        return NETDEV_TX_OK;
    }

    /* 피어 디바이스로 패킷 전달 */
    skb->dev = rcv;                      /* 수신 디바이스 변경 */
    skb->protocol = eth_type_trans(skb, rcv);
    skb->pkt_type = PACKET_HOST;

    /* XDP 프로그램이 있으면 실행 */
    if (likely(veth_xdp_rx(rq, skb)))
        goto xmit;

    /* NAPI를 통해 피어 네임스페이스에서 수신 처리 */
    if (netif_receive_skb(skb) == NET_RX_SUCCESS)
        return NETDEV_TX_OK;

    return NETDEV_TX_OK;
}
코드 설명
  • 6행veth의 각 끝은 peer 포인터로 상대방을 가리킵니다. 한쪽에서 전송하면 상대방의 수신 경로로 패킷이 전달됩니다.
  • 14-16행sk_buff의 dev를 피어 디바이스로 변경하고, eth_type_trans()로 프로토콜을 설정합니다. 패킷이 마치 피어 디바이스에서 수신된 것처럼 처리됩니다.
  • 22행netif_receive_skb()로 피어 네임스페이스의 네트워크 스택에 패킷을 주입합니다. 이 호출은 softirq 컨텍스트에서 실행되므로 추가적인 CPU 비용이 발생합니다.
# 가상 디바이스 생성 명령어 모음

# veth 페어 생성
ip link add veth0 type veth peer name veth1
ip link set veth1 netns ns1

# Linux 브릿지 생성
ip link add br0 type bridge
ip link set eth0 master br0
ip link set veth0 master br0
ip link set br0 up

# MACVLAN 생성
ip link add macvlan0 link eth0 type macvlan mode bridge
ip link set macvlan0 netns ns1

# IPVLAN 생성
ip link add ipvlan0 link eth0 type ipvlan mode l3

# VXLAN 터널 생성
ip link add vxlan100 type vxlan id 100 \
  local 192.168.1.1 remote 192.168.1.2 \
  dstport 4789 dev eth0

# WireGuard 인터페이스 생성
ip link add wg0 type wireguard
wg set wg0 private-key /path/to/private.key \
  listen-port 51820 \
  peer PEER_PUBLIC_KEY= endpoint 10.0.0.2:51820 \
  allowed-ips 10.10.10.0/24

# TUN 디바이스 생성 (유저 공간 프로그램에서)
ip tuntap add dev tun0 mode tun
veth 성능 최적화: veth를 통한 패킷 전달은 추가적인 softirq 처리와 sk_buff 조작을 수반합니다. 고성능 컨테이너 환경에서는 다음 최적화를 고려해야 합니다: (1) eBPF 경로 단축 — TC BPF로 veth 한쪽에서 바로 리다이렉트하여 커널 스택 재진입을 방지합니다 (Cilium 방식). (2) MACVLAN/IPVLAN — veth 대신 사용하면 브릿지 학습과 softirq 오버헤드가 제거됩니다. (3) SR-IOV VF — NIC 하드웨어에서 가상 기능을 생성하여 veth 오버헤드를 완전히 제거합니다.

제로카피 네트워킹 (Zero-Copy Networking)

제로카피(Zero-Copy)는 유저 공간과 커널 공간 사이의 데이터 복사를 제거하여 CPU 사용률과 메모리 대역폭을 절약하는 기법입니다. Linux 네트워크 스택은 송신(sendfile, MSG_ZEROCOPY), 수신(tcp_mmap), 그리고 io_uring을 통한 비동기 소켓 I/O까지 다양한 제로카피 메커니즘을 제공합니다.

제로카피 네트워킹 데이터 경로 비교: 일반 경로 vs sendfile vs MSG_ZEROCOPY 제로카피 데이터 경로 비교 일반 경로 (4회 복사) ① 디스크 → 페이지 캐시 (DMA) ② 페이지 캐시 → 유저 버퍼 (CPU) ③ 유저 버퍼 → 소켓 버퍼 (CPU) ④ 소켓 버퍼 → NIC (DMA) sendfile() (2회 복사) ① 디스크 → 페이지 캐시 (DMA) ② 페이지 캐시 → skb frags[] 참조 ③ frags[] → NIC (scatter-gather DMA) 유저 공간 복사 0회 MSG_ZEROCOPY (1회 복사) ① 유저 버퍼 pin (get_user_pages) ② skb frags[] → 유저 페이지 직접 참조 ③ NIC DMA (유저 페이지에서 직접) ④ 완료 통지 (errqueue 콜백) 성능 비교 (10Gbps, 64KB 블록) 일반 send() ~5 Gbps, CPU 100% sendfile() ~9.5 Gbps, CPU ~40% MSG_ZEROCOPY ~9.8 Gbps, CPU ~30% ● CPU 복사 (비효율) ● 참조 전달 (효율) ● 페이지 핀 + DMA 직접 전송 sendfile은 파일→소켓 전용, MSG_ZEROCOPY는 메모리→소켓 범용 (10KB 이상 블록에서 효과적) MSG_ZEROCOPY는 완료 통지 처리 비용이 있어 소형 패킷에서는 일반 send()보다 느릴 수 있습니다
일반 경로, sendfile(), MSG_ZEROCOPY의 데이터 복사 횟수와 성능 비교

io_uring 소켓 통합

io_uring은 커널 5.6부터 소켓 I/O를 지원하며, IORING_OP_SEND, IORING_OP_RECV, IORING_OP_ACCEPT 등의 비동기 소켓 연산을 제공합니다. Submission Queue(SQ)와 Completion Queue(CQ)를 유저-커널 간 공유하여 시스템 콜 오버헤드를 최소화합니다.

/* io_uring 제로카피 소켓 전송 (IORING_OP_SEND_ZC) */
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);

/* 제로카피 전송 설정 */
io_uring_prep_send_zc(sqe, sockfd, buf, len, 0, 0);
sqe->flags |= IOSQE_CQE_SKIP_SUCCESS;

/* 제출 — 시스템 콜 없이 SQ에 직접 기록 */
io_uring_submit(ring);

/* 완료 확인 — CQ에서 결과 수집 */
struct io_uring_cqe *cqe;
io_uring_wait_cqe(ring, &cqe);

/* IORING_CQE_F_NOTIF: 제로카피 완료 통지 */
/* 이 CQE 이후 buf를 재사용할 수 있습니다 */
if (cqe->flags & IORING_CQE_F_NOTIF)
    io_uring_cqe_seen(ring, cqe);
코드 설명
  • 5행io_uring_prep_send_zc()IORING_OP_SEND_ZC 연산을 준비합니다. 내부적으로 MSG_ZEROCOPY와 동일한 메커니즘(페이지 핀)을 사용합니다.
  • 9행io_uring_submit()은 SQ에 기록된 요청을 커널에 제출합니다. IORING_SETUP_SQPOLL 모드에서는 커널 스레드가 SQ를 폴링하므로 시스템 콜이 필요 없습니다.
  • 17행IORING_CQE_F_NOTIF 플래그가 설정된 CQE는 DMA 전송이 완료되어 유저 버퍼를 재사용할 수 있음을 알립니다.
메커니즘방향데이터 소스커널 버전특징
sendfile()TX파일 → 소켓2.2+페이지 캐시 직접 참조, 가장 성숙
splice()TX/RX파이프 기반2.6.17+sendfile보다 유연, 파이프 필요
MSG_ZEROCOPYTX유저 버퍼 → 소켓4.14+범용, 완료 통지 필요, 10KB+ 효율적
tcp_mmap()RX소켓 → 유저 매핑4.18+TCP 수신 제로카피, 실험적
io_uring SEND_ZCTX유저 버퍼6.0+비동기 + 제로카피, 최고 성능
AF_XDPTX/RXUMEM4.18+커널 스택 우회, 최고 PPS

splice와 vmsplice

splice()는 파이프를 중간 매개체로 사용하여 두 파일 디스크립터(File Descriptor) 간에 제로카피 데이터 전송을 수행합니다. vmsplice()는 유저 공간 메모리를 파이프에 제로카피로 주입합니다.

/* splice 기반 소켓 전송 — 유저 공간 */
int pipefd[2];
pipe(pipefd);

/* 1. 파일 → 파이프 (splice) */
splice(file_fd, &off, pipefd[1], NULL, len, SPLICE_F_MOVE);

/* 2. 파이프 → 소켓 (splice) */
splice(pipefd[0], NULL, sock_fd, NULL, len, SPLICE_F_MOVE);

/* vmsplice: 유저 메모리 → 파이프 (제로카피) */
struct iovec iov = { .iov_base = buf, .iov_len = len };
vmsplice(pipefd[1], &iov, 1, SPLICE_F_GIFT);
/* SPLICE_F_GIFT: 페이지 소유권을 파이프에 양도 */
/* → 이후 buf 수정 금지, 파이프가 페이지 해제 */
코드 설명
  • 6행splice()는 파일의 페이지 캐시에서 파이프 버퍼로 페이지 참조를 이동합니다. 실제 데이터 복사 없이 페이지 참조만 전달됩니다.
  • 9행파이프에서 소켓으로의 splice()는 파이프 버퍼의 페이지를 sk_buff의 frags[]에 매핑합니다. sendfile()과 동일한 효과이지만, 파이프를 통해 더 유연한 데이터 흐름이 가능합니다.
  • 13행vmsplice()SPLICE_F_GIFT를 사용하면 유저 페이지의 소유권을 파이프에 양도합니다. 이 경우 유저 공간에서 해당 메모리를 수정하면 안 됩니다.

성능 비교와 선택 가이드

사용 시나리오권장 메커니즘근거
정적 파일 서빙 (Nginx, Apache)sendfile()가장 성숙, 페이지 캐시 활용
프록시 (리버스 프록시, LB)splice()소켓→소켓 전달, 중간 수정 가능
애플리케이션 데이터 전송 (DB 복제)MSG_ZEROCOPY유저 버퍼 직접 전송, 10KB+ 효율
고성능 비동기 I/Oio_uring SEND_ZCsyscall 오버헤드 최소화
패킷 처리 (DPI, IDS)AF_XDP커널 스택 완전 우회
소형 패킷 (<4KB)일반 send()제로카피 오버헤드가 복사보다 큼
제로카피의 한계: 제로카피는 대형 블록 전송에서만 효과적입니다. MSG_ZEROCOPY의 경우 get_user_pages()의 오버헤드(TLB flush, 페이지 테이블 조작)로 인해 약 10KB 이하의 전송에서는 일반 send()보다 느릴 수 있습니다. 또한 완료 통지를 위한 recvmsg(MSG_ERRQUEUE) 호출이 추가로 필요합니다. 벤치마크로 실제 워크로드에서의 효과를 반드시 확인해야 합니다.

네트워크 초기화 시퀀스 (Network Initialization)

Linux 커널의 네트워크 스택은 부팅 시 특정 순서로 초기화됩니다. 각 서브시스템은 의존성을 고려하여 올바른 순서로 로드되어야 하며, core_initcall, subsys_initcall, fs_initcall 등의 initcall 매크로로 순서를 지정합니다.

/* net/socket.c — 네트워크 초기화 진입점 */
static int __init sock_init(void)
{
    /* 1. sk_buff SLAB 캐시 초기화 */
    skb_init();

    /* 2. VFS 소켓 파일시스템 등록 */
    init_inodecache();
    register_filesystem(&sock_fs_type);
    sock_mnt = kern_mount(&sock_fs_type);

    /* 3. Netfilter 프레임워크 초기화 */
    netfilter_init();

    return 0;
}
core_initcall(sock_init);  /* 최우선 초기화 */

/* net/ipv4/af_inet.c — inet_init() IPv4 스택 초기화 */
static int __init inet_init(void)
{
    /* 1. 프로토콜 구조체 등록 */
    proto_register(&tcp_prot, 1);
    proto_register(&udp_prot, 1);
    proto_register(&raw_prot, 1);
    proto_register(&ping_prot, 1);

    /* 2. AF_INET 소켓 패밀리 등록 */
    sock_register(&inet_family_ops);

    /* 3. ARP 초기화 */
    arp_init();

    /* 4. IP 계층 초기화 */
    ip_init();

    /* 5. TCP/UDP L4 프로토콜 등록 */
    inet_add_protocol(&icmp_protocol, IPPROTO_ICMP);
    inet_add_protocol(&udp_protocol, IPPROTO_UDP);
    inet_add_protocol(&tcp_protocol, IPPROTO_TCP);

    /* 6. TCP 서브시스템 초기화 */
    tcp_init();          /* 해시 테이블, 메모리, 타이머 */
    udp_init();          /* UDP 해시 테이블 */
    udplite_init();      /* UDP-Lite */

    /* 7. ICMP/IGMP 초기화 */
    icmp_init();
    igmp_mc_init();

    /* 8. IP L3 패킷 핸들러 등록 (EtherType 0x0800) */
    dev_add_pack(&ip_packet_type);

    /* 9. IP 단편화, FIB, 이웃 테이블 등 */
    ip_mr_init();
    inet_initpeers();

    return 0;
}
fs_initcall(inet_init);  /* sock_init 이후 실행 */
코드 설명
  • 2-17행sock_init()core_initcall로 등록되어 부팅 초기에 실행됩니다. sk_buff 캐시, 소켓 VFS, Netfilter 프레임워크를 초기화합니다. 모든 프로토콜의 기반이 됩니다.
  • 23-26행TCP, UDP, RAW, PING 프로토콜의 struct proto를 등록합니다. 이 단계에서 SLAB 캐시(tcp_sock, udp_sock 등)가 생성됩니다.
  • 30행sock_register()로 AF_INET 패밀리를 등록합니다. 이후 socket(AF_INET, ...) 호출 시 inet_create()가 호출됩니다.
  • 38-40행IP 프로토콜 번호(ICMP=1, TCP=6, UDP=17)에 L4 핸들러를 등록합니다. ip_local_deliver_finish()에서 이 핸들러가 호출됩니다.
  • 43행tcp_init()은 ESTABLISHED/LISTEN 해시 테이블, TCP 메모리 한도(tcp_mem), 혼잡 제어 모듈 등을 초기화합니다.
  • 52행dev_add_pack()으로 EtherType 0x0800(IPv4)에 ip_rcv() 핸들러를 등록합니다. 이 시점부터 IPv4 패킷 수신이 가능해집니다.
  • 59행fs_initcallcore_initcall 이후에 실행되므로, sock_init()이 완료된 후 inet_init()이 실행됨이 보장됩니다.

초기화 순서와 의존성

initcall 레벨함수초기화 내용의존성
core_initcallsock_init()sk_buff, 소켓 VFS, Netfilter 기반메모리 관리자
core_initcallnet_dev_init()softnet_data, NET_RX/TX_SOFTIRQsoftirq
subsys_initcallnet_ns_init()init_net 네임스페이스sock_init
subsys_initcallnetdev_kobject_init()sysfs 네트워크 클래스sysfs
fs_initcallinet_init()IPv4 전체 (TCP/UDP/ICMP/ARP)sock_init, net_dev_init
module_initinet6_init()IPv6 전체inet_init
module_initnf_conntrack_init()연결 추적 (모듈)netfilter_init
late_initcalltcp_congestion_default()기본 혼잡 제어 설정tcp_init
/* net/core/dev.c — net_dev_init() per-CPU 구조체 초기화 */
static int __init net_dev_init(void)
{
    int i;

    /* per-CPU softnet_data 초기화 */
    for_each_possible_cpu(i) {
        struct softnet_data *sd = &per_cpu(softnet_data, i);

        INIT_LIST_HEAD(&sd->poll_list);
        skb_queue_head_init(&sd->input_pkt_queue);
        skb_queue_head_init(&sd->process_queue);
        sd->backlog.poll = process_backlog;
        sd->backlog.weight = 64;  /* NAPI weight */
    }

    /* NET_RX_SOFTIRQ / NET_TX_SOFTIRQ 등록 */
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);

    return 0;
}
subsys_initcall(net_dev_init);
코드 설명
  • 7-15행각 CPU마다 softnet_data 구조체를 초기화합니다. poll_list(NAPI 폴링 대기 큐), input_pkt_queue(RPS 입력 큐), process_queue(처리 중 큐)를 설정합니다.
  • 13-14행per-CPU backlog NAPI 구조체를 초기화합니다. non-NAPI 드라이버나 RPS가 사용하는 기본 NAPI 인스턴스로, process_backlog() 함수가 poll 콜백으로 등록됩니다.
  • 18-19행open_softirq()로 네트워크 softirq를 등록합니다. net_rx_action()은 앞서 설명한 NAPI 폴링 루프를 실행하고, net_tx_action()은 지연된 TX 완료 처리를 수행합니다.
모듈 로딩 순서: 빌트인(y) 기능은 initcall 레벨로 순서가 보장되지만, 모듈(m)로 빌드된 기능은 modprobe 시점에 의존성이 해결됩니다. 예를 들어 nf_conntrack 모듈은 nf_defrag_ipv4에 의존하므로, modprobe nf_conntrack 시 자동으로 의존 모듈이 먼저 로드됩니다. lsmod | grep nf_로 로드된 Netfilter 모듈과 의존 관계를 확인할 수 있습니다.

부팅 로그에서 네트워크 초기화 확인

# 네트워크 초기화 관련 부팅 메시지 확인
dmesg | grep -iE "(tcp|udp|ipv4|ipv6|netfilter|napi|conntrack)" | head -30

# 전형적인 출력 예시:
# [    0.123456] TCP established hash table entries: 524288
# [    0.123789] TCP bind hash table entries: 65536
# [    0.124012] TCP: Hash tables configured (established 524288 bind 65536)
# [    0.124234] UDP hash table entries: 32768
# [    0.124567] UDP-Lite hash table entries: 32768
# [    0.456789] NET: Registered PF_INET protocol family
# [    0.567890] NET: Registered PF_INET6 protocol family
# [    1.234567] nf_conntrack version 0.5.0 (131072 buckets, 524288 max)

# initcall 타이밍 분석 (커널 파라미터 initcall_debug 필요)
dmesg | grep "initcall.*returned" | grep -i net

# 프로토콜 패밀리 등록 상태
cat /proc/net/protocols                # 등록된 프로토콜 목록
# protocol  size sockets  memory press maxhdr  slab module
# TCPv6     2416    123     234    no    320   yes  kernel
# UDPv6     1216     45      12    no      0   yes  kernel
# TCP       2080    567     890    no    320   yes  kernel
# UDP        960    234      56    no      0   yes  kernel

네트워크 스택 추적(Tracing) 포인트

커널은 네트워크 스택의 주요 경로에 tracepoint를 제공합니다. 이를 통해 패킷의 전체 경로를 추적하고, 성능 병목과 드롭 원인을 정밀하게 분석할 수 있습니다.

# 사용 가능한 네트워크 tracepoint 확인
ls /sys/kernel/debug/tracing/events/net/
ls /sys/kernel/debug/tracing/events/skb/
ls /sys/kernel/debug/tracing/events/napi/
ls /sys/kernel/debug/tracing/events/tcp/
ls /sys/kernel/debug/tracing/events/sock/

# 패킷 드롭 추적 (kfree_skb)
perf trace --no-syscalls -e skb:kfree_skb -a -- sleep 5

# 특정 인터페이스의 NAPI 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/napi/napi_poll/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -20

# TCP 재전송 추적
perf trace --no-syscalls -e tcp:tcp_retransmit_skb -a -- sleep 10

# TCP 상태 전이 추적
perf trace --no-syscalls -e sock:inet_sock_set_state -a -- sleep 5

# bpftrace를 이용한 고급 추적
bpftrace -e 'tracepoint:skb:kfree_skb {
    printf("drop reason=%d location=%s\n",
           args->reason, ksym(args->location));
}'

# 네트워크 계층별 지연 측정 (bpftrace)
bpftrace -e 'kprobe:ip_rcv { @start[tid] = nsecs; }
             kretprobe:ip_rcv /@start[tid]/ {
                 @latency_ns = hist(nsecs - @start[tid]);
                 delete(@start[tid]);
             }'
Tracepoint 카테고리주요 이벤트용도
skb:kfree_skb패킷 드롭드롭 원인과 위치 추적
skb:consume_skb패킷 정상 소비처리 완료 확인
napi:napi_pollNAPI 폴링 실행CPU 사용률, budget 소비 분석
net:netif_receive_skbRX 수신 진입RX 경로 타이밍
net:net_dev_xmitTX 전송 완료TX 경로 타이밍, 전송 실패
tcp:tcp_retransmit_skbTCP 재전송네트워크 문제 탐지
tcp:tcp_probeTCP 상태 샘플링cwnd, srtt, ssthresh 추적
sock:inet_sock_set_state소켓 상태 전이연결 수립/종료 모니터링
fib:fib_table_lookupFIB 라우팅 조회라우팅 성능 분석
qdisc:qdisc_dequeueqdisc dequeueqdisc 지연 측정

커널 빌드 옵션 (Kconfig)

네트워크 스택의 각 기능은 커널 빌드 시 Kconfig 옵션으로 활성화/비활성화합니다. 프로덕션 환경에서 필요한 기능을 정확히 활성화하면 커널 크기를 줄이고 보안 공격 면을 최소화할 수 있습니다.

핵심 네트워크 Kconfig 옵션

옵션기본값역할비활성화 시 영향
CONFIG_NETy네트워킹 지원네트워크 기능 전체 비활성화
CONFIG_INETyTCP/IP 네트워킹IPv4/IPv6 스택 비활성화
CONFIG_IPV6m/yIPv6 프로토콜 지원IPv6 연결 불가
CONFIG_NETFILTERyNetfilter 프레임워크방화벽/NAT 불가
CONFIG_NF_CONNTRACKm/y연결 추적stateful 방화벽/NAT 불가
CONFIG_NET_SCHEDyTC/qdisc트래픽 셰이핑 불가
CONFIG_BPF_SYSCALLyeBPF 시스템 콜XDP/TC BPF 프로그램 불가
CONFIG_XDP_SOCKETSm/yAF_XDP 소켓AF_XDP 기반 고성능 처리 불가
CONFIG_BRIDGEm이더넷 브릿지L2 브릿지 기능 불가
CONFIG_VLAN_8021Qm802.1Q VLANVLAN 태깅 불가
CONFIG_NET_CLS_BPFmTC BPF classifierTC BPF 프로그램 부착 불가
CONFIG_TLSm커널 TLS (kTLS)TLS 오프로드 불가
CONFIG_NET_NSy네트워크 네임스페이스컨테이너 네트워크 격리 불가

NIC 드라이버 Kconfig

옵션NIC비고
CONFIG_IXGBEIntel 10G (ixgbe)82599, X520, X540
CONFIG_I40EIntel 25/40G (i40e)XL710, XXV710
CONFIG_ICEIntel 100G (ice)E810
CONFIG_MLX5_COREMellanox ConnectX-5/6/7mlx5 드라이버
CONFIG_BNXTBroadcom NetXtreme-Ebnxt_en 드라이버
CONFIG_VIRTIO_NETvirtio-netKVM/QEMU 가상 NIC
CONFIG_R8169Realtek 8168/8111일반 데스크톱 NIC
# 현재 커널의 네트워크 관련 Kconfig 확인
zcat /proc/config.gz | grep -E "^CONFIG_(NET|INET|IPV6|NETFILTER|BPF|XDP)" 2>/dev/null || \
grep -E "^CONFIG_(NET|INET|IPV6|NETFILTER|BPF|XDP)" /boot/config-$(uname -r)

# 특정 드라이버 모듈 로드 상태 확인
lsmod | grep -E "ixgbe|i40e|ice|mlx5|bnxt|virtio_net"

# 커널 빌드 시 네트워크 설정 메뉴
make menuconfig   # Networking support → Networking options

Kconfig 의존성 관계

네트워크 Kconfig 옵션들은 복잡한 의존성 관계를 가집니다. 아래는 주요 의존성 체인입니다:

기능필수 의존성권장 옵션
TC BPF classifierCONFIG_NET_SCHED + CONFIG_BPF_SYSCALLCONFIG_NET_CLS_ACT
XDPCONFIG_BPF_SYSCALL + NIC 드라이버CONFIG_BPF_JIT
AF_XDPCONFIG_XDP_SOCKETS + CONFIG_BPF_SYSCALLCONFIG_BPF_JIT
NATCONFIG_NETFILTER + CONFIG_NF_CONNTRACKCONFIG_NF_NAT
nftablesCONFIG_NETFILTER + CONFIG_NF_TABLESCONFIG_NF_TABLES_INET
VXLANCONFIG_VXLAN + CONFIG_INETCONFIG_NET_UDP_TUNNEL
kTLSCONFIG_TLS + CONFIG_INETCONFIG_TLS_DEVICE
WireGuardCONFIG_WIREGUARD + CONFIG_INETCONFIG_CRYPTO_CHACHA20POLY1305
BondingCONFIG_BONDINGCONFIG_NET_TEAM (대안)
OVSCONFIG_OPENVSWITCH + CONFIG_BRIDGECONFIG_OPENVSWITCH_VXLAN

프로덕션 환경별 권장 Kconfig

일반 서버 (웹/API):

CONFIG_NET=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_NETFILTER=y
CONFIG_NF_CONNTRACK=y
CONFIG_NF_NAT=y
CONFIG_NF_TABLES=y
CONFIG_NET_SCHED=y
CONFIG_NET_SCH_FQ_CODEL=y
CONFIG_TCP_CONG_BBR=m
CONFIG_TLS=m
CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT=y

컨테이너 호스트 (Docker/K8s):

CONFIG_NET_NS=y                # 필수: 네트워크 네임스페이스
CONFIG_BRIDGE=m                # Docker 기본 네트워킹
CONFIG_VXLAN=m                 # Flannel/Calico VXLAN 모드
CONFIG_GENEVE=m                # OVN GENEVE 오버레이
CONFIG_MACVLAN=m               # MACVLAN 네트워킹
CONFIG_IPVLAN=m                # IPVLAN 네트워킹
CONFIG_VETH=y                  # 필수: veth 쌍
CONFIG_NETFILTER=y             # iptables/nftables
CONFIG_NF_CONNTRACK=y          # conntrack
CONFIG_OPENVSWITCH=m           # OVS 사용 시
CONFIG_NET_CLS_BPF=m           # TC BPF (Cilium 등)

고성능 네트워크 장비 (방화벽/로드밸런서):

CONFIG_BPF_SYSCALL=y           # eBPF 필수
CONFIG_BPF_JIT=y               # JIT 컴파일
CONFIG_XDP_SOCKETS=y           # AF_XDP
CONFIG_NET_CLS_BPF=y           # TC BPF
CONFIG_NETFILTER=y             # Netfilter
CONFIG_NF_FLOW_TABLE=m         # Flowtable 가속
CONFIG_NET_SCH_FQ=y            # Fair Queueing (BBR용)
CONFIG_TCP_CONG_BBR=y          # BBR 혼잡 제어
CONFIG_NET_ACT_CT=m            # TC conntrack action
CONFIG_NET_ACT_POLICE=m        # TC 폴리싱

네트워크 성능 튜닝 가이드

네트워크 성능 문제를 체계적으로 분석하고 튜닝하기 위한 핵심 매개변수와 관찰 도구를 정리합니다.

핵심 sysctl 매개변수

매개변수기본값설명권장 조정
net.core.rmem_max212992소켓 수신 버퍼 최대고대역폭: 16MB+
net.core.wmem_max212992소켓 송신 버퍼 최대고대역폭: 16MB+
net.ipv4.tcp_rmem4096 131072 6291456TCP 수신 버퍼 (min/default/max)고대역폭: max 증가
net.ipv4.tcp_wmem4096 16384 4194304TCP 송신 버퍼 (min/default/max)고대역폭: max 증가
net.core.netdev_max_backlog1000per-CPU 수신 백로그 큐 크기고PPS: 5000~30000
net.core.netdev_budget300NAPI poll 총 budget고PPS: 600~1200
net.core.somaxconn4096listen() backlog 최대고연결: 65535
net.ipv4.tcp_max_syn_backlog2048SYN 큐 최대 크기고연결: 8192~65535
net.ipv4.tcp_tw_reuse2TIME_WAIT 소켓 재사용고연결: 1 (활성화)
net.ipv4.tcp_congestion_controlcubic혼잡 제어 알고리즘환경별: bbr, cubic
net.nf_conntrack_max262144conntrack 최대 항목 수고연결: 1048576+

성능 관찰 도구

# 인터페이스 통계 (패킷 수, 바이트, 에러, 드롭)
ip -s link show eth0
ethtool -S eth0 | grep -E "rx_|tx_|drop|error"

# softirq 처리 통계 (드롭, 시간 초과, CPU 분포)
cat /proc/net/softnet_stat

# TCP 연결 상태 분포
ss -s                                # 요약 통계
ss -tnp                              # TCP 연결 목록
ss -ti                               # TCP 내부 정보 (cwnd, RTT, retrans)

# 소켓 메모리 사용량
cat /proc/net/sockstat

# conntrack 상태
conntrack -C                         # 현재 추적 수
conntrack -S                         # 통계 (drop, insert_failed 등)

# 패킷 드롭 추적 (perf/dropwatch)
perf record -g -e skb:kfree_skb      # kfree_skb tracepoint
dropwatch -l kas                     # 드롭 위치 추적

일반적인 병목 포인트

증상확인 방법원인해결
RX 드롭 증가ethtool -S: rx_droppedRX Ring Buffer 부족ethtool -G eth0 rx 4096
softnet backlog 드롭/proc/net/softnet_stat 2번째 열netdev_max_backlog 부족sysctl 증가 또는 RPS 설정
TCP 재전송 증가ss -ti: retrans네트워크 혼잡/패킷 손실혼잡 제어 변경(BBR), MTU 조정
conntrack 풀conntrack -C = max연결 추적 테이블 소진nf_conntrack_max 증가
소켓 버퍼 부족/proc/net/sockstat: mem수신/송신 버퍼 부족tcp_rmem/tcp_wmem max 증가
단일 CPU 과부하mpstat -P ALL인터럽트/softirq 집중RSS/RPS 설정, IRQ 분산

시나리오별 sysctl 튜닝 프로파일

웹 서버 (고연결 수):

# 고연결 웹 서버 최적화
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=15
sysctl -w net.ipv4.tcp_keepalive_time=600
sysctl -w net.ipv4.tcp_keepalive_intvl=30
sysctl -w net.ipv4.tcp_keepalive_probes=5
sysctl -w net.ipv4.ip_local_port_range="1024 65535"

고대역폭 파일 전송 서버:

# 대용량 파일 전송 최적화 (10Gbps+)
sysctl -w net.core.rmem_max=67108864
sysctl -w net.core.wmem_max=67108864
sysctl -w net.ipv4.tcp_rmem="4096 87380 67108864"
sysctl -w net.ipv4.tcp_wmem="4096 65536 67108864"
sysctl -w net.ipv4.tcp_congestion_control=bbr
sysctl -w net.ipv4.tcp_mtu_probing=1
sysctl -w net.core.default_qdisc=fq

고PPS 패킷 처리 (방화벽/로드밸런서):

# 고PPS 환경 최적화
sysctl -w net.core.netdev_max_backlog=30000
sysctl -w net.core.netdev_budget=600
sysctl -w net.core.netdev_budget_usecs=8000
sysctl -w net.nf_conntrack_max=2097152
sysctl -w net.netfilter.nf_conntrack_buckets=524288
sysctl -w net.ipv4.tcp_max_orphans=262144

네트워크 성능 벤치마크 도구

도구측정 항목용도사용 예
iperf3TCP/UDP 대역폭기본 대역폭 측정iperf3 -s / iperf3 -c host
netperfTCP/UDP 대역폭, 지연다양한 네트워크 벤치마크netperf -H host -t TCP_RR
nuttcpTCP/UDP 대역폭UDP 멀티캐스트, 양방향 테스트nuttcp -S / nuttcp host
pktgenPPS (패킷/초)커널 내장 패킷 생성기, 최대 PPS 측정modprobe pktgen
wrkHTTP RPS, 지연HTTP 벤치마크wrk -t4 -c100 -d30s url
sockperf소켓 지연마이크로초 단위 지연 측정sockperf sr --tcp -p 12345
xdp-benchXDP PPSXDP 드롭/리다이렉트 성능xdp-bench drop eth0
# iperf3 TCP 대역폭 측정
iperf3 -s                                     # 서버
iperf3 -c 10.0.0.1 -t 30 -P 4                # 클라이언트: 30초, 4 스트림

# netperf TCP Request/Response 지연 측정
netperf -H 10.0.0.1 -t TCP_RR -l 30 -- -o min_latency,max_latency,mean_latency

# 커널 pktgen으로 최대 PPS 측정
echo "rem_device_all" > /proc/net/pktgen/kpktgend_0
echo "add_device eth0" > /proc/net/pktgen/kpktgend_0
echo "pkt_size 64" > /proc/net/pktgen/eth0
echo "count 10000000" > /proc/net/pktgen/eth0
echo "dst 10.0.0.2" > /proc/net/pktgen/eth0
echo "dst_mac aa:bb:cc:dd:ee:ff" > /proc/net/pktgen/eth0
echo "start" > /proc/net/pktgen/pgctrl
cat /proc/net/pktgen/eth0

실무 트러블슈팅 체크리스트

네트워크 문제를 체계적으로 진단하기 위한 단계별 체크리스트입니다:

  1. 물리 계층 확인ethtool eth0으로 링크 상태, 속도, 듀플렉스 확인. ethtool -S eth0으로 CRC 에러, alignment 에러 확인.
  2. L2 확인ip link show로 인터페이스 상태, MTU, MAC 주소 확인. bridge fdb show로 브릿지 FDB 확인. ip neigh show로 ARP/NDP 캐시 확인.
  3. L3 확인ip route get [대상IP]로 라우팅 결정 확인. traceroute 또는 mtr로 경로 추적. ping으로 연결성 확인.
  4. L4 확인ss -tnp으로 TCP 연결 상태 확인. ss -ti로 cwnd, RTT, 재전송 확인. nstat으로 TCP/IP 통계 확인.
  5. 방화벽 확인nft list ruleset 또는 iptables -L -n -v로 규칙 확인. conntrack -L로 연결 추적 상태 확인.
  6. 드롭 추적/proc/net/softnet_stat으로 softirq 드롭 확인. ethtool -S으로 NIC 드롭 확인. perf record -e skb:kfree_skb으로 커널 드롭 위치 추적.
  7. 성능 프로파일링(Profiling)perf top으로 CPU 핫스팟 확인. mpstat -P ALL 1로 CPU별 부하 분포 확인. sar -n DEV 1로 인터페이스별 처리량 모니터링.
드롭 원인 분류: 패킷 드롭은 크게 3가지로 분류됩니다: (1) NIC 드롭 — RX Ring Full, CRC 에러 (ethtool -S), (2) 커널 드롭 — backlog 초과, Netfilter DROP, 소켓 버퍼 초과 (/proc/net/softnet_stat, nstat), (3) 애플리케이션 드롭 — 소켓 버퍼에서 데이터를 빠르게 소비하지 못함 (ss -tnp의 Recv-Q). 각 드롭 유형에 따라 해결 방법이 다르므로 정확한 분류가 중요합니다.

nstat과 SNMP 카운터

nstat은 커널 네트워크 SNMP 카운터의 변화량을 표시하는 도구로, TCP 재전송, IP 단편화, ICMP 에러 등의 통계를 효율적으로 모니터링할 수 있습니다.

# nstat — 변화량 표시 (가장 유용)
nstat -a                               # 모든 카운터
nstat -az                              # 0인 카운터도 표시
nstat -s                               # 1회성 스냅샷
nstat TcpRetransSegs                   # 특정 카운터만

# 주요 TCP 관찰 카운터
# TcpRetransSegs — 재전송된 세그먼트 수
# TcpInErrs — 오류 수신 세그먼트
# TcpExtTCPLostRetransmit — SACK로 감지된 재전송 손실
# TcpExtTCPTimeouts — RTO 타임아웃 발생 횟수
# TcpExtListenDrops — accept queue 오버플로우
# TcpExtListenOverflows — SYN 큐 오버플로우
# TcpExtTCPBacklogDrop — backlog 큐 드롭

# 주요 IP 관찰 카운터
# IpInReceives — 수신 패킷 총수
# IpOutRequests — 송신 패킷 총수
# IpReasmFails — 재조립 실패
# IpFragFails — 단편화 실패 (DF 비트 등)

# 변화 추적 (1초 간격)
watch -n1 'nstat -s TcpRetransSegs TcpExtListenDrops TcpExtTCPTimeouts'

ss 명령어 고급 활용

# TCP 내부 정보 (cwnd, rtt, mss, 재전송 등)
ss -ti state established

# 전형적인 출력 해석:
# cubic wscale:7,7 rto:204 rtt:1.234/0.567 ato:40 mss:1448
# pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 ssthresh:7
# bytes_sent:12345 bytes_acked:12345 bytes_received:67890
# segs_out:100 segs_in:200 data_segs_out:80 data_segs_in:150
# send 93.8Mbps lastsnd:42 lastrcv:42 lastack:42
# pacing_rate 187Mbps delivery_rate 93.8Mbps
# busy:5ms retrans:0/0 dsack_dups:0 rcv_rtt:1

# 특정 상태의 소켓만 필터링
ss -tnp state time-wait             # TIME_WAIT 소켓
ss -tnp state close-wait            # CLOSE_WAIT 소켓 (서버 문제 시)
ss -tnp state syn-recv              # SYN_RECV (SYN flood 시 증가)

# 소켓 메모리 사용량 표시
ss -tnm state established
# skmem:(r0,rb131072,t0,tb46080,f0,w0,o0,bl0,d0)
# r=수신큐, rb=수신버퍼, t=송신큐, tb=송신버퍼
# f=forward_alloc, w=wmem_queued, o=opt_mem

# 특정 포트/프로세스 필터
ss -tnp 'sport == :80'              # 소스 포트 80
ss -tnp 'dport == :443'             # 목적지 포트 443
ss -tnp '( sport == :80 or sport == :443 )'

perf와 eBPF 기반 네트워크 프로파일링

# 네트워크 스택 CPU 프로파일링
perf top --sort comm,dso,symbol       # 실시간 핫스팟 확인

# 네트워크 관련 함수 프로파일링
perf record -g -p $(pgrep nginx) -- sleep 30
perf report --sort comm,symbol | grep -E "tcp_|ip_|napi_|netif_"

# flamegraph 생성 (네트워크 스택 시각화)
perf script | stackcollapse-perf.pl | flamegraph.pl > net-flame.svg

# bpftrace: TCP 연결 지연 히스토그램
bpftrace -e 'kprobe:tcp_v4_connect { @start[tid] = nsecs; }
             kretprobe:tcp_v4_connect /@start[tid]/ {
                 @connect_us = hist((nsecs - @start[tid]) / 1000);
                 delete(@start[tid]);
             }'

# bpftrace: 소켓 수신 큐 깊이 모니터링
bpftrace -e 'kprobe:tcp_recvmsg {
    $sk = (struct sock *)arg0;
    @recv_q = hist($sk->sk_receive_queue.qlen);
}'

# BCC 도구 (bcc-tools 패키지)
tcplife -D                            # TCP 연결 수명/바이트 추적
tcpretrans                            # TCP 재전송 실시간 추적
tcpconnect                            # TCP 연결 시도 추적
tcpaccept                             # TCP accept 추적
tcpdrop                               # TCP 드롭 추적 (드롭 원인 포함)
tcpsynbl                              # SYN backlog 크기 추적
체계적 성능 분석 순서: (1) sar -n DEV 1으로 인터페이스 처리량 확인, (2) mpstat -P ALL 1로 CPU별 softirq/user 비율 확인, (3) /proc/net/softnet_stat으로 NAPI budget 초과/드롭 확인, (4) ethtool -S으로 NIC 에러 확인, (5) ss -ti로 TCP 재전송/cwnd 확인, (6) 필요시 perf/bpftrace로 커널 내부 프로파일링 수행. 이 순서로 진행하면 대부분의 네트워크 성능 문제를 체계적으로 분석할 수 있습니다.

고성능 패킷 처리와 주제

기본 네트워크 스택 성능이 요구 사항을 충족하지 못할 때 사용할 수 있는 고성능 기술과 주제를 정리합니다.

XDP (eXpress Data Path)

XDP는 NIC 드라이버 단에서 eBPF 프로그램을 실행하여 sk_buff 할당 전에 패킷을 처리합니다. XDP_DROP(드롭), XDP_TX(송신 반사), XDP_REDIRECT(다른 인터페이스/AF_XDP 소켓 전달), XDP_PASS(정상 스택 진행) 동작을 지원합니다. DDoS 방어, 로드밸런싱에서 수백만 PPS 처리가 가능합니다.

AF_XDP

AF_XDP 소켓은 XDP와 유저 공간을 공유 메모리(UMEM)로 연결하여 커널 바이패스에 가까운 성능을 제공합니다. DPDK와 유사한 성능이지만 커널 보안 모델을 유지합니다. AF_XDP의 핵심 구성 요소는 4개의 링 버퍼(Ring Buffer)입니다:

/* AF_XDP 소켓 기본 설정 패턴 */
struct xsk_socket_config cfg = {
    .rx_size = 4096,
    .tx_size = 4096,
    .libbpf_flags = XSK_LIBBPF_FLAGS__INHIBIT_PROG_LOAD,
};

/* UMEM 설정 */
struct xsk_umem_config umem_cfg = {
    .fill_size = 4096,
    .comp_size = 4096,
    .frame_size = 4096,
    .frame_headroom = 0,
};

/* UMEM 생성 (4096 프레임 x 4096 바이트 = 16MB) */
xsk_umem__create(&umem, buffer, FRAME_NUM * FRAME_SIZE,
                 &fq, &cq, &umem_cfg);

/* AF_XDP 소켓 생성 */
xsk_socket__create(&xsk, "eth0", 0, umem,
                    &rx, &tx, &cfg);

AF_XDP 성능 최적화

/* AF_XDP RX 루프 — 고성능 패턴 */
while (1) {
    unsigned int rcvd, i;
    u32 idx_rx = 0, idx_fq = 0;

    /* 1. RX 링에서 수신 패킷 확인 */
    rcvd = xsk_ring_cons__peek(&xsk->rx, BATCH_SIZE, &idx_rx);
    if (!rcvd) {
        /* busy poll 또는 poll()/epoll() 대기 */
        poll(fds, 1, 1000);
        continue;
    }

    /* 2. FILL 링에 빈 프레임 보충 */
    xsk_ring_prod__reserve(&xsk->umem->fq, rcvd, &idx_fq);
    for (i = 0; i < rcvd; i++) {
        u64 addr = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx)->addr;
        u32 len = xsk_ring_cons__rx_desc(&xsk->rx, idx_rx)->len;

        /* 3. 패킷 데이터 접근 (UMEM + addr) */
        void *pkt = xsk_umem__get_data(xsk->umem->buffer, addr);
        process_packet(pkt, len);

        /* FILL 링에 프레임 반환 */
        *xsk_ring_prod__fill_addr(&xsk->umem->fq, idx_fq++) = addr;
        idx_rx++;
    }
    xsk_ring_prod__submit(&xsk->umem->fq, rcvd);
    xsk_ring_cons__release(&xsk->rx, rcvd);
}
코드 설명
  • 7행xsk_ring_cons__peek()는 RX 링에서 사용 가능한 패킷 수를 확인합니다. 배치 처리를 위해 최대 BATCH_SIZE만큼 한 번에 가져옵니다.
  • 15행FILL 링에 빈 UMEM 프레임을 보충합니다. 커널은 FILL 링에서 프레임을 가져와 다음 수신 패킷에 사용합니다.
  • 21행xsk_umem__get_data()는 UMEM 베이스 주소와 프레임 오프셋으로 패킷 데이터 포인터를 계산합니다. 커널-유저 간 데이터 복사가 없습니다.
  • 28-29행처리 완료 후 FILL 링과 RX 링의 인덱스를 업데이트합니다. 커널은 이 인덱스를 확인하여 프레임 재활용과 새 패킷 수신을 진행합니다.
# AF_XDP 성능 측정
# xdp-bench를 이용한 AF_XDP 수신 성능 테스트
xdp-bench rxdrop -d eth0 -M af_xdp

# AF_XDP 소켓 상태 확인
bpftool map show                       # XSK_MAP 확인

# AF_XDP 최적화 설정
# 1. NIC 큐와 XSK 1:1 바인딩 (전용 모드)
# 2. XDP_FLAGS_DRV_MODE (네이티브 XDP)
# 3. zero-copy 모드 (XDP_ZEROCOPY)
# 4. NUMA 친화 메모리 할당
# 5. CPU 고정 (taskset/isolcpus)

DPDK (Data Plane Development Kit)

DPDK는 커널 네트워크 스택을 완전히 우회하는 유저 공간 패킷 처리 프레임워크입니다. PMD(Poll Mode Driver)가 전용 CPU 코어에서 NIC 큐를 직접 폴링하여 인터럽트, 컨텍스트 스위칭(Context Switching), 커널 오버헤드를 완전히 제거합니다. 통신사, 금융 거래, NFV(Network Functions Virtualization) 환경에서 사용됩니다.

특성AF_XDPDPDK
커널 관여최소 (XDP 연동)없음 (완전 바이패스)
보안 모델커널이 UMEM 관리커널 보안 미적용
NIC 지원XDP 지원 NICDPDK PMD 지원 NIC
배포 복잡도낮음 (커널 내장)높음 (별도 라이브러리/드라이버)
Hugepage선택적필수
CPU 전용 할당선택적권장 (isolcpus)
성능 (64B PPS)~25Mpps/core~35Mpps/core
생태계libbpf, 커널 도구VPP, OVS-DPDK, SPDK

커널 바이패스 기술 비교

기술커널 관여도프로그래밍 모델성능보안 모델
일반 소켓전체 스택BSD 소켓 API기준선커널 보안 완전 적용
XDP드라이버 단 eBPFeBPF 프로그램10-20xeBPF 검증기
AF_XDP최소 (공유 메모리)UMEM ring50-100x커널 관리 UMEM
DPDK없음 (완전 바이패스)PMD 폴링최대커널 보안 미적용

기술 선택 가이드

네트워크 요구사항에 따라 적절한 기술을 선택하는 것이 중요합니다. 아래는 일반적인 시나리오별 권장 기술 스택입니다:

시나리오PPS 요구지연 요구권장 기술대안
일반 웹 서버< 100K< 1ms기본 스택 + BBRepoll + 소켓 튜닝
리버스 프록시< 1M< 100usNAPI 튜닝 + RPSSO_REUSEPORT
소프트웨어 방화벽1-10M< 50usnftables + FlowtableXDP + eBPF
L4 로드밸런서1-50M< 10usXDP + eBPFIPVS + Maglev
DDoS 완화> 10M< 1usXDP_DROPNIC flow director
패킷 캡처/분석1-100M비실시간AF_XDP + UMEMPF_RING, AF_PACKET v3
통신사 NFV> 50M< 5usDPDK + VPPAF_XDP
금융 거래< 1M< 1usDPDK/RDMA kernel bypassSolarflare OpenOnload

eBPF 네트워킹 프로그램 유형

eBPF는 네트워크 스택의 다양한 지점에 프로그램을 부착할 수 있으며, 각 유형은 고유한 입력 컨텍스트와 동작을 가집니다.

프로그램 유형부착 지점입력 컨텍스트성능 수준사용 예
BPF_PROG_TYPE_XDPNIC 드라이버xdp_md (raw 패킷)최고 (sk_buff 전)DDoS 방어, LB
BPF_PROG_TYPE_SCHED_CLSTC ingress/egress__sk_buff높음패킷 분류/수정
BPF_PROG_TYPE_SOCK_OPSTCP 이벤트bpf_sock_ops보통TCP 튜닝
BPF_PROG_TYPE_SK_SKB소켓 데이터__sk_buff보통sockmap 리다이렉트
BPF_PROG_TYPE_CGROUP_SKBcgroup 소켓__sk_buff보통컨테이너 정책
BPF_PROG_TYPE_SK_REUSEPORTSO_REUSEPORTsk_reuseport_md높음커스텀 LB
BPF_PROG_TYPE_FLOW_DISSECTOR플로우 파싱__sk_buff높음커스텀 해시
BPF_PROG_TYPE_LWT_*라우팅 (LWT)__sk_buff보통패킷 캡슐화
/* TC BPF 프로그램 예시: 패킷 리다이렉트 */
SEC("tc")
int tc_redirect(struct __sk_buff *skb)
{
    void *data = (void *)(long)skb->data;
    void *data_end = (void *)(long)skb->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return TC_ACT_OK;

    /* MAC 주소 교환 */
    unsigned char tmp[ETH_ALEN];
    __builtin_memcpy(tmp, eth->h_source, ETH_ALEN);
    __builtin_memcpy(eth->h_source, eth->h_dest, ETH_ALEN);
    __builtin_memcpy(eth->h_dest, tmp, ETH_ALEN);

    /* 다른 인터페이스로 리다이렉트 */
    return bpf_redirect(ifindex_peer, 0);
}

/* SOCK_OPS BPF: TCP 옵션 튜닝 */
SEC("sockops")
int bpf_sockops(struct bpf_sock_ops *skops)
{
    switch (skops->op) {
    case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB:
        /* 연결 수립 시 혼잡 제어 변경 */
        bpf_setsockopt(skops, SOL_TCP, TCP_CONGESTION,
                       "bbr", 4);
        break;
    case BPF_SOCK_OPS_RTO_CB:
        /* RTO 이벤트 로깅 */
        break;
    }
    return 1;
}
코드 설명
  • 2-20행TC BPF 프로그램은 __sk_buff 컨텍스트로 패킷에 접근합니다. XDP보다 상위 계층에서 동작하므로 L3/L4 헤더 파싱이 완료된 상태이며, bpf_redirect()로 다른 인터페이스로 패킷을 전달할 수 있습니다.
  • 23-36행SOCK_OPS 프로그램은 TCP 이벤트(연결 수립, RTO, ECN 등)에 반응합니다. bpf_setsockopt()로 소켓 옵션을 동적으로 변경할 수 있어, 연결 대상에 따라 혼잡 제어를 자동으로 선택하는 등의 정책을 구현할 수 있습니다.

sockmap과 소켓 리다이렉트

sockmap은 eBPF 맵을 이용하여 소켓 간 데이터를 커널 내부에서 직접 전달하는 메커니즘입니다. 프록시 서버에서 유저 공간으로의 데이터 복사를 제거하여 처리량과 지연을 크게 개선합니다.

/* sockmap 기반 L7 프록시 가속 */

/* BPF 맵: 소켓 쌍 저장 */
struct {
    __uint(type, BPF_MAP_TYPE_SOCKHASH);
    __uint(max_entries, 65536);
    __type(key, struct sock_key);
    __type(value, int);
} sock_map SEC(".maps");

/* SK_SKB 프로그램: 소켓 데이터 리다이렉트 */
SEC("sk_skb/stream_verdict")
int bpf_prog_verdict(struct __sk_buff *skb)
{
    struct sock_key key = {};
    key.sip = skb->remote_ip4;
    key.dport = skb->remote_port;

    /* 대상 소켓을 sockmap에서 찾아 리다이렉트 */
    return bpf_sk_redirect_hash(skb, &sock_map, &key, BPF_F_INGRESS);
    /* 유저 공간 복사 없이 소켓→소켓 직접 전달 */
    /* 프록시 throughput 3-5배 향상 */
}
코드 설명
  • 4-9행BPF_MAP_TYPE_SOCKHASH는 소켓 참조를 해시 맵에 저장합니다. 프록시가 클라이언트-업스트림 소켓 쌍을 이 맵에 등록합니다.
  • 20행bpf_sk_redirect_hash()는 수신된 데이터를 sockmap에서 찾은 대상 소켓으로 직접 전달합니다. 유저 공간의 recv() + send() 루프를 완전히 제거합니다.
Cilium의 sockmap 활용: Kubernetes 네트워킹 솔루션인 Cilium은 sockmap을 사용하여 같은 노드의 Pod 간 통신을 가속합니다. Pod A → veth → 브릿지 → veth → Pod B 경로 대신, sockmap으로 소켓 간 직접 전달하여 TCP 처리량을 3배, 지연을 50% 줄입니다. bpf_sk_msg_verdict 프로그램으로 트래픽 정책도 동시에 적용할 수 있습니다.

추가 주제:

커널 버전별 주요 네트워킹 변경 사항

커널 버전주요 변경영향
4.8+XDP 초기 지원고성능 패킷 처리의 새 시대
4.18+AF_XDP 소켓유저 공간 고성능 패킷 I/O
5.0+BPF 트램펄린BPF-to-BPF 호출, 확장된 프로그래밍
5.1+Netfilter Flowtableconntrack 가속, ESTABLISHED 빠른 경로
5.4+MPTCP v0, BBRv2 (실험)멀티패스 TCP, 개선된 혼잡 제어
5.6+WireGuard 메인라인현대적 VPN 프로토콜 기본 지원
5.8+devlink 포트 분리/합병SmartNIC eSwitch 세밀 제어
5.10+MPTCP 안정화프로덕션 멀티패스 TCP
5.15+page_pool 성능 개선RX 경로 메모리 할당 최적화
6.0+BIG TCP (IPv6 GRO)512KB+ 이상 GRO 패킷 지원
6.1+네트워크 다중 큐 개선NAPI 스레딩, 더 나은 멀티코어 스케일링
6.6+netdev GenL APIethtool/devlink 통합 Netlink API

핵심 함수 빠른 참조

네트워크 스택의 주요 함수를 경로별로 정리합니다. 커널 소스 분석이나 ftrace/kprobe 설정 시 참고할 수 있습니다.

RX 경로 (NIC → 유저 공간)

단계함수소스 파일역할
IRQnapi_schedule()include/linux/netdevice.hNAPI 폴링 예약
SoftIRQnet_rx_action()net/core/dev.cNAPI poll 루프 실행
NAPInapi_gro_receive()net/core/gro.cGRO 병합 시도
L2__netif_receive_skb()net/core/dev.c프로토콜 디멀티플렉싱
L3ip_rcv()net/ipv4/ip_input.cIP 헤더 검증
L3ip_rcv_finish()net/ipv4/ip_input.cFIB 조회, early demux
L3ip_local_deliver()net/ipv4/ip_input.c로컬 패킷 L4 전달
L4 (TCP)tcp_v4_rcv()net/ipv4/tcp_ipv4.cTCP 수신 진입점
L4 (TCP)tcp_rcv_established()net/ipv4/tcp_input.cFast/Slow path 분기
L4 (UDP)udp_rcv()net/ipv4/udp.cUDP 수신 진입점
소켓sock_queue_rcv_skb()net/core/sock.c소켓 수신 큐 삽입
소켓sk->sk_data_ready()net/core/sock.c대기 프로세스 깨움

TX 경로 (유저 공간 → NIC)

단계함수소스 파일역할
소켓sock_sendmsg()net/socket.c소켓 계층 진입
L4 (TCP)tcp_sendmsg()net/ipv4/tcp.cTCP 데이터 큐잉
L4 (TCP)tcp_write_xmit()net/ipv4/tcp_output.ccwnd/Nagle 확인, 전송 결정
L4 (UDP)udp_sendmsg()net/ipv4/udp.cUDP 즉시 전달
L3ip_queue_xmit()net/ipv4/ip_output.cIP 헤더, 라우팅
L3ip_output()net/ipv4/ip_output.cNetfilter POSTROUTING
L2.5__dev_queue_xmit()net/core/dev.cqdisc enqueue/dequeue
L2dev_hard_start_xmit()net/core/dev.cGSO 처리, 드라이버 호출
드라이버ndo_start_xmit()드라이버별DMA 매핑, TX Ring

Netfilter 훅 경로

호출 시점핵심 함수주요 용도
PRE_ROUTINGip_rcv() 내부NF_HOOK(NF_INET_PRE_ROUTING)DNAT, conntrack 생성
LOCAL_INip_local_deliver()NF_HOOK(NF_INET_LOCAL_IN)입력 필터링
FORWARDip_forward()NF_HOOK(NF_INET_FORWARD)포워딩 필터링
LOCAL_OUT__ip_local_out()NF_HOOK(NF_INET_LOCAL_OUT)출력 필터링
POST_ROUTINGip_output()NF_HOOK(NF_INET_POST_ROUTING)SNAT/Masquerade

핵심 소스 파일 맵

경로내용주요 함수
net/core/dev.c네트워크 디바이스 핵심netif_receive_skb, __dev_queue_xmit
net/core/skbuff.csk_buff 관리alloc_skb, kfree_skb, skb_clone
net/core/sock.c소켓 핵심sock_sendmsg, sock_recvmsg
net/core/gro.cGRO 프레임워크dev_gro_receive, napi_gro_receive
net/ipv4/ip_input.cIPv4 수신ip_rcv, ip_rcv_finish
net/ipv4/ip_output.cIPv4 송신ip_queue_xmit, ip_output, ip_fragment
net/ipv4/tcp_input.cTCP 수신tcp_rcv_established, tcp_data_queue
net/ipv4/tcp_output.cTCP 송신tcp_write_xmit, tcp_transmit_skb
net/ipv4/tcp_ipv4.cTCP IPv4 연동tcp_v4_rcv, tcp_v4_connect
net/ipv4/udp.cUDP 구현udp_sendmsg, udp_rcv
net/ipv4/route.cIPv4 라우팅ip_route_input, ip_route_output
net/ipv4/fib_trie.cFIB LC-triefib_table_lookup
net/netfilter/core.cNetfilter 핵심nf_hook_slow, nf_register_net_hook
net/netfilter/nf_conntrack_core.c연결 추적nf_conntrack_in, resolve_normal_ct
net/sched/sch_generic.cqdisc 프레임워크__qdisc_run, qdisc_restart

용어 정리

약어/용어전체 이름설명
NAPINew API인터럽트/폴링 하이브리드 수신 프레임워크
GROGeneric Receive Offload수신 패킷 병합 (소프트웨어)
GSOGeneric Segmentation Offload대형 패킷 세그먼테이션 (소프트웨어)
TSOTCP Segmentation OffloadNIC 하드웨어 세그먼테이션
BQLByte Queue LimitsTX 큐 바이트 제한 (bufferbloat 방지)
RSSReceive Side ScalingNIC 하드웨어 RX 큐 분배
RPSReceive Packet Steering소프트웨어 RX CPU 분배
RFSReceive Flow Steering소켓 CPU 친화성 기반 분배
XPSTransmit Packet SteeringCPU-TX 큐 매핑
FIBForwarding Information Base라우팅 테이블 (LC-trie)
LPMLongest Prefix Match라우팅 조회 알고리즘
NUDNeighbor Unreachability DetectionARP/NDP 이웃 상태 머신
cwndCongestion WindowTCP 혼잡 윈도우
rwndReceive WindowTCP 수신 윈도우
RTTRound Trip Time패킷 왕복 시간
RTORetransmission Timeout재전송 타임아웃
SRTTSmoothed Round Trip Time평활화된 RTT
BDPBandwidth-Delay Product대역폭 × 지연 (필요 버퍼 크기)
MSSMaximum Segment SizeTCP 최대 세그먼트 크기
MTUMaximum Transmission Unit인터페이스 최대 전송 단위
PPSPackets Per Second초당 패킷 처리 수
PMTUDPath MTU Discovery경로 MTU 탐색
TFOTCP Fast Open0-RTT TCP 핸드셰이크
TLPTail Loss Probe꼬리 손실 프로브 (빠른 손실 감지)
RACKRecent ACKnowledgment시간 기반 손실 감지
ECNExplicit Congestion Notification명시적 혼잡 알림
SACKSelective ACKnowledgment선택적 확인 응답
kTLSKernel TLS커널 내 TLS 레코드 암호화
ULPUpper Layer ProtocolTCP 상위 계층 프로토콜 플러그인
XDPeXpress Data Path드라이버 단 eBPF 패킷 처리
UMEMUser MemoryAF_XDP 공유 메모리 영역
PMDPoll Mode DriverDPDK 폴링 기반 드라이버
VRFVirtual Routing and Forwarding가상 라우팅 도메인

하위 문서 네비게이션 가이드

이 페이지(Page)는 네트워크 스택의 전체 구조를 조감하는 게이트웨이 문서입니다. 아래 가이드는 학습 목적과 역할에 따라 어떤 하위 문서를 읽어야 하는지 안내합니다.

네트워크 스택 하위 문서 네비게이션 맵: 역할별 권장 학습 경로 네트워크 스택 문서 네비게이션 맵 네트워크 스택 개요 (현재) 핵심 자료구조 sk_buff NAPI (New API) GSO/GRO 오프로드 L3/L4 프로토콜 IP (IPv4/IPv6) TCP UDP / SCTP / ICMP L2/디바이스 net_device 드라이버 이더넷 / MAC 주소 ethtool 인프라/정책 Netfilter / conntrack 라우팅 / FIB TC (Traffic Control) 네트워크 네임스페이스 고성능 BPF/eBPF/XDP AF_XDP / DPDK 패킷 흐름 / 디버깅
역할별 권장 학습 경로

문서 활용 팁

이 사이트의 네트워크 관련 문서는 약 35개 페이지로 구성되어 있습니다. 효율적으로 활용하려면 다음을 참고하세요:

역할별 권장 학습 순서

역할1순위2순위3순위
NIC 드라이버 개발자sk_buff, NAPI, net_device 드라이버GSO/GRO, ethtoolXDP, AF_XDP
프로토콜 개발자IP, TCP, UDP소켓 계층, sk_buffSCTP, kTLS
네트워크 관리자라우팅, NetfilterTC, 네트워크 네임스페이스패킷 흐름/디버깅, ethtool
보안 엔지니어Netfilter, conntrack네트워크 공격 방어, IPSec/xfrmeBPF 보안 정책
성능 엔지니어패킷 흐름/디버깅, ethtoolBPF/XDP, AF_XDPDPDK, SmartNIC/DPU
컨테이너/클라우드네트워크 네임스페이스, VXLAN/GENEVEBridge, Bonding/MACVLANOVS, eBPF, VRF

이 페이지는 Linux 네트워크 스택의 전체 아키텍처를 조감하는 게이트웨이 문서입니다. 각 주제에 대해 독립적인 기술 문서가 준비되어 있으며, 해당 문서에서 커널 소스 코드 수준의 상세한 설명, 성능 벤치마크, 실전 트러블슈팅 사례를 다룹니다. 학습 순서는 위의 하위 문서 네비게이션 가이드를 참고하세요.

핵심 자료구조 & 수신 경로

L2 계층

L3/L4 프로토콜

인프라 & 제어 평면

가상 네트워크 & 오버레이

고성능 네트워킹

네트워크 보안

참고 자료: 커널 네트워킹의 공식 문서는 Documentation/networking/ 디렉토리에 있습니다. 특히 scaling.txt(RSS/RPS/RFS/XPS), ip-sysctl.txt(sysctl 매개변수), af_xdp.rst(AF_XDP API)는 필수 참고 문서입니다. 커뮤니티 리소스로는 kernel.org 네트워킹 문서, netdev 메일링 리스트, Brendan Gregg의 네트워크 분석 자료가 유용합니다.