LLM WikiAccess-protected knowledge portal
← 스터디 홈
5편 · 약 24분

네트워킹 스택

왜 계층으로 나뉘는가

데이터가 네트워크를 오가는 과정은 복잡하다. 애플리케이션은 "data.json 파일을 서버에 보내라"고 생각하지만, 실제 하드웨어는 전기 신호나 광 펄스로 이루어진 비트 스트림을 전송할 뿐이다. 이 두 세계를 연결하려면 각 계층이 바로 아래 계층의 세부사항을 감추고 위 계층에게 깔끔한 인터페이스를 제공해야 한다.

Linux 커널은 OSI 7계층을 실용적으로 압축해 4계층 구조로 구현한다.

애플리케이션 Socket API (POSIX) socket / bind / connect
send / recv / select
전송 계층 (L4) TCP / UDP / SCTP 포트·연결 관리
흐름 제어·혼잡 제어
네트워크 계층 (L3) IPv4 / IPv6 라우팅·단편화
Netfilter 훅
데이터링크 + 물리 NIC 드라이버 (NAPI) 이더넷 프레임
DMA·링 버퍼·인터럽트
커널 내부 핵심 구조체 sk_buff — 모든 계층을 관통하는 패킷 캐리어 struct socket / struct sock — BSD 소켓 커널 표현 net_device — NIC 추상화 객체
Linux 네트워킹 계층 구조

sk_buff: 패킷의 여권

struct sk_buff(소켓 버퍼, 약어 skb)는 하나의 네트워크 패킷을 표현하는 핵심 자료구조다. NIC 드라이버가 패킷을 수신하는 순간 생성되어, L2 → L3 → L4 → 소켓 수신 큐에 이르기까지 동일한 skb가 계층을 관통한다. 각 계층은 데이터를 복사하지 않고 포인터만 이동시켜 헤더를 드러내거나 감춘다.

struct sk_buff {
    /* 연결 리스트 */
    struct sk_buff      *next;
    struct sk_buff      *prev;

    /* 데이터 영역 포인터 */
    unsigned char       *head;   // 할당된 버퍼 시작 (헤더 여분 포함)
    unsigned char       *data;   // 현재 페이로드 시작
    unsigned char       *tail;   // 현재 페이로드 끝
    unsigned char       *end;    // 할당된 버퍼 끝

    /* 메타데이터 */
    __u32               len;     // 총 데이터 길이
    __be16              protocol;// ETH_P_IP, ETH_P_IPV6, ...
    __u32               priority;
    struct net_device   *dev;    // 수신/발신 네트워크 인터페이스

    /* L3 / L4 헤더 포인터 (파싱 시 설정) */
    struct iphdr        *nh_iph; // IP 헤더 위치
    union { struct tcphdr *th; struct udphdr *uh; } h; // L4 헤더

    /* 페이지 기반 비선형 데이터 (Zero-copy 수신) */
    struct skb_shared_info  *shinfo; // frags[] 배열 (paged data)
};

헤더 추가와 제거: push / pull

헤더를 추가(캡슐화)할 때는 skb_push()data 포인터를 앞으로 이동해 head와의 공간에 헤더를 씀. 헤더를 제거(역캡슐화)할 때는 skb_pull()data 포인터를 뒤로 이동해 헤더 영역을 숨김.

// L4 → L3 캡슐화 (TCP → IP)
skb_push(skb, sizeof(struct iphdr));  // data를 앞으로 당겨 IP 헤더 공간 확보
ip_hdr(skb)->version = 4;             // data가 가리키는 위치에 IP 헤더 작성

이 포인터 이동만으로 헤더 조작이 이루어지기 때문에, 전체 스택에서 데이터 복사가 최소화된다.

패킷 수신 경로: NIC → 소켓

패킷 하나가 NIC로 들어와 recv() 시스템 콜로 애플리케이션에 전달되기까지의 전 여정이다.

① 하드웨어 / 드라이버
패킷 도착
(이더넷 프레임)
NIC DMA
→ Ring Buffer
HardIRQ 발생
(인터럽트)
NAPI 스케줄
(SoftIRQ NET_RX)
skb 할당
netif_receive_skb()
② L3 처리 (IPv4)
ip_rcv() Netfilter
PREROUTING
라우팅 결정
ip_route_input()
Netfilter
LOCAL_IN
ip_local_deliver()
단편 재조립
③ L4 처리 (TCP)
tcp_v4_rcv() TCP 상태 머신
(SYN/ACK/DATA)
수신 큐
sk_receive_queue
애플리케이션
recv() 반환
패킷 수신 경로 (Receive Path)

NAPI: 인터럽트 폭주 해결

고속 NIC에서 패킷이 밀려올 때 매 패킷마다 인터럽트를 발생시키면 CPU가 인터럽트 처리에만 매달린다. NAPI(New API)는 첫 패킷에서만 인터럽트를 발생시키고, 이후 SoftIRQ 컨텍스트에서 폴링(polling)으로 전환해 budget개(기본 300개) 패킷을 한 번에 처리한다. 처리할 패킷이 없으면 다시 인터럽트 모드로 복귀한다.

# NAPI 통계: SoftIRQ 패킷 처리 수
cat /proc/net/softnet_stat
# 열: total processed, dropped(budget 초과), time_squeeze(budget 소진), ...

# 수신 큐 깊이 및 드롭 통계
ethtool -S eth0 | grep -E 'rx_dropped|rx_missed'

패킷 송신 경로: 소켓 → NIC

