1 คะแนน โดย GN⁺ 2025-03-05 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • เป็นขั้นแรกของซีรีส์ที่เรียนรู้เลเยอร์เครือข่ายด้วยการสร้าง TCP/IP stack ใน user space บน Linux เอง โดยเริ่มจากการจัดการเฟรม Ethernet ในเลเยอร์ 2 และการตอบกลับ ARP
  • ทราฟฟิกเครือข่ายระดับต่ำของเคอร์เนลรับเข้ามาผ่าน อุปกรณ์ TAP และสามารถ read/write บัฟเฟอร์ Ethernet ของอุปกรณ์เสมือนผ่าน file descriptor ที่ได้คืนมา
  • Ethernet header จัดการด้วย dmac, smac, ethertype, payload และถ้าค่า ethertype ตั้งแต่ 1536 ขึ้นไปจะหมายถึงชนิดของ payload แต่ถ้าน้อยกว่านั้นจะหมายถึงความยาวของ payload
  • ARP แมปที่อยู่โปรโตคอล เช่น ที่อยู่ IPv4 เข้ากับ ที่อยู่ MAC 48 บิต แบบไดนามิก และถูกใช้งานเป็นโฟลว์ที่อัปเดตตารางแปลงที่อยู่ขณะตอบกลับคำขอ
  • ในการทดสอบด้วย arping เคอร์เนล Linux รับรู้การตอบกลับ ARP ของ stack ที่สร้างเอง และเพิ่มรายการ 10.0.0.4 ลงใน ARP cache ของอินเทอร์เฟซ tap0

จุดเริ่มต้นของ TCP/IP stack ใน user space

  • เป้าหมายคือการทำความเข้าใจเครือข่ายและการเขียนโปรแกรมระบบให้ลึกขึ้น ผ่านการสร้าง TCP/IP stack ขั้นต่ำใน user space บน Linux
  • TCP มีความซับซ้อนเพราะมีสเปกสะสมมานานกว่า 30 ปี แต่ส่วนสำคัญของการใช้งานสามารถจำกัดให้เหลือการพาร์ส TCP header, state machine, congestion control และการคำนวณ retransmission timeout
  • Ethernet และ IP มีความซับซ้อนน้อยกว่า TCP ดังนั้นซีรีส์นี้จึงเริ่มสร้างจากเลเยอร์ 2

รับทราฟฟิก Ethernet ด้วยอุปกรณ์ TAP

  • ใช้ อุปกรณ์ Linux TAP เพื่อดักจับทราฟฟิกเครือข่ายระดับต่ำของเคอร์เนล Linux
  • อุปกรณ์ TUN/TAP มักถูกใช้เมื่อแอปพลิเคชันเครือข่ายใน user space ต้องจัดการทราฟฟิก L3/L2 ตามลำดับ
    • Tunneling คือวิธีห่อแพ็กเก็ตไว้ภายใน payload ของแพ็กเก็ตอื่น
    • โปรแกรมอย่าง OpenVPN ก็ใช้อุปกรณ์ TUN/TAP เช่นกัน
  • เนื่องจากจะสร้าง network stack ตั้งแต่เลเยอร์ 2 จึงต้องใช้ อุปกรณ์ TAP ไม่ใช่ TUN
  • อุปกรณ์ TAP สร้างโดยเปิด /dev/net/tap แล้วเรียก ioctl(fd, TUNSETIFF, ...)
    • IFF_TAP เลือกอุปกรณ์ TAP
    • IFF_NO_PI ทำให้ไม่มีข้อมูลแพ็กเก็ตที่ไม่จำเป็นถูกเติมไว้หน้าเฟรม Ethernet
  • หลังสร้างแล้ว จะอ่านและเขียน บัฟเฟอร์ Ethernet ของอุปกรณ์เสมือนได้ผ่าน file descriptor fd ที่ได้คืนมา

รูปแบบเฟรม Ethernet

  • Ethernet เป็นเทคโนโลยีพื้นฐานสำหรับเชื่อมต่อคอมพิวเตอร์ใน LAN และมาตรฐาน Ethernet แรกเผยแพร่ในปี 1980 โดย Digital Equipment Corporation, Intel และ Xerox
  • เวอร์ชันแรกมีความเร็วประมาณ 10Mb/s และใช้การสื่อสารแบบ half-duplex จึงต้องมีโปรโตคอล MAC เพื่อจัดการการไหลของข้อมูล
    • ในอินเทอร์เฟซ Ethernet แบบ half-duplex ต้องใช้ CSMA/CD เป็นวิธี MAC
    • 100BASE-T ใช้สาย twisted-pair เพื่อให้สื่อสารแบบ full-duplex และมี throughput สูงขึ้น
    • เมื่อ Ethernet switch แพร่หลาย ความจำเป็นของ CSMA/CD โดยรวมจึงลดลง
  • มาตรฐาน Ethernet ดูแลโดยคณะทำงาน IEEE 802.3

