And Brain said,

엔디언(Endian) 행진곡. 3 - 제1장: Network 본문

IT/엔디언 행진곡

엔디언(Endian) 행진곡. 3 - 제1장: Network

The Man 2023. 7. 5. 18:49
반응형

엔디언(Endian) 행진곡

0. 멀티바이트의 질서있는 행진
1. 빅이냐 리틀이냐 그것이 문제로다
2. Rust로 다시 쓰여지다
3. 제1장: Network
4. 제2장: FIle
5. 행진이 끝나고

예제 코드

 

엔디언은 또한 네트워크 통신에서도 중요한 역할을 합니다. 대부분의 네트워크 프로토콜은 빅 엔디언, 또는 '네트워크 바이트 오더'를 사용합니다.

 

먼저, 네트워크 패킷을 다루는 경우에는 고수준의 API 기능들로는 충분하지 않을 수 있습니다. 저수준의 네트워크 프로그래밍에서는 엔디언에 대한 이해가 필수적입니다. 이때 엔디언 변환이 필요하게 됩니다.

 

Rust로 작성된 간단한 패킷 구조체를 생성하고, 이를 바이트 배열로 변환한 다음 다시 원래의 구조체로 복원해 보겠습니다.

use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::Cursor;

#[derive(Debug)]
struct Packet {
    src_port: u16,
    dst_port: u16,
    length: u16,
    checksum: u16,
}

impl Packet {
    fn new(src_port: u16, dst_port: u16, length: u16, checksum: u16) -> Packet {
        Packet {
            src_port,
            dst_port,
            length,
            checksum,
        }
    }

    fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = vec![];
        bytes.write_u16::<BigEndian>(self.src_port).unwrap();
        bytes.write_u16::<BigEndian>(self.dst_port).unwrap();
        bytes.write_u16::<BigEndian>(self.length).unwrap();
        bytes.write_u16::<BigEndian>(self.checksum).unwrap();
        bytes
    }

    fn from_bytes(bytes: Vec<u8>) -> Packet {
        let mut rdr = Cursor::new(bytes);
        let src_port = rdr.read_u16::<BigEndian>().unwrap();
        let dst_port = rdr.read_u16::<BigEndian>().unwrap();
        let length = rdr.read_u16::<BigEndian>().unwrap();
        let checksum = rdr.read_u16::<BigEndian>().unwrap();

        Packet {
            src_port,
            dst_port,
            length,
            checksum,
        }
    }
}

fn main() {
    let packet = Packet::new(12345, 6789, 1024, 5678);
    println!("Origianl Packet: {:?}", packet);

    let bytes = packet.to_bytes();
    println!("Packet as Bytes: {:?}", bytes);

    let restored_packet = Packet::from_bytes(bytes);
    println!("Restored Pactet: {:?}", restored_packet);
}

 

네트워크 패킷을 나타내는 src_port, dst_port, length, checksum과 같은 필드를 가지는 Packet 구조체를 생성한 후, Packet 구조체의 인스턴스를 만들고, 이를 바이트 배열로 변환합니다. 이 변환은 빅 엔디언 방식을 사용합니다. 그 후, 다시 바이트 배열을 Packet으로 변환합니다.

 

 

이번에는 IPv4 헤더를 파싱하고 생성해 보겠습니다.

use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::{Cursor, Read};

#[derive(Debug)]
struct IPv4Header {
    version: u8,
    ihl: u8,
    dscp: u8,
    ecn: u8,
    total_length: u16,
    identification: u16,
    flags: u16,
    fragment_offset: u16,
    ttl: u8,
    protocol: u8,
    header_checksum: u16,
    source_ip: [u8; 4],
    dest_ip: [u8; 4],
}

impl IPv4Header {
    fn new() -> IPv4Header {
        IPv4Header {
            version: 4,
            ihl: 5,
            dscp: 0,
            ecn: 0,
            total_length: 20,
            identification: 0,
            flags: 2,
            fragment_offset: 0,
            ttl: 64,
            protocol: 1,
            header_checksum: 0,
            source_ip: [192, 168, 1, 1],
            dest_ip: [192, 168, 1, 2],
        }
    }

    fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = vec![];
        bytes.write_u8((self.version << 4) | self.ihl).unwrap();
        bytes.write_u8((self.dscp << 2) | self.ecn).unwrap();
        bytes.write_u16::<BigEndian>(self.total_length).unwrap();
        bytes.write_u16::<BigEndian>(self.identification).unwrap();
        bytes
            .write_u16::<BigEndian>((self.flags << 13) | self.fragment_offset)
            .unwrap();
        bytes.write_u8(self.ttl).unwrap();
        bytes.write_u8(self.protocol).unwrap();
        bytes.write_u16::<BigEndian>(self.header_checksum).unwrap();
        bytes.extend_from_slice(&self.source_ip);
        bytes.extend_from_slice(&self.dest_ip);
        bytes
    }

    fn from_bytes(bytes: Vec<u8>) -> IPv4Header {
        let mut rdr = Cursor::new(bytes);
        let version_ihl = rdr.read_u8().unwrap();
        let version = version_ihl >> 4;
        let ihl = version_ihl & 0xf;
        let dscp_ecn = rdr.read_u8().unwrap();
        let dscp = dscp_ecn >> 2;
        let ecn = dscp_ecn & 0x3;
        let total_length = rdr.read_u16::<BigEndian>().unwrap();
        let identification = rdr.read_u16::<BigEndian>().unwrap();
        let flags_fragment_offset = rdr.read_u16::<BigEndian>().unwrap();
        let flags = (flags_fragment_offset >> 13) & 0x7;
        let fragment_offset = flags_fragment_offset & 0x1fff;
        let ttl = rdr.read_u8().unwrap();
        let protocol = rdr.read_u8().unwrap();
        let header_checksum = rdr.read_u16::<BigEndian>().unwrap();
        let mut source_ip = [0u8; 4];
        rdr.read_exact(&mut source_ip).unwrap();
        let mut dest_ip = [0u8; 4];
        rdr.read_exact(&mut dest_ip).unwrap();

        IPv4Header {
            version,
            ihl,
            dscp,
            ecn,
            total_length,
            identification,
            flags,
            fragment_offset,
            ttl,
            protocol,
            header_checksum,
            source_ip,
            dest_ip,
        }
    }
}

fn main() {
    let header = IPv4Header::new();
    println!("Original Header: {:?}", header);

    let bytes = header.to_bytes();
    println!("Header as Bytes: {:?}", bytes);

    let restored_header = IPv4Header::from_bytes(bytes);
    println!("Restored Header: {:?}", restored_header)
}

 

 IPv4 헤더는 다양한 필드를 가지고 있습니다. 이 예제에서는 간단하게 버전, IHL, DSCP, ECN, 전체 길이, 식별자, 플래그, 프래그먼트 오프셋, TTL, 프로토콜, 헤더 체크섬, 소스 IP, 목적지 IP 등의 필드를 포함하고 있습니다. 각 필드에 대한 설명은 아래 접은 글을 참조.

더보기

version: 이 필드는 IP 프로토콜의 버전을 나타냅니다. 여기서는 IPv4를 사용하므로 값은 4입니다.

ihl (Internet Header Length): 이 필드는 IP 헤더의 길이를 32비트 단위로 나타냅니다. 일반적으로 5입니다(즉, 헤더는 20 바이트입니다).

dscp (Differentiated Services Code Point): 이 필드는 패킷이 네트워크 내에서 어떻게 처리되어야 하는지를 나타내는 QoS(품질 서비스) 메커니즘입니다.

ecn (Explicit Congestion Notification): 이 필드는 네트워크의 혼잡 상태를 알리는 데 사용됩니다.

total_length: 이 필드는 IP 헤더와 데이터의 전체 길이를 바이트 단위로 나타냅니다.

identification: 이 필드는 각각의 패킷을 구분하기 위한 ID를 제공합니다. 패킷이 분할되어 전송되는 경우, 이 필드를 사용하여 원래의 패킷을 재구성합니다.

flags: 이 필드는 패킷의 분할, 재결합 등과 같은 다른 옵션을 제어합니다.

fragment_offset: 이 필드는 분할된 패킷이 원래의 패킷에서 어떤 위치에 해당하는지를 나타냅니다.

ttl (Time to Live): 이 필드는 패킷이 네트워크를 통해 전송될 수 있는 최대 시간(또는 'hop' 수)을 제한합니다. 이를 통해 무한히 패킷이 순환하는 것을 방지합니다.

