컴퓨터공학 300 주제 시리즈의 149번째 글이다. 전체 지도는 여기.

한 줄 요약

UDP 는 포트 번호와 체크섬만 붙여 IP 위에 데이터그램을 그대로 실어 보내는 프로토콜이다. 연결도, 재전송도, 순서 보장도, 혼잡 제어도 없다. 그 빈자리는 응용이 채운다.

왜 필요한가

TCP 의 신뢰성은 공짜가 아니다. 핸드셰이크 왕복, 손실 시 뒤 데이터까지 멈추는 순서 보장, 커널이 정한 혼잡 제어가 따라온다. 다음 경우에는 그게 오히려 짐이다.

  • DNS 질의처럼 요청 하나, 응답 하나로 끝나는 짧은 교환.
  • 음성·영상 통화처럼 늦게 온 데이터는 버리는 게 나은 실시간 전송.
  • QUIC 처럼 TCP 의 동작을 사용자 공간에서 새로 설계하고 싶을 때.

UDP 를 이해하면 “왜 DNS 는 UDP 를 쓰나”, “왜 HTTP/3 는 UDP 위에 올라가나”, “왜 UDP 서비스는 방화벽과 NAT 에서 자주 문제를 일으키나”를 설명할 수 있다.

핵심 개념

헤더

UDP 는 1980년 RFC 768 에서 정의됐다. 명세 전체가 세 쪽 남짓이다. 헤더는 8바이트다.

 0      7 8     15 16    23 24    31
+--------+--------+--------+--------+
|   출발지 포트    |   목적지 포트    |
+--------+--------+--------+--------+
|      길이       |     체크섬       |
+--------+--------+--------+--------+
|            데이터 ...              |
  • 길이: 헤더 포함 데이터그램 전체 길이. 최소 8.
  • 체크섬: IP 주소가 담긴 의사 헤더(pseudo-header)와 UDP 헤더, 데이터를 16비트 1의 보수 합으로 계산한다. IPv4 에서는 0 을 넣어 “계산 안 함”을 표시할 수 있지만, IPv6 에서는 반드시 계산해야 한다(RFC 8200).

길이 필드가 16비트이므로 데이터그램 최대는 65,535바이트다. IPv4 헤더 20바이트와 UDP 헤더 8바이트를 빼면 IPv4 에서 실을 수 있는 데이터는 최대 65,507바이트다. 하지만 이더넷 MTU(1500)를 넘으면 IP 단편화가 일어나고, 조각 하나만 잃어도 데이터그램 전체를 잃는다. 그래서 실무에서는 경로 MTU 보다 작게 보내는 것이 원칙이다.

TCP 와 비교

항목 TCP UDP
연결 핸드셰이크로 수립 없음. 바로 보냄
단위 바이트 스트림(경계 없음) 데이터그램(경계 보존)
신뢰성 재전송, 순서 보장 없음. 손실·중복·뒤바뀜 가능
흐름·혼잡 제어 커널이 수행 없음. 응용 책임
헤더 최소 20바이트 8바이트
대표 사용 HTTP/1.1·2, SSH, DB DNS, QUIC(HTTP/3), RTP, DHCP, NTP

“경계 보존”은 중요한 차이다. UDP 로 100바이트를 두 번 보내면 받는 쪽은 recvfrom() 두 번으로 100바이트씩 받는다. TCP 는 200바이트가 한 번에 올 수도, 50바이트씩 네 번 올 수도 있다(150번 글).

UDP 를 쓰는 응용의 책임

IETF 는 UDP 사용 지침을 RFC 8085 로 정리했다. 요지는 “TCP 가 해 주던 일 중 필요한 것을 응용이 직접 하라”다.

  • 혼잡 제어: 손실이 늘면 보내는 속도를 줄여야 한다. 안 그러면 다른 흐름까지 망친다.
  • 메시지 크기: 경로 MTU 를 넘지 않게 한다.
  • 신뢰성: 필요하면 타임아웃과 재전송, 순서 번호를 직접 구현한다. DNS 클라이언트는 응답이 없으면 다시 묻는다.
  • 연결 상태 유지: NAT 와 방화벽은 UDP 매핑을 시간으로 지운다. 오래 유지할 흐름이면 주기적으로 keepalive 를 보낸다.

보안 측면

UDP 는 핸드셰이크가 없어 출발지 주소 위조가 쉽다. 공격자가 피해자 주소로 위조한 작은 요청을 보내 큰 응답을 피해자에게 쏟아지게 하는 반사·증폭 공격이 여기서 나온다. QUIC 이 첫 패킷 크기를 일정 이상으로 채우고 주소 검증을 하는 이유다(RFC 9000).

직접 해 보기

로컬호스트에서 UDP 소켓 두 개로 경계 보존을 확인하고, 체크섬을 직접 계산해 본다. 체크섬 계산에는 문서용 주소를 쓴다.

import socket, struct

