본문 바로가기

캡슐화와 역캡슐화

HTTP 메시지에 TCP, IPv4, 이더넷 헤더를 바이트 단위로 붙여 프레임을 만들고, 받는 쪽에서 다시 벗겨 내는 과정을 실행해 봅니다. 각 헤더의 필드와 작은 패킷의 채움 바이트, 라우터를 지날 때 바뀌는 MAC 주소와 TTL과 체크섬을 확인하고, 패킷 캡처로 실제 바이트를 읽는 방법을 정리합니다.

브라우저가 서버에 HTTP 요청을 보낸다고 합시다. 요청이 상대에게 닿으려면 응용 계층에서 시작해 계층을 하나씩 내려가며 각 계층의 정보가 덧붙고, 받는 쪽에서는 아래 계층부터 올라가며 그 정보를 하나씩 벗겨 냅니다. 앞의 과정을 캡슐화(encapsulation), 뒤의 과정을 역캡슐화(decapsulation)라고 합니다. 이 글에서는 이 과정을 실제 바이트로 만들어 보고, 중간의 라우터가 무엇을 바꾸는지까지 따라갑니다.

보내는 쪽: 계층마다 헤더를 붙인다

캡슐화는 데이터가 위 계층에서 아래 계층으로 내려가면서, 각 계층이 자기 일에 필요한 제어 정보를 덧붙이는 과정입니다. 대부분은 데이터 앞에 붙는 헤더(header)이지만, 이더넷처럼 뒤에 오류 검사용 트레일러(trailer)를 붙이기도 합니다.

하위 계층 페이로드에 포함되는 상위 PDU

전송할 응용 바이트를 준비합니다. IP 패킷을 Ethernet 헤더와 FCS 사이에 싣습니다. 각 단계의 값과 전환 이유를 비교합니다.

ENCAPSULATION · CONTAINMENT / OUTER CONTROL

상위 단위 전체가 하위 계층의 페이로드에 포함된다

TCP 헤더와 응용 데이터는 IPv4의 페이로드이고, IPv4 헤더까지 포함한 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 수신은 바깥 제어 정보를 처리하며 안쪽 데이터에 접근합니다.

계층별 PDU의 포함 관계응용 데이터 앞에 TCP 헤더가 붙고, TCP 세그먼트 앞에 IPv4 헤더가 붙습니다. 이 IPv4 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 아래로 갈수록 바깥 계층의 전달 정보가 추가됩니다.송신: 위에서 아래로 감싸기응용 데이터TCP 헤더응용 데이터IPv4 헤더TCP 헤더응용 데이터Ethernet헤더IPv4 헤더TCP 헤더응용 데이터FCS수신: 아래에서 위로 바깥 전달 정보 처리
계층별 PDU의 포함 관계응용 데이터 앞에 TCP 헤더가 붙고, TCP 세그먼트 앞에 IPv4 헤더가 붙습니다. 이 IPv4 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 아래로 갈수록 바깥 계층의 전달 정보가 추가됩니다.프레임 안의 포함 관계Ethernet 프레임Ethernet 헤더IPv4 패킷IPv4 헤더TCP 세그먼트TCP 헤더응용데이터FCS

IPv4/TCP를 Ethernet에 싣는 개념도이며 크기 비율은 실제 바이트 수와 다릅니다. 한 응용 메시지가 한 TCP 세그먼트에 대응한다는 뜻은 아닙니다.

청록 영역은 실리는 응용 데이터입니다. 가로 그림은 추가되는 헤더를 행별로 보여 주고, 모바일은 프레임·IP 패킷·TCP 세그먼트 전체의 포함 관계를 중첩으로 보여 줍니다. 각 헤더가 안쪽 데이터를 전부 포함한다는 뜻은 아닙니다. TCP 메시지 경계·HTTPS 평문·실제 바이트 크기는 이 개념도만으로 판단하지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 응용 데이터부터 Ethernet 프레임까지 진행합니다.
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
제목하위 계층 페이로드에 포함되는 상위 PDU
예의 값과 변화
관찰·상황값·보충 정보변화·이유
응용 데이터

전송할 응용 바이트를 준비합니다.

  • 페이로드·PDU 1: application bytes
  • 응용 메시지와 세그먼트가 1:1이라는 보장은 없음
TCP 세그먼트

응용 바이트 앞에 TCP 제어 헤더를 둡니다.

  • 페이로드·PDU 1: TCP header+app bytes
  • TCP 세그먼트 구성
IPv4 패킷

TCP 세그먼트 전체를 IP 페이로드에 싣습니다.

  • 페이로드·PDU 1: IPv4 header+[TCP+app]
  • IPv4 헤더와 TCP 페이로드로 패킷 구성
Ethernet 프레임

IP 패킷을 Ethernet 헤더와 FCS 사이에 싣습니다.

  • 페이로드·PDU 1: Ethernet header+[IPv4+TCP+app]+FCS
  • 프레임 구성; 헤더 자체가 안쪽 전체를 포함하는 것은 아님