Ethernet header ที่ใช้ในการใช้งาน

  • การใช้งานนี้ include if_ether.h ของ Linux เพื่อใช้การแมประหว่าง ethertype กับค่าเลขฐานสิบหก
  • Ethernet header ถูกแทนใน C struct ด้วยฟิลด์ต่อไปนี้
    • dmac: ที่อยู่ MAC ปลายทาง
    • smac: ที่อยู่ MAC ต้นทาง
    • ethertype: ความยาวหรือชนิดของ payload
    • payload: pointer ไปยัง payload ที่บรรจุแพ็กเก็ต ARP หรือ IPv4
  • ethertype เป็น ฟิลด์ขนาด 2 octet และความหมายเปลี่ยนไปตามค่า
    • ถ้าค่าตั้งแต่ 1536 ขึ้นไป จะแสดงชนิดของ payload เช่น IPv4, ARP
    • ถ้าค่าน้อยกว่านั้น จะแสดงความยาวของ payload
  • เฟรม Ethernet อาจมี tag ที่แสดง VLAN หรือ QoS แต่การใช้งานนี้ตัด frame tag ออก
  • หากความยาว payload น้อยกว่า 48 ไบต์ ซึ่งเป็นขนาดขั้นต่ำที่ต้องการเมื่อไม่มี tag จะมี padding byte ต่อท้าย
  • ท้าย Ethernet Frame Format มีฟิลด์ Frame Check Sequence ที่ใช้ CRC เพื่อตรวจสอบความสมบูรณ์ แต่การใช้งานนี้ไม่ประมวลผลส่วนนั้น

วิธีพาร์สเฟรม Ethernet

  • คุณสมบัติ packed ในการประกาศ struct ทำให้ GNU C compiler ไม่ปรับ layout หน่วยความจำของ struct ด้วย padding byte สำหรับการจัดแนวข้อมูล
  • การพาร์สในการใช้งานนี้ทำโดย type casting บัฟเฟอร์เป็น struct ของโปรโตคอลที่เหมาะสม
    • ตัวอย่าง: struct eth_hdr *hdr = (struct eth_hdr *) buf;
  • วิธีที่พกพาได้มากกว่าคือ serialize ข้อมูลโปรโตคอลด้วยตัวเอง
    • ในกรณีนี้ compiler อาจเติม padding byte ให้ตรงกับข้อกำหนดการจัดแนวข้อมูลเฉพาะของโปรเซสเซอร์
  • การประมวลผลเฟรม Ethernet ที่รับเข้ามาเป็นไปตามโฟลว์ง่าย ๆ
    • อ่านบัฟเฟอร์จากอุปกรณ์ TAP
    • เริ่มต้น Ethernet header ด้วย init_eth_hdr(buf)
    • handle_frame(&netdev, hdr) ดูค่า ethertype แล้วตัดสินใจว่าจะทำอะไรต่อ

โครงสร้างและบทบาทของแพ็กเก็ต ARP

  • ARP(Address Resolution Protocol) แมปที่อยู่โปรโตคอล เช่น ที่อยู่ IPv4 เข้ากับที่อยู่ MAC ซึ่งเป็นที่อยู่ Ethernet ขนาด 48 บิตแบบไดนามิก
  • ARP ไม่ได้จำกัดเฉพาะ IPv4 แต่สามารถใช้ร่วมกับโปรโตคอล L3 ได้หลายชนิด
    • ตัวอย่างเช่น CHAOS ประกาศที่อยู่โปรโตคอลขนาด 16 บิต
  • ในการสื่อสาร LAN ทั่วไป แม้จะรู้ที่อยู่ IP ของบริการ แต่การส่งจริงต้องใช้ที่อยู่ MAC
  • ARP ส่งคำถามแบบ broadcast ไปยังเครือข่าย เพื่อให้เจ้าของที่อยู่ IP นั้นประกาศที่อยู่ฮาร์ดแวร์ของตน

