0. Contents

1. Overview

커널Network Namespace — 문·주소·라우팅이 따로인 방3인터페이스 — lo · ens3 · veth · bridge2소켓 — socket() = 3 · accept4() = 41curl · python3유저 공간syscall — 부탁패킷의 길Netfilter · Conntrack · Routing02-2 에서① 소켓 — 손잡이프로그램은 여기에 대고 부탁한다 · 2장② 인터페이스 — 문패킷이 드나드는 곳 · 3장③ Network Namespace — 방네트워크가 통째로 따로 · 4장 (컨테이너는 5장)프로그램은 손잡이에 대고 부탁하고, 커널은 문과 방을 관리한다 — 셋은 겹겹이 놓여 있다
컴퓨터 한 대 안에서 커널이 가진 세 가지 — 손잡이, 문, 방. 이 편은 그 셋을 우분투에서 하나씩 만져 본다

1장과 2장의 범위

  • 1장은 선 위의 이야기였다
    • curl 한 번이 어떤 모양의 패킷이 되는지 tcpdump 로 찍어서 뜯어봤다
  • 2장은 그 패킷이 만들어지기 전과 도착한 뒤, 리눅스 커널 안에서 무슨 일이 일어나는지를 본다

책의 입장: Pod 는 리눅스 프로세스

  • Pod 는 마법이 아니라 리눅스 프로세스다
  • Pod 의 네트워크는 커널 기능을 조합한 것이다
  • 그래서 커널의 네트워킹을 모르면 Pod 네트워크 장애를 못 읽는다
  • 01-2 마지막에 “Pod, Service, CNI 는 새로운 네트워크가 아니라 조합이다” 라고 썼다 → 2장은 그 조합의 부품 목록이다

2장의 두 편

  • 이 글(02-1): 커널이 가진 정적인 것들을 우분투 VM 에서 하나씩 만들어 본다
    • 소켓 — 프로그램이 커널에 부탁하는 창구
    • 인터페이스 — 패킷이 드나드는 문
    • Network Namespace — 문과 주소와 라우팅이 통째로 격리된 방
  • 02-2: 그 문들을 지나는 패킷을 커널이 어떻게 검문하고(Netfilter), 기억하고(Conntrack), 바꾸는지(NAT, 라우팅)

실습 환경

  • 오라클 클라우드의 Ubuntu 22.04 VM 에서 했다
  • 1장의 macOS 에는 strace, ip netns, nsenter 가 없어서 리눅스가 필요하다
  • 명령과 출력은 전부 실제 것이다
  • 호스트 주소와 MAC 은 가렸다

2. Sockets — 프로그램은 커널에 부탁만 한다

2.1 syscall 과 FD — 손잡이 하나로

curl 이 할 수 있는 일: 부탁뿐

  • curl 은 네트워크 카드를 만질 수 없다
  • 일반 프로그램은 유저 공간에서 돌고, 하드웨어는 커널의 권한이다
  • 그래서 curl 이 할 수 있는 건 커널에게 부탁하는 것뿐이다
    • “이 주소로 연결해 줘”
    • “이 바이트 보내 줘”

부탁의 창구: 시스템 호출(syscall)

  • socket(), connect(), write(), read() 같은 함수 이름으로 보인다
  • 부르면 CPU 가 커널 모드로 넘어간다 → 커널 코드가 일을 한 뒤 결과를 들고 돌아온다

손잡이: 소켓과 FD

  • 연결 하나에 부탁은 여러 번 오간다 — 연결해라, 보내라, 받아라, 닫아라
  • 매번 “127.0.0.1 의 8080 에” 라고 다시 말하지 않는다
    • 처음에 socket() 으로 손잡이를 하나 받는다
    • 이후 부탁은 전부 그 손잡이에 대고 한다
  • 그 손잡이가 소켓이다
  • 프로그램 쪽에서는 파일 디스크립터(FD)라는 정수 하나로 보인다

책의 문장: “연결이 파일로 표현된다”

  • 네트워크 연결이 디스크 파일과 같다는 뜻은 아니다
  • 리눅스가 파일, 파이프, 소켓을 전부 같은 모양의 손잡이(FD)로 통일했다는 뜻이다 → read()/write() 를 똑같이 쓸 수 있다
  • 2.4 에서 /proc 을 열어 보면 그 손잡이가 socket:[번호] 로 보인다

2.2 네 단계 — socket, bind, listen, accept4

도구: strace

  • 프로세스와 커널 사이의 창구에 서서 한 줄씩 적어 주는 도구다
    • 무슨 부탁을 했는지
    • 커널이 뭐라고 답했는지
  • 필터를 걸어 서버가 뜰 때의 부탁 세 가지만 본다