# 1) 데이터그램 경계 보존
rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
rx.bind(("127.0.0.1", 0)); rx.settimeout(2)
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for msg in (b"first", b"second-message", b"3"):
    tx.sendto(msg, rx.getsockname())
for _ in range(3):
    data, addr = rx.recvfrom(2048)
    print("받음:", data, len(data))

# 2) UDP 체크섬 (RFC 768: 의사 헤더 + UDP 헤더 + 데이터, 16비트 1의 보수 합)
def csum16(b):
    if len(b) % 2: b += b"\x00"
    s = sum(struct.unpack(f"!{len(b)//2}H", b))
    while s >> 16: s = (s & 0xFFFF) + (s >> 16)
    return ~s & 0xFFFF

def udp_checksum(src, dst, sport, dport, payload):
    length = 8 + len(payload)
    pseudo = socket.inet_aton(src) + socket.inet_aton(dst) + struct.pack("!BBH", 0, 17, length)
    hdr = struct.pack("!HHHH", sport, dport, length, 0)
    c = csum16(pseudo + hdr + payload)
    return c or 0xFFFF            # 계산 결과 0 은 0xFFFF 로 보낸다

c = udp_checksum("192.0.2.10", "198.51.100.20", 50000, 53, b"query")
print("체크섬: 0x%04x" % c)

# 받는 쪽 검증: 체크섬 필드까지 넣고 다시 합하면 0 이 나와야 한다
length = 8 + 5
pseudo = socket.inet_aton("192.0.2.10") + socket.inet_aton("198.51.100.20") + struct.pack("!BBH", 0, 17, length)
seg = struct.pack("!HHHH", 50000, 53, length, c) + b"query"
print("검증 결과(0 이면 정상):", csum16(pseudo + seg))
print("IPv4 최대 UDP 데이터:", 65535 - 20 - 8)
rx.close(); tx.close()
받음: b'first' 5
받음: b'second-message' 14
받음: b'3' 1
체크섬: 0x0014
검증 결과(0 이면 정상): 0
IPv4 최대 UDP 데이터: 65507

세 번 보낸 것이 정확히 세 덩어리로 왔다. 로컬호스트라 손실이 없었을 뿐, 실제 망에서는 일부가 사라지거나 순서가 바뀌어도 UDP 는 아무 말을 하지 않는다.

현업에서는

  • 쿠버네티스 클러스터의 DNS(CoreDNS)는 기본적으로 UDP 53 을 쓴다. UDP DNS 질의가 conntrack 경쟁 조건에 걸려 늦어지거나 conntrack 표를 채우는 문제가 알려져 있어, 노드마다 DNS 캐시를 두는 NodeLocal DNSCache 구성이 공식 문서에 소개돼 있다.
  • 방화벽 규칙을 열 때 “TCP 443 만 열었는데 HTTP/3 가 안 된다”는 일이 생긴다. HTTP/3 는 UDP 443 을 쓴다. 브라우저는 실패하면 TCP 로 돌아가므로 장애로 보이진 않지만 성능 이점을 잃는다.
  • VPN(WireGuard)과 오버레이 네트워크(VXLAN)도 UDP 위에 터널을 만든다. 상태가 없는 UDP 가 캡슐화 운반체로 쓰기 좋기 때문이다(158번 글).
  • UDP 서비스 장애는 “응답이 없다” 하나로만 보인다. 서버가 죽었는지, 패킷이 버려졌는지, 응답이 다른 경로로 갔는지 구분하려면 양쪽에서 패킷 캡처를 해야 한다.

확인 문제

  1. UDP 헤더의 네 필드는 무엇인가?
  2. IPv4 에서 UDP 체크섬 값 0 은 무슨 의미인가? IPv6 에서는 어떤가?
  3. UDP 로 2000바이트 데이터그램을 이더넷(MTU 1500) 위로 보내면 어떤 일이 생기고, 왜 위험한가?
  4. TCP 와 달리 UDP 수신 쪽에서 메시지 구분자를 따로 둘 필요가 없는 이유는?
  5. RFC 8085 가 UDP 응용에 요구하는 책임 두 가지를 들라.

풀이

  1. 출발지 포트, 목적지 포트, 길이, 체크섬.
  2. IPv4 에서는 송신자가 체크섬을 계산하지 않았다는 뜻이다. IPv6 에서는 체크섬이 필수라 0 을 쓸 수 없다(터널 같은 특정 예외 제외).
  3. IP 단편화로 두 조각이 된다. 한 조각이라도 잃으면 데이터그램 전체가 버려지고, 단편을 막는 방화벽도 있어 전달 실패 가능성이 커진다.
  4. UDP 는 데이터그램 단위로 경계를 보존해, 한 번 보낸 것은 한 번에 받기 때문이다.
  5. 혼잡 제어(손실 시 속도 줄이기), 경로 MTU 이하로 메시지 크기 제한. 그 밖에 필요한 신뢰성 구현, NAT 매핑 유지 등이 있다.

더 읽을거리 (References)