하위 계층 페이로드에 포함되는 상위 PDU

전송할 응용 바이트를 준비합니다. IP 패킷을 Ethernet 헤더와 FCS 사이에 싣습니다. 각 단계의 값과 전환 이유를 비교합니다.

ENCAPSULATION · CONTAINMENT / OUTER CONTROL

상위 단위 전체가 하위 계층의 페이로드에 포함된다

TCP 헤더와 응용 데이터는 IPv4의 페이로드이고, IPv4 헤더까지 포함한 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 수신은 바깥 제어 정보를 처리하며 안쪽 데이터에 접근합니다.

계층별 PDU의 포함 관계응용 데이터 앞에 TCP 헤더가 붙고, TCP 세그먼트 앞에 IPv4 헤더가 붙습니다. 이 IPv4 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 아래로 갈수록 바깥 계층의 전달 정보가 추가됩니다.송신: 위에서 아래로 감싸기응용 데이터TCP 헤더응용 데이터IPv4 헤더TCP 헤더응용 데이터Ethernet헤더IPv4 헤더TCP 헤더응용 데이터FCS수신: 아래에서 위로 바깥 전달 정보 처리
계층별 PDU의 포함 관계응용 데이터 앞에 TCP 헤더가 붙고, TCP 세그먼트 앞에 IPv4 헤더가 붙습니다. 이 IPv4 패킷은 Ethernet 헤더와 FCS 사이에 실립니다. 아래로 갈수록 바깥 계층의 전달 정보가 추가됩니다.프레임 안의 포함 관계Ethernet 프레임Ethernet 헤더IPv4 패킷IPv4 헤더TCP 세그먼트TCP 헤더응용데이터FCS

IPv4/TCP를 Ethernet에 싣는 개념도이며 크기 비율은 실제 바이트 수와 다릅니다. 한 응용 메시지가 한 TCP 세그먼트에 대응한다는 뜻은 아닙니다.

청록 영역은 실리는 응용 데이터입니다. 가로 그림은 추가되는 헤더를 행별로 보여 주고, 모바일은 프레임·IP 패킷·TCP 세그먼트 전체의 포함 관계를 중첩으로 보여 줍니다. 각 헤더가 안쪽 데이터를 전부 포함한다는 뜻은 아닙니다. TCP 메시지 경계·HTTPS 평문·실제 바이트 크기는 이 개념도만으로 판단하지 않습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 응용 데이터부터 Ethernet 프레임까지 진행합니다.

관계도의 보충 설명

전체 도표의 표기·관계·조건
구분전체 도표의 내용
제목하위 계층 페이로드에 포함되는 상위 PDU
예의 값과 변화
관찰·상황값·보충 정보변화·이유
응용 데이터

전송할 응용 바이트를 준비합니다.

  • 페이로드·PDU 1: application bytes
  • 응용 메시지와 세그먼트가 1:1이라는 보장은 없음
TCP 세그먼트

응용 바이트 앞에 TCP 제어 헤더를 둡니다.

  • 페이로드·PDU 1: TCP header+app bytes
  • TCP 세그먼트 구성
IPv4 패킷

TCP 세그먼트 전체를 IP 페이로드에 싣습니다.

  • 페이로드·PDU 1: IPv4 header+[TCP+app]
  • IPv4 헤더와 TCP 페이로드로 패킷 구성
Ethernet 프레임

IP 패킷을 Ethernet 헤더와 FCS 사이에 싣습니다.

  • 페이로드·PDU 1: Ethernet header+[IPv4+TCP+app]+FCS
  • 프레임 구성; 헤더 자체가 안쪽 전체를 포함하는 것은 아님

응용 계층에서 브라우저가 요청 메서드와 URL, 헤더를 담은 HTTP 메시지를 만듭니다. 전송 계층의 TCP는 이 바이트들에 출발지와 목적지 포트, 순서 번호, 확인 번호, 윈도우 크기를 담은 TCP 헤더를 붙여 세그먼트(segment)를 만듭니다. TCP는 응용의 데이터를 바이트의 흐름으로 다루므로, 메시지 하나가 여러 세그먼트로 나뉘거나 여러 메시지가 한 세그먼트에 함께 실릴 수도 있습니다.

인터넷 계층의 IP는 세그먼트 앞에 출발지와 목적지 IP 주소, TTL, 상위 프로토콜의 번호를 담은 IP 헤더를 붙여 패킷(packet)을 만들고, 링크 계층의 이더넷은 패킷 앞에 목적지와 출발지 MAC 주소, 안에 든 프로토콜의 종류(EtherType)를 담은 헤더를, 뒤에 FCS를 붙여 프레임(frame)을 만듭니다. 프레임은 마지막으로 비트가 되어 케이블이나 전파로 나갑니다.

다음 코드는 이 과정을 파이썬의 struct 모듈로 직접 해 봅니다. IP 주소는 문서용으로 정해 둔 예시 주소를, MAC 주소는 임의로 지은 값을 썼습니다. TCP 체크섬은 0으로 두었고, FCS는 보통 네트워크 카드가 붙이므로 넣지 않았습니다.

