1 điểm bởi GN⁺ 2025-03-05 | 1 bình luận | Chia sẻ qua WhatsApp
  • Đây là bước đầu tiên trong loạt bài tự xây dựng TCP/IP stack ở không gian người dùng trên Linux để tìm hiểu các tầng mạng, bắt đầu từ xử lý khung Ethernet ở layer 2 và phản hồi ARP
  • Lưu lượng mạng mức thấp của kernel được nhận qua thiết bị TAP, và thông qua file descriptor được trả về có thể read/write bộ đệm Ethernet của thiết bị ảo
  • Header Ethernet được xử lý với dmac, smac, ethertype, payload; khi giá trị ethertype từ 1536 trở lên thì là loại payload, còn nhỏ hơn thì biểu thị độ dài payload
  • ARP ánh xạ động địa chỉ giao thức như địa chỉ IPv4 sang địa chỉ MAC 48-bit, và được triển khai theo luồng phản hồi request đồng thời cập nhật bảng ánh xạ
  • Trong bài kiểm tra arping, phản hồi ARP từ stack tùy biến được kernel Linux nhận ra và thêm mục 10.0.0.4 vào ARP cache của giao diện tap0

Điểm khởi đầu của TCP/IP stack ở không gian người dùng

  • Mục tiêu là hiện thực một TCP/IP stack tối giản ở không gian người dùng trên Linux để hiểu sâu hơn về mạng và lập trình hệ thống
  • TCP đã trở nên phức tạp do hơn 30 năm tích lũy nhiều đặc tả, nhưng các thành phần cốt lõi có thể thu gọn thành phân tích header TCP, state machine, điều khiển tắc nghẽn và tính toán thời gian chờ truyền lại
  • Ethernet và IP có độ phức tạp thấp hơn TCP, vì vậy loạt bài bắt đầu hiện thực từ layer 2

Nhận lưu lượng Ethernet bằng thiết bị TAP

  • Để chặn lưu lượng mạng mức thấp của kernel Linux, bài viết sử dụng thiết bị Linux TAP
  • Thiết bị TUN/TAP thường được dùng khi các ứng dụng mạng ở không gian người dùng thao tác với lưu lượng L3/L2 tương ứng
    • Tunneling là cách bọc một gói tin vào trong payload của một gói tin khác
    • Các chương trình như OpenVPN cũng dùng thiết bị TUN/TAP
  • Vì xây dựng network stack từ layer 2 nên cần thiết bị TAP chứ không phải TUN
  • Thiết bị TAP được tạo bằng cách mở /dev/net/tap và gọi ioctl(fd, TUNSETIFF, ...)
    • IFF_TAP chọn thiết bị TAP
    • IFF_NO_PI ngăn việc gắn thêm thông tin gói tin không cần thiết trước khung Ethernet
  • Sau khi tạo, có thể dùng file descriptor fd được trả về để đọc và ghi bộ đệm Ethernet của thiết bị ảo

Định dạng khung Ethernet

  • Ethernet là công nghệ nền tảng kết nối các máy tính trong LAN; tiêu chuẩn Ethernet đầu tiên được Digital Equipment Corporation, Intel và Xerox công bố vào năm 1980
  • Phiên bản đầu tiên dùng tốc độ khoảng 10Mb/s và truyền thông bán song công, nên cần một giao thức MAC để điều phối luồng dữ liệu
    • Trong giao diện Ethernet bán song công, CSMA/CD là cơ chế MAC cần thiết
    • 100BASE-T sử dụng cáp twisted-pair để hỗ trợ truyền thông song công toàn phần và thông lượng cao hơn
    • Khi switch Ethernet trở nên phổ biến, nhu cầu đối với CSMA/CD phần lớn đã giảm đi
  • Tiêu chuẩn Ethernet do nhóm công tác IEEE 802.3 quản lý