strace -e trace=socket,bind,listen python3 -m http.server 8080 --bind 127.0.0.1
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 3
bind(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
listen(3, 5)                            = 0

서버의 준비 단계: 세 줄이 전부

  • socket() — “TCP(SOCK_STREAM)용 IPv4(AF_INET) 손잡이 하나 줘”
    • 커널이 3번을 준다
    • 아직 주소도 포트도 없는 빈 손잡이다
  • bind(3, 127.0.0.1:8080) — “3번은 이제 이 주소, 이 포트 담당”
    • = 0 은 성공
    • 다른 프로세스가 이미 8080 을 잡고 있으면 여기서 실패한다 → address already in use 가 나는 자리다
    • htons(8080) 은 포트를 네트워크 바이트 순서로 적는다는 뜻인데 몰라도 된다 — 그냥 8080 이다
  • listen(3, 5) — “3번으로 오는 연결을 받아서 줄 세워 둬. 줄은 최대 5명”
    • 이때부터 커널은 SYN 이 오면 서버 프로그램 몰래 1장의 3-way handshake 를 끝낸다
    • 완성된 연결은 대기열에 넣어 둔다
    • 5 가 그 대기열(backlog) 길이다 — Go 는 128 이었다

사이에 끼인 AF_UNIX 소켓 4번

  • 두 번 만들어졌다 닫힌다
  • 같은 컴퓨터 안의 프로세스끼리 쓰는 유닉스 도메인 소켓이다
  • Python 이 시작하면서 내부 용도로 만든 것이라 무시한다
  • strace 는 내가 시키지 않은 부탁도 전부 보여 준다는 것만 알면 된다
python3 -m http.server 8080 — 프로세스 하나socket() = 3bind(3, :8080)listen(3, 5)1FD 3LISTEN문지기2FD 4:40962FD 4:40968FD 4:40980FD 4:409963시간 → 손님이 올 때마다 accept4(3) = 4① 준비 — 세 부탁이 3번을 문지기로socket → bind → listen② FD 3 은 계속 서 있다데이터는 다루지 않는다③ 손님마다 FD 4 — 쓰고 닫는다recvfrom(4) · sendto(4) · close(4)④ 닫힌 4 는 다음 손님에게점선 = 이미 닫힌 손님. 구분은 포트네 번 다 4 지만 네 개의 다른 연결 — 번호는 돌려쓰고, 연결의 구분은 손님의 포트 번호
문지기 하나(3)가 계속 서 있고, 손님이 올 때마다 전용 번호(4)를 하나 받아 그 손님하고만 이야기한다

3번 소켓: 연결을 받는 전용

  • 준비가 끝나면 3번 소켓은 연결을 받는 일만 한다
  • 데이터는 절대 이걸로 주고받지 않는다
  • 요청이 올 때 무슨 일이 일어나는지는 떠 있는 서버에 strace 를 붙여서 봤다
sudo strace -f -p <서버 PID> -e trace=accept4,recvfrom,sendto,close
요청 하나를 처리하는 동안의 부탁 네 번 — strace -p 로 붙어서 본 것부탁syscall 이름3 / 4손잡이(FD)내용상대·데이터= 답커널이 돌려준 값1손님 받기accept4(3,손님 127.0.0.1:40962)= 4문지기 3번에게서 손님 전용 손잡이 4번을 받는다. 상대 포트가 연결의 이름표2읽기recvfrom(4,"GET / HTTP/1.1…")= 784번으로 78바이트를 읽는다 — 1장에서 tcpdump 로 본 그 GET3쓰기sendto(4,"HTTP/1.0 200 OK…")= 1564번으로 응답 헤더 156바이트. 본문 367바이트는 한 번 더4닫기close(4)= 04번 반납. 번호 4는 빈 자리가 되어 다음 손님에게 간다3번은 네 줄 중 첫 줄에만 나온다 — 받기만 하고 데이터는 4번이 전부
줄마다 같은 네 조각이다 — 무슨 부탁인지, 어느 손잡이에, 무엇을, 커널이 뭐라고 답했는지

accept4(3): 손님 전용 손잡이

  • 서버는 여기서 멈춰 기다린다
  • 대기열에 연결이 들어오면 커널이 깨우고, 그 연결 전용 손잡이로 4번을 준다
  • 상대는 127.0.0.1:40962, curl 이 받은 임시 포트다(1장 4.1)
  • 3번은 여전히 문지기로 서 있고, 4번이 이 손님 전용이다

읽고, 쓰고, 반납하기

  • recvfrom(4) 로 요청 78바이트를 읽는다
  • sendto(4) 로 응답 헤더 156바이트와 본문 367바이트를 쓴다
  • 1장에서 tcpdump 로 본 GET / HTTP/1.1 과 200 OK 가 서버 쪽에서는 이렇게 보인다
  • 그리고 close(4) — 4번 손잡이를 반납한다

프로그램의 몫과 커널의 몫

  • 여기까지 프로그램이 한 일은 손잡이에 대고 부탁한 것뿐이다
  • SYN 을 만들고, 세그먼트로 자르고, 헤더를 붙이고, 재전송하는 건 전부 커널이다
  • 1장에서 tcpdump 로 본 패킷 모양은 전부 커널이 만든 것이다

2.3 FD 4 가 네 번 — 번호는 돌려쓴다 [실습]

curl 네 번의 기록

  • curl 을 네 번 보냈다
  • strace 의 accept4 줄만 모으면 이렇다
accept4(3, {... sin_port=htons(40962) ...}) = 4
accept4(3, {... sin_port=htons(40968) ...}) = 4
accept4(3, {... sin_port=htons(40980) ...}) = 4
accept4(3, {... sin_port=htons(40996) ...}) = 4

FD 번호: 닫히면 돌려쓴다

  • 네 번 다 4 다. 그렇다고 같은 연결이 아니다
  • 첫 손님을 close(4) 로 닫는 순간 4번은 빈 자리가 된다
  • 다음 accept4 는 비어 있는 가장 작은 번호를 준다 → 다시 4 가 된다
  • FD 번호는 프로세스 안에서만 의미가 있고, 닫히면 돌려쓴다

연결을 구분하는 열쇠: 손님의 포트

  • 연결을 구분하는 건 FD 가 아니라 손님의 포트다
  • 40962, 40968, 40980, 40996 — 전부 다르다
  • 1장 4.1 의 임시 번호표가 여기서 연결을 구분하는 열쇠가 된다
  • 02-2 의 Conntrack 이 연결을 기억하는 키도 이 포트다

Go 예제의 손님 FD 5

  • Go 예제에서 손님 FD 가 5 였던 것도 같은 이유다
  • Go 런타임이 epoll 용으로 4번을 먼저 쓰고 있었다
  • 번호 자체에 의미는 없다

2.4 같은 소켓을 세 방향에서 — strace, ss, /proc [실습]

지금 상태 보기: ss

  • strace 가 “만드는 과정” 이었다면, 이쪽은 지금 상태를 본다
  • 쓴 옵션
    • 리스닝 소켓만(-l)
    • 이름 변환 없이(-n)
    • TCP 만(-t)
    • 프로세스와 함께(-p)
ss -lntp | grep 8080
LISTEN 0      5          127.0.0.1:8080       0.0.0.0:*    users:(("python3",pid=2595202,fd=3))

LISTEN 줄 읽기

  • LISTEN 은 문지기 상태
  • 127.0.0.1:8080 은 bind 한 주소
  • 0.0.0.0:* 는 아직 상대가 없다는 뜻
  • fd=3 은 strace 에서 본 그 3번
  • 0 5 는 Recv-Q 와 Send-Q 인데, LISTEN 소켓에서는 뜻이 특수하다
    • Send-Q 5 가 listen(3, 5) 의 backlog 다
    • Recv-Q 0 은 지금 대기열에서 accept 를 기다리는 연결이 0개라는 뜻이다

inode 까지 보기

  • -e 를 더하면 inode 가 나온다
ss -lntpe | grep 8080
LISTEN 0 5 127.0.0.1:8080 0.0.0.0:* users:(("python3",pid=2595202,fd=3)) uid:1001 ino:56686806 ...

inode: 커널 쪽 소켓 식별자

  • 이 번호가 커널이 소켓 객체에 붙인 식별자다
  • 프로세스 쪽 손잡이(FD)와 커널 쪽 객체(inode)를 잇는 고리를 /proc 에서 확인할 수 있다
  • /proc/PID/fd/ 는 그 프로세스가 쥐고 있는 FD 를 심볼릭 링크로 보여 주는 가상 디렉터리다
ls -l /proc/2595202/fd

FD 목록: 세 도구가 본 한 소켓

  • 0, 1, 2 는 표준 입출력(파일로 보냈다)
  • 3 은 socket:[56686806] — ss 의 ino: 와 같은 번호다
  • 아래 셋이 한 소켓이다
    • strace 의 socket() = 3
    • ss 의 fd=3 ino:56686806
    • /proc 의 3 -> socket:[56686806]

세 번호의 역할: PID·FD·inode

  • PID 는 프로세스를 가리킨다(시스템 전체에서 유일)
  • FD 는 그 프로세스 안의 손잡이를 가리킨다(프로세스 안에서만 의미)
  • inode 는 커널 안의 소켓 객체를 가리킨다(프로세스와 무관하게 유일)
  • inode 가 따로 필요한 이유: 소켓 하나를 여러 프로세스가 공유할 수 있다
    • 서버가 fork 하면 자식도 같은 3번을 물려받는다
    • 둘의 3번은 같은 inode 를 가리킨다
커널소켓ino 56686806127.0.0.1:8080프로세스 python3 · pid 2595202FD 3socket() = 3123① strace — 경계에서 부탁을 본다프로세스 ↔ 커널 사이, 부탁과 답만② ss — 커널의 소켓 목록을 묻는다:8080 … fd=3 ino:56686806③ /proc/PID/fd — 손잡이 표를 읽는다3 -> socket:[56686806]같은 실물인지는 inode 로fork 한 자식도 같은 소켓을 쥘 수 있다소켓은 커널에 하나, FD 3 은 프로세스의 이름표, inode 56686806 이 둘을 잇는 번호
strace 는 경계에서 부탁을, ss 는 커널의 소켓을, /proc 은 프로세스의 손잡이 표를 본다 — 셋이 같은 inode 를 가리킨다

손님 쪽 소켓

  • 연결을 하나 열어 둔 채로 보면 손님 쪽 소켓도 보인다
  • -l 을 빼면 접속 중인 연결이 나온다
ss -ntp | grep 8080
ESTAB 0 0 127.0.0.1:45286 127.0.0.1:8080  users:(("bash",pid=2595194,fd=3))
ESTAB 0 0 127.0.0.1:8080  127.0.0.1:45286 users:(("python3",pid=2595202,fd=4))

연결 하나, 두 줄

  • 손님 쪽 끝(bash 의 3번)과 서버 쪽 끝(python3 의 4번)
  • 같은 컴퓨터라 양쪽 다 보인다
  • 이때 서버의 /proc/PID/fd 에는 4 -> socket:[56685485] 가 하나 더 생겨 있다
  • 02-2 에서 Conntrack 이 같은 연결을 한 줄에 두 방향으로 보여 주는 것과 비교하게 된다

2.5 epoll — 손님이 많을 때

문제: 한 번에 한 부탁에서만 멈춘다

  • 2.2 에서 서버가 accept4(3) 에서 멈춰 기다린다고 했다
  • read(4) 도 마찬가지다 — 4번에 온 데이터가 없으면 거기서 멈춘다
  • 손님이 한 명이면 상관없는데, 손님이 둘이면 문제가 된다
    • read(4) 에서 멈춰 있는 동안 5번 손님이 데이터를 보내도 받아 줄 수가 없다
  • 프로세스는 한 번에 한 부탁에서만 멈출 수 있다

옛 해법: 손님마다 스레드

  • 각자 자기 FD 에서 멈춰 있으면 된다
  • 단순하지만 손님 1만 명이면 스레드 1만 개다
  • Python 의 http.server 가 이 방식이다
    • strace 에 -f 를 주면 요청마다 다른 스레드 번호가 보인다

epoll: 멈추는 자리는 하나

  • 옛 해법을 뒤집은 것이다
  • FD 들을 커널의 장부에 등록해 두고, 프로세스는 epoll_wait() 한 곳에서만 멈춘다
  • 커널은 등록된 FD 중 어느 것이든 준비되면 그 목록을 들고 프로세스를 깨운다
  • 프로세스는 목록에 있는 FD 만 처리하고 다시 잠든다
  • 멈추는 자리는 하나인데 지켜보는 FD 는 수만 개가 된다
같은 상황 — 손님 셋(4·5·6)이 앉아 있고 5번이 데이터를 보냈다. 손님마다 소켓 하나 = FD 하나직원(스레드)은 한 번에 한 자리에서만 기다릴 수 있다 — 그래서 방법이 둘로 갈린다손님마다 직원 하나 — 스레드 (Python http.server)메인accept4(3)스레드 Aread(4)스레드 Bread(5)스레드 Cread(6)각자 자기 자리에서 기다린다FD 3문지기FD 4손님FD 5손님FD 6손님데이터 도착✓ 스레드 B 가 받는다단순하다. 손님 1만 명이면 스레드 1만 개직원 하나, 벨 패널 — epoll (Go)직원 하나epoll_wait()epoll 장부 (벨 패널)올린 것: 3 4 5 6불 들어온 것: 5어느 것이든 오면 깨운다여기 한 곳에서만 기다린다"5번" — 깨운다FD 3문지기FD 4손님FD 5손님FD 6손님데이터 도착✓ read(5) 하고 복귀직원은 하나, 벨은 수만 개둘 다 5번을 받는다. 다른 건 기다리는 자리가 손님 수만큼이냐(스레드) 하나냐(장부)이고, 그래서 손님이 많을수록 오른쪽이 가볍다
epoll 은 연결을 받는 방법이 아니라 기다리는 방법이다 — listen·accept4·read 는 그대로이고, 직원이 서 있는 곳만 벨 패널로 바뀐다

Go 서버의 동작

  • 문지기 3번과 모든 손님 FD 를 장부에 올려 두고 epoll_wait() 에서 잔다
  • 새 손님이면 “3번 준비됨” 하고 깨어나 accept4(3) 을 부른다
  • 기존 손님 데이터면 “5번 준비됨” 하고 깨어나 read(5) 를 부른다
  • Go 가 적은 스레드로 많은 연결을 버티는 방식이다
  • 고루틴은 이 위에 얹힌 것이다

Python 실습과의 차이: epoll_wait 없음

  • Python 실습의 strace 에 안 보인 이유가 이것이다
  • 손님이 한 번에 한 명이라고 가정하고 accept4 에서 그냥 기다린다
  • 틀린 게 아니라 다른 방식이다

전체 명령과 출력 — 01-sockets


3. Interfaces — 컴퓨터에 달린 문

3.1 문마다 주소 — IP 는 컴퓨터가 아니라 문에 붙는다

인터페이스: 패킷이 지나는 문

  • 커널이 만든 패킷은 문을 지나 밖으로 나간다
  • 그 문이 인터페이스다
  • 문마다 이름이 있고, 문마다 IP 가 붙는다
ip -br addr

VM 한 대의 문 넷

  • lo — 자기 자신에게 보내는 문
  • ens3 — 진짜 이더넷 카드
  • docker0 과 br-8abe — 도커가 만든 가상 스위치
  • 그래서 “이 컴퓨터의 IP” 라는 말은 부정확하다 → “이 문의 IP” 가 맞다
  • 1장 3.1 에서 0.0.0.0 이 “모든 문” 이었던 이유다
VM 한 대 — ip -br addr 의 네 줄lo127.0.0.1/81ens310.0.0.198/24밖2docker0172.17.0.1/16 · DOWN3br-8abe…172.18.0.1/16 · DOWN4① lo — 거울 문127.x 전부. 넣으면 나한테 온다② ens3 — 진짜 문선 너머에 상대가 있다③ ④ docker0 · br-… — 가짜 스위치도커가 만든 브리지. 꽂힌 게 없어 DOWN문 넷, 주소 넷IP 는 컴퓨터가 아니라 문에 붙는다"이 컴퓨터의 IP" 는 없다 — "이 문의 IP" 가 넷 있을 뿐
ip -br addr 한 줄에 문 하나 — 진짜 문은 ens3 하나이고 나머지 셋은 커널이 만든 가짜 문이다

문의 두 종류: 진짜 문과 가짜 문

  • 진짜 문: 선이나 전파가 실제로 붙어 있는 랜카드
  • 가짜 문: 커널이 소프트웨어로 만든 것 — lo, veth, bridge, 터널, VPN
  • 커널은 둘을 똑같이 다룬다
    • 패킷을 문으로 밀어 넣으면 진짜 문은 선으로 내보낸다
    • 가짜 문은 커널 안 어딘가로 보낸다

쿠버네티스 노드의 문 수십 개

  • 쿠버네티스 노드에 문이 수십 개 보이는 이유가 이것이다
  • 물리 카드는 한두 개이고 나머지는 전부 가짜 문이다
  • Pod 가 생길 때마다 하나씩 늘어난다
  • 4장에서 그 문을 직접 만든다

책의 ifconfig 출력에서 읽을 것 셋

  • UP, RUNNING — 켜져 있고(UP) 실제로 연결돼 있다(RUNNING)
    • UP 만 있고 RUNNING 이 없으면 선이 안 꽂힌 상태다
  • inet / inet6 — 이 문의 IPv4 / IPv6 주소
  • ether — 이 문의 MAC 주소, 1장 5.1 의 “이번 한 칸” 주소
    • 가짜 문에도 MAC 이 있다 → 이것 때문에 Pod 끼리도 ARP 가 된다

3.2 lo — 받은 것과 보낸 것이 같다 [실습]

자기한테 되돌아오는 패킷

  • lo 로 내보낸 패킷은 밖으로 안 나가고 바로 자기한테 되돌아온다
  • 커널이 송신 경로에서 “목적지가 나 자신이네” 하고 수신 경로로 돌려 넣는다
  • 2장 실습의 curl 요청이 Python 서버에 닿은 길이 이것이다 — 선을 한 번도 안 탔다
  • 01-1 에서 macOS 의 lo0 으로 캡처했던 그 문이다

숫자로 확인

ip -s link show lo

RX·TX 비교

  • RX 와 TX 가 15,073,184 패킷, 5,623,076,770 바이트로 정확히 같다 → 내가 보낸 걸 내가 받으니까
  • ens3 은 RX 와 TX 가 다르다 → 진짜 상대가 있다

주소 범위와 MTU

  • 127 로 시작하는 주소는 전부 이 문이다
  • 127.0.0.1/8 이니 127.0.0.2 도 자기 자신이다
  • IPv6 에서는 ::1 이 같은 역할이다
  • mtu 65536 은 선이 없으니 크게 잡아도 된다는 뜻이다

주의: 127.0.0.1 바인딩과 보안

  • 127.0.0.1 에 바인딩했다고 보안 경계가 생기는 건 아니다
  • 사례: CVE-2020-8558
    • 2020년에 kube-proxy 가 넣은 규칙 하나 때문에 노드의 localhost 전용 서비스에 밖에서 닿을 수 있었다
  • 02-2 에서 패킷이 커널 안에서 어떤 길을 가는지 본 다음에 설명한다
  • 지금은 “lo 바인딩은 설정이지 인증이 아니다” 까지만

3.3 파이프의 반대쪽 끝 — ens3, veth, lo

문을 나누는 기준: 파이프의 반대쪽 끝

  • 랜카드의 구멍은 파이프의 끝이다
  • 그러면 반대쪽 끝은 어디인가 — 문의 종류를 이 질문 하나로 나눌 수 있다
내 컴퓨터 (호스트 커널)ens310.0.0.198스위치 포트케이블1방 net1veth110.0.0.1방 net2veth210.0.0.2파이프 — 선이 없다2lo127.0.0.13① ens3 — 진짜 문반대쪽 끝은 케이블 너머의 다른 장비② veth — 양쪽 끝이 다 내 안커널이 짝을 지어 준다. 끝을 다른 방에 넣을 수 있다③ lo — 양 끝이 같은 문넣으면 자기한테 나온다 · RX == TX문은 전부 파이프의 끝이다. 다른 건 반대쪽 끝이 어디 있느냐
진짜 문(ens3)은 선 너머에 상대가 있고, 가짜 문(veth·lo)은 반대쪽 끝까지 내 커널 안에 있다

ens3: 반대쪽 끝은 선 너머

  • 스위치나 공유기의 포트다
  • 내 커널은 이 문으로 프레임을 밀어 넣는 것까지만 한다
  • 그 뒤는 전기 신호가 되어 선을 탄다
  • 반대편에서 뭐가 받는지 내 커널은 모른다

veth: 양쪽 끝이 다 내 커널 안

  • 선이 없다 → 커널이 한쪽에 넣은 프레임을 메모리 복사로 반대쪽에 옮긴다
  • 진짜 문은 선이 반대편을 정해 준다
  • 가짜 문은 선이 없다 → 커널이 “이 문의 반대편은 저 문” 이라고 명시적으로 짝을 지어 줘야 한다
  • 그래서 veth 는 항상 둘이 한 세트로 만들어지고, 하나만 만들 수는 없다
  • 핵심: 두 끝을 서로 다른 방에 넣을 수 있다 — 4.3 에서 한다

lo: 양 끝이 같은 문

  • 넣으면 자기한테 나온다
  • RX 와 TX 가 같았던 이유다

전체 명령과 출력 — 02-interfaces


4. Namespaces — 격리된 방

4.1 빈 방 — lo 하나, 라우팅 없음

네임스페이스: 프로세스의 시야 자르기

  • 프로세스가 보는 세상을 좁히는 커널 기능이다
  • 커널은 하나인데 프로세스마다 “너는 이것만 보여” 하고 시야를 잘라 준다
  • 자르는 대상마다 종류가 있다
    • pid — 프로세스 번호(방 안에서는 자기가 1번인 줄 안다)
    • mnt — 파일시스템(자기 루트가 진짜 루트인 줄 안다)
    • uts — hostname
    • net — 인터페이스, IP, 라우팅, 포트, 방화벽 규칙(이 장의 주인공)
  • 컨테이너는 이 네임스페이스 몇 개를 겹쳐 씌운 프로세스다

빈 방 만들기

  • 방을 하나 만들어 본다
  • 새로 만든 방 안에서만 일어나는 일이라 원격 서버에서 해도 SSH 가 끊기지 않는다
sudo ip netns add net1
sudo ip netns exec net1 ip -br addr
sudo ip netns exec net1 ip route
sudo ip netns exec net1 ss -lnt

새 방의 상태

  • ip netns exec net1 <명령> 은 “그 방 안에서 이 명령을 실행해라” 다
  • 결과
    • 문 하나(lo, 그것도 꺼져 있다)
    • 라우팅 0줄
    • 듣는 소켓 0개
  • 호스트의 ens3 도, docker0 도, 기본 경로도 없다
  • 네트워크가 통째로 새로 생겼다
호스트문 (ip -br addr)lo 127.0.0.1/8ens3 10.0.0.198/24docker0 172.17.0.1/16라우팅 (ip route)default via 10.0.0.1 dev ens310.0.0.0/24 dev ens3듣는 소켓 (ss -lntp)LISTEN 127.0.0.1:8080 python3 pid=2594502문 넷, 경로 아홉 줄, 소켓 여덟방 net1 — ip netns add 직후문lo DOWN라우팅(없음)듣는 소켓LISTEN 127.0.0.1:8080 python3 pid=2594507문 하나(꺼짐), 경로 없음 — 그런데 8080 은 또 열린다같은 커널, 같은 포트 번호 8080 — 방이 다르면 충돌하지 않는다
방을 만들면 네트워크가 통째로 새로 생긴다. Pod 50개가 전부 8080 을 열어도 되는 이유

4.2 포트 공간 — 호스트의 8080 과 방 안의 8080

격리 확인: 같은 포트 두 번 열기

  • 방이 격리됐다는 걸 가장 실감나게 보여 주는 건 포트다
  • 호스트에서 8080 을 쓰고 있는 채로 방 안에서도 8080 을 연다
python3 -m http.server 8080 --bind 127.0.0.1 &
sudo ip netns exec net1 python3 -m http.server 8080 --bind 127.0.0.1 &
ss -lntp | grep 8080
sudo ip netns exec net1 ss -lntp
LISTEN 0 5 127.0.0.1:8080 0.0.0.0:* users:(("python3",pid=2594502,fd=3))

LISTEN 0 5 127.0.0.1:8080 0.0.0.0:* users:(("python3",pid=2594507,fd=3))

결과: 둘 다 성공

  • 2.2 에서 bind() 가 address already in use 로 실패한다고 했다
  • 그 “이미 쓰는 중” 판단이 방 단위로 이루어진다
  • 그래서 Pod 50개가 전부 8080 에 바인딩해도 충돌이 없다
  • 노드의 ss -lntp 에는 Pod 의 8080 이 안 보인다 → 방이 다르니까

이름이 같은 다른 것: Kubernetes Namespace

  • 이름이 같아서 생기는 혼동이다 — Kubernetes Namespace 는 이것과 무관하다
  • 그건 API 서버의 폴더다
    • default, dev, prod 처럼 리소스를 묶어 관리하는 이름표다
    • 네트워크를 격리하지 않는다
  • dev 네임스페이스의 Pod 가 prod 네임스페이스의 Pod IP 로 그냥 통신할 수 있다
  • 막으려면 NetworkPolicy 를 따로 써야 한다

4.3 veth — 방 둘을 파이프로

방에 문 달기: veth

  • 방이 격리되어 있으니 밖과 통하려면 문을 달아 줘야 한다
  • 3.3 의 veth 다
  • 한 줄로 두 방에 끝을 하나씩 넣는다
방 둘을 파이프로 잇는 명령 — 만들기, 그리고 주소 붙여 켜기어느 방에서ip netns exec명령무엇을 하라대상어느 문에옵션어떻게확인보기만A. 파이프 만들기1방 만들기ip netns addnet2두 번째 방. net1 은 4.1 에서 만들었다2파이프 만들기ip link addveth1netns net1type veth peer nameveth2netns net2한 줄로 양 끝을 각 방에 — veth1 은 net1 에, veth2 는 net2 에. 호스트에는 아무것도 안 남는다3확인ip netns exec net1ip -d link showveth1link-netns net2 — 반대쪽 끝이 어느 방에 있는지 커널이 안다B. 주소 붙이고 켜기4주소 붙이기ip netns exec net1ip addr add10.0.0.1/24dev veth1방 안에 들어가서 veth1 에 주소를. /24 = 10.0.0.x 는 같은 동네5주소 붙이기ip netns exec net2ip addr add10.0.0.2/24dev veth2net2 쪽 끝에도. 같은 /24 라 둘이 직접 통한다6켜기ip netns exec net1ip link setveth1up만든 문은 꺼져 있다7켜기ip netns exec net2ip link setveth2up반대쪽도 켜야 NO-CARRIER 가 풀린다8확인ip netns exec net1ip route주소를 붙이자 생긴 줄 — 10.0.0.0/24 dev veth19통신ip netns exec net1ping -c 210.0.0.20.03ms. 선이 없으니까호스트에서는 veth1 도 veth2 도 안 보인다 — 양 끝이 다 방 안이라, 호스트의 ping 10.0.0.2 는 100% 손실
보라색이 붙은 줄은 방 안에서 치는 명령이다 — 방마다 문·주소·라우팅이 따로라서 들어가서 쳐야 한다
2: veth1@if2: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN ...
    link/ether ... link-netns net2
    veth

link-netns net2: 반대쪽 끝의 위치

  • 이 문의 반대쪽 끝이 net2 방에 있다고 커널이 알고 있다
  • 이름 뒤의 @if2 는 짝의 인터페이스 번호다
  • 주소를 붙이고 켠다

주소를 붙인 뒤

  • 라우팅 한 줄이 저절로 생겼다 — 1장 3.1 의 “직접 연결된 네트워크” 줄이다
  • ping 은 0.03ms 에 돌아온다 → 선이 없으니까

호스트에서는 안 보이는 방

  • 호스트는 이 방들을 모른다
    • 호스트에서 ip -br addr 을 쳐도 veth1 은 안 보인다
    • 호스트에서 ping 10.0.0.2 는 100% 손실이다
  • 파이프의 양 끝이 둘 다 방 안에 들어가 있다 → 호스트에는 아무것도 남지 않았다

파이프 한쪽 내리기

  • 파이프의 한쪽을 내리면 반대쪽도 영향을 받는다
  • net2 에서 veth2 를 down 하면 net1 의 veth1 에 NO-CARRIER 가 뜬다
  • 선이 빠진 것과 같은 상태다

4.4 bridge — 파이프 끝 셋을 스위치에

파이프 끝이 많아질 때

  • veth 로 방과 방을 이었다
  • 그런데 Pod 가 50개면 파이프 끝이 50개다
  • 방 A 가 방 B 에게 보내려면 A 의 파이프 끝에서 나온 프레임을 B 의 파이프 끝으로 넣어 줘야 한다
    • 누가 그걸 하는가

브리지: 가상 스위치

  • 그 일을 브리지가 한다
  • 1장 5장의 스위치처럼 포트 여러 개를 가진다
  • 프레임의 목적지 MAC 을 보고 어느 포트로 내보낼지 정한다
  • 노드에 cni0, docker0 같은 이름으로 보이는 게 이것이다
  • 이번에는 파이프의 한 끝만 방에 넣고 다른 끝은 호스트에 남겨 브리지에 꽂는다
방 셋을 스위치에 꽂는 명령 — 한 방(net1) 분량명령무엇을 하라대상어느 문에옵션어떻게확인보기만1스위치 만들기ip link addbr0type bridge가상 스위치 하나. 아직 꽂힌 포트는 없다2켜기ip link setbr0up문은 만들면 꺼져 있다 — 켜야 프레임이 지난다3파이프 만들기ip link addveth1type veth peer nameveth1-br양 끝이 한 쌍 — veth1 과 veth1-br. 둘 다 아직 호스트에4한 끝을 방에ip link setveth1netns net1veth1 이 방 net1 로 들어간다. 호스트에선 사라지고 방 안에서 보인다5다른 끝을 스위치에ip link setveth1-brmaster br0호스트에 남은 끝을 br0 의 포트로 꽂는다 — 책의 brctl addif6켜기ip link setveth1-brup꽂은 끝도 켜야 한다 (방 안의 veth1 은 방 안에서 따로 켠다)7확인bridge link showbr0 에 꽂힌 포트 목록 — master br0 state forwardingnet2, net3 도 같은 다섯 줄 — 파이프 한 끝은 방에, 다른 끝은 스위치에
줄마다 같은 세 조각이다 — 무슨 명령인지, 어느 문에, 어떻게. 02-1 의 방·파이프·스위치가 명령 한 줄씩에 대응한다
21: veth1-br@if22: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding
23: veth2-br@if24: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding
25: veth3-br@if26: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br0 state forwarding

브리지 포트 상태

  • master br0 가 “이 인터페이스를 br0 의 포트로 꽂아라” 다
  • state forwarding 은 그 포트가 프레임을 전달 중이라는 뜻
  • 책의 brctl addif 가 이것이고, 요즘은 ip 와 bridge 명령으로 한다
노드 (호스트 커널)br0 — 가상 스위치 (노드의 cni0)3방 net1veth110.0.0.1veth1-br방 net2veth210.0.0.2veth2-br방 net3veth310.0.0.3veth3-br12ens34① 방 셋 — 각자 lo 와 veth 한 끝② 파이프의 바깥 끝 → 브리지 포트ip link set vethN-br master br0③ 브리지는 MAC 표로 포트를 고른다bridge fdb show — ping 한 번에 배운다④ ens3 는 브리지에 안 꽂았다노드 밖은 라우팅의 몫 (02-2)net1 → veth1 → 파이프 → veth1-br → br0 (MAC 표 조회) → veth3-br → 파이프 → net3
파이프 끝이 쌓이면 스위치가 필요하다. 브리지는 목적지 MAC 을 보고 어느 파이프로 보낼지 정한다

브리지의 판단 근거: MAC 표

  • 브리지가 어느 포트로 보낼지 아는 근거는 MAC 표다
  • 스위치의 MAC 테이블과 같은 것이다
  • 프레임이 지나갈 때 출발지 MAC 을 보고 배운다
bridge fdb show br br0 | grep -v permanent
f6:bd:...:32 dev veth1-br master br0
d6:80:...:14 dev veth2-br master br0
ea:1f:...:f4 dev veth3-br master br0

MAC 표의 세 줄

  • 세 방의 veth MAC 이 각각 어느 포트 뒤에 있는지 적혀 있다
  • 3.1 에서 “가짜 문에도 MAC 이 있다” 고 한 게 여기서 쓰인다
  • 브리지는 MAC 으로 판단한다

주의: 물리 인터페이스는 브리지에 꽂지 않는다

  • 책의 예제 2-3 에는 ip link set eth0 master br0 가 있다 → 원격 서버에서 이 줄을 치면 안 된다
  • 물리 인터페이스를 브리지 포트로 바꾸는 순간 거기 붙어 있던 IP 가 의미를 잃고 SSH 가 끊긴다
  • 실습에서는 veth 끝만 꽂고 ens3 은 건드리지 않았다
  • 그림에서 ens3 이 브리지 밖에 따로 있는 이유다

4.5 ARP 는 되는데 ping 은 안 된다 — 02-2 의 예고 [실습]

실험: 브리지 너머로 ping

  • 브리지에 셋을 꽂고 net1 에서 net3 으로 ping 을 보냈다
sudo ip netns exec net1 ping -c 2 -W 1 10.0.0.3
sudo ip netns exec net1 ip neigh
2 packets transmitted, 0 received, 100% packet loss

10.0.0.3 dev veth1 lladdr ea:1f:...:f4 REACHABLE

결과: ARP 는 성공, ICMP 는 실패

  • ping 은 전부 실패했는데 ip neigh 는 REACHABLE 이다
  • 1장 5.2 의 ARP 는 성공해서 상대 MAC 을 알아냈다
  • 브리지의 MAC 표에도 세 줄이 적혔다
  • 그런데 ICMP 만 안 간다 → 브리지는 제대로 동작한 것이다

원인: 브리지 바깥

sysctl net.bridge.bridge-nf-call-iptables
sudo iptables -S FORWARD

브리지 트래픽도 iptables 로

  • bridge-nf-call-iptables = 1 은 “브리지를 지나는 프레임도 IP 면 iptables 를 거쳐라” 는 설정이다
  • 이 VM 의 FORWARD 체인은 끝이 REJECT 다
  • 브리지는 2계층 장치라 방화벽과 무관할 것 같다
    • 그러나 이 설정이 켜져 있으면 브리지 안에서 포트 사이를 오가는 IP 패킷까지 커널의 검문소를 지난다
  • ARP 는 IP 가 아니라서 안 걸렸다(그래서 MAC 은 배웠다)
  • ICMP 는 IP 라서 거절됐다
sudo iptables -I FORWARD -i br0 -o br0 -j ACCEPT
sudo ip netns exec net1 ping -c 2 10.0.0.3
64 bytes from 10.0.0.3: icmp_seq=1 ttl=64 time=0.040 ms
64 bytes from 10.0.0.3: icmp_seq=2 ttl=64 time=0.050 ms

해결: br0 안의 트래픽만 통과

  • br0 안에서 br0 로 가는 것만 통과시키는 규칙 한 줄을 넣자 통했다
  • 02-2 의 첫 장에서 다룰 것
    • 이 검문소가 무엇인지
    • 왜 브리지 트래픽까지 잡는지
    • FORWARD 가 다섯 검문소 중 어디인지
  • 여기서는 “문과 방과 파이프를 다 제대로 이어도, 그 사이를 지나는 패킷은 커널의 검문을 받는다” 까지만 가져간다

전체 명령과 출력 — 03-veth-netns


5. Containers — 도커도 방이다

5.1 ip netns list 에는 없다 — nsenter [실습]

도커가 대신 만든 방

  • 4장에서 손으로 만든 “방 + 파이프 + 브리지” 를 도커가 대신 만들어 준 것이 컨테이너 네트워크다
  • 확인하려고 컨테이너를 하나 띄웠다
  • 포트는 호스트에 publish 하지 않았다
docker run -d --name ch02-web nginx:alpine
ip -br link
bridge link show

컨테이너를 띄운 직후의 호스트

  • 호스트에 veth9a2d62c 가 생겼다
  • 꺼져 있던 docker0 이 UP 됐다
  • 그 veth 가 docker0 에 꽂혔다
  • 4.4 와 똑같은 구조다 — 그러면 방은 어디 있는가
ip netns list
(빈 출력)

목록에 없는 방

  • 없다
  • ip netns list 는 /var/run/netns/ 에 이름이 등록된 방만 보여 준다
  • 도커와 containerd 는 거기 등록을 안 한다
  • 노드에서 이 명령을 쳤는데 비어 있다고 Pod 에 방이 없는 게 아니다
  • 방을 보려면 그 안에서 도는 프로세스의 PID 로 들어가야 한다
CPID=$(docker inspect -f '{{.State.Pid}}' ch02-web)
readlink /proc/self/ns/net
sudo readlink /proc/$CPID/ns/net
sudo nsenter -t $CPID -n ip -br addr
sudo nsenter -t $CPID -n ip route

방 번호 확인: readlink /proc/PID/ns/net

  • 그 프로세스가 어느 방에 있는지 번호로 알려 준다
  • 2.4 의 소켓 inode 와 같은 발상이다
  • 호스트와 컨테이너의 번호가 다르다 → 다른 방이다

방에 들어가기: nsenter -t PID -n <명령>

  • “그 PID 의 네트워크 방에 들어가서 이 명령을 실행해라” 다
  • 들어가 보면 lo 와 eth0 뿐이다
  • eth0@if27 의 27 은 호스트 쪽 veth9a2d62c 의 번호다 → 4.3 의 파이프와 같은 구조다
  • 라우팅은 default via 172.17.0.1 — docker0 의 주소다
  • 방 안의 프로그램은 자기가 독립된 컴퓨터인 줄 안다

5.2 포트 80 은 방 안에만 [실습]

컨테이너 nginx 의 80

  • 컨테이너의 nginx 는 80 을 듣는다
  • 어디서 보이는가
sudo nsenter -t $CPID -n ss -lntp
ss -lnt | grep ':80 '
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:80/
curl -s -o /dev/null -w '%{http_code}\n' http://172.17.0.2/
sudo nsenter -t $CPID -n curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/

결과: 방마다 따로인 80

  • 방 안의 ss 에는 80 이 있고 호스트의 ss 에는 없다 → 4.2 의 포트 공간 분리다
  • 호스트에서
    • 127.0.0.1:80 은 거절(000)
    • 172.17.0.2:80 은 200 — 브리지를 통해 방의 문으로 들어간 것이다
  • 방 안에서 127.0.0.1:80 은 200
    • 방마다 lo 가 따로 있다 → 방 안의 127.0.0.1 은 그 방 자신이다

publish 와 DNAT

  • docker run -p 8080:80 으로 publish 하면 호스트의 8080 이 방 안의 80 으로 이어진다
  • 그건 iptables 의 DNAT 규칙으로 하는 일이다
  • 02-2 의 NAT 장에서 같은 규칙을 손으로 넣어 본다

5.3 Pod — 방 하나를 여러 컨테이너가

컨테이너의 네임스페이스 확인

  • ls -l /proc/$CPID/ns 를 호스트의 /proc/self/ns 와 나란히 보면 된다
  • user 만 같은 번호다
  • net, mnt, pid, uts, ipc, cgroup, time 은 전부 다르다

도커 단독과 Pod 의 차이

  • 도커 단독: 컨테이너 하나가 방 한 세트를 가진다
    • 네트워크, 파일시스템, 프로세스 번호, hostname 전부 따로다(user 만 기본값으로 호스트와 같다)
  • 쿠버네티스 Pod: 네트워크 방은 Pod 에 하나다
    • 그 안의 컨테이너들이 나눠 쓴다
Podnetwork namespace — 하나. pause 컨테이너가 만들어 붙잡고 있다1eth010.244.1.3lo2앱 컨테이너 :8080mntpid사이드카 :9090mntpid34localhost① 네트워크 방은 Pod 에 하나같은 IP, 같은 포트 공간② eth0 / lo 도 Pod 에 하나컨테이너가 나눠 쓴다③ 컨테이너는 그 위에 얹힌다서로 localhost 로 통신④ mnt · pid 는 컨테이너마다파일시스템과 프로세스 번호는 따로"컨테이너 하나 = 방 하나" 는 종류별로 다르다 — 네트워크는 Pod 단위, 파일시스템·PID 는 컨테이너 단위
사이드카가 localhost 로 앱에 붙을 수 있는 이유 — 네트워크 방 하나를 Pod 가 통째로 나눠 쓴다

pause 컨테이너: 방을 붙잡는 역할

  • Pod 가 생길 때 kubelet 은 먼저 pause 라는 빈 컨테이너를 하나 띄운다
  • 아무것도 안 하고 잠만 자는 프로세스다
  • 역할: 네트워크 방을 만들어서 붙잡고 있는 것
  • 그 다음 앱 컨테이너와 사이드카를 띄울 때 “새 방 만들지 말고 pause 의 방에 들어가라” 고 한다
  • 노드에서 crictl ps 를 치면 Pod 하나당 컨테이너가 spec 보다 하나 더 보인다 → 그게 pause 다

같은 Pod 안: 나눠 쓰는 것과 따로인 것

  • 같은 eth0, 같은 IP, 같은 포트 공간을 쓴다
    • 서로 localhost 로 통신한다
    • 둘이 같은 포트를 열면 충돌한다
  • mnt 와 pid 는 컨테이너마다 따로다
    • 앱 컨테이너에서 사이드카의 파일이 안 보인다
    • ps 를 쳐도 사이드카 프로세스가 안 보인다
  • “컨테이너 하나 = 방 하나” 는 종류별로 다르다

설계 이유: 사이드카 패턴

  • 앱 옆에 로그 수집기나 프록시를 붙일 때 둘이 localhost 로 통하고 같은 IP 를 쓰는 게 편하다
  • 네트워크를 Pod 단위로 묶으면 그게 공짜로 된다
  • 파일시스템은 따로 두어 이미지는 독립적으로 유지한다
  • 다른 Pod 는 같은 Kubernetes Namespace 에 있어도 방이 다르다

전체 명령과 출력 — 04-docker-netns


6. Discussion

6.1 도구 지도 — 질문에서 명령으로

도구를 나누는 세 질문

  • 프로그램이 커널에 무엇을 부탁했나
  • 지금 커널의 상태는 어떤가
  • 그 상태를 어디서 만들었나
  • 하나가 다른 하나를 대신하지 못한다
프로세스 쪽 — 손잡이
커널에 뭘 부탁했나 → strace -e trace=network
누가 8080 을 듣나 → ss -lntp
소켓의 inode 는 → ss -lntpe
FD 3 이 뭘 가리키나 → ls -l /proc/PID/fd
문 — 인터페이스
문이 몇 개인가 → ip -br addr
문의 통계·MAC → ip -s link show
파이프의 짝은 → ip -d link show
브리지에 뭐가 꽂혔나 → bridge link show
브리지의 MAC 표 → bridge fdb show
방 — 네임스페이스
내가 어느 방인가 → readlink /proc/self/ns/net
이름 붙은 방 목록 → ip netns list
방 안에서 실행 → ip netns exec
컨테이너의 방에 들어가기 → nsenter -t PID -n

권한 문제와 네트워크 문제의 구분

  • root 가 아니면 다른 프로세스의 FD 나 방이 안 보인다
  • ss -p 에서 프로세스 이름이 빈칸이거나 nsenter 가 거부되면 권한 문제지 네트워크 문제가 아니다

핵심 메시지

소켓
프로그램은 손잡이(FD)에 대고 부탁만 한다. 패킷은 커널이 만든다
FD 4
문지기 3 은 그대로, 손님마다 4 를 받아 쓰고 돌려준다. 연결의 구분은 포트
문
IP 는 문에 붙는다. lo 는 거울, veth 는 양 끝이 내 안인 파이프
방
네트워크가 통째로 새로 생긴다. 8080 이 방마다 따로 있다
Pod
네트워크 방 하나를 컨테이너들이 나눠 쓴다. 그래서 localhost

커널 안에서 curl 한 번을 따라가면, 프로그램이 한 일은 손잡이에 대고 부탁한 것뿐이고 나머지는 전부 커널이다. 그 커널이 가진 문과 방을 손으로 만들어 보면, 노드에서 ip -br addr 이 수십 줄 나오는 것도 ip netns list 가 비어 있는 것도 더 이상 이상하지 않다. 다음 편은 그 문과 방 사이를 지나는 패킷이 커널 안에서 어떤 검문을 받고, 어떻게 기억되고, 어디서 주소가 바뀌는지 — 4.5 에서 ping 을 막았던 그 검문소부터 시작한다.


7. References

네트워크 네임스페이스가 무엇을 격리하는지는 man 페이지 첫 문단에 적혀 있다.
"Network namespaces provide isolation of the system resources associated with networking: network devices, IPv4 and IPv6 protocol stacks, IP routing tables, firewall rules, ... port numbers (sockets), and so on."
[해석] "네트워크 네임스페이스는 네트워킹과 관련된 시스템 자원 — 네트워크 장치, IPv4·IPv6 스택, 라우팅 테이블, 방화벽 규칙, 포트 번호(소켓) — 을 격리한다." — 4.1 의 빈 방과 4.2 의 포트 공간 분리의 근거다. 물리 장치는 한 네임스페이스에만 속할 수 있고, 네임스페이스가 사라지면 물리 장치는 초기 네임스페이스로 돌아가고 가상 장치(veth)는 함께 사라진다는 것도 여기 있다.
→ man7.org — network_namespaces(7)
→ man7.org — ip-netns(8)
veth 가 왜 반드시 쌍인지는 veth(4) 의 설명 첫 줄이 말한다.
"Packets transmitted on one device in the pair are immediately received on the other device."
[해석] "쌍의 한 장치로 보낸 패킷은 즉시 다른 장치에서 수신된다." — 3.3 의 "양 끝이 다 내 안인 파이프" 와 4.3 의 0.03ms ping 의 근거다. 한쪽이 내려가면 짝의 링크 상태도 내려간다는 것(4.3 의 NO-CARRIER)도 같은 페이지에 있다.
→ man7.org — veth(4)
→ kernel.org — Ethernet Bridging
epoll 이 무엇을 대신하는지는 epoll(7) 의 첫 문단에 있다.
"The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them."
[해석] "epoll API 는 poll(2) 과 비슷한 일을 한다 — 여러 파일 디스크립터 중 어느 것에서든 I/O 가 가능한지를 감시한다." — 2.5 의 "장부 하나에서 멈춘다" 의 근거다. `/proc/PID/fd` 와 `/proc/PID/ns` 의 의미는 proc(5) 에 있다.
→ man7.org — epoll(7)
→ man7.org — proc(5)
→ man7.org — ss(8)
Pod 안의 컨테이너가 네트워크를 공유한다는 것은 쿠버네티스 공식 문서의 Pod 설명에 명시되어 있다.
"Containers within a Pod share an IP address and port space, and can find each other via localhost."
[해석] "Pod 안의 컨테이너들은 IP 주소와 포트 공간을 공유하며 localhost 로 서로를 찾을 수 있다." — 5.3 의 "네트워크 방은 Pod 에 하나" 의 근거다. 같은 문서에 Pod 안 컨테이너가 네임스페이스와 파일시스템 볼륨을 공유한다는 설명이 이어진다.
→ kubernetes.io — Pods