Traceroute 작동 방식: Rust로 처음부터 구현하기
Traceroute는 인터넷을 통해 패킷이 지나가는 경로를 드러내는 네트워크 진단 도구입니다. IP 헤더의 TTL(생존 시간)과 ICMP 오류 메시지라는 두 가지 핵심 메커니즘에 의존합니다. 각 단계에서 TTL을 점진적으로 증가시킴으로써 라우터는 패킷의 수명이 만료될 때 알림을 보내 경로 지도를 구축합니다. Rust로 단 80줄의 코드로 우리만의 버전을 구현하면 프로토콜에 대한 깊은 이해를 통해 저수준 네트워크 작업이 얼마나 명확해지는지 알 수 있습니다.
UDP Probe 아키텍처
Traceroute의 핵심 요소는 제한된 TTL로 대상화된 패킷을 보내는 것입니다. UDP는 TCP 대신 세 가지 이유로 사용됩니다:
- 핸드셰이크가 없어 오버헤드가 적음
- 전달 보장이 없음—패킷은 의도적으로 드롭되도록 설계됨
- 높은 포트 번호(33434부터 시작)로 실행 중인 서비스와의 충돌 최소화
코드에서는 두 개의 소켓을 초기화합니다:
let send_sock = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
send_sock.set_ttl_v4(ttl)?;
let recv_sock = Socket::new(
Domain::IPV4,
Type::from(libc::SOCK_RAW),
Some(Protocol::ICMPV4),
)?;
첫 번째 소켓은 지정된 TTL로 버려질 패킷을 보내고, 두 번째 소켓은 ICMP 응답을 가로챕니다. 중요: raw 소켓은 네트워크 스택 수준에서 작동하므로 root 권한이 필요합니다.
ICMP 응답 파싱
라우터가 TTL=0에서 패킷을 드롭하면 유형 11(Time Exceeded)의 ICMP 메시지를 보냅니다. 데이터 구조는 다음과 같습니다:
- 처음 20바이트—응답의 IP 헤더
- 바이트 12-15—라우터의 IP 주소
- 바이트 20—ICMP 메시지 유형
초기 구현에서는 IP 주소만 파싱했습니다:
if buf.len() >= 20 {
let ip = Ipv4Addr::new(buf[12], buf[13], buf[14], buf[15]);
Ok(Some(ip))
}
이로 인해 종단점 감지에 오류가 발생했습니다. 올바른 처리를 위해 ICMP 유형을 확인해야 합니다:
match buf[20] {
11 => Ok(ProbeResult::Hop(ip)),
3 if ip == target => Ok(ProbeResult::Reached(ip)),
3 => Ok(ProbeResult::Hop(ip)),
_ => Ok(ProbeResult::Timeout),
}
유형 3(Destination Unreachable)은 IP가 대상과 일치할 때만 대상 도달을 나타냅니다.
Traceroute 최적화
원래 traceroute는 기본 구현에 없는 두 가지 기법을 사용합니다:
- 포트 번호 증가—후속 패킷마다 포트 +1로 보냄. UDP 헤더의 Identification 필드를 통해 ICMP 응답을 요청에 명확히 매칭할 수 있습니다.
- TCP 모드 지원—방화벽에서 UDP가 차단될 때 낮은 TTL의 SYN 패킷을 사용합니다. 홉 감지 메커니즘은 동일합니다.
초기 코드 버전의 치명적 오류—유형 3 수신 시 IP 주소 확인 무시. if ip == target 조건이 없으면 첫 번째 Destination Unreachable을 반환하는 라우터에서 추적이 멈춥니다.
중요한 점
- TTL을 제어 도구로—값을 점진적으로 증가시켜 각 홉을 순차적으로 발견
- ICMP 메시지—진단의 기반—Time Exceeded(유형 11)와 Destination Unreachable(유형 3)이 경로 지도를 형성
- Raw 소켓은 권한 필요—root 권한 없이 저수준 네트워크 작업 불가
- UDP vs TCP—프로토콜 선택이 방화벽 우회에 영향을 주지만 기본 알고리즘은 동일
- 바이너리 데이터 파싱—패킷 바이트에 직접 접근하려면 IP/ICMP 헤더 구조 지식 필요
종료 조건 구현
주요 개선 사항—대상이 도달했을 때 올바르게 감지. 간단한 Option<Ipv4Addr> 대신 ProbeResult enum을 사용합니다:
enum ProbeResult {
Hop(Ipv4Addr),
Reached(Ipv4Addr),
Timeout,
}
메인 루프에서 결과 처리는 다음과 같습니다:
match hop {
ProbeResult::Hop(ip) => println!("{:>2} {}", ttl, ip),
ProbeResult::Reached(ip) => {
println!("{:>2} {}", ttl, ip);
break;
}
ProbeResult::Timeout => println!("{:>2} *", ttl),
}
이렇게 하면 타임아웃이 아닌 대상 호스트 도달 시 추적이 멈춥니다. 실제 시나리오에서는 다음도 추가하세요:
- TTL당 3회 프로브로 지연 통계 수집
- 파편화된 패킷 처리
- IPv6 지원
- 거리 기반 타임아웃
요약 및 제한 사항
구현된 버전은 traceroute의 핵심을 보여주지만 제한이 있습니다:
- 비대칭 경로 고려 안 함
- ICMP 속도 제한 처리 안 함
- 현대 네트워크의 MPLS 태그 무시
- 슈퍼유저 권한 필요
프로덕션 솔루션에서는 pnet_packet 같은 라이브러리를 사용해 수동 바이트 파싱을 피하세요. 하지만 raw 버전을 직접 작성하는 것이 네트워크 기본을 이해하는 최고의 방법입니다. 모든 네트워크 엔지니어는 인터넷을 통해 데이터가 어떻게 이동하는지 파악하기 위해 최소 한 번 traceroute를 구현해야 합니다.
— Editorial Team
아직 댓글이 없습니다.