send() 시스템 콜 tcp_sendmsg()
소켓 송신 버퍼
tcp_write_xmit()
세그먼트 생성
ip_queue_xmit()
IP 헤더 추가
Netfilter
LOCAL_OUT
POSTROUTING
QDisc
(트래픽 쉐이핑)
dev_queue_xmit()
→ NIC 드라이버
DMA → 물리
전송
송신 버퍼 사이즈 제어 SO_SNDBUF: 소켓별 송신 버퍼 크기 (기본 ~128 KB) net.core.wmem_default / wmem_max: 시스템 전역 기본/최대값 net.ipv4.tcp_wmem: min, default, max 세 값으로 TCP 자동 튜닝
패킷 송신 경로 (Transmit Path)

QDisc(Queueing Discipline)는 패킷 스케줄러다. 기본은 pfifo_fast(3단계 우선순위 FIFO)이며, tc 명령으로 HTB·fq·cake 등의 정교한 스케줄러로 교체할 수 있다.

Netfilter와 Conntrack

Netfilter는 패킷 경로 5곳에 훅(hook) 포인트를 삽입한다. 커널 모듈이 이 훅에 콜백을 등록해 방화벽·NAT·패킷 수정 등을 구현한다.

수신 패킷 (로컬 수신)
NIC IN
PREROUTING
라우팅
LOCAL_IN
로컬 소켓
수신 패킷 (포워딩)
PREROUTING 후
FORWARD

POSTROUTING
NIC OUT
송신 패킷 (로컬 생성)
로컬 소켓
LOCAL_OUT
라우팅
POSTROUTING
NIC OUT
훅 활용 사례 iptables / nftables: PREROUTING(DNAT), POSTROUTING(SNAT/MASQUERADE), FORWARD(필터링) conntrack: PREROUTING에서 연결 상태(NEW/ESTABLISHED/RELATED/INVALID) 추적 Kubernetes kube-proxy: PREROUTING에 DNAT 규칙으로 Service IP → Pod IP 변환
Netfilter 5 훅 포인트

Connection Tracking(conntrack)은 상태 기반 방화벽의 핵심이다. 패킷이 PREROUTING 훅을 통과할 때 (src_ip, dst_ip, src_port, dst_port, protocol) 5-tuple로 연결 상태 테이블을 조회·갱신한다.

# 현재 conntrack 테이블
conntrack -L | head -20
# tcp  6 299 ESTABLISHED src=10.0.0.5 dst=10.0.0.1 sport=54321 dport=443 ...

# conntrack 테이블 용량 (기본 65536)
cat /proc/sys/net/netfilter/nf_conntrack_max

# 커넥션이 꽉 찰 경우 (컨테이너 환경에서 흔한 이슈)
dmesg | grep "nf_conntrack: table full"
# → /etc/sysctl.conf 에서 nf_conntrack_max 늘리기

TCP 소켓 내부: send buffer와 receive buffer

소켓은 두 개의 큐를 갖는다:

  • sk_rcvbuf (수신 버퍼): NIC → 커널이 채우고 recv() 호출 시 애플리케이션이 비운다. 가득 차면 패킷을 드롭한다.
  • sk_sndbuf (송신 버퍼): send() 호출 시 애플리케이션이 채우고 커널이 비운다. 가득 차면 send()가 블로킹된다.
# 수신/송신 버퍼 크기 확인 (단위: bytes)
ss -tmi | grep -A1 "ESTABLISHED"
# rcvbuf:87380  sndbuf:2626560

# 전역 TCP 버퍼 튜닝 (고성능 서버 기본 설정)
sysctl -w net.core.rmem_max=134217728      # 128 MB
sysctl -w net.core.wmem_max=134217728
sysctl -w net.ipv4.tcp_rmem="4096 87380 134217728"
sysctl -w net.ipv4.tcp_wmem="4096 65536 134217728"
sysctl -w net.ipv4.tcp_mem="94500000 915000000 927000000"

TCP_NODELAY: Nagle 알고리즘을 비활성화한다. 작은 메시지를 여러 번 보내는 프로토콜(Redis, gRPC)에서 레이턴시를 낮추려면 반드시 설정해야 한다.

int one = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));

XDP: 커널 바이패스 없는 고속 패킷 처리

XDP(eXpress Data Path)는 eBPF 프로그램을 NIC 드라이버 직후에 실행해 sk_buff 할당 전에 패킷을 처리한다. 커널을 완전히 우회하는 DPDK보다 안전하면서도 수십 Mpps 처리가 가능하다.

방식위치장점단점
XDP_DROP드라이버DDoS 패킷 즉시 폐기, 최고속커널 네트워킹 기능 없음
XDP_PASS드라이버선택적 패킷을 커널 스택으로 올림일반 처리 경로
XDP_TX드라이버패킷 수정 후 동일 인터페이스로 재전송L3 라우팅 불가
XDP_REDIRECT드라이버다른 인터페이스로 포워딩, AF_XDP 소켓으로 유저스페이스 전달프로그래밍 복잡도
# XDP 프로그램 로드 (ip 명령 활용)
ip link set dev eth0 xdp obj xdp_drop.o sec xdp

# 현재 XDP 상태 확인
ip link show eth0 | grep xdp

네트워킹 성능 진단

# 수신 큐 드롭 통계 (NIC Ring Buffer 오버플로)
ethtool -S eth0 | grep -i drop

# SS로 소켓 상태 상세 확인
ss -tinp sport = :443
# State  Recv-Q  Send-Q  rtt:1.23/0.45 retrans:0/0 cwnd:10 ...

# TCP 재전송 카운터 (지속적으로 증가하면 네트워크 문제)
netstat -s | grep -i retransmit
# 또는: ss -s

# NAPI 폴링과 SoftIRQ 부하 확인
watch -n1 'cat /proc/softirqs | grep NET_RX'

# 패킷 캡처 (커널 레벨)
tcpdump -i eth0 -nn port 8080 -w /tmp/trace.pcap

References