RFC 1149 : 조류 캐리어 프로토콜(AIP) 구현하기

IETF(국제 인터넷 표준화 기구)의 RFC 1149에 정의된 조류 캐리어 프로토콜(IP over Avian Carriers) 공식적으로 문서 제목을 직역하면 "조류 운반자를 이용한 IP 데이터그램 전송 표준" 정도로 번역할 수 있습니다, 앞으로 이 분서에서는 AIP(Avian Internet Protocol)로 부르겠습니다.

이 프로토콜의 주요 내용은 무엇일까요? 기존의 OSI 7 layer를 기준으로 layer 1인 랜선을 대신하여 조류가 데이터그램을 운반하는 것입니다. 이 방식의 단점은 다음과 같습니다.

  • High Delay(높은 딜레이)
  • Packet Loss(패킷 유실)
  • Low Throughput(작은 대역폭)
  • Low Altitude(저고도)

그럼 장점은 무엇일까요? 놀랍게도 아주 많습니다. 아래 목록을 참고하세요.

  • 충돌 회피 시스템
  • 버그(벌레) 탐지 및 박멸
  • 3차원 공간 활용
  • 자동 로그(똥) 생성
  • 오토 힐링(자식 생산)

이쯤 되면 눈치채셨을 것입니다. 이 문서는 장난용 문서입니다.

IETF나 되는 기관에서 왜 이런 문서를 발행하냐고 물으신다면.. "재밌으니까"입니다. 물론 아무 때나 내는 것은 아니고 4월 1일 만우절마다 하나씩 나오곤 합니다.

이런 목록이 더 궁금하다면 위키피디아의 April Fools' Day Request for Comments 문서를 참고하세요.

실제 스펙대로 구현하려면 전서구가 필요하므로, 우리는 이를 가상의 세상에서 구현할 것입니다. 그럼 의미가 없는 것 아니냐구요? 그렇게 생각하신다면, 애초에 전서구 프로토콜은 의미가 없습니다.

데이터그램 설계

문서에서 데이터그램은 다음과 같이 작성되어있습니다.

IP 데이터그램은 작은 두루마리 종이에 16진수로 인쇄되며, 각 옥텟(octet)은 흰 것(whitestuff)과 검은 것(blackstuff)으로 구분됩니다. 이 종이 두루마리는 조류 운반자의 한쪽 다리에 감깁니다. 데이터그램의 가장자리를 고정하기 위해 덕트 테이프 밴드가 사용됩니다. 대역폭(bandwidth)은 다리 길이에 의해 제한됩니다. MTU(최대 전송 단위)는 가변적이며, 역설적으로 운반자의 나이가 많아질수록 일반적으로 증가합니다. 일반적인 MTU는 256밀리그램입니다. 약간의 데이터그램 패딩이 필요할 수 있습니다.

전서구 하나당 하나의 데이터그램이라고 가정하고 지니고 있어야 할 정보는 다음과 같습니다. :

  • 운반자 식별자(예: 비둘기 번호 또는 이름)
  • 데이터그램 무게
  • 데이터그램 내용(16진수로 인코딩된 IP 패킷)

먼저 프로토콜 헤더에 들어갈 값들의 사이즈와 오프셋을 정의합니다. 사이즈는 여기서 carrierIDSize = 4와 같이 작성되어있는 건 4비트라는 뜻입니다. 즉, 데이터그램의 첫 4비트는 carrierIDSize(새의 아이디 크기)를 나타냅니다. 다른 상수들도 그렇게 생각하면 됩니다.

const (
	carrierIDSize  = 4
	weightSize     = 2
	payloadLenSize = 4

	HeaderSize = carrierIDSize + weightSize + payloadLenSize
)

const (
	carrierIDOffset  = 0
	weightOffset     = carrierIDOffset + carrierIDSize
	payloadLenOffset = weightOffset + weightSize
)

위와같이 직접 정의하는것보다 실용적인 방법이 있지만, 프로토콜이 구성되는 과정을 보여주고 싶어 이렇게 작성하겠습니다. 간혹 프로토콜을 너무 추상적인 개념으로 생각할 수 있는데, 이런 경우 프로토콜을 이용한 통신을 머릿속에서 구체화시키기 힘들어집니다.

그리고 대충 값들을 정의해주고 byte 배열에 넣어줍니다. 이 과정에서 빅엔디안으로 값들을 저장하게됩니다. 네트워크 통신에서는 보통 빅엔디안으로 데이터를 직렬화(serialize)하게 되는데 이는 human-readable하게 데이터를 전송하기 위해서라고 알려져있지만, 이미 네트워크 스택이 추상화되어있는 현대에 이르러서는 큰 쓸모로 느껴지지는 않습니다. 리틀 엔디안을 가져감으로써 얻을 수 있는 여러가지 성능적 이점도 있기 때문인데 이 부분은 추후에 다른 글로 설명하겠습니다.

빅엔디안(Big Endian): 가장 중요한 바이트(최상위 바이트)를 가장 앞(작은 주소)에 저장합니다. 네트워크 프로토콜에서 주로 사용되며, "네트워크 바이트 오더"라고도 불립니다.
리틀엔디안(Little Endian): 가장 덜 중요한 바이트(최하위 바이트)를 가장 앞(작은 주소)에 저장합니다. x86 아키텍처 등 많은 PC에서 사용됩니다.

	carrierID := uint32(1234)
	weight := uint16(56)
	payloadLen := uint32(78)

	body := []byte("hello world")

	payloadLen = uint32(len(body))

	bytes := make([]byte, HeaderSize+len(body))

	binary.BigEndian.PutUint32(bytes[carrierIDOffset:], carrierID)
	binary.BigEndian.PutUint16(bytes[weightOffset:], weight)
	binary.BigEndian.PutUint32(bytes[payloadLenOffset:], payloadLen)

이렇게 정의된 오프셋 위치에 데이터들을 밀어넣고 출력해보면 다음과 같은 결과가 나옵니다. 16진수로 출력하는 이유는 보통 hexcode와 같은 바이너리 뷰어는 16비트가 익숙하기 때문입니다.

fmt.Printf("%x\n", bytes)
// 출력 결과 : 000004d200380000000b68656c6c6f20776f726c64

이제 우린 이 16진수 문자열을 해석할 수 있습니다. 각 비트 사이즈별로 해석해봅시다.
carrierID : 000004d2 : 1234
weight : 0038 : 56
payloadLen : 0000000b : 11
body : 68656c6c6f20776f726c64 : hello world

body 값은 16진수 아스키 코드표와 대입하면 알 수 있습니다.

실제로 역직렬화를 위해 만든 deSerialization함수를 사용해 출력해보면 아래와 같은 결과를 얻을 수 있습니다.

[yhw@hyunwooyoon rfc1149]$ go run .
000004d200380000000b68656c6c6f20776f726c64
CarrierID: 1234
Weight: 56
PayloadLen: 11
Body: hello world

이렇게 RFC 1149의 AIP를 실제 코드로 구현해보았습니다. 비록 만우절 RFC고 실용성은 전혀 없지만 네트워크 프로토콜이 어떻게 동작하는지 머릿속에서 실체화시켜 생각하는데 더 도움이 되었다면 좋겠습니다.

pseudo 코드 느낌으로 볼 수 있게 일부의 코드들만 작성해두었는데, 더 자세한 구현체를 보고싶다면 해당 리포지토리를 참고하세요.