헤더는 struct.pack()으로 만듭니다. 형식 문자열에서 B는 1바이트, H는 2바이트, I는 4바이트 부호 없는 정수이고, 4s는 4바이트의 바이트열입니다. 맨 앞의 !는 여러 바이트로 된 숫자를 네트워크 바이트 순서, 곧 큰 자리 바이트부터 적는 빅 엔디언으로 담으라는 뜻으로, 이더넷과 IP, TCP 헤더의 숫자는 모두 이 순서로 적습니다.

import struct

def ipv4_checksum(header):
    total = sum(struct.unpack("!10H", header))      # 20바이트 헤더를 16비트씩 더한다
    while total > 0xFFFF:
        total = (total & 0xFFFF) + (total >> 16)      # 넘친 자리는 다시 더한다
    return ~total & 0xFFFF

def mac(text):
    return bytes.fromhex(text.replace(":", ""))

# 보내는 쪽: 위에서 아래로 내려가며 헤더를 붙인다
message = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"                    # 응용: HTTP 메시지
segment = struct.pack("!HHIIBBHHH", 52373, 80, 1, 1, 5 << 4, 0x18, 65535, 0, 0) + message   # 전송: TCP 헤더 20바이트(PSH+ACK)
header = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(segment), 0, 0x4000, 64, 6, 0,
                     bytes([192, 0, 2, 10]), bytes([198, 51, 100, 20]))
packet = header[:10] + struct.pack("!H", ipv4_checksum(header)) + header[12:] + segment   # 인터넷: IPv4 헤더 20바이트
frame = mac("aa:bb:cc:dd:ee:01") + mac("00:1a:2b:3c:4d:5e") + b"\x08\x00" + packet         # 링크: 이더넷 헤더 14바이트
for name, unit in (("메시지", message), ("세그먼트", segment), ("패킷", packet), ("프레임", frame)):
    print(f"{name}: {len(unit)}바이트")

# 받는 쪽: 아래에서 위로 올라가며 헤더를 벗긴다
ethertype = struct.unpack("!H", frame[12:14])[0]
print(f"이더넷: {frame[6:12].hex(':')} -> {frame[0:6].hex(':')}, EtherType 0x{ethertype:04x}")
ip = frame[14:]
ihl, total_length, ttl, proto = (ip[0] & 0x0F) * 4, struct.unpack("!H", ip[2:4])[0], ip[8], ip[9]
print(f"IPv4: {'.'.join(map(str, ip[12:16]))} -> {'.'.join(map(str, ip[16:20]))}, TTL {ttl}, "
      f"프로토콜 {proto}, 체크섬 {'정상' if ipv4_checksum(ip[:20]) == 0 else '오류'}")
tcp = ip[ihl:total_length]                         # IP 헤더의 전체 길이까지만 IP의 데이터다
src_port, dst_port = struct.unpack("!HH", tcp[:4])
offset = (tcp[12] >> 4) * 4
print(f"TCP: {src_port} -> {dst_port}, 헤더 {offset}바이트")
print("응용 데이터의 첫 줄:", tcp[offset:].split(b"\r\n")[0])
메시지: 37바이트
세그먼트: 57바이트
패킷: 77바이트
프레임: 91바이트
이더넷: 00:1a:2b:3c:4d:5e -> aa:bb:cc:dd:ee:01, EtherType 0x0800
IPv4: 192.0.2.10 -> 198.51.100.20, TTL 64, 프로토콜 6, 체크섬 정상
TCP: 52373 -> 80, 헤더 20바이트
응용 데이터의 첫 줄: b'GET / HTTP/1.1'

37바이트의 메시지가 계층을 내려가며 20, 20, 14바이트씩 커져 91바이트의 프레임이 되었습니다. 받는 쪽은 EtherType 0x0800을 보고 안에 IPv4 패킷이 들었음을 알고, IP 헤더의 프로토콜 번호 6을 보고 TCP 세그먼트가 들었음을 압니다. 이렇게 각 헤더가 바로 위 계층이 무엇인지 알려 주기 때문에 받는 쪽이 올바른 순서로 헤더를 벗길 수 있습니다. IPv4 헤더의 체크섬은 체크섬 칸까지 포함해 다시 계산하면 0이 나와야 정상입니다.

각 헤더에 든 것

이더넷 헤더

이더넷 헤더는 목적지 MAC 주소 6바이트, 출발지 MAC 주소 6바이트, EtherType 2바이트로 모두 14바이트입니다. EtherType은 안에 든 것이 IPv4이면 0x0800, IPv6이면 0x86DD, ARP이면 0x0806입니다. 프레임 끝의 FCS 4바이트는 받는 네트워크 카드가 검사한 뒤 떼어 내는 경우가 많아서, 패킷 캡처 도구에서는 대개 보이지 않습니다.

IPv4 헤더

