0. Contents

1. Overview

유저 공간규칙을 넣거나 상태를 읽는다iptables · nftconntrack -Lip routess커널문ens3 · lo102-1 3장Netfilter검문소2통과·폐기 — 2장Conntrack메모장3어느 연결인가 — 3장Routing이정표4내 것인가 — 5장소켓FD 3 · 4502-1 2장주소를 바꾸는 건 검문소에서, 메모장을 보고 — 4장프로세스python3결과만 받는다한 번도 안 깨어남문에서 소켓까지의 네 가지 일은 전부 커널 안에서 일어난다 — 프로세스는 결과만 받는다
부서는 커널 안에, 도구는 밖에. iptables 는 규칙을 써 넣고 끝나고, 패킷을 보는 건 커널이다

02-1 에서 본 것: 정적인 구조

  • 02-1 은 커널이 가진 것들의 이야기였다
    • 프로그램이 부탁하는 손잡이(소켓)
    • 패킷이 드나드는 문(인터페이스)
    • 문과 주소가 통째로 격리된 방(Network Namespace)
  • 손으로 만들어 보니 전부 정적인 구조였다
    • 방을 만들고 파이프를 꽂고 스위치에 꽂으면 거기 그대로 있다

02-2: 패킷이 지나갈 때 커널이 하는 네 가지 일

  • 02-2 는 그 사이를 패킷이 지나갈 때의 이야기다
  • 문으로 들어온 패킷이 소켓에 닿기까지 커널은 네 가지 일을 한다
    • 통과시킬지 버릴지 정한다(Netfilter)
    • 이게 어느 연결의 패킷인지 기억한다(Conntrack)
    • 필요하면 주소를 바꾼다(NAT)
    • 내 것인지 남 것인지, 어느 문으로 보낼지 정한다(Routing)
  • 그리고 마지막에 — 이 전부가 커널 안에서 일어나고, 프로세스는 한 번도 깨어나지 않는다

Service 와 이어지는 지점

  • 쿠버네티스의 Service 가 이 넷의 조합이다 → 그래서 중요하다
    • 어디에도 없는 주소로 보낸 패킷이 진짜 Pod 에 닿는다
    • 답장은 없는 주소에서 온 것처럼 돌아온다
  • 책은 iptables 규칙으로 설명한다
  • 이 글에서는 그 규칙을 VM 에서 손으로 넣고 메모장을 열어 본다

실습 환경과 출발점

  • 실습은 02-1 과 같은 오라클 VM 이다
  • 02-1 의 4.5 에서 브리지에 꽂은 방끼리 ping 이 안 됐던 일이 있었다
  • 그 이유를 찾는 데서 시작한다

2. Netfilter — 패킷 길의 검문소

2.1 훅 — 자리와 규칙은 다르다

Netfilter: 검문소를 세울 수 있는 자리

  • Netfilter 는 검문소가 아니라 검문소를 세울 수 있는 자리다
  • 커널의 패킷 길에는 정해진 다섯 지점이 있다
  • 거기에 “패킷이 여기 오면 이 함수를 불러라” 하고 훅(hook)을 걸 수 있다
  • 훅에 걸린 함수가 패킷을 보고 통과(ACCEPT), 폐기(DROP), 수정 중 하나를 돌려준다

다섯 자리의 순서

  • 패킷 입장에서 기억하는 게 낫다
  • 문으로 들어오자마자 PREROUTING
  • 그다음 라우팅 결정 — 내 것인가 남 것인가
    • 내 것이면 소켓으로 가기 직전에 INPUT
    • 남 것이면 다른 문으로 나가기 전에 FORWARD
  • 내 프로세스가 만든 패킷은 OUTPUT 에서 시작한다
  • 어느 쪽이든 문으로 나가기 직전이 POSTROUTING
커널문들어옴문나감PREROUTING들어오자마자라우팅 결정내 것? 남 것?INPUT내 것 — 소켓 직전FORWARD남 것 — 넘기기 전OUTPUT내가 만든 것POSTROUTING나가기 직전소켓프로세스내 것남 것응답 · 내가 보낸 것자리는 다섯, 길은 넷 — 들어와서 받기(PRE→INPUT) · 들어와서 넘기기(PRE→FORWARD→POST) · 내보내기(OUTPUT→POST) · lo 는 전부
패킷 입장에서 외우면 된다 — 들어오자마자, 내 것이면 소켓 직전, 남 것이면 넘기기 전, 내가 만든 건 시작점, 나갈 땐 마지막

자리와 그 위에 거는 것들

  • Netfilter 자체는 규칙이 없고 자리만 제공한다
  • 그 자리에 거는 것
    • 규칙: iptables 와 nftables
    • 연결 장부: Conntrack
    • 6장에서 보듯 eBPF 프로그램도 걸 수 있다
  • 02-1 의 strace/ss 와 같은 구도다
    • 부서는 커널에 있다
    • 우리가 치는 명령은 그 부서에 규칙을 넣거나 상태를 읽는 도구다
  • iptables 명령은 치고 나면 끝나고, 패킷마다 실행되는 게 아니다

iptables 와 nftables: 문법은 달라도 자리는 같다

  • 이 우분투의 iptables --version 은 (nf_tables) 를 돌려준다
    • 02-1 1.2 의 ifconfig/ip 처럼 구세대 명령이 신세대 엔진 위에서 도는 것이다
  • nft list ruleset 을 치면 같은 규칙이 어느 훅에 걸렸는지가 그대로 보인다
  • 규칙 문법은 둘이 다르지만 자리는 같다
  • 이 글에서는 책과 같은 iptables 문법으로 쓴다

2.2 카운터로 세어 보기 — lo 는 네 자리를 다 지난다 [실습]

타깃 없는 규칙으로 세기

  • 패킷이 어느 자리를 지나는지 직접 셀 수 있다
  • iptables 규칙에 타깃(-j)을 안 붙이면 → 아무것도 안 하고 지나간 패킷 수만 센다
  • 그걸 다섯 자리에 하나씩 꽂았다
sudo iptables -t raw    -I PREROUTING  -p tcp --dport 8080
sudo iptables -t raw    -I OUTPUT      -p tcp --dport 8080
sudo iptables -t mangle -I POSTROUTING -p tcp --dport 8080
sudo iptables -t filter -I INPUT       -p tcp --dport 8080
sudo iptables -t filter -I FORWARD     -p tcp --dport 8080

테이블 이름과 실행

  • -t raw, -t mangle 은 테이블 이름이다
    • 같은 훅 자리에 테이블이 여러 겹 걸려 있다
    • 자리마다 아무 테이블이나 하나 골라 꽂은 것이다 — 몰라도 된다
  • 02-1 의 Python 서버를 띄우고 lo 로 curl 한 번