Header Ethernet dùng trong phần hiện thực

  • Phần hiện thực bao gồm if_ether.h của Linux để dùng ánh xạ giữa ethertype và các giá trị thập lục phân
  • Header Ethernet được biểu diễn trong struct C với các trường sau
    • dmac: địa chỉ MAC đích
    • smac: địa chỉ MAC nguồn
    • ethertype: độ dài hoặc loại payload
    • payload: con trỏ payload chứa gói ARP hoặc IPv4
  • ethertypetrường 2 octet và ý nghĩa thay đổi theo giá trị
    • Nếu giá trị từ 1536 trở lên thì biểu thị loại payload như IPv4, ARP
    • Nếu nhỏ hơn thì biểu thị độ dài payload
  • Khung Ethernet có thể kèm thẻ biểu thị VLAN hoặc QoS, nhưng phần hiện thực này loại trừ frame tag
  • Nếu độ dài payload nhỏ hơn kích thước tối thiểu yêu cầu 48 byte khi không có tag, các byte đệm sẽ được thêm vào cuối
  • Cuối Ethernet Frame Format có trường Frame Check Sequence dùng CRC để kiểm tra tính toàn vẹn, nhưng phần hiện thực này không xử lý

Cách phân tích khung Ethernet

  • Thuộc tính packed trong khai báo struct ngăn trình biên dịch GNU C tối ưu layout bộ nhớ của struct bằng cách chèn các byte đệm để căn chỉnh dữ liệu
  • Việc phân tích trong phần hiện thực dùng cách ép kiểu bộ đệm sang struct giao thức phù hợp
    • Ví dụ: struct eth_hdr *hdr = (struct eth_hdr *) buf;
  • Cách có tính di động cao hơn là tuần tự hóa dữ liệu giao thức một cách thủ công
    • Trong trường hợp đó, trình biên dịch có thể thêm byte đệm để đáp ứng yêu cầu căn chỉnh dữ liệu theo từng bộ xử lý
  • Xử lý khung Ethernet nhận được đi theo một luồng đơn giản
    • Đọc bộ đệm từ thiết bị TAP
    • Khởi tạo header Ethernet bằng init_eth_hdr(buf)
    • handle_frame(&netdev, hdr) quyết định bước tiếp theo dựa trên giá trị ethertype

Cấu trúc và vai trò của gói ARP

  • ARP (Address Resolution Protocol) ánh xạ động địa chỉ giao thức như địa chỉ IPv4 sang địa chỉ MAC là địa chỉ Ethernet 48-bit
  • ARP không chỉ giới hạn ở IPv4 mà còn có thể dùng với nhiều giao thức L3 khác
    • Ví dụ, CHAOS khai báo địa chỉ giao thức 16-bit
  • Trong giao tiếp LAN thông thường, dù biết địa chỉ IP của dịch vụ thì vẫn cần địa chỉ MAC để truyền thực tế
  • ARP gửi truy vấn broadcast lên mạng để chủ sở hữu của địa chỉ IP tương ứng tự thông báo địa chỉ phần cứng của mình

Header ARP và payload cho IPv4

  • Header ARP gồm các trường sau
    • hwtype: trường 2 octet biểu thị loại liên kết, với Ethernet có giá trị 0x0001
    • protype: trường 2 octet biểu thị loại giao thức, với IPv4 có giá trị 0x0800
    • hwsize: trường 1 octet biểu thị kích thước địa chỉ phần cứng, địa chỉ MAC là 6 byte
    • prosize: trường 1 octet biểu thị kích thước địa chỉ giao thức, địa chỉ IPv4 là 4 byte
    • opcode: trường 2 octet biểu thị loại bản tin ARP
  • Giá trị opcode được chia thành bốn loại
    • ARP request: 1
    • ARP reply: 2
    • RARP request: 3
    • RARP reply: 4
  • Dữ liệu ARP cho IPv4 được xử lý bằng struct arp_ipv4
    • smac: địa chỉ MAC của bên gửi
    • sip: địa chỉ IP của bên gửi
    • dmac: địa chỉ MAC của bên nhận
    • dip: địa chỉ IP của bên nhận

Thuật toán phân giải địa chỉ và cache

  • Thuật toán phân giải địa chỉ trong RFC 826 hoạt động theo luồng kiểm tra loại phần cứng và loại giao thức, sau đó cập nhật bảng ánh xạ, rồi nếu địa chỉ đích là chính mình thì tạo phản hồi
  • translation table lưu kết quả ARP để host có thể tra cứu các mục đã có trong cache
  • Cache này giúp giảm việc mạng bị lấp đầy không cần thiết bởi các ARP request trùng lặp
  • Mã hiện thực nằm trong arp.c