ARP header และ payload สำหรับ IPv4

  • ARP header ประกอบด้วยฟิลด์ต่อไปนี้
    • hwtype: ฟิลด์ 2 octet ที่แสดงชนิดของ link layer โดยค่าของ Ethernet คือ 0x0001
    • protype: ฟิลด์ 2 octet ที่แสดงชนิดของโปรโตคอล โดยค่าของ IPv4 คือ 0x0800
    • hwsize: ฟิลด์ 1 octet ที่แสดงขนาดที่อยู่ฮาร์ดแวร์ โดยที่อยู่ MAC มีขนาด 6 ไบต์
    • prosize: ฟิลด์ 1 octet ที่แสดงขนาดที่อยู่โปรโตคอล โดยที่อยู่ IPv4 มีขนาด 4 ไบต์
    • opcode: ฟิลด์ 2 octet ที่แสดงชนิดข้อความ ARP
  • ค่า opcode แบ่งเป็นสี่แบบ
    • ARP request: 1
    • ARP reply: 2
    • RARP request: 3
    • RARP reply: 4
  • ข้อมูล ARP สำหรับ IPv4 จัดการด้วย struct 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 ได้รับการตอบกลับแบบ unicast จาก 10.0.0.4 ด้วยที่อยู่ MAC 00:0C:29:6D:50:25
  • จากนั้นในผลลัพธ์ของ arp มีรายการ 10.0.0.4 ether 00:0c:29:6d:50:25 tap0 เกิดขึ้นใน ARP cache ของเคอร์เนล Linux
  • เพียงการจัดการเฟรม Ethernet ขั้นต่ำและการใช้งาน ARP ก็สามารถยืนยันได้ว่าอุปกรณ์ Ethernet ที่สร้างเองเติม ARP cache ของโฮสต์ Linux ได้
  • ซอร์สโค้ดของโปรเจกต์อยู่บน GitHub และขั้นต่อไปคือการใช้งาน ping ซึ่งเป็น ICMP echo/reply และการพาร์สแพ็กเก็ต IPv4