curl -s http://127.0.0.1:8080/ -o /dev/null

결과: lo 패킷은 네 자리를 전부 지난다

  • 내가 만들어 나한테 보낸 패킷은 네 자리를 전부 지난다
    • 나가는 쪽 둘(OUTPUT → POSTROUTING)을 지나 lo 로 내보내진다
    • lo 가 되돌린 패킷이 들어오는 쪽 둘(PREROUTING → INPUT)을 지난다
  • 02-1 3.2 의 “lo 로 내보낸 패킷은 바로 수신 경로로 돌아온다” 가 검문소 입장에서는 이렇게 보인다
  • 6 은 8080 으로 가는 방향의 패킷 수 — SYN, ACK, GET, FIN 과 그 ACK 들이다
  • FORWARD 는 0, 남의 패킷이 아니니까
커널 — 05 실습의 카운터 (타깃 없는 규칙은 세기만 한다)문lo · ens3PREROUTINGlo 6 · ssh 110라우팅 결정내 것? 남 것?INPUTlo 6 · ssh 110FORWARDlo 0 · ssh 0OUTPUTlo 6 · ssh 81POSTROUTINGlo 6 · ssh 81소켓:8080문lo · ens3lo — 나간 패킷이 그대로 되돌아온다남 것 — 0lo 로 보낸 curl 한 번 = 네 자리 전부 6 · 밖에서 온 ssh = PRE·INPUT 110, 답장은 OUTPUT·POST 81 · FORWARD 는 둘 다 0
같은 다섯 자리인데 패킷의 방향에 따라 지나는 조합이 다르다 — lo 는 한 번 나갔다가 다시 들어오니 전부 지난다

2.3 방향이 다르면 길이 다르다 — SSH 는 둘만 지난다 [실습]

SSH 카운터: 방향마다 둘씩

  • 같은 카운터를 22번 포트에 꽂고 노트북에서 VM 으로 ssh 를 몇 번 때렸다
    • 들어오는 패킷: PREROUTING 과 INPUT 에서 110씩 세였다
    • 답장: OUTPUT 과 POSTROUTING 에서 81씩 세였다
  • 밖에서 들어온 패킷은 앞의 둘만, 그 답장은 뒤의 둘만 지난다
    • 같은 세션인데 방향에 따라 다른 자리를 지난다
  • FORWARD 는 역시 0 이다 → 이 VM 은 남의 패킷을 넘긴 적이 없다

다섯 자리가 만드는 네 길

  • 다섯 자리를 조합하면 길은 넷이다
    • 밖에서 와서 내가 받는 것(PRE → INPUT)
    • 밖에서 와서 남에게 넘기는 것(PRE → FORWARD → POST)
    • 내가 만들어 내보내는 것(OUTPUT → POST)
    • 내가 만들어 나한테 보내는 것(네 자리 전부)
  • 쿠버네티스에 대입하면
    • 노드가 Pod 의 패킷을 다른 노드로 넘기는 건 FORWARD 길이다
    • Service IP 를 Pod IP 로 바꾸는 건 PREROUTING(밖에서 온 것)이나 OUTPUT(노드 자신이 만든 것)에서 일어난다
  • 4장에서 한다

2.4 브리지도 검문을 받는다 — 02-1 의 ping 이 막힌 이유 [실습]

증상: ARP 는 되고 ICMP 만 막힘

  • 02-1 4.5 에서 방 셋을 브리지 br0 에 꽂았는데 ping 이 100% 실패했다
  • ARP 는 됐다
    • ip neigh 에 REACHABLE 이 떴다
    • 브리지의 MAC 표도 채워졌다
  • ICMP 만 안 갔다
  • 브리지는 제대로 동작한 것이다 → 원인은 이 VM 의 FORWARD 체인이었다
sysctl net.bridge.bridge-nf-call-iptables
sudo iptables -S FORWARD

원인: 브리지 트래픽도 FORWARD 를 지난다

  • 브리지는 2계층 장치라 IP 방화벽과 무관할 것 같다
  • 하지만 bridge-nf-call-iptables = 1 이면 브리지 안에서 포트 사이를 오가는 프레임도 IP 패킷이면 FORWARD 훅을 지난다
    • 브리지 입장에서는 한 포트로 들어와 다른 포트로 나가는 것 → “남의 패킷” 이다
    • 이 VM 의 FORWARD 는 끝이 REJECT 다
  • ARP 는 IP 가 아니라 이 훅을 안 지난다 → MAC 은 배웠다
  • ICMP 는 IP 라 거절됐다
sudo iptables -I FORWARD -i br0 -o br0 -j ACCEPT

해결: br0 안쪽만 통과시키는 한 줄

  • br0 에서 들어와 br0 로 나가는 것만 통과시키는 규칙 한 줄을 맨 앞에 넣자 ping 이 갔다
  • 쿠버네티스 노드의 cni0 브리지에서 Pod 끼리 통신이 되는 것도 CNI 가 이런 규칙을 넣어 두었기 때문이다
  • 이 sysctl 은 kubelet 이 켜라고 요구하는 값이다
  • Pod 사이의 트래픽에 NetworkPolicy 를 걸 수 있는 이유가 바로 “브리지 트래픽도 FORWARD 를 지나게 해 두었기” 때문이다

전체 명령과 출력 — 05-netfilter-routing · 03-veth-netns


3. Conntrack — 검문소의 메모장

3.1 왜 필요한가 — 답장만 받아라

훅의 한계: 기억이 없다

  • 훅은 패킷을 한 장씩 보고, 앞에 뭐가 지나갔는지 모른다
  • 그런데 제일 흔한 방화벽 규칙은 이것이다
    • “밖에서 먼저 들어오는 건 막되, 내가 시작한 연결의 답장은 받아라”
  • 패킷 한 장만 보면 답장인지 새 연결인지 구분이 안 된다
  • 누군가 “내가 보낸 요청이 있었다” 를 기억하고 있어야 한다 → 그 메모장이 Conntrack 이다

INPUT 체인에 이미 있는 규칙

  • 2.1 에서 본 이 VM 의 INPUT 체인에 그 규칙이 이미 있다
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
...
-A INPUT -j REJECT --reject-with icmp-host-prohibited

ESTABLISHED 한 줄의 역할

  • 메모장에 있는 연결이면(ESTABLISHED) 통과, 없으면 맨 아래 REJECT
  • 02-1 의 Python 서버에 lo 로 curl 이 됐던 것도, 노트북에서 ssh 가 되는 것도 이 한 줄 덕분이다
  • 메모장이 없으면 “내가 시작한 연결” 이라는 개념 자체가 성립하지 않는다

