1P by GN⁺ | ★ favorite | 댓글 1개
  • Linux에서 사용자 공간 TCP/IP 스택을 직접 만들며 네트워크 계층을 배우는 시리즈의 첫 단계로, 레이어 2의 이더넷 프레임 처리와 ARP 응답부터 구현함
  • 커널의 저수준 네트워크 트래픽은 TAP 장치로 받아오며, 반환된 파일 디스크립터를 통해 가상 장치의 이더넷 버퍼를 read/write할 수 있음
  • 이더넷 헤더는 dmac, smac, ethertype, payload로 다루고, ethertype 값이 1536 이상이면 페이로드 타입, 그보다 작으면 페이로드 길이를 뜻함
  • ARP는 IPv4 주소 같은 프로토콜 주소를 48비트 MAC 주소에 동적으로 매핑하며, 요청에 응답하면서 변환 테이블을 갱신하는 흐름으로 구현됨
  • arping 테스트에서 커스텀 스택의 ARP 응답을 Linux 커널이 인식해 tap0 인터페이스의 ARP 캐시10.0.0.4 항목이 추가됨

사용자 공간 TCP/IP 스택의 출발점

  • 목표는 Linux에서 최소 사용자 공간 TCP/IP 스택을 구현하면서 네트워크와 시스템 프로그래밍을 더 깊게 이해하는 것임
  • TCP는 30년 넘게 여러 명세가 쌓여 복잡하지만, 핵심 구현 요소는 TCP 헤더 파싱, 상태 머신, 혼잡 제어, 재전송 타임아웃 계산으로 좁혀볼 수 있음
  • 이더넷과 IP는 TCP보다 복잡도가 낮아, 시리즈는 레이어 2부터 구현을 시작함

TAP 장치로 이더넷 트래픽 받기

  • Linux 커널의 저수준 네트워크 트래픽을 가로채기 위해 Linux TAP 장치를 사용함
  • TUN/TAP 장치는 사용자 공간 네트워킹 애플리케이션이 각각 L3/L2 트래픽을 조작할 때 자주 쓰임
    • 터널링은 패킷을 다른 패킷의 페이로드 안에 감싸는 방식임
    • OpenVPN 같은 프로그램도 TUN/TAP 장치를 사용함
  • 레이어 2부터 네트워크 스택을 만들기 때문에 TUN이 아니라 TAP 장치가 필요함
  • TAP 장치는 /dev/net/tap을 열고 ioctl(fd, TUNSETIFF, ...)로 생성함
    • IFF_TAP은 TAP 장치를 선택함
    • IFF_NO_PI는 이더넷 프레임 앞에 불필요한 패킷 정보가 붙지 않게 함
  • 생성 후 반환된 파일 디스크립터 fd로 가상 장치의 이더넷 버퍼를 읽고 씀

이더넷 프레임 형식

  • 이더넷은 LAN에서 컴퓨터를 연결하는 기반 기술이며, 첫 이더넷 표준은 1980년에 Digital Equipment Corporation, Intel, Xerox가 발표함
  • 첫 버전은 약 10Mb/s 속도와 반이중 통신을 사용했기 때문에 데이터 흐름을 조정하는 MAC 프로토콜이 필요했음
    • 반이중 이더넷 인터페이스에서는 CSMA/CD가 MAC 방식으로 필요함
    • 100BASE-T는 twisted-pair 배선을 사용해 전이중 통신과 더 높은 처리량을 가능하게 함
    • 이더넷 스위치가 널리 보급되면서 CSMA/CD의 필요성은 대체로 줄어듦
  • 이더넷 표준은 IEEE 802.3 작업 그룹이 관리함

구현에 쓰는 이더넷 헤더

  • 구현은 Linux의 if_ether.h를 포함해 ethertype과 16진수 값의 매핑을 사용함
  • 이더넷 헤더는 C 구조체에서 다음 필드로 표현됨
    • dmac: 목적지 MAC 주소
    • smac: 출발지 MAC 주소
    • ethertype: 페이로드 길이 또는 타입
    • payload: ARP 또는 IPv4 패킷을 담는 페이로드 포인터
  • ethertype2옥텟 필드이며 값에 따라 의미가 달라짐
    • 값이 1536 이상이면 IPv4, ARP 같은 페이로드 타입을 나타냄
    • 값이 그보다 작으면 페이로드 길이를 나타냄
  • 이더넷 프레임에는 VLAN이나 QoS를 나타내는 태그가 붙을 수 있지만, 이 구현에서는 프레임 태그를 제외함
  • 페이로드 길이가 태그 없이 최소 요구 크기인 48바이트보다 작으면 끝에 패딩 바이트가 붙음
  • Ethernet Frame Format 끝에는 CRC로 무결성을 확인하는 Frame Check Sequence 필드가 있지만, 이 구현에서는 처리하지 않음

