📚 네트워크 · 패킷 분석 › 01. TCP/IP 구조 이해 — 28편
이전 글: 27. Ethernet Frame · 다음 글: 29. TCP Segment
참고(리눅스 시스템 기초): Linux 네트워크 구조 — 인터페이스와 커널 네트워크 스택 개요
링크마다 한 번에 실어 보낼 수 있는 크기에는 한계가 있습니다. 이 한계를 넘는 IP 패킷은 쪼개지거나(단편화), 버려지고 오류 메시지가 돌아오거나 둘 중 하나입니다.
관제 관점에서 단편화는 두 가지 이유로 중요합니다.
| 구간 예시 | MTU | 비고 |
|---|---|---|
| 일반 Ethernet | 1500 | 기본값 |
| PPPoE 회선 | 1492 | PPPoE 헤더 8바이트만큼 감소 |
| VPN·터널(GRE, IPsec 등) | 1500보다 작음 | 캡슐화 헤더만큼 감소, 방식마다 다름 |
| 점보 프레임 | 9000 전후 | 데이터센터 내부 등 장비 전체가 지원할 때만 |
| 루프백(lo) | 65536 (Linux 기본) | 실제 링크가 아니라 캡처 시 크기가 크게 보임 |
경로 전체에서 가장 작은 MTU를 Path MTU라고 합니다. 양 끝단이 1500이라도 중간에 터널이 있으면 Path MTU는 그보다 작습니다.
| 필드 | 크기 | 역할 |
|---|---|---|
| Identification | 16비트 | 같은 원본 패킷에서 나온 조각을 묶는 번호 (조각끼리 같은 값) |
| Flags – DF (Don't Fragment) | 1비트 | 1이면 쪼개지 말 것. 넘치면 버리고 ICMP 오류 반환 |
| Flags – MF (More Fragments) | 1비트 | 1이면 뒤에 조각이 더 있음. 마지막 조각만 0 |
| Fragment Offset | 13비트 | 이 조각 데이터가 원본 데이터의 어디부터인지, 8바이트 단위 |
| Total Length | 16비트 | 이 조각 자체의 길이(헤더 포함) |
Fragment Offset이 8바이트 단위이기 때문에, 마지막 조각을 제외한 모든 조각의 데이터 길이는 8의 배수여야 합니다. MTU 1500에서 IP 헤더 20을 빼면 1480이고, 1480 = 8 × 185이므로 조건을 만족합니다.
현대 OS의 TCP는 대부분 DF=1로 보내고 Path MTU를 스스로 찾는 방식(PMTUD, Path MTU Discovery) 을 씁니다. 단편화는 성능과 신뢰성에 불리하기 때문입니다.
이 과정은 ICMP가 돌아와야만 동작합니다. 중간 방화벽이 ICMP를 전부 막으면 출발지는 이유를 모른 채 큰 패킷만 계속 잃게 되는데, 이를 PMTUD 블랙홀이라고 합니다. Handshake와 작은 요청은 되는데 큰 응답만 멈추는 증상이 대표적입니다. 이를 피하려고 경로 장비에서 SYN의 MSS 값을 낮춰 주는 MSS Clamping을 쓰기도 합니다.
ICMP 데이터 3000바이트 + ICMP 헤더 8바이트 = IP 데이터 3008바이트짜리 패킷을 MTU 1500 링크로 DF=0으로 보낸다고 해 보겠습니다.

그림 1. 세 조각은 같은 ID를 공유하고, offset과 MF 플래그로 순서·끝을 표시합니다
원본 IPv4 패킷: IP 헤더 20 + 데이터 3008 (ID=0x1a2b)
↓ MTU 1500 → 조각당 데이터 최대 1480 (8의 배수)
┌─────────────────────────────────────────────────────────────────┐
│ 조각1: ID=0x1a2b MF=1 Offset=0 (0/8) 데이터 1480 TotalLen 1500 │
│ 조각2: ID=0x1a2b MF=1 Offset=185 (1480/8) 데이터 1480 TotalLen 1500 │
│ 조각3: ID=0x1a2b MF=0 Offset=370 (2960/8) 데이터 48 TotalLen 68 │
└─────────────────────────────────────────────────────────────────┘
↓ 각 조각은 독립된 IP 패킷으로 전달
목적지: 같은 (출발지, 목적지, 프로토콜, ID) 조각 수집
↓ Offset 순서대로 배치, MF=0 조각으로 끝 확인
재조립 완료 → 3008바이트 → ICMP 계층으로 전달
여기서 관제에 중요한 사실은 첫 번째 조각에만 상위 계층(TCP/UDP/ICMP) 헤더가 들어 있다는 점입니다. 두 번째 조각부터는 포트 번호가 없습니다. 포트 기반 방화벽 규칙이나 5-tuple 로그가 조각을 온전히 판단하려면 재조립이나 상태 추적이 필요한 이유입니다.
실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu), 인터페이스 이름은 ens33 예시입니다. 대상은 같은 대역의 게이트웨이나 다른 VM(192.168.10.1)으로 합니다.
# Rocky Linux (tracepath는 iputils에 포함)
sudo dnf install -y tcpdump iputils iproute
# Ubuntu
sudo apt update && sudo apt install -y tcpdump iputils-ping iputils-tracepath iproute2
ip link show ens33 # "mtu 1500" 확인
ip -s link show ens33 # 송수신 오류·드롭 카운터도 함께
ping의 -s는 ICMP 데이터 크기입니다. 1472 + ICMP 헤더 8 + IP 헤더 20 = 1500이 됩니다.
# DF=1로 전송 (-M do): 1500 딱 맞음 → 성공해야 정상
ping -M do -s 1472 -c 3 192.168.10.1
# 1바이트 초과 → 로컬에서 이미 거부됨
ping -M do -s 1473 -c 3 192.168.10.1
# 터미널 1: 조각만 캡처 (MF=1 이거나 Offset≠0 인 IPv4 패킷)
sudo tcpdump -i ens33 -nn -v 'ip[6:2] & 0x3fff != 0'
# 터미널 2: DF를 끄고(-M dont) 큰 ping 1회
ping -M dont -s 3000 -c 1 192.168.10.1
ip[6:2]는 IP 헤더 6번째 바이트부터 2바이트(Flags + Fragment Offset)이고, 0x3fff는 MF 비트와 13비트 Offset만 골라내는 마스크입니다. DF 비트(0x4000)는 제외됩니다.
📷 [실습 화면 삽입] tcpdump
-v출력에서 세 조각의id,offset,flags [+],length값이 3장 예시와 일치하는 부분
tracepath -n 192.168.10.1 # 경로상 pmtu 표시
ip route get 192.168.10.1 # 경로 캐시에 낮아진 mtu가 기록돼 있으면 함께 표시됨
nstat -az IpFragCreates IpFragOKs IpFragFails IpReasmReqds IpReasmOKs IpReasmFails
sysctl net.ipv4.ip_no_pmtu_disc net.ipv4.ipfrag_time
ping -M do -s 1473 출력 형식 예시(값은 환경마다 다름):
ping: local error: message too long, mtu=1500
tcpdump -v 조각 출력 형식 예시(값은 환경마다 다름):
IP (tos 0x0, ttl 64, id 6699, offset 0, flags [+], proto ICMP (1), length 1500)
192.168.10.20 > 192.168.10.1: ICMP echo request, id 7, seq 1, length 1480
IP (tos 0x0, ttl 64, id 6699, offset 1480, flags [+], proto ICMP (1), length 1500)
192.168.10.20 > 192.168.10.1: ip-proto-1
IP (tos 0x0, ttl 64, id 6699, offset 2960, flags [none], proto ICMP (1), length 68)
192.168.10.20 > 192.168.10.1: ip-proto-1
tcpdump는 offset을 바이트로 환산해서(185 × 8 = 1480) 보여 주고, Wireshark는 Fragment Offset: 1480처럼 바이트로 표시하는 경우가 많습니다. 헤더 원값은 8로 나눈 185·370이라는 점을 구분해야 합니다. 두 번째 조각부터는 ICMP 헤더가 없어 ip-proto-1로만 표시되는 것도 확인할 수 있습니다.
| 확인 항목 | 기대값 |
|---|---|
| 세 조각의 id | 모두 동일 |
| flags | 앞 두 조각 [+](MF=1), 마지막 [none] |
| offset | 0 → 1480 → 2960 (바이트 환산) |
| length 합계 - 헤더 | 1480 + 1480 + 48 = 3008 |
ip link로 ens33 MTU를 확인했다-M do -s 1472 성공, -s 1473 실패를 확인하고 계산 근거(1472+8+20)를 설명할 수 있다nstat에서 IpFragCreates / IpReasm 카운터 변화를 확인했다| 도구 | 필터 | 의미 |
|---|---|---|
| Wireshark | ip.flags.mf == 1 or ip.frag_offset > 0 | 모든 IPv4 조각 |
| Wireshark | ip.flags.df == 1 | DF가 설정된 패킷 |
| Wireshark | icmp.type == 3 && icmp.code == 4 | Fragmentation Needed (PMTUD 신호) |
| tcpdump | 'ip[6:2] & 0x3fff != 0' | 모든 IPv4 조각 |
| tcpdump | 'icmp[0] == 3 and icmp[1] == 4' | Fragmentation Needed |
Wireshark는 기본적으로 조각을 재조립해 마지막 조각 위치에 [Reassembled IPv4 in frame: N]처럼 표시합니다. 조각 하나하나를 보려면 IPv4 프로토콜 설정의 재조립 옵션을 끄고 비교해 보는 것이 좋습니다.
| 관찰 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
| UDP 조각 | DNS 대용량 응답(EDNS), 일부 VPN·터널, NFS 등 | 출발지 다수에서 조각만 대량 유입 → 조각 재조립 자원을 소모시키는 DoS 원리 |
| 아주 작은 첫 조각 | 거의 없음 | 첫 조각에 TCP 헤더조차 다 담기지 않을 만큼 작음 → 포트 필터 우회 시도 형태 |
| 조각끼리 Offset이 겹침 | 정상 스택에서는 발생하지 않음 | 재조립 방식 차이를 노린 IDS 회피 형태, 과거 OS 취약점(Teardrop류) 원리 |
| 재조립 결과가 65,535 초과 | 발생할 수 없음 | 과거 Ping of Death 원리, 현대 OS는 폐기 |
| ICMP Type 3 Code 4 | 터널·PPPoE 구간이 있는 정상 PMTUD | 내부 경로와 무관한 출발지에서 대량 수신 → 위조 ICMP 가능성 확인 |
| 큰 응답만 끊김 | — | ICMP 전면 차단으로 인한 PMTUD 블랙홀 (보안 정책 점검 대상) |
# pcap에서 조각 수와 출발지별 분포 확인
tshark -r capture.pcap -Y 'ip.flags.mf == 1 || ip.frag_offset > 0' \
-T fields -e ip.src -e ip.id | sort | uniq -c | sort -rn | head
📷 [실습 화면 삽입] Wireshark에서 재조립 옵션을 끈 상태와 켠 상태의 패킷 목록 비교 —
Fragmented IP protocol표시와Reassembled표시
흔적이 남는 곳
nstat의 IpReasmFails가 급증하면 조각 유실 또는 비정상 조각 유입을 의심할 수 있습니다.관제 판단 포인트
한계와 오탐 주의