NAT 에도 필요한 장부

  • NAT 도 마찬가지다
    • 첫 패킷에서 주소를 바꿨으면 같은 연결의 모든 패킷을 똑같이 바꿔야 한다
    • 답장은 반대로 되돌려야 한다
  • 장부 없이는 불가능하다 → 4장이 그 이야기다

3.2 한 줄의 모양 — 두 방향 튜플

메모장의 키: 5-tuple

  • 연결을 구분하는 다섯 값이다
    • 출발 IP, 출발 포트, 목적지 IP, 목적지 포트, 프로토콜
  • 5-tuple 이라고 부른다
  • 02-1 2.3 에서 “FD 4 가 네 번 나와도 다른 연결, 구분은 포트” 라고 한 근거가 이것이다

열린 연결 하나 읽기

  • 02-1 의 서버에 연결 하나를 열어 둔 채로 메모장을 읽었다
sudo conntrack -L -p tcp --dport 8080 --state ESTABLISHED
ss -ntp | grep 8080
sudo conntrack -L -p tcp --dport 8080 --state ESTABLISHEDtcp 6 86399 ESTABLISHED src=127.0.0.1 dst=127.0.0.1 sport=53424 dport=8080 src=127.0.0.1 dst=127.0.0.1 sport=8080 dport=53424번호남은 초상태원래 방향 — 손님 53424 → 서버 8080응답 방향 — 서버 8080 → 손님 53424ss -ntp 는 같은 연결을 두 줄(양쪽 소켓)로, conntrack 은 한 줄에 두 방향으로 적는다. 키는 5-tuple — 출발 IP·포트, 목적지 IP·포트, 프로토콜NAT 가 없으면 둘째 줄은 첫째 줄을 뒤집은 것. NAT 가 있으면 둘째 줄에 바뀐 주소가 적힌다 (4장)sudo conntrack -E (curl 한 번 동안의 이벤트)[NEW]SYN_SENT[UNREPLIED]SYN 을 봤다. 답은 아직[UPDATE]SYN_RECVSYN-ACK 를 봤다[UPDATE]ESTABLISHED[ASSURED]양방향 확인 — 86400초[UPDATE]TIME_WAIT[ASSURED]끝났지만 120초 더 남긴다메모 한 줄 = 연결 하나. 1장의 handshake 가 1밀리초 안에 네 상태를 지난다
검문소는 기억이 없어서 메모장이 필요하다 — 메모의 키는 5-tuple, 값은 상태와 응답 방향

ss 와 conntrack 이 보여 주는 모양

  • 같은 연결 하나를 두 도구가 다르게 보여 준다
    • ss: 양쪽 끝의 소켓 두 개(bash 의 3번, python3 의 4번)
    • conntrack: 한 줄에 두 방향
  • 첫째 튜플: 원래 방향(손님 53424 → 서버 8080)
  • 둘째 튜플: 응답 방향(서버 8080 → 손님 53424)
  • 86399 는 이 줄이 지워지기까지 남은 초다
    • 패킷이 오갈 때마다 다시 채워진다
    • IP 헤더의 TTL 과는 무관하다

NAT 유무와 둘째 줄

  • NAT 가 없으면 둘째 줄은 첫째 줄을 뒤집은 것뿐이다
  • NAT 가 있으면 둘째 줄에 바뀐 주소가 적힌다
  • 4.2 에서 그 모양을 본다

3.3 이벤트 — NEW 에서 TIME_WAIT 까지 [실습]

메모 변화 찍기

  • 메모가 바뀌는 순간을 찍을 수 있다
  • conntrack -E 를 켜 두고 curl 한 번
sudo conntrack -E -p tcp --dport 8080 -o timestamp &
curl -s http://127.0.0.1:8080/ -o /dev/null

handshake 를 따라 바뀌는 상태

  • SYN 을 보자 [NEW] 로 줄이 생기고 [UNREPLIED] — 답은 아직
  • SYN-ACK 를 보자 SYN_RECV
  • 그다음 ACK 로 ESTABLISHED 가 된다
    • [ASSURED] 가 붙는다
    • 타임아웃이 120초에서 86400초(하루)로 늘어난다
  • 1장 4.3 의 3-way handshake 를 메모장이 따라 적은 것이다
  • 전부 1밀리초 안에 지나갔다

연결이 끝난 뒤: TIME_WAIT 120초

  • 끝난 뒤에도 바로 지워지지 않는다 → TIME_WAIT 로 120초 남는다
  • 그래서 curl 을 두 번 하면 메모장에 출발 포트만 다른 두 줄이 보인다
  • 02-1 의 “FD 4 가 두 번” 을 연결 쪽에서 본 것이다

주의: 두 ESTABLISHED 의 기준 차이

  • Conntrack 의 ESTABLISHED 와 TCP 의 ESTABLISHED 는 기준이 다르다
    • TCP: handshake 가 끝나야 ESTABLISHED
    • Conntrack: 양방향으로 패킷이 한 번씩만 오가면 그렇게 적는다
  • 그래서 연결이 없는 UDP 도 메모장에 적힌다
    • DNS 질의 한 장이 나가면 [NEW] [UNREPLIED]
    • 응답 한 장이 오면 UNREPLIED 가 빠진다
    • 그걸로 방화벽의 ESTABLISHED 규칙이 성립한다
    • 타임아웃은 30초로 짧다

3.4 메모장이 차면 — count, max, conntrack -S

메모장의 크기

  • 메모장은 크기가 있다
cat /proc/sys/net/netfilter/nf_conntrack_count /proc/sys/net/netfilter/nf_conntrack_max
53
131072

꽉 찼을 때: table full

  • 지금 53줄, 최대 131072줄
  • 꽉 차면 새 연결을 적을 자리가 없다 → 패킷을 버린다
  • 로그에 nf_conntrack: table full, dropping packet 이 뜨는 장애가 이것이다
  • conntrack -S 의 insert_failed 와 drop 이 그 흔적이다
  • 이 VM 에도 이미 수백 건 있다
    • 22번 포트로 들어오는 스캐너들이 SYN 만 던지고 가는 것들이다

실무에서 겪는 곳과 교훈

  • 연결을 많이 만드는 서비스(DNS, 프록시)가 도는 쿠버네티스 노드에서 실제로 겪는다
  • SYN 만 잔뜩 보내 메모장을 채우는 공격도 같은 원리다
  • 실무 교훈: max 를 무작정 키우기 전에 먼저 볼 것
    • count/max 비율
    • 어떤 상태의 줄이 쌓이는지(conntrack -L | awk '{print $4}' | sort | uniq -c)

전체 명령과 출력 — 06-conntrack


4. NAT — 규칙은 첫 패킷에만

4.1 DNAT — 없는 주소로 보내면