protocol: 이 필드는 사용되는 전송 계층의 프로토콜(예: TCP, UDP 등)을 나타냅니다.

header_checksum: 이 필드는 헤더의 오류를 검출하는 데 사용되는 체크섬 값입니다.

source_ip: 이 필드는 패킷의 발신지 IP 주소를 나타냅니다.

dest_ip: 이 필드는 패킷의 목적지 IP 주소를 나타냅니다.

 

각 필드의 타입과 크기는 IPv4 헤더의 정의에 따라 결정됩니다.

이 예제에서는 byteorder 라이브러리의 BigEndian 엔디언 변환 메소드를 사용하여 IPv4 헤더의 각 필드를 바이트 배열로 변환하고, 이를 다시 복원하는 과정을 보여주고 있습니다.

 

 

이번엔 Tcp 헤더를 다뤄보도록 하겠습니다.

#[derive(Debug)]
struct TcpHeader {
    source_port: u16,
    destination_port: u16,
    sequence_number: u32,
    acknowledgement_number: u32,
    data_offset: u8,
    reserved: u8,
    flags: u16,
    window_size: u16,
    checksum: u16,
    urgent_pointer: u16,
    options: Vec<u8>,
}

impl TcpHeader {
    fn from_bytes(bytes: &[u8]) -> TcpHeader {
        let mut iter = bytes.iter();

        let source_port = Self::u16_from_iter(&mut iter);
        let destination_port = Self::u16_from_iter(&mut iter);
        let sequence_number = Self::u32_from_iter(&mut iter);
        let acknowledgement_number = Self::u32_from_iter(&mut iter);

        let data_offset_and_reserved_and_flags = Self::u16_from_iter(&mut iter);
        let data_offset = (data_offset_and_reserved_and_flags >> 12) as u8;
        let reserved = ((data_offset_and_reserved_and_flags & 0b0000_0111_0000_0000) >> 9) as u8;
        let flags = data_offset_and_reserved_and_flags & 0b0000_0000_1_1111_1111;

        let window_size = Self::u16_from_iter(&mut iter);
        let checksum = Self::u16_from_iter(&mut iter);
        let urgent_pointer = Self::u16_from_iter(&mut iter);

        let options = iter.cloned().collect::<Vec<_>>();

        TcpHeader {
            source_port,
            destination_port,
            sequence_number,
            acknowledgement_number,
            data_offset,
            reserved,
            flags,
            window_size,
            checksum,
            urgent_pointer,
            options,
        }
    }

    fn to_bytes(&self) -> Vec<u8> {
        let mut bytes = Vec::new();

        bytes.extend(&self.source_port.to_be_bytes());
        bytes.extend(&self.destination_port.to_be_bytes());
        bytes.extend(&self.sequence_number.to_be_bytes());
        bytes.extend(&self.acknowledgement_number.to_be_bytes());

        let data_offset_and_reserved_and_flags =
            ((self.data_offset as u16) << 12) | ((self.reserved as u16) << 9) | self.flags;
        bytes.extend(&data_offset_and_reserved_and_flags.to_be_bytes());

        bytes.extend(&self.window_size.to_be_bytes());
        bytes.extend(&self.checksum.to_be_bytes());
        bytes.extend(&self.urgent_pointer.to_be_bytes());
        bytes.extend(&self.options);

        bytes
    }

    fn u16_from_iter(iter: &mut std::slice::Iter<'_, u8>) -> u16 {
        let b1 = *iter.next().unwrap() as u16;
        let b2 = *iter.next().unwrap() as u16;

        (b1 << 8) | b2
    }

    fn u32_from_iter(iter: &mut std::slice::Iter<'_, u8>) -> u32 {
        let b1 = *iter.next().unwrap() as u32;
        let b2 = *iter.next().unwrap() as u32;
        let b3 = *iter.next().unwrap() as u32;
        let b4 = *iter.next().unwrap() as u32;

        (b1 << 24) | (b2 << 16) | (b3 << 8) | b4
    }
}