필드크기뜻
버전4비트IPv4는 4
헤더 길이(IHL)4비트32비트 단위의 헤더 길이. 옵션이 없으면 5, 곧 20바이트
전체 길이2바이트IP 헤더와 데이터를 합한 길이
TTL1바이트라우터를 지날 때마다 1씩 줄고, 0이 되면 버려진다
프로토콜1바이트안에 든 것. TCP는 6, UDP는 17, ICMP는 1
헤더 체크섬2바이트헤더가 손상되지 않았는지 검사하는 값
출발지와 목적지 주소각 4바이트IPv4 주소

TCP 헤더

필드크기뜻
출발지와 목적지 포트각 2바이트0부터 65535. 어느 프로세스의 통신인지 가린다
순서 번호4바이트이 세그먼트의 데이터가 바이트 흐름의 어디부터인지
확인 번호4바이트다음에 받기를 기대하는 바이트의 번호
헤더 길이4비트32비트 단위의 헤더 길이. 옵션이 있으면 길어진다
플래그비트 묶음SYN, ACK, FIN, RST, PSH 같은 제어 신호
윈도우 크기2바이트받는 쪽이 지금 더 받을 수 있는 양
체크섬2바이트전송 중의 오류를 찾는 값. 위조를 막아 주지는 않는다

TCP 헤더의 각 필드가 연결과 흐름 제어에서 어떻게 쓰이는지는 6장에서 자세히 봅니다.

받는 쪽: 아래에서 위로 벗긴다

역캡슐화는 대체로 캡슐화의 반대입니다. 신호가 도착하면 링크 계층이 비트를 프레임으로 되살리고, 목적지 MAC 주소가 자기 것이거나 브로드캐스트인지, FCS가 맞는지 확인한 뒤 헤더와 트레일러를 벗겨 패킷을 인터넷 계층에 올립니다. 인터넷 계층은 목적지 IP 주소가 자기 것인지 보고 IP 헤더를 벗겨 세그먼트를 전송 계층에 올리며, 전송 계층은 주소와 포트로 어느 연결의 데이터인지 가려 해당 소켓에 바이트를 넘깁니다.

실제로는 중간에 할 일이 더 있습니다. FCS가 맞지 않는 프레임은 버리고, 단편화된 IP 패킷은 다시 합치며, TCP는 순서가 뒤바뀐 세그먼트를 정렬하고 빠진 것을 기다립니다. HTTPS라면 TLS가 데이터를 복호화한 뒤에야 HTTP 메시지가 보입니다. 위 코드에서 TCP 세그먼트를 IP 헤더의 전체 길이까지만 잘라 낸 것도 중요한데, 다음 절에서 보듯 프레임 끝에는 IP 패킷에 속하지 않는 바이트가 붙어 있을 수 있기 때문입니다.

작은 패킷과 채움 바이트

이더넷 프레임은 목적지 MAC 주소부터 FCS까지 최소 64바이트여야 합니다. 그래서 데이터가 아주 작으면 모자란 만큼을 0으로 채운 채움 바이트(padding)를 넣습니다.

ETH_HEADER, FCS, IP_HEADER, TCP_HEADER, MIN_FRAME = 14, 4, 20, 20, 64

for data in (1, 10):
    unpadded = ETH_HEADER + IP_HEADER + TCP_HEADER + data + FCS
    frame = max(MIN_FRAME, unpadded)              # 최소 크기보다 작으면 0으로 채운다
    print(f"데이터 {data}바이트: 프레임 {frame}바이트, 채움 {frame - unpadded}바이트, 효율 {data / frame:.1%}")
데이터 1바이트: 프레임 64바이트, 채움 5바이트, 효율 1.6%
데이터 10바이트: 프레임 68바이트, 채움 0바이트, 효율 14.7%

키 하나를 누를 때마다 1바이트를 보내는 원격 터미널이라면, 프레임의 1.6%만 데이터이고 나머지는 헤더와 채움 바이트입니다. 데이터가 10바이트이면 프레임이 이미 64바이트를 넘으므로 채움 바이트가 없습니다. RFC 894에 따르면 채움 바이트는 IP 패킷의 일부가 아니어서 IP 헤더의 전체 길이에 들어가지 않으므로, 받는 쪽은 전체 길이를 보고 IP 패킷이 어디서 끝나는지 알아야 합니다. TCP의 Nagle 알고리즘처럼 작은 데이터를 모아서 보내는 기법은 이런 낭비를 줄이려는 것이지만, 지연이 늘 수 있어 응용의 성격에 맞게 써야 합니다.

라우터를 지날 때 바뀌는 것

캡슐화와 역캡슐화는 양 끝에서만 일어나는 것이 아닙니다. 중간의 라우터도 받은 프레임의 링크 계층 헤더를 벗기고, IP 헤더를 읽어 다음 홉을 정한 뒤, 다음 링크에 맞는 새 링크 계층 헤더를 붙여 내보냅니다.

라우터 경계에서 바뀌는 링크 주소