NAT 의 두 방향: SNAT 와 DNAT

  • 1장 2.3 의 공유기는 나가는 패킷의 출발지를 바꿨다(SNAT)
  • 반대로 들어오는 패킷의 목적지를 바꾸는 게 DNAT 이다
  • 쿠버네티스 Service 가 이것이다
    • 클라이언트가 Service IP 로 보낸 패킷의 목적지를 Pod IP 로 바꾼다

실습 구성: 쿠버네티스 없이 만든 Pod 와 Service IP

  • VM 에서 그대로 만들어 봤다 — 쿠버네티스는 없다
  • 02-1 4장의 방식으로 방 둘을 만들어 br0 에 꽂고 각각 서버를 띄웠다
    • pod-a(10.200.0.2), pod-b(10.200.0.3) 라고 불렀다
  • 커널 입장에서 Pod 는 정확히 이것 — 방 하나, veth 하나, 그 안의 서버
    • CNI 가 하는 일을 손으로 한 셈이다
  • 그리고 10.96.0.5 — 어느 문에도 없는 주소를 Service IP 로 쓴다
ip route get 10.96.0.5
curl -s -m 2 -o /dev/null -w 'http %{http_code}\n' http://10.96.0.5/
10.96.0.5 via 10.0.0.1 dev ens3 src 10.0.0.198

http 000

규칙 전: 아무도 받지 않는 주소

  • 라우팅 표에 없는 주소라 default 로 ens3 를 통해 나간다
  • 아무도 안 받아서 타임아웃
  • 1장 01-2 7.2 의 “Service IP 는 어떤 기기에도 없는 주소” 가 딱 이 상태다
  • 규칙 한 줄을 넣는다
sudo iptables -t nat -A OUTPUT -d 10.96.0.5 -p tcp --dport 80 -j DNAT --to 10.200.0.2:8080

규칙 후: curl 의 믿음과 선 위의 주소

  • 같은 curl 이 이번엔 pod-a 를 받았다
  • -v 로 보면 Connected to 10.96.0.5 port 80 이라고 믿고 있다
  • 그 사이에 br0 를 tcpdump 로 찍으면 전혀 다른 주소가 오간다

주소가 바뀐 자리: OUTPUT

  • 선 위에는 10.96.0.5 가 한 번도 안 나온다
  • curl 이 쓴 주소는 커널의 OUTPUT 자리에서 바뀌었다
  • 서버는 10.0.0.198 이 자기한테 직접 보낸 줄로 안다
  • 내가 만든 패킷이라 PREROUTING 이 아니라 OUTPUT 이다
    • 밖에서 들어온 패킷이었다면 PREROUTING 에서 같은 일이 일어난다
  • 둘 다 라우팅 결정 앞이다
    • 목적지를 바꿔야 “내 것인지, 어느 문으로 갈지” 를 바뀐 주소로 판단할 수 있으니까
호스트 커널방 net1 — 02-1 의 그 방pod-a10.200.0.2:8080Pod 를 흉내 낸 서버curl유저 공간에서 부탁DNAT@OUTPUT · 첫 패킷에만13dst 10.96.0.5:80어디에도 없는 주소dst 10.200.0.2:8080고쳐 쓴 뒤메모장 (Conntrack)→ 원래 dst 10.96.0.5:80← 응답 src 10.200.0.2:80802적는다src 10.200.0.2:8080src 10.96.0.5:80curl 은 10.96.0.5 가 답한 줄 안다서버는 10.0.0.198 이 보낸 줄 안다규칙은 연결당 한 번. 나머지 패킷과 답장은 메모장이 처리한다 — 반대 방향 규칙은 없다
쿠버네티스 없이 만든 Service — 없는 주소(10.96.0.5) + 규칙 한 줄 + 메모장. kube-proxy 가 노드마다 하는 일이 이것이다

4.2 답장은 어떻게 돌아오나 — 메모장의 둘째 줄

남은 의문: 10.96.0.5 에서 온 답장

  • 서버는 10.0.0.198 에게 답장을 보낸다 — 10.96.0.5 는 모른다
  • 그런데 curl 은 10.96.0.5 에서 온 답장을 받았다
  • 누가 되돌렸는가
sudo conntrack -L -d 10.96.0.5
tcp 6 86399 ESTABLISHED src=10.0.0.198 dst=10.96.0.5 sport=45026 dport=80   src=10.200.0.2 dst=10.0.0.198 sport=8080 dport=45026 [ASSURED]

DNAT 뒤의 두 방향 튜플

  • 3.2 의 두 방향 튜플이 여기서 달라진다
    • 첫째 줄: curl 이 보낸 그대로(dst=10.96.0.5:80)
    • 둘째 줄: 바뀐 주소(src=10.200.0.2:8080)
  • DNAT 규칙이 첫 패킷의 목적지를 바꾸면서 “답장은 이 모양으로 올 것” 을 둘째 줄에 적어 둔 것이다

답장 되돌리기: 규칙이 아닌 메모의 역적용

  • 답장이 PREROUTING 에 들어오면 Conntrack 이 먼저 메모장과 대조한다
  • 둘째 줄과 맞으니 “아, 첫째 줄 연결의 답장” 하고, 출발지를 10.96.0.5:80 으로 거꾸로 되돌린다
  • 이때 DNAT 규칙을 다시 보지 않는다
  • 모양은 반대 방향 NAT 지만 규칙이 아니라 메모의 역적용이다

NAT 규칙은 첫 패킷에만

  • 그래서 NAT 규칙은 연결의 첫 패킷에만 돈다
  • 나머지 패킷은 전부 메모장이 한다
    • 가는 건 똑같이 바꾼다
    • 오는 건 거꾸로 되돌린다
  • DNAT 규칙 하나만 걸어도 답장이 알아서 돌아온다 → 반대 방향 규칙을 따로 쓰지 않는 이유다
  • 1장의 공유기가 SNAT 규칙 하나로 답장을 노트북에 돌려주는 것도 같은 원리다

4.3 로드밸런싱 — kube-proxy 의 iptables 모드 [실습]

Pod 둘로 나눠 보내기

  • Service 뒤에 Pod 가 둘이면 나눠 보내야 한다
  • kube-proxy 의 iptables 모드가 쓰는 규칙을 그대로 넣었다
sudo iptables -t nat -A OUTPUT -d 10.96.0.5 -p tcp --dport 80 \
  -m statistic --mode random --probability 0.5 -j DNAT --to 10.200.0.2:8080
sudo iptables -t nat -A OUTPUT -d 10.96.0.5 -p tcp --dport 80 \
  -j DNAT --to 10.200.0.3:8080
for i in $(seq 1 10); do curl -s http://10.96.0.5/; done
pod-b  pod-a  pod-a  pod-a  pod-a  pod-b  pod-a  pod-b  pod-b  pod-b