fn main() {
    // Create a dummy TCP header as bytes
    let tcp_header_bytes: [u8; 20] = [
        0x00, 0x50, // Source Port: 80
        0x01, 0xBB, // Destination Port: 443
        0x12, 0x34, 0x56, 0x78, // Sequence Number
        0x9A, 0xBC, 0xDE, 0xF0, // Acknowledgement Number
        0x50, 0x02, // Data Offset, Reserved, Flags
        0xFF, 0xFF, // Window Size
        0x00, 0x00, // Checksum
        0x00, 0x00, // Urgent Pointer
    ];

    let header = TcpHeader::from_bytes(&tcp_header_bytes);
    println!("{:?}", header);

    let header_bytes = header.to_bytes();
    println!("{:?}", header_bytes);
}

 

위 예제는 TCP 헤더를 나타내는 TCPHeader 구조체와 그 구조체를 바이트 배열로 변환하거나 바이트 배열로부터 생성하는 기능을 구현합니다.

 

TCP는 데이터 패킷으로 분할하고 이를 신뢰성 있게 전송하는 데 사용되는 주요 네트워크 프로토콜로, TCP 헤더는 패킷의 메타데이터를 포함하며, 각 필드는 특정 의미를 가지고 있습니다. 각 필드에 대한 설명은 아래 접은 글 참조.

더보기

source_port: 이 필드는 16비트로, 데이터를 전송하는 출발지 포트 번호를 나타냅니다. 네트워크 서비스는 특정 포트 번호를 사용하며, 이 필드를 통해 패킷이 어떤 프로세스로부터 왔는지 알 수 있습니다.

destination_port: 이 필드 역시 16비트로, 데이터를 받을 목적지 포트 번호를 나타냅니다. 패킷이 어떤 프로세스에게 가야 하는지 결정하는 데 사용됩니다.

sequence_number: 32비트 필드로, 전송하는 데이터의 순서 번호를 나타냅니다. TCP는 데이터를 여러 패킷으로 분할하여 전송하므로 이 필드를 통해 원래의 순서대로 재조립할 수 있습니다.

acknowledgement_number: 32비트 필드로, 수신 측이 다음에 받기를 원하는 시퀀스 번호를 나타냅니다. 이는 패킷을 성공적으로 받았음을 확인하는 데 사용됩니다.

data_offset: 4비트 필드로, TCP 헤더의 길이를 나타냅니다. 이는 TCP 헤더와 데이터를 구분하는 데 사용됩니다.

reserved: 4비트 필드로, 현재는 사용되지 않고 미래의 사용을 위해 예약되어 있습니다. 일반적으로 0으로 설정됩니다.

flags: 8비트 필드로, TCP의 여러 제어 플래그를 나타냅니다. SYN, ACK, FIN, RST 등의 플래그가 이 필드를 통해 설정됩니다.

window_size: 16비트 필드로, 수신 버퍼의 사용 가능 크기를 나타냅니다. 이는 흐름 제어를 위해 사용됩니다.

checksum: 16비트 필드로, 헤더와 데이터의 에러 검사를 위한 체크섬 값을 가집니다.

urgent_pointer: 16비트 필드로, 긴급 데이터의 위치를 가리키는 데 사용됩니다. 이 필드는 URG 플래그가 설정된 경우에만 유효합니다.

options: 가변 길이 필드로, 필요에 따라 추가적인 "옵션" 정보를 가집니다. 예를 들어, 최대 세그먼트 크기(MSS), 윈도우 크기 스케일링 등의 TCP 옵션을 설정할 수 있습니다.

 

코드에 대해 좀 더 자세히 설명하자면, 바이트 배열에서 이 헤더 정보를 추출하거나(from_bytes), TCP 헤더 정보를 바이트 배열로 변환하는데(to_bytes), 이때 모두 빅엔디언 바이트 순서를 사용합니다.

 

 

 

이러한 바이트 배열 변환 방식은 네트워크 통신에서 일반적으로 사용되며, 특히 TCP와 같은 프로토콜에서는 이러한 방식이 필수적입니다. 다양한 시스템 간의 네트워크 통신을 위해선 공통된 데이터 표현 방식이 필요하고, 그래서 바이트 순서, 데이터 구조 등을 정의한 네트워크 프로토콜이 중요한 역할을 합니다.

 

 

Thanks for watching, Have a nice day.

 

References

https://datatracker.ietf.org/doc/html/rfc793

반응형
Comments