서버 IP 패킷을 다음 홉 A로 가는 프레임에 싣습니다. B 인터페이스에서 새 프레임을 만듭니다. 각 단계의 값과 전환 이유를 비교합니다.

ROUTER FORWARDING · LOCAL MAC / ENDPOINT IP

라우터 경계에서 MAC은 다음 링크에 맞추고 종단 IP는 유지한다

NAT·터널이 없는 두 Ethernet 링크의 예시입니다. 라우터 A/B는 서로 다른 인터페이스이며 다음 링크의 헤더를 새로 만듭니다. 같은 L2의 스위치를 지날 때마다 MAC을 바꾸는 규칙은 아닙니다.

라우터 경계의 링크 주소 교체PC에서 라우터로 가는 첫 링크는 PC MAC과 라우터의 첫 인터페이스 MAC을 사용합니다. 라우터에서 서버로 가는 두 번째 링크는 다른 라우터 인터페이스 MAC과 서버 MAC을 사용합니다. NAT가 없는 예시에서 출발지와 목적지 IP는 유지되며 TTL은 감소하고 IPv4 헤더 체크섬은 갱신됩니다.PC라우터서버첫 링크MAC: PC → 라우터 AIP: PC → 서버두 번째 링크MAC: 라우터 B → 서버IP: PC → 서버라우터: TTL 감소 · IPv4 헤더 체크섬 갱신
라우터 경계의 링크 주소 교체PC에서 라우터로 가는 첫 링크는 PC MAC과 라우터의 첫 인터페이스 MAC을 사용합니다. 라우터에서 서버로 가는 두 번째 링크는 다른 라우터 인터페이스 MAC과 서버 MAC을 사용합니다. NAT가 없는 예시에서 출발지와 목적지 IP는 유지되며 TTL은 감소하고 IPv4 헤더 체크섬은 갱신됩니다.PC첫 링크MAC: PC →라우터 AIP: PC → 서버라우터두 번째 링크MAC: 라우터 B→ 서버IP: PC → 서버서버TTL 감소 · IPv4헤더 체크섬 갱신

NAT·터널이 없는 두 Ethernet 링크의 예시입니다. A와 B는 라우터의 서로 다른 인터페이스입니다.

바뀌는 필드와 유지되는 필드를 따로 확인

교체: 출발지·목적지 MAC · 새 링크 FCS

갱신: TTL 감소 · IPv4 헤더 체크섬

유지: 종단 IP 주소 · 일반 전달의 TCP/응용 데이터

NAT·방화벽·프록시·터널링에서는 이 예시보다 깊은 정보를 보거나 주소를 바꿀 수 있습니다. 프레임 캡처에서는 FCS가 NIC·드라이버에 의해 빠질 수 있습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 첫 Ethernet 링크부터 둘째 링크까지 진행합니다.
관계도의 보충 설명
전체 도표의 표기·관계·조건
구분전체 도표의 내용
제목라우터 경계에서 바뀌는 링크 주소
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 Ethernet 링크

서버 IP 패킷을 다음 홉 A로 가는 프레임에 싣습니다.

  • MAC·IP·TTL 1: MAC PC→routerA
  • MAC·IP·TTL 2: IP PC→server
  • 같은 링크의 스위치를 지나도 링크 주소는 유지
라우터 처리

라우터가 링크 정보를 처리하고 다음 홉을 고릅니다.

  • MAC·IP·TTL 1: TTL 감소
  • MAC·IP·TTL 2: IPv4 checksum 갱신
  • TTL 감소·IPv4 체크섬 갱신; IP 주소 유지가 모든 헤더 비트 불변을 뜻하지 않음
둘째 링크

B 인터페이스에서 새 프레임을 만듭니다.

  • MAC·IP·TTL 1: MAC routerB→server
  • MAC·IP·TTL 2: IP PC→server
  • MAC·IP·TTL 3: 새 FCS
  • 새 MAC 주소와 FCS 사용; NAT·터널을 제외한 일반 전달

라우터 경계에서 바뀌는 링크 주소

서버 IP 패킷을 다음 홉 A로 가는 프레임에 싣습니다. B 인터페이스에서 새 프레임을 만듭니다. 각 단계의 값과 전환 이유를 비교합니다.

ROUTER FORWARDING · LOCAL MAC / ENDPOINT IP

라우터 경계에서 MAC은 다음 링크에 맞추고 종단 IP는 유지한다

NAT·터널이 없는 두 Ethernet 링크의 예시입니다. 라우터 A/B는 서로 다른 인터페이스이며 다음 링크의 헤더를 새로 만듭니다. 같은 L2의 스위치를 지날 때마다 MAC을 바꾸는 규칙은 아닙니다.