확률 규칙의 분배

  • 첫 규칙이 50% 확률로 pod-a 를 고른다
  • 안 걸린 나머지는 둘째 규칙으로 pod-b
  • 열 번 중 다섯 번씩 갔다
  • Pod 가 셋이면 첫 규칙 1/3, 둘째 규칙 1/2, 셋째 규칙 나머지로 늘어난다

연결 단위로 고정해야 하는 이유

  • 여기서 패킷마다 다른 Pod 로 보내면 안 된다
  • SYN 은 pod-a 로, 다음 데이터는 pod-b 로 가면 → pod-b 는 “이게 무슨 연결이야” 하고 RST 를 날린다
  • 4.2 의 메모장이 그걸 막는다 — 증거가 둘 있다

증거: 규칙 카운터와 메모장의 응답 방향

  • iptables -t nat -L OUTPUT -v 로 두 DNAT 규칙의 카운터를 보면 5 + 5 = 10
    • 연결 수와 같다
    • 같은 패킷을 2.2 의 방식으로 raw 자리에서 세면 70 — 열 연결이 보낸 패킷 전부다
    • 규칙은 첫 패킷 10개만 봤고 나머지 60개는 메모장이 처리했다
  • 메모장의 응답 방향을 세면 src=10.200.0.2 5줄, src=10.200.0.3 5줄
    • 연결마다 백엔드가 고정되어 있다
호스트 커널 — nat OUTPUTcurl ×10 → 10.96.0.5statistic 50%→ pod-a (5)나머지→ pod-b (5)1메모장 10줄5 → .2 · 5 → .32pod-a 10.200.0.2pod-b 10.200.0.33① 규칙 둘 — kube-proxy 의 방식-m statistic --probability 0.5② 연결마다 한 번만 고른다nat 카운터 5+5 = 10 = 연결 수패킷은 70개였다raw 카운터 — 나머지 60개는 메모장③ 한 연결은 끝까지 같은 PodSYN 이 A 로 갔으면 데이터도 A 로패킷마다 Pod 를 다시 고르면 TCP 가 깨진다 — 첫 패킷에서 고르고 메모장이 고정한다
Service 의 정체 — 없는 주소로 보내면 DNAT 가 고르고, 메모장이 그 선택을 연결 끝까지 유지한다

정리: Service 의 정체와 다른 구현들

  • 이게 Service 의 정체다
    • 없는 주소로 보낸 첫 패킷에서 DNAT 가 Pod 를 고른다
    • 메모장이 그 선택을 연결 끝까지 유지한다
  • 책은 iptables 로 설명하지만 지금은 선택지가 더 있다
    • ipvs 모드: 커널의 로드밸런서 모듈이 해시 테이블로 같은 일을 한다
    • nftables 모드: 새 기본값으로 가는 중이다
    • Cilium 같은 CNI: kube-proxy 를 빼고 6장의 eBPF 로 한다
  • 원리는 같고 규칙을 넣는 도구만 다르다

4.4 MASQUERADE — 방 안에서 인터넷으로 [실습]

반대 방향: 방 안에서 인터넷으로

  • 방 안의 pod-a(10.200.0.2)가 인터넷으로 나간다
  • 먼저 이 VM 의 FORWARD 가 REJECT 라 br0 → ens3 를 열어 주었다
  • 그리고 NAT 없이 보내 봤다

NAT 없이: 나갔지만 답장이 없다

  • 나가긴 했다
    • 메모장에 SYN_SENT … [UNREPLIED] 줄이 생겼다
    • FORWARD 를 지났다
  • 그런데 답장이 한 번도 안 왔다
  • 출발지가 10.200.0.2 인 채로 ens3 를 떠났다
    • 1.1.1.1 입장에서 10.200.0.2 는 돌아갈 길이 없는 사설 주소다
  • 1장의 “공유기가 출발지를 바꿔 쓰는 이유” 를 실패로 먼저 봤다
sudo iptables -t nat -A POSTROUTING -s 10.200.0.0/24 -o ens3 -j MASQUERADE
sudo ip netns exec net1 curl -s -m 5 -o /dev/null -w 'http %{http_code}\n' http://1.1.1.1/
sudo conntrack -L -s 10.200.0.2 -d 1.1.1.1
http 301

tcp 6 119 TIME_WAIT src=10.200.0.2 dst=1.1.1.1 sport=55510 dport=80 src=1.1.1.1 dst=10.0.0.198 sport=80 dport=55510 [ASSURED]

MASQUERADE 후: 답장은 ens3 의 주소로

  • 한 줄 뒤 답이 왔다
    • 301 은 HTTPS 로 가라는 응답이고, 왔다는 게 중요하다
  • 메모장의 둘째 줄을 보면 dst=10.0.0.198 — 답장은 ens3 의 주소로 온다
  • 1.1.1.1 은 10.200.0.2 를 본 적이 없다
  • 되돌리는 건 4.2 와 같다
    • 둘째 줄과 맞춰 목적지를 10.200.0.2 로 되돌려 br0 로 넘긴다
호스트 커널 — ip_forward = 1방 net110.200.0.2FORWARD1MASQUERADE@POSTROUTING2메모장→ src 10.200.0.2← dst 10.0.0.1983ens310.0.0.1981.1.1.1답장 dst=10.0.0.198 → 메모장 → 10.200.0.2① 남의 패킷을 넘긴다ip_forward=1 · FORWARD 통과② 나가기 직전에 출발지를10.200.0.2 → ens3 의 10.0.0.198③ 메모장에 두 방향을 적는다답장은 10.0.0.198 로 온다MASQUERADE 없이는[UNREPLIED] — 답장이 못 돌아온다공유기가 하는 일 그대로 — 도커의 172.17.0.0/16 MASQUERADE 도 같은 줄이다
SNAT 는 나가기 직전(POSTROUTING)에, 답장은 메모장 둘째 줄의 주소로 돌아온다

자리의 논리: DNAT 는 라우팅 앞, SNAT 는 뒤

  • MASQUERADE 가 POSTROUTING 에 있는 이유도 논리로 나온다
  • 출발지는 어느 문으로 나갈지 정해진 뒤에 그 문의 주소로 바꾸면 된다 → 마지막 자리다
  • DNAT 가 라우팅 앞(PREROUTING/OUTPUT)이고 SNAT 가 라우팅 뒤(POSTROUTING)인 것
    • 외울 게 아니라 순서상 그럴 수밖에 없다
sudo iptables -t nat -S POSTROUTING
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A POSTROUTING -s 10.200.0.0/24 -o ens3 -j MASQUERADE