Kiểm tra phản hồi ARP và bước tiếp theo

  • Bài kiểm tra cuối cùng của phần hiện thực ARP là xác nhận nó phản hồi đúng với request
  • Kết quả chạy arping -I tap0 10.0.0.4 cho thấy phản hồi unicast quay về từ 10.0.0.4 với địa chỉ MAC 00:0C:29:6D:50:25
  • Sau đó, trong đầu ra arp, xuất hiện mục 10.0.0.4 ether 00:0c:29:6d:50:25 tap0 trong ARP cache của kernel Linux
  • Chỉ với xử lý khung Ethernet tối thiểu và hiện thực ARP, có thể xác nhận thiết bị Ethernet tùy biến đã điền ARP cache của host Linux
  • Mã nguồn dự án có trên GitHub, và bước tiếp theo là hiện thực ICMP echo/reply cho ping cùng phân tích gói IPv4

1 bình luận

 
GN⁺ 2025-03-05
Ý kiến trên Hacker News
  • Vài năm trước, tôi từng tự làm một network stack ở user space bằng C, xử lý packet thô qua giao diện TUN và khiến nó chạy được ở mức nào đó
    Hiện giờ nó có một shell đơn giản để thiết lập địa chỉ IP, route, v.v., còn các packet mạng được chứa trong một cấu trúc lai trông như pha trộn giữa mbuf và sk_buf
    Tuy nhiên sau khi hoàn tất phần triển khai UDP, tôi không còn thời gian hay động lực để triển khai TCP nữa; mã nằm ở đây: https://github.com/cakturk/unet

    • Từ rất lâu trước đây, tôi từng viết một parser pcap/tcpdump bằng bash thuần, vì khi đó đó là công cụ duy nhất tôi biết có thể dùng để viết “chương trình”
      Dĩ nhiên nó gần như là thứ chậm nhất và dễ vỡ nhất trong lịch sử, nhưng nó thực sự chạy được và cũng khá vui. Hy vọng đoạn mã đó vẫn còn sót lại đâu đó
    • Rất nhiều thiết bị nhúng dùng lwip làm triển khai TCP/IP
      “POSIX port” của lwip cũng lấy các byte Ethernet thô từ thiết bị TUN/TAP theo cùng cách
      https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
  • Nếu biên dịch một Linux kernel tối thiểu không có TCP/IP stack thì nó là 400KB, còn thêm TCP/IP stack vào thì thành 800KB
    Trong một dự án chỉ cần gửi nhiệt độ, chúng tôi đưa giá trị vào các thông điệp UDP tự làm bằng một chương trình C nhỏ ở user space, nhờ đó giảm được nhiều dung lượng và độ phức tạp

    • Với một người không biết gì thì chuyện này khá đáng ngạc nhiên, nhưng tôi tự hỏi điều đó chắc không có nghĩa phần TCP/IP chiếm một nửa toàn bộ mã nguồn kernel chứ?
    • Tôi tò mò vì sao IP stack lại lớn đến vậy. Binary 400KB là khá nhiều mã; có phải vì nó được tối ưu hóa cao cho mục đích dùng trên các server lớn không?
  • Nếu tắt ARP, bạn có thể đặt cùng một IP cho nhiều server trong cùng mạng
    Nếu một server đóng vai trò frontend định tuyến có thể chuyển packet tới giao diện mạng của backend server dựa trên địa chỉ MAC, backend đó sẽ nhận ra mình là đích, đổi IP nguồn/đích rồi trả lời trực tiếp cho client. Khi đó nó không đi qua frontend định tuyến lần nữa
    Hoặc có thể không tắt ARP mà thêm địa chỉ IP chung làm alias trên giao diện loopback để đạt hiệu quả tương tự; backend vẫn nhận ra mình là đích đồng thời tránh xung đột ARP. Đây là mẹo mà bộ cân bằng tải phần mềm IBM WebSphere dùng trong thập niên 90–00

    • Cisco IOS SLB cũng có thể hoạt động tương tự. Cách làm là thêm IP ảo làm alias trên loopback của từng server trong server farm
      Điểm tốt hơn so với cân bằng tải L3 phổ biến hơn là không cần viết lại header của IP packet
    • Cách này còn được biết đến với tên DSR (Direct Server Return): https://www.haproxy.com/blog/layer-4-load-balancing-direct-s...
    • Nếu tắt ARP và đặt cùng một IP cho nhiều server trong cùng mạng, switch/bridge sẽ không học được địa chỉ MAC, nên sẽ liên tục flooding/broadcast packet ra tất cả các port trong segment đó
      Vì vậy nếu dùng cách này thì nên tạo một VLAN riêng
    • F5 có thiết lập ARP proxy, nên không cần làm như vậy. Nhược điểm là nó thường làm hỏng DHCP
    • Với những trò nghịch cấp thấp kiểu này, bạn cũng có thể thử DPDK. ARP mặc định bị tắt
  • Tôi từng làm một thứ tương tự bằng Python: https://github.com/georgek/notebooks/blob/master/internet.ip...
    Có lẽ chất lượng mã kém hơn, và nói thật là thuật toán phân giải địa chỉ cũng chỉ do tôi tự bịa ra. Tôi đã gửi ping tới host trên Internet bằng ICMP thành công
    Tôi thích điểm là toàn bộ nằm gọn trong một notebook ngắn. Bài gốc đã lược bỏ trong phần nội dung nhiều chi tiết nằm ở phần mã nguồn lớn hơn được tham chiếu
    Tôi không đọc bài này mà chỉ xem Wikipedia để làm. Tuy nhiên từ TCP trở đi độ phức tạp tăng vọt nên tôi hơi mất hứng. Nghe nói phần 3 sẽ nói về đoạn đó, nên có thể một ngày nào đó tôi sẽ đọc và hoàn tất. Nếu quan tâm đến networking, tôi nghĩ đây là việc đáng thử và đáng công với lập trình viên ở bất kỳ trình độ nào

  • Vài năm trước tôi làm về đo lường trong nhà máy điện hạt nhân. Phần phát triển phía client được làm trên workstation Sun, và thực ra tôi được tuyển nhờ kinh nghiệm TCP/IP; kinh nghiệm đó tôi có được từ môn “Hệ điều hành” ở CMU
    Trong khi đó, máy tính của nhà máy điện là minicomputer không có TCP/IP stack, nên nhóm đó phải tự làm stack

  • Khoảng 1 phút đầu bài có câu “dmac và smac là các trường khá hiển nhiên”, nhưng độc giả không biết chúng là gì có thể bỏ cuộc ngay
    Họ sẽ nghĩ: “Bài này dành cho những người thấy các trường này là hiển nhiên. Nó không dành cho mình, nên thôi không đọc nữa”

    • Cả câu là “dmac và smac là các trường khá hiển nhiên. Chúng chứa địa chỉ MAC của các bên giao tiếp (lần lượt là đích và nguồn)”, nên thực ra đã giải thích rồi
      Hơn nữa, nếu là bài viết về xây dựng network stack thì có thể an toàn giả định rằng độc giả biết về networking ở mức nào đó
    • Nếu không phải vừa mới được cập nhật, thì ngay câu tiếp theo đã giải thích “Chúng chứa địa chỉ MAC của các bên giao tiếp (lần lượt là đích và nguồn)”
  • Các bài liên quan:
    Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - tháng 6/2021, 49 bình luận
    Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - tháng 6/2018, 47 bình luận
    Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - tháng 7/2017, 30 bình luận
    Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - tháng 3/2016, 49 bình luận

  • Không rõ tác giả lấy địa chỉ IP 10.0.0.4 dùng trong bài kiểm thử phân giải ARP từ đâu
    Đó là địa chỉ của cái gì? Là một thiết bị giả có thể truy cập từ thiết bị Ethernet giả tạo ở đây, hay là một thiết bị thực sự có trong mạng của tác giả?

    • Bài viết không nói, nhưng đó là giá trị tác giả đã hard-code khi khởi tạo interface: https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85...
      Thiết bị TAP giống như một liên kết Ethernet được mô phỏng bằng phần mềm. Khi gửi packet vào đó, chúng được chuyển thẳng tới chương trình ở user space, và chương trình đó quyết định nó sẽ có địa chỉ IP nào cũng như phản hồi ARP ra sao
      Thông thường hệ điều hành xử lý những việc này, và cần quyền root để thêm địa chỉ IP cho interface. Việc mở thiết bị TAP cũng vậy. Networking nhìn chung vận hành theo kiểu hợp tác; một tác nhân độc hại có quyền root trong mạng có thể làm những việc xấu
  • Theo tôi nhớ thì ARP chỉ hoạt động trong segment cục bộ. Router sẽ điền địa chỉ của chính nó rồi chuyển tiếp packet
    Còn có cả rarp nữa, là một trong những cách để hỏi “mạng” địa chỉ IP của chính mình. Không rõ rarp hiện nay còn hoạt động trong môi trường thực tế hay không