개발이군고구마

[리눅스 심화 ②] 패킷은 어떻게 흐르는가: 소켓부터 컨테이너 네트워크까지 본문

카테고리 없음

[리눅스 심화 ②] 패킷은 어떻게 흐르는가: 소켓부터 컨테이너 네트워크까지

김구황 2026. 7. 12. 21:00
728x90

 

0. 들어가며: 네트워크도 결국 파일과 프로세스다

이 시리즈의 관통 주제는 "고급 개념은 리눅스 기초의 조합"임. 네트워크도 예외가 아님.

  • 소켓은 결국 파일 디스크립터(FD)임 → [기초편]의 "모든 것은 파일이다"와 연결됨.
  • 컨테이너 네트워크는 NET namespace + iptables의 조합임 → [프로세스편]과 연결됨.

과거 스테이징 환경 디버깅에서 정리했던 OSI 계층(L3/L4/L7), 포워드 프록시, SNAT 개념이 이 글에서 하나의 그림으로 합쳐짐. 아래 순서로 진행함.


 

1. 패킷의 여정: 애플리케이션에서 NIC까지

애플리케이션이 데이터를 보낼 때, 그 데이터는 곧바로 랜선으로 나가지 않음. 커널의 여러 계층을 통과하며 각 계층이 헤더(주소표)를 하나씩 덧붙이는 과정을 거침. 이를 캡슐화(encapsulation)라 함.

수신은 정확히 이 반대 순서로, 각 계층이 헤더를 하나씩 벗겨내며(역캡슐화) 최종적으로 애플리케이션에 데이터를 전달함.

1.1 OSI 계층과 실무 개념의 매핑

과거에 정리했던 OSI 계층을 실무에서 마주치는 장비·개념과 연결하면 아래와 같음. "이 문제는 몇 계층 문제인가"를 판단하는 것이 네트워크 트러블슈팅의 첫걸음임.

계층 이름 다루는 주소 대표 장비·개념
L7 애플리케이션 URL, HTTP 헤더 프록시(nginx), API Gateway, WAF
L4 전송 포트 번호 L4 로드밸런서, TCP/UDP, 방화벽
L3 네트워크 IP 주소 라우터, NAT, iptables
L2 데이터링크 MAC 주소 스위치, ARP

💡 SNAT vs 프록시의 근본 차이가 여기서 명확해짐. SNAT는 L3(IP 헤더)만 바꿔치기하는 것이라 애플리케이션은 눈치채지 못함. 반면 프록시는 L7에서 연결을 한 번 끊고 대신 맺어주는 것이라 연결 자체가 두 개로 나뉨. 같은 "중계"처럼 보여도 동작 계층이 완전히 다름. 이 차이가 스테이징 프록시 디버깅에서 헷갈렸던 핵심이었음.


 

2. 소켓과 포트: 프로세스가 네트워크에 연결되는 접점

2.1 소켓은 파일 디스크립터다

[기초편]에서 "모든 것은 파일"이라 했음. 네트워크 통신도 예외가 아님. 프로세스가 네트워크로 통신할 때, 커널은 소켓(socket)이라는 특수 파일을 열어주고, 그 소켓에 접근하는 파일 디스크립터(FD)를 프로세스에 발급함.

즉 프로세스 입장에서 네트워크 통신은 "어떤 FD에 데이터를 읽고 쓰는 것"일 뿐임. 디스크 파일에 쓰는 것과 문법상 동일함. 이것이 리눅스 네트워크의 출발점임.

 

2.2 포트: 하나의 IP 안에서 프로세스를 구분하는 번호

IP 주소는 "어느 서버인가"를 식별함(L3). 하지만 서버 하나에는 웹 서버, DB, SSH 등 여러 프로세스가 동시에 통신함. 이들을 구분하는 것이 포트(port)임(L4).

  • IP:Port 조합이 통신의 최종 종착지를 결정함. (예: 10.0.1.5:8080)
  • 하나의 연결은 출발지 IP:Port ↔ 도착지 IP:Port의 4가지 값 조합으로 유일하게 식별됨.

💡 [프로세스편]에서 "컨테이너마다 80 포트를 각자 쓸 수 있다"고 한 것이 이제 명확해짐. 각 컨테이너가 독립된 NET namespace를 가져 자기만의 IP:Port 공간을 갖기 때문임.


 