도커가 먼저 건 같은 줄

  • 도커가 이미 같은 줄을 걸어 두고 있었다
  • 02-1 5장의 컨테이너가 인터넷에 나갈 수 있는 이유다
  • 도커도 공유기다
  • 쿠버네티스 노드도 Pod 가 클러스터 밖으로 나갈 때 같은 일을 한다

전체 명령과 출력 — 07-nat


5. Routing — 내 것인가 남 것인가

5.1 ip route get — 표를 읽지 말고 커널에게 묻는다

1장에 더하는 두 가지

  • 1장 01-2 3.1 에서 라우팅 표를 “가장 길게 맞는 줄” 로 읽는 법을 봤다
  • 2장이 더하는 것
    • 그 결정이 검문소 사이에 끼어 있다는 위치
    • 표를 머릿속으로 맞춰 볼 필요 없이 커널에게 직접 묻는 명령
ip route get 1.1.1.1
ip route get 10.0.0.198
ip route get 127.0.0.1
ip route get 172.17.0.9
1.1.1.1 via 10.0.0.1 dev ens3 src 10.0.0.198
local 10.0.0.198 dev lo src 10.0.0.198
local 127.0.0.1 dev lo src 127.0.0.1
172.17.0.9 dev docker0 src 172.17.0.1

ip route get 결과 읽기

  • “이 주소면 어느 문으로, 누구에게, 어느 출발 주소로” 가 한 줄로 나온다
  • 1.1.1.1 은 default 로 ens3 를 통해 10.0.0.1 에게
  • 172.17.0.9 는 도커 브리지로 직접
  • 그리고 둘째 줄 — 내 ens3 주소 10.0.0.198 로 보내는 건 local … dev lo
    • 내 주소로 보내면 선을 안 타고 lo 로 돈다는 뜻이다
    • 02-1 3.2 의 “127.x 만 lo 가 아니다” 의 근거다
  • 이 local 줄들은 ip route show table local 에 따로 있다

5.2 ip_forward — 노드는 라우터다

FORWARD 길의 전제

  • 2.3 의 FORWARD 길은 “남의 패킷을 넘길 때” 였다
  • 그런데 일반 컴퓨터는 남의 패킷을 넘기지 않는다
sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 1

ip_forward 값: 노트북 0, 이 VM 1

  • 노트북은 이 값이 0 이다
    • 목적지가 내가 아닌 패킷이 들어오면 FORWARD 까지 가지도 않고 버린다
    • 종점이지 경유지가 아니니까
  • 이 VM 이 1 인 건 도커가 켰기 때문이다
    • 컨테이너의 패킷을 ens3 로 넘겨 주려면 라우터 노릇을 해야 한다
  • 4.4 에서 방 안의 curl 이 “나가긴 했다” 가 된 것도 이 값 덕분이다

쿠버네티스 노드의 값

  • 쿠버네티스 노드도 1 이다
  • kubelet 이 요구하고, 안 켜져 있으면 설치가 실패한다
  • 노드가 Pod 들의 라우터이기 때문이다
노드 1 · 10.0.0.4Pod A10.244.1.3라우팅dst ≠ 나 → FORWARDens3노드 2 · 10.0.0.5Pod B10.244.2.7라우팅dst ≠ 나 → FORWARDens310.244.2.0/24 via 10.0.0.5123노드에게 Pod 의 패킷은 "남의 패킷" 이다 — ip_forward=1 로 넘기고, 표의 한 줄이 어느 노드인지 정한다노트북은 ip_forward=0 이라 남의 패킷을 버린다. 노드는 라우터라서 켜 둔다
Pod A → veth → 노드 1 (내 것 아님 → FORWARD) → ens3 → 노드 2 (FORWARD) → veth → Pod B

다른 노드의 Pod 로 가는 길

  • Pod A(10.244.1.3)가 다른 노드의 Pod B(10.244.2.7)에게 보내면, 노드 1 입장에서 목적지는 내가 아니다 → 남의 패킷
  • 라우팅 표의 10.244.2.0/24 via 10.0.0.5 줄이 “저 대역은 노드 2 에게” 라고 말한다
  • FORWARD 를 지나 ens3 로 나간다
  • 노드 2 가 받아서 또 FORWARD, veth 로 Pod B 에게
  • 1장 01-2 5.3 의 “라우터 한 칸 — 벗기고, 보고, 새로 붙인다” 를 노드가 매 패킷마다 한다
  • 그 via 10.0.0.5 줄을 누가 넣느냐가 CNI 장의 이야기다

5.3 밖에서 들어온 패킷 — 0.0.0.0 과 127.0.0.1 [실습]

실험 구성: 노트북에서 공인 IP 로

  • 지금까지는 lo 와 방 안의 패킷이었다
  • 진짜 문으로 들어오는 패킷을 보려고 노트북에서 VM 의 공인 IP 로 curl 을 쐈다
  • VM 에서는 ens3 를 tcpdump 하고 PREROUTING 과 INPUT 에 카운터를 꽂았다
  • 서버는 두 번 띄웠다
    • 처음엔 0.0.0.0:8080
    • 다음엔 127.0.0.1:8080
서버 0.0.0.0:8080   — 노트북: 200 OK        VM 카운터: PRE 13 / INPUT 13   서버 로그: GET / 두 줄
서버 127.0.0.1:8080 — 노트북: Connection refused   VM 카운터: PRE 15 / INPUT 15   서버 로그: 없음

둘째 경우의 tcpdump

  • ens3 의 tcpdump 로 둘째 경우를 보면 이렇다
IP <노트북>.51657 > 10.0.0.198.8080: Flags [S], seq 3073052855, ...
IP 10.0.0.198.8080 > <노트북>.51657: Flags [R.], seq 0, ack 3073052856, win 0, length 0

막힌 곳: 소켓 찾기

  • SYN 이 들어왔고, 커널이 바로 RST 를 돌려보냈다
  • 카운터가 늘었다 → 패킷은 PREROUTING 과 INPUT 을 다 지났다
  • 막힌 건 그다음, 소켓 찾기다
    • 커널이 “10.0.0.198 의 8080 을 듣는 소켓” 을 찾는다
    • 127.0.0.1 에만 바인딩된 소켓은 그 조건에 안 맞는다
    • 없으니 RST
  • 서버는 아무것도 몰랐다
서버가 0.0.0.0:8080노트북ens3PRE → INPUT소켓 찾기0.0.0.0:8080200 OK · 서버 로그에 남는다서버가 127.0.0.1:8080노트북ens3PRE → INPUT소켓 찾기127.0.0.1:8080 뿐RST [R.] · 서버는 모른다두 경우 모두 PREROUTING·INPUT 카운터는 늘었다 — 패킷은 커널까지 왔다. 막힌 건 마지막 소켓 찾기127.0.0.1 바인딩은 방화벽이 아니라 "ens3 주소로 듣는 소켓이 없음" 이다
08 실습 — 노트북에서 공인 IP 로 보낸 같은 SYN 이, 바인딩 주소 하나로 200 과 Connection refused 로 갈린다