라우터 경계의 링크 주소 교체PC에서 라우터로 가는 첫 링크는 PC MAC과 라우터의 첫 인터페이스 MAC을 사용합니다. 라우터에서 서버로 가는 두 번째 링크는 다른 라우터 인터페이스 MAC과 서버 MAC을 사용합니다. NAT가 없는 예시에서 출발지와 목적지 IP는 유지되며 TTL은 감소하고 IPv4 헤더 체크섬은 갱신됩니다.PC라우터서버첫 링크MAC: PC → 라우터 AIP: PC → 서버두 번째 링크MAC: 라우터 B → 서버IP: PC → 서버라우터: TTL 감소 · IPv4 헤더 체크섬 갱신
라우터 경계의 링크 주소 교체PC에서 라우터로 가는 첫 링크는 PC MAC과 라우터의 첫 인터페이스 MAC을 사용합니다. 라우터에서 서버로 가는 두 번째 링크는 다른 라우터 인터페이스 MAC과 서버 MAC을 사용합니다. NAT가 없는 예시에서 출발지와 목적지 IP는 유지되며 TTL은 감소하고 IPv4 헤더 체크섬은 갱신됩니다.PC첫 링크MAC: PC →라우터 AIP: PC → 서버라우터두 번째 링크MAC: 라우터 B→ 서버IP: PC → 서버서버TTL 감소 · IPv4헤더 체크섬 갱신

NAT·터널이 없는 두 Ethernet 링크의 예시입니다. A와 B는 라우터의 서로 다른 인터페이스입니다.

바뀌는 필드와 유지되는 필드를 따로 확인

교체: 출발지·목적지 MAC · 새 링크 FCS

갱신: TTL 감소 · IPv4 헤더 체크섬

유지: 종단 IP 주소 · 일반 전달의 TCP/응용 데이터

NAT·방화벽·프록시·터널링에서는 이 예시보다 깊은 정보를 보거나 주소를 바꿀 수 있습니다. 프레임 캡처에서는 FCS가 NIC·드라이버에 의해 빠질 수 있습니다.

이 예의 조건

  • 본문과 전체 도표의 예를 따라 첫 Ethernet 링크부터 둘째 링크까지 진행합니다.

관계도의 보충 설명

전체 도표의 표기·관계·조건
구분전체 도표의 내용
제목라우터 경계에서 바뀌는 링크 주소
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 Ethernet 링크

서버 IP 패킷을 다음 홉 A로 가는 프레임에 싣습니다.

  • MAC·IP·TTL 1: MAC PC→routerA
  • MAC·IP·TTL 2: IP PC→server
  • 같은 링크의 스위치를 지나도 링크 주소는 유지
라우터 처리

라우터가 링크 정보를 처리하고 다음 홉을 고릅니다.

  • MAC·IP·TTL 1: TTL 감소
  • MAC·IP·TTL 2: IPv4 checksum 갱신
  • TTL 감소·IPv4 체크섬 갱신; IP 주소 유지가 모든 헤더 비트 불변을 뜻하지 않음
둘째 링크

B 인터페이스에서 새 프레임을 만듭니다.

  • MAC·IP·TTL 1: MAC routerB→server
  • MAC·IP·TTL 2: IP PC→server
  • MAC·IP·TTL 3: 새 FCS
  • 새 MAC 주소와 FCS 사용; NAT·터널을 제외한 일반 전달

다음 코드는 PC가 라우터의 A 인터페이스로 보낸 프레임을 라우터가 B 인터페이스를 통해 서버 쪽 링크로 넘기는 과정을 흉내 냅니다.

import struct

def ipv4_checksum(header):
    total = sum(struct.unpack("!10H", header))
    while total > 0xFFFF:
        total = (total & 0xFFFF) + (total >> 16)
    return ~total & 0xFFFF

def with_checksum(header):
    zeroed = header[:10] + b"\x00\x00" + header[12:]
    return zeroed[:10] + struct.pack("!H", ipv4_checksum(zeroed)) + zeroed[12:]

def summary(frame):
    ip = frame[14:34]
    return (f"MAC {frame[6:12].hex(':')} -> {frame[0:6].hex(':')} | "
            f"IP {'.'.join(map(str, ip[12:16]))} -> {'.'.join(map(str, ip[16:20]))} | "
            f"TTL {ip[8]} | 체크섬 0x{struct.unpack('!H', ip[10:12])[0]:04x}")

ip = with_checksum(struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20, 0, 0x4000, 64, 6, 0,
                               bytes([192, 0, 2, 10]), bytes([198, 51, 100, 20])))
pc, router_a, router_b, server = (bytes.fromhex(x) for x in ("001a2b3c4d5e", "aabbccdd0001", "aabbccdd0002", "02aabb000099"))
frame_in = router_a + pc + b"\x08\x00" + ip                   # 첫 링크: PC -> 라우터의 A 인터페이스

ip_out = bytearray(frame_in[14:])                              # 라우터: 링크 헤더를 벗기고
ip_out[8] -= 1                                                 # TTL을 하나 줄인 뒤
frame_out = server + router_b + b"\x08\x00" + with_checksum(bytes(ip_out))   # 체크섬을 다시 계산해 새 프레임에 담는다