3. ss로 TCP 상태 읽기: LISTEN, ESTABLISHED, TIME_WAIT

소켓의 상태를 읽는 도구가 ss임(구형 netstat의 후계). TCP 연결이 지금 어떤 상태인지를 보여줌.

3.1 자주 쓰는 ss 명령어

ss -tnlp    # TCP, 숫자표시, LISTEN 중인 것, 프로세스명 → "무슨 포트가 열려있나"
ss -tnp     # 현재 맺어진 연결 확인 → "누가 나에게 붙어있나"
ss -s       # 전체 소켓 통계 요약
ss state time-wait   # 특정 상태만 필터링

 

3.2 TCP 연결의 생성과 종료

TCP 상태를 이해하려면 연결의 시작(3-way handshake)과 끝(4-way termination)을 알아야 함.

 

3.3 실무에서 반드시 구분해야 할 두 상태

상태 의미 많이 쌓이면?
LISTEN 연결을 기다리는 서버 소켓 정상 (서비스가 떠 있다는 뜻)
ESTABLISHED 현재 데이터를 주고받는 연결 정상 (동시 접속 수 지표)
TIME_WAIT 연결을 먼저 끊은 쪽이 잔여 패킷 처리를 위해 잠시 대기 단기 연결이 폭증하는 신호
CLOSE_WAIT 상대는 끊었는데 내 애플리케이션이 소켓을 안 닫음 애플리케이션 버그

 

이 두 가지는 반드시 구분해야 함. 원인 주체가 정반대이기 때문임.

  • TIME_WAIT 폭증: 대개 정상 동작의 부작용임. 짧은 연결을 대량으로 맺고 끊을 때(예: 커넥션 풀 없이 매 요청마다 새 연결) 발생함. 커널 파라미터 튜닝이나 커넥션 재사용(keep-alive, 커넥션 풀)로 완화함.
  • CLOSE_WAIT 폭증: 거의 항상 애플리케이션 버그임. 상대방이 FIN을 보냈는데 내 코드가 소켓을 close()하지 않아 생김. 방치하면 FD가 고갈되어 "Too many open files" 에러로 이어짐. 코드에서 소켓·커넥션을 제대로 닫는지 점검해야 함.

💡 [기초편]에서 lsof로 프로세스가 연 FD를 봤던 것을 기억할 것. CLOSE_WAIT이 쌓이는 건 결국 FD 누수임. "Too many open files"와 CLOSE_WAIT은 같은 문제의 다른 얼굴임.


 

4. iptables와 NAT: 컨테이너가 외부와 통신하는 원리

4.1 iptables란

iptables는 리눅스 커널의 방화벽·패킷 처리 프레임워크임. 패킷이 커널을 통과하는 지점마다 규칙(rule)을 걸어 허용/차단/변조함. 크게 두 가지 용도로 씀.

  • filter 테이블: 패킷을 허용하거나 막음(방화벽). INPUT, OUTPUT, FORWARD 체인.
  • nat 테이블: 패킷의 IP·포트를 바꿔치기함(주소 변환). PREROUTING, POSTROUTING 체인.

 

4.2 NAT: 주소를 바꿔치기하는 L3 기술

NAT(Network Address Translation)는 패킷의 IP 주소를 변환하는 기술임. 방향에 따라 두 종류로 나뉨.

종류 변환 대상 용도 방향
SNAT (Source NAT) 출발지 IP를 바꿈 내부 사설 IP → 공인 IP (밖으로 나갈 때) 아웃바운드
DNAT (Destination NAT) 도착지 IP를 바꿈 공인 IP:Port → 내부 서버 (안으로 들어올 때) 인바운드

💡 SNAT는 프록시가 아님(재확인). SNAT는 L3에서 IP 헤더의 출발지 주소만 갈아끼울 뿐, 연결 자체는 끊지 않고 그대로 통과시킴. 그래서 애플리케이션은 자신의 IP가 바뀐 줄 모름. 이것이 프록시(L7에서 연결을 새로 맺음)와의 결정적 차이임. docker run -p 8080:80이 바로 DNAT 규칙을 iptables에 추가하는 동작임.


 

5. DNS 조회 흐름: 이름이 IP로 바뀌는 과정