lo 바인딩: 설정이지 인증이 아니다

  • 02-1 3.2 에서 미뤄 둔 “lo 바인딩은 설정이지 인증이 아니다” 가 이 뜻이다
  • 127.0.0.1 바인딩은 패킷을 막는 게 아니다
    • 패킷은 커널까지 들어오고 검문소도 다 지난다
    • 마지막 소켓 찾기에서 실패하게 만드는 것뿐이다
  • 그 찾기를 속일 수 있으면 뚫린다

5.4 Martian 과 route_localnet — CVE-2020-8558

Martian 패킷

  • 있을 수 없는 주소를 달고 온 패킷이다
  • 대표: 출발지나 목적지가 127.x 인데 ens3 로 들어온 것
  • 127.x 는 lo 에서만 나와야 한다 → 밖에서 그런 게 오면 조작이다
  • 리눅스는 기본으로 버린다
sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.ens3.route_localnet
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.ens3.route_localnet = 0

두 sysctl 의 뜻

  • rp_filter: 출발지 검증
    • “이 출발지로 답장을 보낸다면 이 문으로 나가는 게 맞나” 를 라우팅 표로 확인한다
    • 아니면 버린다
  • route_localnet = 0: 127.x 를 lo 밖에서 라우팅하지 않는다는 뜻

CVE-2020-8558: 원인과 결론

  • 2020년의 CVE-2020-8558 은 kube-proxy 가 이 route_localnet 을 1 로 바꿔서 생겼다
    • NodePort 를 localhost 로도 접근 가능하게 하려는 편의였다
  • 그 결과 ens3 로 들어온 목적지 127.0.0.1 패킷도 lo 로 라우팅됐다
    • 5.3 의 소켓 찾기에서 “127.0.0.1 의 8080 을 듣는 소켓” 이 이번엔 조건에 맞는다
    • 노드의 localhost 전용 서비스에 같은 네트워크의 다른 호스트가 닿을 수 있었다
  • 5.3 과 이어서 보면 결론이 하나다 → 127.0.0.1 에 바인딩한 서비스에도 인증을 걸어야 한다

전체 명령과 출력 — 05-netfilter-routing · 08-external


6. eBPF — 검문소에 내 코드를

6.1 올릴 때 한 번, 돌 때는 커널

기존 개입 방법과 한계

  • 지금까지 패킷 처리에 끼어드는 방법은 둘이었다
  • iptables 규칙: “조건이면 행동” 의 정해진 틀만 된다 → 복잡한 판단은 못 한다
  • NFQUEUE: 패킷을 유저 공간 프로그램에 넘길 수도 있다
    • 패킷마다 커널 밖으로 나갔다 들어오니 느리다
  • 커널 모듈: 직접 짜면 빠르다
    • 버그 하나로 서버가 통째로 죽는다

eBPF: 검증을 거쳐 커널 안에서 도는 프로그램

  • eBPF 는 그 중간이다
  • 작은 프로그램을 짜서 커널에 올린다
  • 커널의 검증기가 “무한 루프 없나, 엉뚱한 메모리 안 건드리나” 를 먼저 확인한다
  • 통과한 것만 커널 안에서 돌린다
  • 커널 모듈만큼 빠르면서 커널을 죽이지 않는다
커널유저 공간로더Go · Rust · bpftraceprog검증기무한 루프·메모리 검사1NICXDP드라이버 바로 위TC큐Netfilter훅 · 6.4+소켓2bpf() — 올릴 때 한 번tracepoint패킷 길 밖 — 관찰용 (bpftrace)① 올릴 때 — 유저 공간이 한 번bpf() 로 넘기고 검증기가 거른다② 돌 때 — 커널만패킷마다 커널 안에서. 로더는 꺼져도 됨붙이는 자리가 일을 정한다패킷 길 위는 결정, tracepoint 는 관찰Netfilter 와 경쟁이 아니다iptables 규칙 자리에 프로그램을 건다NFQUEUE 는 패킷마다 유저 공간을 오가지만, eBPF 는 코드를 커널에 올려 두고 선을 넘지 않는다
검증된 작은 프로그램을 커널 안에 올려 돌린다 — 붙이는 자리에 따라 관찰도 패킷 결정도 한다

흐름 두 단계

  1. 올릴 때
    • 유저 공간의 로더(Go, Rust, 또는 bpftrace)가 bpf() 시스템 호출로 프로그램을 커널에 넘긴다
    • 검증기를 통과하면 원하는 자리에 붙인다
    • 02-1 2.1 의 “커널에 부탁” 이 여기서는 한 번뿐이다
  2. 돌 때
    • 그 자리에 패킷이 올 때마다 커널이 프로그램을 실행한다
    • 로더는 더 이상 관여하지 않고, 꺼져 있어도 된다

NFQUEUE 와 헷갈리지 않기

  • “유저 공간 프로그램이 패킷을 처리한다” 고 생각하면 NFQUEUE 와 헷갈린다
  • 그게 아니라 유저 공간에서 짜서 커널에 넘겨준 코드가 커널 안에서 돈다
  • 선을 넘는 건 올릴 때 한 번이다

6.2 붙이는 자리 — XDP, TC, Netfilter, tracepoint

붙이는 자리에 따라 갈리는 일

  • 같은 eBPF 프로그램이라도 어디에 붙이느냐로 하는 일이 갈린다
  • 패킷이 들어오는 길에 자리가 셋 있다

패킷 길의 세 자리: XDP, TC, Netfilter 훅

  • XDP: 랜카드 드라이버 바로 위
    • 패킷이 커널 스택에 들어오기도 전이다 → 여기서 버리면 제일 싸게 버린다
    • DDoS 방어에 쓴다
  • TC: 인터페이스의 큐
    • 스택에는 들어왔지만 Netfilter 전
  • Netfilter 훅: 2장의 다섯 자리
    • Linux 6.4 부터 BPF_PROG_TYPE_NETFILTER 로 여기에도 붙일 수 있다
    • 이 VM 의 커널 6.8 이 그 범위다
    • iptables 규칙 대신 프로그램이 ACCEPT/DROP 을 돌려준다

Netfilter 와 eBPF 의 관계

  • 그래서 Netfilter 와 eBPF 는 경쟁 관계가 아니다
    • Netfilter: 자리
    • iptables 규칙: 그 자리에 거는 정해진 틀의 판단
    • eBPF 프로그램: 그 자리에 거는 내가 짠 판단
  • 셋 다 커널 안이다