이더넷 프레임 파싱 방식

  • 구조체 선언의 packed 속성은 GNU C 컴파일러가 데이터 정렬용 패딩 바이트로 구조체 메모리 레이아웃을 최적화하지 않게 함
  • 구현의 파싱은 버퍼를 적절한 프로토콜 구조체로 타입 캐스팅하는 방식임
    • 예: struct eth_hdr *hdr = (struct eth_hdr *) buf;
  • 더 이식성 있는 방식은 프로토콜 데이터를 수동으로 직렬화하는 것임
    • 이 경우 컴파일러가 프로세서별 데이터 정렬 요구에 맞춰 패딩 바이트를 추가할 수 있음
  • 수신 이더넷 프레임 처리는 단순한 흐름을 따름
    • TAP 장치에서 버퍼를 읽음
    • init_eth_hdr(buf)로 이더넷 헤더를 초기화함
    • handle_frame(&netdev, hdr)ethertype 값을 보고 다음 동작을 결정함

ARP 패킷 구조와 역할

  • ARP(Address Resolution Protocol) 는 IPv4 주소 같은 프로토콜 주소를 48비트 이더넷 주소인 MAC 주소에 동적으로 매핑함
  • ARP는 IPv4에만 한정되지 않고 여러 L3 프로토콜과 함께 사용할 수 있음
    • 예시로 CHAOS는 16비트 프로토콜 주소를 선언함
  • 일반적인 LAN 통신에서는 서비스의 IP 주소를 알고 있어도 실제 전송에는 MAC 주소가 필요함
  • ARP는 네트워크에 브로드캐스트 질의를 보내 해당 IP 주소의 소유자가 자신의 하드웨어 주소를 알리게 함

ARP 헤더와 IPv4용 페이로드

  • ARP 헤더는 다음 필드로 구성됨
    • hwtype: 링크 계층 타입을 나타내는 2옥텟 필드이며, 이더넷의 값은 0x0001
    • protype: 프로토콜 타입을 나타내는 2옥텟 필드이며, IPv4의 값은 0x0800
    • hwsize: 하드웨어 주소 크기를 나타내는 1옥텟 필드이며, MAC 주소는 6바이트
    • prosize: 프로토콜 주소 크기를 나타내는 1옥텟 필드이며, IPv4 주소는 4바이트
    • opcode: ARP 메시지 타입을 나타내는 2옥텟 필드
  • opcode 값은 네 가지로 구분됨
    • ARP request: 1
    • ARP reply: 2
    • RARP request: 3
    • RARP reply: 4
  • IPv4용 ARP 데이터는 arp_ipv4 구조체로 다룸
    • smac: 송신자 MAC 주소
    • sip: 송신자 IP 주소
    • dmac: 수신자 MAC 주소
    • dip: 수신자 IP 주소

주소 해석 알고리듬과 캐시

  • RFC 826의 주소 해석 알고리듬은 하드웨어 타입과 프로토콜 타입을 확인한 뒤 변환 테이블을 갱신하고, 대상 주소가 자신이면 응답을 만드는 흐름임
  • translation table은 ARP 결과를 저장해 호스트가 이미 보유한 항목을 캐시에서 조회할 수 있게 함
  • 이 캐시는 중복 ARP 요청으로 네트워크가 불필요하게 채워지는 일을 줄여줌
  • 구현 코드는 arp.c에 있음