print("라우터 앞:", summary(frame_in))
print("라우터 뒤:", summary(frame_out))
라우터 앞: MAC 00:1a:2b:3c:4d:5e -> aa:bb:cc:dd:00:01 | IP 192.0.2.10 -> 198.51.100.20 | TTL 64 | 체크섬 0x4e92
라우터 뒤: MAC aa:bb:cc:dd:00:02 -> 02:aa:bb:00:00:99 | IP 192.0.2.10 -> 198.51.100.20 | TTL 63 | 체크섬 0x4f92

MAC 주소는 링크마다 완전히 바뀌었지만, IP의 출발지와 목적지 주소는 그대로입니다. 링크 계층 헤더는 그 링크 안에서만 쓰는 봉투이고, IP 헤더는 처음부터 끝까지 가는 주소표이기 때문입니다. 그렇다고 IP 헤더가 한 비트도 바뀌지 않는 것은 아니어서, IPv4 라우터는 TTL을 1 줄이고 그에 맞게 헤더 체크섬을 다시 계산합니다. TTL이 0x40에서 0x3F로 줄자 체크섬이 정확히 0x0100만큼 늘어난 것을 볼 수 있습니다. 보통의 라우터는 TCP 헤더와 그 안의 데이터는 건드리지 않으며, NAT나 방화벽, 프록시, 터널 장비는 그보다 깊은 정보를 보거나 주소를 바꾸기도 합니다.

더 깊이: 라우터는 체크섬을 어떻게 빨리 고치나