패킷 길 밖의 자리: tracepoint 와 kprobe

  • “이 커널 함수가 불렸다” 를 알려 주는 자리다 → 여기 붙이면 관찰 도구가 된다
  • bpftrace -e 'tracepoint:syscalls:sys_enter_execve { ... }' 가 이것이고 패킷과 무관하다
  • 같은 eBPF 인데 붙이는 자리가 관찰이냐 결정이냐를 가른다

Cilium 이 kube-proxy 를 없앨 수 있는 이유

  • 4.3 의 iptables 규칙은 목록이다
    • Service 가 1,000개면 패킷마다 수천 줄을 위에서부터 훑는다
    • Pod 하나가 바뀌어도 전체를 다시 쓴다
  • eBPF 프로그램 하나를 TC 자리에 걸고 Service 정보는 커널 안의 해시 맵에 넣으면
    • 패킷마다 조회 한 번
    • 갱신은 한 칸
  • 3.5 의 Conntrack 이 5-tuple 을 해시로 찾는 것과 같은 발상이다
  • CNI 장에서 다시 본다

7. Discussion

7.1 하나의 요청으로 — curl 한 번의 열다섯 단계

curl 한 번의 전체 경로

  • 02-1 과 02-2 를 합치면 curl http://127.0.0.1:8080/ 한 번이 이렇게 된다
  • 왼쪽 번호 뒤가 어느 절인지다

프로세스의 몫과 커널의 몫

  • 프로세스가 한 일은 1, 2, 12~15 뿐이다
  • 나머지 아홉은 전부 커널이고, 그중 5, 6, 8, 9 가 이 편이다
  • 쿠버네티스 Pod 로 옮기면 이 열다섯에 몇 단계가 끼어든다
    • 앞뒤로 방(02-1 4장)과 veth(02-1 4.3)
    • 목적지가 Service 면 OUTPUT 이나 PREROUTING 의 DNAT(4.1)
    • 다른 노드면 FORWARD(5.2)
    • CNI 가 eBPF 기반이면 그 자리의 프로그램(6.2)
  • 소켓은 새로 만들지 않는다
    • 같은 socket() 과 accept4() 이고, 커널 부서들 사이에 몇 단계가 끼는 것뿐이다

핵심 메시지

Netfilter
검문 자리 다섯. 방향에 따라 지나는 조합이 넷이고, lo 는 전부 지난다
Conntrack
5-tuple 로 연결을 기억하는 메모장. 한 줄에 두 방향, UDP 도 적힌다
NAT
규칙은 첫 패킷에만, 나머지와 답장은 메모장. Service 는 DNAT + 메모장
Routing
내 것인가 남 것인가. 노드는 ip_forward=1 인 라우터다
eBPF
검증된 코드를 커널에 올려 돈다. 올릴 때 한 번, 붙이는 자리가 일을 정한다

문과 방을 다 제대로 이어도 그 사이를 지나는 패킷은 커널의 검문을 받고, 검문소는 기억이 없어서 메모장이 있고, 주소를 바꾸는 건 메모장 위에서만 성립한다. 02-1 의 ping 을 막았던 REJECT 한 줄에서 시작해 Service 의 DNAT 까지 손으로 넣어 보면, kube-proxy 가 하는 일이 규칙 몇 줄과 메모장 하나라는 게 보인다. 다음은 이 부품들을 CNI 가 어떻게 끼워 맞추는지 — Pod 에 방을 만들고 파이프를 꽂고, via 10.0.0.5 줄을 누가 넣는지다.


8. References

브리지 트래픽이 iptables 를 지나는 이유는 커널 문서의 br_netfilter 설명에 있다.
"bridge-nf-call-iptables — pass bridged IPv4 traffic to iptables' chains."
[해석] "브리지를 지나는 IPv4 트래픽을 iptables 체인에 넘긴다." — 2.4 에서 ARP 는 통과하고 ICMP 만 FORWARD 에서 거절된 이유다. 쿠버네티스가 이 값을 요구하는 것은 kubeadm 설치 문서의 전제 조건에 있다.
→ kernel.org — Netfilter sysctl (nf_conntrack, bridge-nf-call)
→ kubernetes.io — Container Runtimes (ip_forward, br_netfilter 전제 조건)
Conntrack 의 상태가 TCP 의 상태와 다르다는 것은 nftables 위키의 연결 추적 설명에 적혀 있다.
"The connection tracking system stores information about the state of a connection in a memory structure that contains the source and destination IP addresses, port number pairs, protocol types, state, and timeout."
[해석] "연결 추적 시스템은 출발·목적지 IP, 포트 쌍, 프로토콜, 상태, 타임아웃을 담은 메모리 구조에 연결 상태를 저장한다." — 3.2 의 5-tuple 과 두 방향 튜플, 남은 초의 근거다. ESTABLISHED 가 "양방향으로 패킷을 봤다" 는 뜻이라 UDP 에도 성립한다는 것(3.4)도 같은 문서에 있다.
→ nftables wiki — Connection Tracking System
→ nftables wiki — Matching connection tracking stateful metainformation
kube-proxy 의 iptables 모드가 4.3 의 statistic 규칙으로 백엔드를 고른다는 것과, 다른 모드가 있다는 것은 공식 문서의 Virtual IPs 설명에 있다.
"In this mode, kube-proxy configures packet forwarding rules using the iptables API of the kernel netfilter subsystem."
[해석] "이 모드에서 kube-proxy 는 커널 netfilter 하위 시스템의 iptables API 로 패킷 전달 규칙을 설정한다." — 4.3 의 "kube-proxy 는 규칙을 써 넣는 유저 공간 도구" 의 근거다. ipvs, nftables 모드와 CVE-2020-8558 에서 route_localnet 이 어떻게 쓰였는지도 같은 페이지와 이슈에 있다.
→ kubernetes.io — Virtual IPs and Service Proxies
→ GitHub — kubernetes/kubernetes#90259 CVE-2020-8558: Node setting allows for neighboring hosts to bypass localhost boundary
eBPF 프로그램이 Netfilter 훅에 직접 붙는 프로그램 유형은 커널 6.4 에 들어왔다.
"BPF_PROG_TYPE_NETFILTER — programs of this type are attached to netfilter hooks and can accept or drop packets."
[해석] "이 유형의 프로그램은 netfilter 훅에 붙어 패킷을 통과시키거나 버릴 수 있다." — 6.2 의 "iptables 규칙 자리에 프로그램을 건다" 의 근거다. 검증기와 로드 과정은 bpf(2), 붙이는 자리별 프로그램 유형은 libbpf 문서에 있다.
→ docs.ebpf.io — BPF_PROG_TYPE_NETFILTER
→ man7.org — bpf(2)
→ man7.org — ip-route(8) (ip route get)