ARP 응답 테스트와 다음 단계

  • ARP 구현의 최종 테스트는 요청에 올바르게 응답하는지 확인하는 것임
  • arping -I tap0 10.0.0.4 실행 결과 10.0.0.4에서 00:0C:29:6D:50:25 MAC 주소로 유니캐스트 응답이 돌아옴
  • 이후 arp 출력에서 Linux 커널의 ARP 캐시에 10.0.0.4 ether 00:0c:29:6d:50:25 tap0 항목이 생김
  • 최소 이더넷 프레임 처리와 ARP 구현만으로도 커스텀 이더넷 장치가 Linux 호스트의 ARP 캐시를 채우는 결과를 확인할 수 있음
  • 프로젝트 소스 코드는 GitHub에 있으며, 다음 단계는 ICMP echo/reply인 ping과 IPv4 패킷 파싱 구현임

댓글과 토론

Hacker News 의견들
  • 몇 년 전 C로 사용자 공간 네트워크 스택을 만들어 본 적이 있고, TUN 인터페이스로 원시 패킷을 처리해서 어느 정도 동작하게 했음
    지금은 IP 주소, 라우트 등을 설정할 수 있는 간단한 셸이 들어 있고, 네트워크 패킷은 mbuf와 sk_buf를 섞은 듯한 하이브리드 구조에 담음
    다만 UDP 구현을 끝낸 뒤 TCP를 구현할 시간이나 의욕을 못 냈고, 코드는 여기 있음: https://github.com/cakturk/unet

    • 아주 오래전에 pcap/tcpdump 파서를 순수 bash로 짠 적이 있는데, 그때는 “프로그램”을 쓸 줄 아는 도구가 그것뿐이었음
      당연히 역사상 가장 느리고 깨지기 쉬운 물건에 가까웠지만 실제로 동작했고 꽤 재미도 있었음. 그 코드가 아직 어딘가에 남아 있으면 좋겠음
    • 많은 임베디드 장치가 TCP/IP 구현으로 lwip를 사용함
      lwip의 “POSIX 포트”도 같은 방식으로 TUN/TAP 장치에서 원시 Ethernet 바이트를 가져옴
      https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
  • 최소 Linux 커널을 TCP/IP 스택 없이 컴파일하면 400KB이고, TCP/IP 스택을 넣으면 800KB가 됨
    온도만 보내면 되는 프로젝트에서는 사용자 공간의 작은 C 프로그램으로 직접 만든 UDP 메시지에 값을 담아 보냈고, 공간과 복잡도를 많이 줄일 수 있었음

    • 아무것도 모르는 입장에서는 꽤 놀라운데, 그렇다고 TCP/IP 부분이 커널 전체 소스 코드의 절반이라는 뜻은 아니지 않나 궁금함
    • 왜 IP 스택이 그렇게 큰지 궁금함. 400KB 바이너리면 코드가 꽤 많은데, 대형 서버 용도에 맞춰 고도로 최적화되어 있어서 그런 건가?
  • ARP를 비활성화하면 같은 네트워크의 여러 서버에 같은 IP를 설정할 수 있음
    라우팅 프런트엔드 역할의 서버가 MAC 주소 기준으로 백엔드 서버의 네트워크 인터페이스에 패킷을 전달할 수 있다면, 그 백엔드는 자신이 목적지라고 인식하고 출발지/목적지 IP를 바꿔 클라이언트에 직접 응답할 수 있음. 이때 라우팅 프런트엔드를 다시 거치지 않음
    또는 ARP를 끄지 않고 공통 IP 주소를 loopback 인터페이스의 별칭으로 추가해도 같은 효과를 낼 수 있고, 백엔드가 자신을 목적지로 인식하면서 ARP 충돌을 피할 수 있음. 90~00년대 IBM WebSphere 소프트웨어 로드밸런서가 쓰던 트릭이었음

    • Cisco IOS SLB도 비슷하게 동작할 수 있음. 서버 팜의 각 서버 loopback에 가상 IP를 별칭으로 추가하는 방식임
      더 널리 쓰이는 L3 부하분산보다 좋은 점은 IP 패킷 헤더를 다시 쓸 필요가 없다는 것임
    • 이것은 DSR(Direct Server Return) 로도 알려져 있음: https://www.haproxy.com/blog/layer-4-load-balancing-direct-s...
    • ARP를 비활성화하고 같은 네트워크의 여러 서버에 같은 IP를 설정하면, 스위치/브리지가 MAC 주소를 학습하지 못해 해당 세그먼트의 모든 포트로 패킷을 계속 플러딩/브로드캐스트하게 됨
      그래서 이 방식을 쓴다면 전용 VLAN을 만드는 게 좋음
    • F5에는 ARP 프록시 설정이 있어서 이렇게 하지 않아도 됨. 단점은 DHCP를 깨뜨리는 경우가 많다는 것임
    • 이런 저수준 장난에는 DPDK도 만져볼 수 있음. ARP는 기본적으로 비활성화되어 있음
  • 비슷한 걸 Python으로 해 본 적이 있음: https://github.com/georgek/notebooks/blob/master/internet.ip...
    아마 코드 품질은 덜 좋고, 솔직히 주소 해석 알고리즘도 그냥 지어냈음. ICMP로 인터넷 호스트에 ping을 보내는 데까지는 성공했음
    짧은 노트북 하나 안에 완전히 들어 있다는 점은 마음에 듦. 원 글은 참조된 더 큰 소스 코드에 있는 많은 세부사항을 본문에서 생략함
    이 글은 못 보고 Wikipedia만 보고 만들었음. 다만 TCP부터는 복잡도가 크게 뛰어서 흥미가 좀 식었음. 3부가 그 부분을 다룬다니 언젠가 읽고 마무리할지도 모르겠음. 네트워킹에 관심이 있다면 어느 수준의 프로그래머든 해볼 만하고 보람 있는 작업이라고 봄

  • 몇 년 전 원자력 발전소 계측 일을 했음. 클라이언트 쪽 개발은 Sun 워크스테이션에서 했고, 실제로 TCP/IP 경험 덕분에 채용됐는데 그 경험은 CMU의 “운영체제” 수업에서 얻었음
    반면 발전소 컴퓨터는 TCP/IP 스택이 없는 미니컴퓨터라서, 그 팀은 직접 스택을 만들어야 했음

  • 글 시작 1분쯤에 “dmac과 smac은 꽤 자명한 필드”라고 하는데, 그게 뭔지 모르는 독자는 바로 떨어져 나갈 수 있음
    “이 글은 이 필드가 자명한 사람들을 위한 글이구나. 나를 위한 글이 아니니 그만 읽어야겠다”라고 생각하게 됨

    • 전체 문장은 “dmac과 smac은 꽤 자명한 필드입니다. 통신 당사자의 MAC 주소를 담습니다(각각 목적지와 출발지)”라서 실제로 설명하고 있음
      게다가 네트워크 스택을 만드는 글이라면 독자가 네트워킹에 대해 어느 정도는 알고 있다고 가정해도 안전함
    • 방금 갱신된 게 아니라면 바로 다음 문장에서 “통신 당사자의 MAC 주소를 담습니다(각각 목적지와 출발지)”라고 설명함
  • 관련 글들:
    Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - 2021년 6월, 댓글 49개
    Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - 2018년 6월, 댓글 47개
    Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - 2017년 7월, 댓글 30개
    Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - 2016년 3월, 댓글 49개

  • 글쓴이가 ARP 해석 테스트에 쓰는 10.0.0.4 IP 주소를 어디서 가져온 건지 모르겠음
    무엇의 주소라는 건가? 여기서 만든 가짜 Ethernet 장치에서 접근 가능한 가짜 장치인가, 아니면 글쓴이 네트워크에 실제로 있는 장치인가?

    • 글에는 나오지 않지만, 인터페이스 초기화 때 글쓴이가 하드코딩해 둔 값임: https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85...
      TAP 장치는 소프트웨어로 에뮬레이션한 Ethernet 링크 같은 것임. 패킷을 거기에 보내면 사용자 수준 프로그램으로 직접 전달되고, 어떤 IP 주소를 가질지와 ARP에 어떻게 응답할지는 그 프로그램이 정함
      보통은 이런 일을 운영체제가 처리하고, 인터페이스에 IP 주소를 추가하려면 root 권한이 필요함. TAP 장치를 여는 것도 마찬가지임. 네트워킹은 대체로 협력적으로 돌아가며, 네트워크에서 root 권한을 가진 악의적 행위자는 나쁜 짓을 할 수 있음
  • 기억하기로 ARP는 로컬 세그먼트에서만 동작함. 라우터가 자기 주소를 채워 넣고 패킷을 전달함
    또 rarp도 있는데, “네트워크”에 자기 IP 주소를 물어보는 방법 중 하나임. rarp가 지금도 실제 환경에서 동작하는지는 모르겠음