IPv4 헤더 체크섬은 인터넷 체크섬(Internet checksum)이라 부르는 방식으로 계산합니다. 헤더를 16비트 단위로 끊어 1의 보수 덧셈(one's complement addition)으로 모두 더한 뒤, 그 합의 비트를 뒤집은 값입니다. 1의 보수 덧셈은 맨 위에서 넘친 올림수를 버리지 않고 맨 아래 비트에 다시 더합니다. RFC 1071은 이 덕분에 더하는 순서와 바이트 순서에 상관없이 같은 결과가 나오고, 바뀐 부분만 반영해 고칠 수도 있다고 설명합니다.

앞의 출력에서 체크섬이 0x0100만큼 늘어난 것도 이 성질로 설명됩니다. TTL은 프로토콜 번호와 함께 16비트 단어 하나를 이루면서 그 단어의 위쪽 바이트에 있으므로, TTL이 1 줄면 그 단어는 0x0100만큼 작아지고, 합을 뒤집은 체크섬은 그만큼 커집니다. 그래서 라우터는 헤더 전체를 다시 더하지 않고 바뀐 단어의 옛 값과 새 값만으로 체크섬을 고칩니다. RFC 1624는 옛 체크섬 HC와 바뀌기 전후의 단어 m, m'으로 이 증분 갱신(incremental update)의 식을 HC' = ~(~HC + ~m + m')로 정리하면서, 앞선 RFC 1141의 식은 결과가 0이어야 할 때 0xFFFF를 내는 경우가 있다고 바로잡았습니다.

NAT도 같은 방법을 씁니다. NAT는 주소와 포트를 바꾸므로 IP 헤더 체크섬뿐 아니라, 주소까지 포함해 계산하는 TCP와 UDP의 체크섬도 고쳐야 합니다. RFC 3022는 이때 바꾸기 전후 값의 차이만 체크섬에 더하면 충분하다고 설명합니다.

패킷 캡처로 바이트를 읽기

Wireshark와 tcpdump는 네트워크 인터페이스를 오가는 패킷을 붙잡아 계층별 헤더를 보여 주는 도구입니다. 서버에서는 주로 tcpdump로 캡처하고, 저장한 파일을 Wireshark로 열어 살펴봅니다.

sudo tcpdump -i eth0 -nn -c 5 port 80          # 80번 포트의 패킷 다섯 개를 주소와 포트를 숫자로 보이며
sudo tcpdump -i eth0 -nn -c 1 -XX port 80      # 링크 헤더부터 바이트를 16진수로
sudo tcpdump -i eth0 -w capture.pcap -c 100    # 파일로 저장해 Wireshark에서 열기

인터페이스 이름 eth0은 ip link로 확인해 바꾸어야 하고, 캡처에는 root 권한이 필요합니다. port 80은 포트로 고른 것일 뿐 그 안의 내용이 HTTP라는 보장은 아닙니다. VLAN 태그가 없는 이더넷과 옵션 없는 IPv4, TCP라면 -XX가 보여 주는 바이트는 다음처럼 나뉩니다.

위치(16진수)내용
0x0000부터 0x000d까지이더넷 헤더: MAC 주소 둘과 EtherType
0x000e부터 0x0021까지IPv4 헤더: 버전, TTL, 주소 등
0x0022부터 0x0035까지TCP 헤더: 포트, 순서 번호, 플래그 등
0x0036부터TCP 데이터. 평문 HTTP라면 여기서 요청이 시작된다

옵션이 있으면 헤더 길이 필드를 읽어 위치를 다시 계산해야 하고, HTTPS라면 TCP 데이터는 암호화되어 있어 HTTP 요청을 읽을 수 없습니다. 장애를 진단할 때도 계층별로 보는 정보가 다릅니다. 링크 계층에서는 MAC 주소와 브로드캐스트, ARP를, 인터넷 계층에서는 IP 주소와 TTL, ICMP 오류를, 전송 계층에서는 포트와 TCP 플래그를, 응용 계층에서는 HTTP 상태 코드나 DNS 응답을 봅니다.

정리

  • 캡슐화는 데이터가 계층을 내려가며 계층마다 헤더와 트레일러가 붙는 과정이고, 역캡슐화는 받는 쪽에서 아래부터 헤더를 벗기는 과정입니다.
  • 각 헤더는 바로 위 계층이 무엇인지 알려 주어, 받는 쪽이 EtherType과 프로토콜 번호를 보고 올바른 순서로 헤더를 벗길 수 있습니다.
  • 이더넷 프레임은 최소 64바이트여서 작은 데이터에는 채움 바이트가 붙으며, 채움 바이트는 IP 패킷에 속하지 않으므로 IP 헤더의 전체 길이로 패킷의 끝을 찾습니다.
  • 라우터는 링크 계층 헤더를 새로 만들어 MAC 주소를 바꾸지만, IP의 출발지와 목적지 주소는 그대로 두고 TTL과 체크섬만 고칩니다.
  • tcpdump와 Wireshark로 실제 바이트를 계층별로 읽을 수 있으며, 헤더의 길이 필드를 읽어 각 계층의 위치를 정해야 합니다.

연습 문제

1. 데이터가 10바이트라면

옵션이 없는 IPv4와 TCP, 이더넷에서 TCP 데이터가 10바이트인 프레임의 크기, 채움 바이트, 데이터의 비율을 구하세요. 이더넷의 최소 크기 64바이트를 모든 프레임에 더하는 것이 맞나요?

해설 보기

10 + 20 + 20 + 14 + 4 = 68바이트로 이미 최소 크기 64바이트를 넘으므로 채움 바이트는 없고, 데이터의 비율은 10 ÷ 68 ≈ 14.7%입니다. 64바이트는 프레임이 지켜야 하는 최소 크기일 뿐, 모든 프레임에 더하는 값이 아닙니다. 계산한 크기가 64바이트보다 작을 때만 그 차이만큼 채움 바이트를 넣습니다.

2. 스위치와 라우터를 지나는 주소

PC, 스위치, 라우터의 A 인터페이스가 첫 번째 이더넷 링크에, 라우터의 B 인터페이스와 서버가 두 번째 이더넷 링크에 있습니다. NAT도 터널도 없습니다. PC가 서버에 보낸 패킷이 스위치를 지날 때 목적지 MAC 주소가 서버의 것으로 바뀌나요? 라우터를 지난 뒤의 MAC 주소와 IP 주소, TTL은 어떻게 되나요?

해설 보기

첫 링크의 프레임은 출발지가 PC의 MAC, 목적지가 라우터 A의 MAC이며, 스위치는 같은 링크 안에서 프레임을 전달할 뿐 주소를 바꾸지 않습니다. 라우터가 두 번째 링크로 새 프레임을 만들 때 출발지 MAC은 라우터 B, 목적지 MAC은 서버가 됩니다. IP의 출발지와 목적지는 처음부터 끝까지 PC와 서버로 그대로이고, TTL은 1 줄며 IPv4 헤더 체크섬도 다시 계산됩니다. 그래서 "IP 패킷은 그대로 간다"는 말은 주소가 그대로라는 뜻이지, 모든 비트가 같다는 뜻은 아닙니다.

3. 전체 길이를 무시하면

본문 코드의 받는 쪽에서 ip[ihl:total_length] 대신 ip[ihl:]로 TCP 세그먼트를 잘라 낸다고 합시다. 데이터가 1바이트여서 프레임 끝에 채움 바이트 5개가 붙은 경우, 응용 프로그램은 무엇을 받게 되나요?

해설 보기

ip[ihl:]는 프레임의 끝까지 모두 잘라 내므로, 실제 TCP 데이터 1바이트 뒤에 붙은 채움 바이트 5개까지 TCP 데이터로 여겨, 응용 프로그램이 보낸 적 없는 0바이트 다섯 개를 받게 됩니다. 채움 바이트는 이더넷이 최소 크기를 맞추려고 붙인 것이지 IP 패킷의 일부가 아니기 때문입니다. IP 헤더의 전체 길이가 패킷의 끝을 알려 주므로, 받는 쪽은 그 값까지만 IP의 데이터로 다루어야 합니다. 실제 운영체제의 네트워크 스택은 전체 길이와 헤더 길이, 체크섬을 모두 검사한 뒤에야 데이터를 위로 올립니다.

다음 장의 물리 계층의 역할에서는 계층 구조의 가장 아래에서 비트가 전기와 빛, 전파의 신호로 바뀌어 전달되는 과정을 살펴봅니다.