1 ความคิดเห็น

 
GN⁺ 2025-03-05
ความเห็นจาก Hacker News
  • เมื่อหลายปีก่อนเคยลองทำ network stack ใน user space ด้วยภาษา C และทำให้มันทำงานได้ระดับหนึ่งโดยประมวลผลแพ็กเก็ตดิบผ่านอินเทอร์เฟซ TUN
    ตอนนี้มีเชลล์แบบง่ายที่ใช้ตั้งค่า IP address, route ฯลฯ ได้ และเก็บแพ็กเก็ตเครือข่ายไว้ในโครงสร้างไฮบริดที่เหมือนผสม mbuf กับ sk_buf
    แต่หลังจากทำ UDP เสร็จ ก็ไม่มีเวลาหรือแรงใจจะไปทำ TCP ต่อ โค้ดอยู่ที่นี่: https://github.com/cakturk/unet

    • นานมากแล้ว เคยเขียน parser สำหรับ pcap/tcpdump ด้วย bash ล้วนๆ ตอนนั้นเครื่องมือที่ผมรู้จักพอจะใช้เขียน “โปรแกรม” ได้ก็มีแค่นั้น
      แน่นอนว่ามันน่าจะใกล้เคียงกับของที่ช้าที่สุดและเปราะบางที่สุดในประวัติศาสตร์ แต่ใช้งานได้จริงและก็สนุกพอสมควร หวังว่าโค้ดนั้นจะยังหลงเหลืออยู่ที่ไหนสักแห่ง
    • อุปกรณ์ embedded จำนวนมากใช้ lwip เป็น implementation ของ TCP/IP
      “POSIX port” ของ lwip ก็ใช้วิธีเดียวกัน คือดึง raw Ethernet bytes จากอุปกรณ์ TUN/TAP
      https://github.com/lwip-tcpip/lwip/blob/master/contrib/ports...
  • ถ้าคอมไพล์ Linux kernel ขั้นต่ำ โดยไม่มี TCP/IP stack จะได้ขนาด 400KB และถ้าใส่ TCP/IP stack เข้าไปจะเป็น 800KB
    ในโปรเจกต์ที่แค่ต้องส่งอุณหภูมิ ผมใช้โปรแกรม C เล็กๆ ใน user space ส่งค่าผ่านข้อความ UDP ที่ทำเองโดยตรง จึงลดพื้นที่และความซับซ้อนได้มาก

    • ในมุมของคนที่ไม่รู้อะไรเลย นี่ค่อนข้างน่าตกใจ แต่ก็สงสัยว่าไม่ได้หมายความว่าส่วน TCP/IP คือ ครึ่งหนึ่งของซอร์สโค้ดทั้ง kernel ใช่ไหม
    • สงสัยว่าทำไม IP stack ถึงใหญ่ขนาดนั้น ไบนารี 400KB นี่ก็ถือว่ามีโค้ดเยอะทีเดียว เป็นเพราะถูก optimize หนักมากให้เหมาะกับการใช้งานบนเซิร์ฟเวอร์ขนาดใหญ่หรือเปล่า?
  • ถ้า ปิดใช้งาน ARP ก็สามารถตั้ง IP เดียวกันให้กับหลายเซิร์ฟเวอร์ในเครือข่ายเดียวกันได้
    หากเซิร์ฟเวอร์ที่ทำหน้าที่เป็น routing frontend สามารถส่งแพ็กเก็ตไปยังอินเทอร์เฟซเครือข่ายของ backend server ตาม MAC address ได้ backend นั้นก็จะรับรู้ว่าตัวเองคือปลายทาง แล้วเปลี่ยน source/destination IP เพื่อตอบกลับ client โดยตรงได้ โดยไม่ต้องย้อนผ่าน routing frontend อีก
    หรือจะไม่ปิด ARP แต่เพิ่ม IP address ร่วมเป็น alias บน loopback interface ก็ให้ผลแบบเดียวกันได้ ทำให้ backend รับรู้ว่าตัวเองเป็นปลายทางและหลีกเลี่ยง ARP conflict ได้ นี่เป็นทริกที่ซอฟต์แวร์ load balancer ของ IBM WebSphere ในยุค 90–00 ใช้

    • Cisco IOS SLB ก็ทำงานคล้ายกันได้ โดยเพิ่ม virtual IP เป็น alias ให้กับ loopback ของแต่ละเซิร์ฟเวอร์ใน server farm
      ข้อดีกว่า load balancing แบบ L3 ที่ใช้กันแพร่หลายกว่าคือไม่ต้องเขียน IP packet header ใหม่
    • สิ่งนี้ยังรู้จักกันในชื่อ DSR (Direct Server Return): https://www.haproxy.com/blog/layer-4-load-balancing-direct-s...
    • ถ้าปิดใช้งาน ARP แล้วตั้ง IP เดียวกันให้กับหลายเซิร์ฟเวอร์ในเครือข่ายเดียวกัน สวิตช์/บริดจ์จะ เรียนรู้ MAC address ไม่ได้ และจะคอย flooding/broadcast แพ็กเก็ตไปยังทุกพอร์ตใน segment นั้น
      ดังนั้นถ้าจะใช้วิธีนี้ ควรสร้าง VLAN เฉพาะ
    • F5 มี การตั้งค่า ARP proxy จึงไม่ต้องทำแบบนี้ ข้อเสียคือมักทำให้ DHCP พัง
    • สำหรับการเล่นระดับล่างแบบนี้ จะลองจับ DPDK ดูก็ได้ ARP ถูกปิดไว้โดยค่าเริ่มต้น
  • เคยลองทำอะไรคล้ายๆ กันด้วย Python: https://github.com/georgek/notebooks/blob/master/internet.ip...
    โค้ดน่าจะคุณภาพด้อยกว่า และพูดตรงๆ อัลกอริทึม address resolution ก็แต่งขึ้นเองเฉยๆ แต่ทำสำเร็จถึงขั้น ping ไปยังโฮสต์บนอินเทอร์เน็ตด้วย ICMP ได้
    ชอบตรงที่มันอยู่ครบใน notebook สั้นๆ อันเดียว บทความต้นทางละรายละเอียดจำนวนมากในซอร์สโค้ดที่ใหญ่กว่าซึ่งอ้างถึงไว้
    ตอนทำไม่ได้เห็นบทความนี้ ดูแค่ Wikipedia เท่านั้น แต่พอถึง TCP ความซับซ้อนกระโดดขึ้นมากจนความสนใจลดลงนิดหน่อย เห็นว่าภาค 3 จะพูดถึงส่วนนั้น บางทีวันหนึ่งอาจอ่านแล้วทำให้จบ ถ้าสนใจ networking ผมคิดว่านี่เป็นงานที่โปรแกรมเมอร์ทุกระดับควรลองทำและคุ้มค่า

  • เมื่อหลายปีก่อนเคยทำงานเกี่ยวกับ instrumentation ของโรงไฟฟ้านิวเคลียร์ งานฝั่ง client development ทำบน Sun workstation และที่จริงได้รับการจ้างเพราะมีประสบการณ์ TCP/IP ซึ่งได้มาจากวิชา “Operating Systems” ที่ CMU
    แต่คอมพิวเตอร์ของโรงไฟฟ้าเป็น minicomputer ที่ไม่มี TCP/IP stack ทีมจึงต้องสร้าง stack เอง

  • ช่วงประมาณ 1 นาทีหลังเริ่มบทความ มีประโยคว่า “dmac และ smac เป็นฟิลด์ที่ค่อนข้างชัดเจนในตัวเอง” แต่ผู้อ่านที่ไม่รู้ว่ามันคืออะไรอาจหลุดออกไปทันที
    ทำให้คิดว่า “บทความนี้เขียนสำหรับคนที่ฟิลด์พวกนี้ชัดเจนอยู่แล้ว ไม่ใช่สำหรับฉัน คงควรเลิกอ่าน”

    • ประโยคเต็มคือ “dmac และ smac เป็นฟิลด์ที่ค่อนข้างชัดเจนในตัวเองครับ มันเก็บ MAC address ของฝ่ายที่สื่อสารกัน (ปลายทางและต้นทางตามลำดับ)” ดังนั้นจริงๆ แล้วก็อธิบายอยู่
      อีกอย่าง ถ้าเป็น บทความเกี่ยวกับการสร้าง network stack ก็ถือว่าปลอดภัยที่จะสมมติว่าผู้อ่านรู้เรื่อง networking อยู่บ้าง
    • ถ้าไม่ได้เพิ่งอัปเดตไปเมื่อกี้ ประโยคถัดมาก็อธิบายว่า “มันเก็บ MAC address ของฝ่ายที่สื่อสารกัน (ปลายทางและต้นทางตามลำดับ)”
  • บทความที่เกี่ยวข้อง:
    Let’s code a TCP/IP stack (2016) - https://news.ycombinator.com/item?id=27654182 - มิถุนายน 2021, ความคิดเห็น 49 รายการ
    Let’s code a TCP/IP stack, 1: Ethernet & ARP (2016) - https://news.ycombinator.com/item?id=17316487 - มิถุนายน 2018, ความคิดเห็น 47 รายการ
    Let’s Code a TCP/IP Stack: TCP Retransmission - https://news.ycombinator.com/item?id=14701199 - กรกฎาคม 2017, ความคิดเห็น 30 รายการ
    Let’s code a TCP/IP stack, 1: Ethernet and ARP - https://news.ycombinator.com/item?id=11234229 - มีนาคม 2016, ความคิดเห็น 49 รายการ

  • ไม่รู้ว่าผู้เขียนเอา IP address 10.0.0.4 ที่ใช้ในการทดสอบการ resolve ARP มาจากไหน
    มันเป็น address ของอะไร? เป็นอุปกรณ์ปลอมที่เข้าถึงได้จากอุปกรณ์ Ethernet ปลอมที่สร้างขึ้นตรงนี้ หรือเป็นอุปกรณ์ที่มีอยู่จริงในเครือข่ายของผู้เขียน?

    • ในบทความไม่ได้ระบุไว้ แต่เป็นค่าที่ผู้เขียน hardcode ไว้ตอน initialize interface: https://github.com/saminiir/level-ip/blob/e9ceb08f01a5499b85...
      อุปกรณ์ TAP เป็นเหมือนลิงก์ Ethernet ที่จำลองด้วยซอฟต์แวร์ ถ้าส่ง packet ไปที่นั่น มันจะถูกส่งตรงไปยังโปรแกรมระดับผู้ใช้ และโปรแกรมนั้นจะเป็นผู้กำหนดว่าจะมี IP address อะไร และจะตอบสนองต่อ ARP อย่างไร
      โดยปกติระบบปฏิบัติการจะจัดการเรื่องแบบนี้ และการเพิ่ม IP address ให้กับ interface ต้องใช้สิทธิ์ root การเปิดอุปกรณ์ TAP ก็เช่นกัน โดยรวมแล้ว networking ทำงานแบบอาศัยความร่วมมือ และผู้ไม่ประสงค์ดีที่มีสิทธิ์ root บนเครือข่ายก็สามารถทำเรื่องไม่ดีได้
  • เท่าที่จำได้ ARP ทำงานเฉพาะใน local segment เท่านั้น router จะใส่ address ของตัวเองแล้ว forward packet ต่อไป
    นอกจากนี้ยังมี rarp ด้วย ซึ่งเป็นหนึ่งในวิธีถาม “เครือข่าย” ว่า IP address ของตัวเองคืออะไร ไม่แน่ใจว่า rarp ยังใช้งานได้จริงในสภาพแวดล้อมปัจจุบันหรือไม่