애플리케이션은 api.example.com 같은 이름으로 통신하지만, 실제 패킷은 IP 주소가 있어야 나감(L3). 이 이름→IP 변환이 DNS 조회(resolution)임.

5.1 조회 순서

리눅스는 정해진 순서로 이름을 찾음. 이 순서를 모르면 "코드는 맞는데 왜 특정 서버에서만 접속이 안 되는가" 같은 문제를 못 풂.

5.2 핵심 설정 파일

파일 역할
/etc/hosts 이름-IP를 직접 매핑(정적). DNS보다 먼저 조회됨
/etc/resolv.conf 질의할 DNS 서버(nameserver) 목록
/etc/nsswitch.conf 조회 순서(files → dns)를 정의

💡 실무 활용: 테스트 시 특정 도메인을 강제로 특정 IP로 보내려면 /etc/hosts에 한 줄 추가하면 됨(DNS보다 우선하므로). 반대로 "hosts엔 없는데 조회가 이상하다"면 /etc/resolv.conf의 DNS 서버가 잘못됐거나 응답이 느린 것임. Docker 컨테이너는 보통 /etc/resolv.conf가 내장 DNS(127.0.0.11)를 가리켜 서비스명으로 컨테이너 간 통신이 됨.


 

6. 종합: 컨테이너 네트워크는 어떻게 완성되는가

이제 모든 조각을 합침. [프로세스편]에서 "컨테이너 = namespace + cgroup + 파일시스템"이라 했음. 그 중 네트워크 격리(NET namespace)가 실제로 어떻게 외부 통신까지 이어지는지를, 이 글의 개념들로 완성함.

컨테이너가 외부와 통신하려면 세 가지 부품이 조합됨.

  1. NET namespace — 컨테이너에 독립된 네트워크 스택(자기만의 IP, 포트, 라우팅 테이블)을 줌.
  2. veth pair (가상 랜선) — 한쪽 끝은 컨테이너 안에, 다른 쪽 끝은 호스트에 꽂히는 가상의 랜선 한 쌍. 격리된 namespace를 호스트와 연결함.
  3. bridge (가상 스위치) + iptables NAT — 호스트의 가상 스위치(docker0)가 컨테이너들을 묶고, SNAT(아웃바운드)·DNAT(포트 포워딩)로 외부와 연결함.

흐름 정리: 컨테이너 A가 외부로 요청을 보내면 → veth를 타고 docker0 브리지에 도달 → iptables가 SNAT로 출발지를 호스트 IP로 바꿈 → NIC를 통해 외부로 나감. 외부에서 컨테이너로 들어올 땐 반대로 DNAT로 도착지를 컨테이너 IP로 바꿈. 이 모든 게 앞에서 다룬 namespace·veth·NAT의 조합임.

한 줄 정리: 컨테이너 네트워크는 마법이 아니라 "격리된 NET namespace를 veth로 호스트에 잇고, iptables NAT로 외부와 연결한 것"임.


 

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

증상 확인 명령어 원인·해석
서비스 접속 안 됨 ss -tnlp | grep <포트> 프로세스가 해당 포트를 LISTEN 중인지 확인
"Too many open files" ss -tnp state close-wait CLOSE_WAIT 누수 — 앱이 소켓을 안 닫음
순간적 커넥션 폭증 ss -s / ss state time-wait TIME_WAIT 확인 — 커넥션 풀·keep-alive 검토
특정 도메인만 접속 실패 cat /etc/resolv.conf, nslookup <도메인> DNS 조회 실패 또는 /etc/hosts 오설정
컨테이너에서 외부 통신 불가 iptables -t nat -L -n SNAT(MASQUERADE) 규칙 존재 여부 확인
방화벽 차단 의심 iptables -L -n filter 테이블의 INPUT/FORWARD 규칙 확인
어느 계층 문제인지 모름 ping(L3) → telnet IP 포트(L4) → curl(L7) 계층을 순서대로 좁혀가며 원인 격리

💡 마지막 행이 핵심임. 문제를 만나면 낮은 계층부터 올라가며 원인을 좁힘. ping으로 L3(IP 도달) 확인 → telnet/nc로 L4(포트 열림) 확인 → curl로 L7(HTTP 응답) 확인. 어느 단계에서 막히는지가 곧 문제의 계층임.