1 คะแนน โดย GN⁺ 2024-03-14 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Fly.io เปลี่ยนมาใช้วิธีเพิ่ม peer เข้าไปในเคอร์เนลตอนที่มีการเชื่อมต่อ โดยไม่ติดตั้ง peer ไว้ล่วงหน้า เพื่อลดภาระด้าน state ของ WireGuard gateway ขณะยังคงการสื่อสารโดยตรงระหว่าง flyctl กับ Fly Machines
  • โฟลว์เดิมคือ GraphQL API ส่งการตั้งค่า peer ผ่าน NATS RPC แล้ว wggwd ลงทะเบียนไว้ใน SQLite และ Linux kernel WireGuard จากนั้น flyctl จึงเชื่อมต่อ
  • เมื่อ ข้อความ NATS สูญหาย มาซ้อนกับการสร้าง peer แบบใช้ครั้งเดียวของงาน CI ทำให้มี peer ที่ไม่ถูกนำกลับมาใช้ซ้ำสะสมบน gateway หลายแสนรายการ และทำให้การทำงานของเคอร์เนลกับการโหลดตอนรีบูตช้าลง
  • วิธีใหม่จับแพ็กเก็ต handshake initiation จาก BPF filter หรือเส้นทางรับ WebSockets แล้วถอดรหัสบางส่วนของ Noise handshake เพื่อระบุ public key จากนั้นดึงเฉพาะ peer ที่จำเป็นผ่าน HTTP API ภายใน
  • หลังนำไปใช้จริงไม่กี่สัปดาห์ จำนวน peer เก่าแทบหายไป และ gateway สามารถจัดการการตั้งค่า peer กับการรีบูตได้เร็วขึ้นโดยมี state น้อยลง

วิธีที่ Fly.io ใช้ WireGuard

  • Fly.io รันคอนเทนเนอร์เป็น VM ที่ใช้ Firecracker และใช้ WireGuard ในหลายจุดราวกับเป็นส่วนหนึ่งของ API สำหรับลูกค้า
  • เมื่อ flyctl ทำงาน มันจะสร้าง TCP/IP stack ที่มีที่อยู่ IPv6 ของตัวเอง และสื่อสารโดยตรงกับ Fly Machines ในเครือข่ายของ Fly.io
  • แนวทางนี้ทำให้ฟีเจอร์อย่าง remote Docker builder ถูกนำเสนอได้ง่ายเหมือนอยู่ใน LAN เดียวกัน แต่การทำให้รันต่อเนื่องอย่างเสถียรนั้นยากกว่า
  • ในท้ายที่สุด Fly.io จึงเปลี่ยนเส้นทางหลักเป็น WireGuard-over-WebSockets

โฟลว์ provisioning ของ gateway แบบเดิม

  • Fly.io เชื่อมต่อการเชื่อมต่อ WireGuard ที่เข้ามายังเซิร์ฟเวอร์ gateway หลายแห่งทั่วโลกเข้ากับ private network ที่เหมาะสม
  • เมื่อ flyctl ต้องสื่อสารกับ Fly Machine เพื่อ build คอนเทนเนอร์, เปิด SSH console, คัดลอกไฟล์ หรือ proxy service มันจะรันหรือเชื่อมต่อกับโปรเซส agent ในเบื้องหลัง
  • เมื่อ agent รันครั้งแรก จะสร้างการตั้งค่า WireGuard peer ใหม่จาก GraphQL API
    • การตั้งค่า peer ประกอบด้วย public key และที่อยู่ที่จะเชื่อมโยง
  • API ส่งการตั้งค่านั้นไปยัง gateway ที่เหมาะสมผ่าน RPC ของระบบ messaging NATS
  • wggwd บน gateway รับการตั้งค่า บันทึกลง SQLite เพิ่มเข้าเคอร์เนลผ่านไลบรารี WireGuard Go แล้วตอบ API ว่าติดตั้งเสร็จแล้ว
  • เมื่อ API ส่งการตั้งค่ากลับในคำขอ GraphQL แล้ว flyctl จะเชื่อมต่อด้วย WireGuard peer ที่ติดตั้งอยู่บน gateway แล้ว

ทำไมโครงสร้างเดิมจึงช้าลง

  • NATS เร็ว แต่ไม่รับประกันการส่งมอบ จึงยากที่จะใช้เป็นฐาน API ที่เชื่อถือได้
    • Fly.io ลดการใช้ NATS ภายในลง เช่น API ภายใน flyd ถูกเปลี่ยนจากแบบที่ใช้ NATS เป็นแบบที่ใช้ HTTP
    • การลดการใช้ NATS ทำให้ WireGuard gateway ดีขึ้น แต่ยังไม่เพียงพอ
  • WireGuard peer ที่สร้างหลังจาก flyctl จบการทำงานยังคงอยู่บน gateway และไม่มีกระบวนการล้าง peer เก่า
    • มีเหตุผลที่เลือกไม่ลบ peer เพราะผู้ใช้อาจกลับมา deploy อีกครั้งในวันถัดไป หรือ debug ด้วย fly ssh console
    • แต่ peer ส่วนใหญ่ถูกสร้างจาก งาน CI ที่ไม่มี persistent storage และไม่สามารถเชื่อมต่อกลับด้วย peer เดิมในการรันครั้งถัดไปได้ จึงสร้าง peer ใหม่ทุกครั้ง
  • ผลคือ gateway มี peer หลายแสนรายการที่อาจไม่ถูกใช้ซ้ำ
    • เมื่อจำนวน peer เก่าเพิ่มขึ้น การทำงานของ kernel WireGuard จึงช้ามาก
    • โดยเฉพาะขั้นตอนโหลด peer ทั้งหมดกลับเข้าเคอร์เนลหลังรีบูตเซิร์ฟเวอร์ gateway นั้นช้ามาก
    • ยังเกิด kernel panic บางครั้งด้วย

การออกแบบให้ติดตั้ง peer ลงเคอร์เนลเฉพาะเมื่อจำเป็น

  • การเก็บประวัติ WireGuard peer ทั้งหมดไว้ใน SQLite เดียวไม่ใช่เรื่องยาก แต่การคง peer ทั้งหมดไว้ใน Linux kernel กลายเป็นคอขวด
  • Fly.io เลือกให้ gateway ดึง peer ตามต้องการ จาก API แทนการ push การตั้งค่าไปยัง gateway
  • หากเพิ่ม peer เข้าเคอร์เนลเฉพาะเมื่อ client พยายามเชื่อมต่อ peer เก่าก็สามารถถูกลบออกจากเคอร์เนลได้ทุกเมื่อ
  • แม้ peer ที่ถูกลบไปแล้วก็เพียงดึงกลับมาและติดตั้งใหม่ในการเชื่อมต่อครั้งถัดไป จึงลดความจำเป็นที่ gateway ต้องถือ state ระยะยาวไว้ตลอด
  • อย่างไรก็ตาม Linux kernel WireGuard ไม่มี API สำหรับ subscribe event “incoming connection attempt”

วิธี implement JIT WireGuard peer

  • อินเทอร์เฟซการตั้งค่า WireGuard ของ Linux kernel คือ Netlink และไลบรารีควบคุม WireGuard Go ใช้ wgctrl-go
  • Fly.io ใช้ข้อเท็จจริงที่ว่าคำขอเชื่อมต่อ WireGuard เป็นแพ็กเก็ตที่ระบุได้ แล้วสร้าง event เองโดยตรงด้วย BPF filter และ packet socket
  • ในเส้นทาง WebSockets WireGuard จะดึงแพ็กเก็ต WireGuard ดิบได้ง่ายกว่า
    • เส้นทางนี้รับส่งแพ็กเก็ต UDP ที่ถูก framed ผ่านการเชื่อมต่อ WebSockets แบบไม่ต้อง authentication กับอินเทอร์เฟซ gateway
    • Fly.io เป็นเจ้าของโค้ด daemon นี้ จึงสามารถ hook เข้ากับฟังก์ชันรับแพ็กเก็ตได้
  • WireGuard ไม่มีแนวคิด “client” และ “server” แต่เป็น โปรโตคอล point-to-point ที่ peer เชื่อมต่อกันเมื่อมีทราฟฟิกจะส่ง
    • ฝั่งที่เริ่มเชื่อมต่อก่อนคือ initiator ส่วนอีกฝั่งคือ responder
    • ใน Fly.io โดยทั่วไป flyctl เป็น initiator และ gateway เป็น responder
  • แพ็กเก็ต UDP แรกตามเอกสาร WireGuard คือ handshake initiation และประเภทแพ็กเก็ตถูกบันทึกเป็นข้อความธรรมดา 1 ไบต์
    • Fly.io จับการเชื่อมต่อขาเข้าด้วย BPF filter udp and dst port 51820 and udp[8] = 1

การระบุ peer จาก Noise handshake

  • WireGuard ใช้พื้นฐานจาก Noise Protocol Framework และ Noise ซ่อนตัวระบุระหว่าง handshake เพื่อ identity hiding
  • ดังนั้นจึงใช้วิธีอ่านค่าบางอย่างอย่างชื่อผู้ใช้จากแพ็กเก็ตเพื่อค้นหาการตั้งค่าโดยตรงไม่ได้
  • Fly.io รันบางส่วนของการเข้ารหัส Noise เพื่อถอดรหัส identity และระบุคำขอขาเข้า
    • โค้ดนี้ค่อนข้างละเอียดอ่อน แต่มีประมาณ 200 บรรทัด
    • อินเทอร์เฟซ Netlink ของเคอร์เนลสามารถให้ private key ของอินเทอร์เฟซแก่โปรเซสที่มีสิทธิ์ได้ จึงได้ค่าลับที่จำเป็น
    • โค้ดที่เกี่ยวข้องถูกเผยแพร่ไว้ใน gist
  • เมื่อผ่านขั้นตอนนี้ จะได้ feed event ของ public key ของผู้ใช้ที่พยายามเชื่อมต่อ WireGuard ไปยัง gateway

การปรับแต่งการติดตั้ง แคช และการลองซ้ำ

  • gateway เก็บ rate-limit cache ไว้ใน SQLite และเมื่อพบ peer ใหม่ จะเรียก HTTP API ภายในเพื่อดึงข้อมูล peer ที่สอดคล้องกันมาติดตั้ง
  • ลอจิกนี้เข้ากันได้ดีกับ daemon ขนาดเล็กเดิมที่จัดการ WireGuard บน gateway
  • peer เก่าสามารถถูกลบเชิงรุกได้ด้วยงาน cron
  • การ query API สำหรับ peer ใหม่อาจไม่เร็วพอที่จะตอบกลับข้อความ handshake initiation แรกได้ทันที
    • WireGuard retry อย่างรวดเร็ว จึงไม่มีปัญหาต่อการทำงานเอง
  • ใช้ฟีเจอร์ Linux WireGuard Netlink ที่ Jason Donenfeld แนะนำเพื่อทำให้การเชื่อมต่อเกิดขึ้นได้เร็วขึ้น
    • จากข้อความ initiation ขาเข้า จะได้ 4-tuple address รวมถึง source port ชั่วคราวของ flyctl
    • gateway ติดตั้ง peer ราวกับว่าตัวเองเป็น initiator และ flyctl เป็น responder
    • Linux kernel เริ่มการเชื่อมต่อ WireGuard ไปยังฝั่ง flyctl และโปรโตคอลไม่ได้พึ่งพาบทบาท server/client มากนัก
    • การเชื่อมต่อใหม่เกิดขึ้นได้ใกล้เคียงกับความเร็วที่ติดตั้งได้

ผลลัพธ์หลังนำไปใช้งานจริง

  • วิธีนี้รันอยู่ใน production มาเป็นเวลาหลายสัปดาห์
  • จำนวน WireGuard peer เก่าที่เคยมีตั้งแต่หลักพันถึงหลักแสนต่อ gateway ลดลงจนแทบเป็น 0
  • state ที่ gateway ต้องถือไว้ลดลง
  • การตั้งค่า peer เร็วขึ้น
  • ตอนรีบูต ไม่จำเป็นต้องโหลด peer ที่ไม่ได้ใช้งานกลับเข้าเคอร์เนลมากเท่าเดิม

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

 
GN⁺ 2024-03-14
ความคิดเห็นบน Hacker News
  • ผมไม่ค่อยเข้าใจคำบอกที่ว่า WireGuard ในเคอร์เนล Linux ไม่มี ความสามารถในการติดตั้ง peer เมื่อจำเป็น เพราะดูเหมือนว่าสามารถเพิ่ม peer ระหว่างรันไทม์ได้อยู่: https://serverfault.com/questions/1101002/wireguard-client-a...
    ถ้าผมเข้าใจถูก จุดนั้นก็สายไปแล้ว และดูเหมือนเขาต้องการยืนยันตัวตนก่อนเพิ่ม peer เพื่อไม่ให้มีรายการเก่าค้างอยู่บนอินเทอร์เฟซ
    ดังนั้นโครงสร้างที่เห็นจึงเหมือนวาง eBPF filter ไว้หน้าอินเทอร์เฟซ แล้วลองเชื่อมต่อเองก่อนว่าเป็นคู่สื่อสารที่ได้รับอนุมัติตามการ route ด้วยคีย์เข้ารหัสหรือไม่ จากนั้นถ้าผ่านจึงค่อยเพิ่ม peer เข้าอินเทอร์เฟซ และลบออกเมื่อหมดเวลา

    • ท้ายที่สุดสิ่งที่ต้องการคือ Netlink API ที่ให้เคอร์เนล WireGuard ส่งต่อรายการ public key ที่เห็นจาก initiator message ออกมา ในระยะกลางดูเหมือน Jason ก็อยากมีความสามารถแบบนี้เช่นกัน และถ้ามี feed นี้ ก็ไม่จำเป็นต้องติดตั้ง WireGuard peer ล่วงหน้าแม้แต่ตัวเดียว
      peer ทั้งหมดอาจเก็บไว้ใน SQLite หรือที่คล้ายกัน แล้วค่อยติดตั้งเมื่อไคลเอนต์พยายามเชื่อมต่อ
      ในมุมของผู้ให้บริการ VPN API ปัจจุบันค่อนข้างหยาบไปหน่อย แม้ในทางปฏิบัติจะมีเพียงบางส่วนของ peer เท่านั้นที่ถูกใช้งาน ณ ช่วงเวลาใดช่วงเวลาหนึ่ง แต่ถ้าจำนวน peer โตจากหลักแสนไปเป็นหลายล้าน การเก็บทั้งหมดไว้ในเคอร์เนลอินสแตนซ์เดียวก็แทบเป็นไปไม่ได้
      ถ้าต้องติดตั้ง peer ไว้ล่วงหน้า สุดท้ายก็จะผูกติดกับเครื่องเซิร์ฟเวอร์เฉพาะตัว
      อย่างที่บทความบอก ทุกวันนี้ก็ยังสร้างอะไรคล้ายอินเทอร์เฟซที่ต้องการได้ด้วยการดักแพ็กเก็ตแบบง่าย ๆ และเพราะ Jason ออกแบบ API ไว้ดีมาก จึงสลับทิศทางการเริ่มต้นระหว่างเซิร์ฟเวอร์กับไคลเอนต์ได้ง่ายมาก
      แม้เคอร์เนลจะทิ้งข้อความเริ่มต้นตัวแรกไป ผู้ใช้ก็ยังรู้สึกเหมือนเชื่อมต่อได้อย่างลื่นไหล
      Jann Horn ไปไกลกว่านั้นอีก โดยบอกว่าสามารถเก็บ แพ็กเก็ตเริ่มต้น ที่ดักจับไว้ แล้วฉีดกลับเข้าเคอร์เนลหลังติดตั้ง peer เสร็จได้ ซึ่งก็ดูเป็นไอเดียที่ดีมาก
      ผมไม่คิดว่านี่เป็นอะไรที่เปลี่ยนชีวิต แต่ออกจะเป็นทริกเรียบ ๆ หลายอย่างที่คนทั่วไปน่าจะอยากรู้มากกว่า
      ขั้นถัดไปคือใช้สิ่งนี้สร้าง floating peers เพื่อแยก peer ออกจาก locality อย่างสมบูรณ์ แบบนั้นผู้ใช้ก็ไม่ต้องสนใจว่า peer ถูกตั้งไว้ใน region ไหน และนี่น่าจะให้ประโยชน์เชิงผลิตภัณฑ์จริง ไม่ได้มีไว้สนุกสำหรับสายเนิร์ดอย่างเดียว
    • ดูเหมือนทำแบบนี้เพื่อเลี่ยงทางเลือกที่ต้องรัน WireGuard นอกเคอร์เนล เพราะ Linux kernel ไม่มีความสามารถในการ route ไปยังที่อยู่เข้ารหัสก่อน แต่ก็ไม่อยากออกจากเคอร์เนล เลยเหมือนแฮ็กยัดเข้าไปแทน
      คำว่า JIT WireGuard ฟังดูแปลกนิดหน่อย ความคิดแรกของผมคือ “ทำไมล่ะ? คอขวดด้านประสิทธิภาพคือการเข้ารหัส และ JIT รายไคลเอนต์ก็ไม่น่าจะช่วยเรื่องนั้น”
      ถ้าเป็นผมคงไปทาง user space ตรง ๆ ใช้พวก tokio-uring หรือ glommio เพื่อดึงประสิทธิภาพเอา
      ถ้ายังฝืนดันทุกอย่างไว้ในเคอร์เนลต่อไป ก็คงชนข้อจำกัดเรื่อย ๆ เพราะ Linux ไม่ได้ถูกสร้างมาเพื่อรองรับ tunnel ที่ active พร้อมกันเป็นหลักล้าน แม้แต่การมี TCP connection หลักล้านบนเคอร์เนลเดียวก็ยังยุ่งยากเป็นบางครั้ง
      ทุกครั้งที่ชนลิมิตก็ต้องมีแฮ็กใหม่ และทุกแฮ็กก็ตามมาด้วย system setting ที่ต้องปรับใช้และดูแล เครื่องมือสาย provisioning สำหรับ Linux physical server ยังตามหลังเครื่องมือพัฒนาแอปและบริการ รวมถึงเครื่องมือจัดการคอนฟิก อยู่มาก
      หรือว่าผมโง่เองและเข้าใจอะไรผิดไป?
  • ถ้าอยากสร้าง WireGuard peer แบบ user space ในแอป Go ลองดูโปรเจ็กต์ทดลองล่าสุด https://github.com/dpeckett/noisysockets ได้
    มันต่อยอดจากงานอันยอดเยี่ยมของ wireguard-go แต่พยายามทำให้เรียบง่ายและเป็น Go มากขึ้นสำหรับการใช้งานในฐานะไลบรารี
    ถ้าเอาไปทำ service mesh ก็น่าจะน่าสนใจ แม้อาจรองรับหลายภาษาได้ยาก แต่ก็น่าจะทำ socket API ขึ้นมาได้
    เพียงแต่ผมยังไม่เห็น hardware acceleration สำหรับการเข้ารหัสของ WireGuard ดังนั้นในแง่ประสิทธิภาพ มันอาจแข่งกับ mTLS ได้ยาก
    อนึ่ง ตอนนี้ผมกำลังหางานฟรีแลนซ์อยู่ ถ้าต้องการ Golang ฟรีแลนซ์สายเครือข่ายความเร็วสูงและความปลอดภัย ก็ติดต่อมาได้

    • ผมมีความฝันว่าจะหยิบโปรเจ็กต์ WireGuard แบบ user space มา แล้วใช้ PAKE แลกเปลี่ยนคีย์ WireGuard ผ่าน relay ด้านหน้า ก่อนจะทำ hole punching เพื่อสร้าง tunnel ตรงกันภายหลัง
      มันจะเหมือน Magic Wormhole สำหรับ tunnel ตามต้องการ และหวังว่าจะช่วยปรับปรุงปัญหาที่การส่งไฟล์บนเครือข่ายระยะไกลแบนด์วิดท์สูงมักตกจาก 20~30 MB/s ลงฮวบได้มากด้วย
    • ผมสงสัยว่า Noisy Transport คล้ายกับ Nebula ของ Slack [0] อยู่พอสมควรหรือเปล่า หรือผมกำลังสับสนเอง
      0 - https://github.com/slackhq/nebula
  • โดยรวมผมเห็นด้วยว่าการส่งข้อความแบบจุดต่อจุดเดี่ยว บางครั้งใช้ HTTP request ตรง ๆ อาจเชื่อถือได้กว่าการผ่าน message queue แต่ก็น่าแปลกใจอยู่บ้างที่ ข้อความใน NATS สูญหายมากขนาดนั้น จนส่งผลกระทบต่อบริการอย่างมีนัยสำคัญ
    ถ้าข้อความหาย NATS ไม่น่าจะส่งซ้ำจนกว่าจะสำเร็จหรือ? อยากรู้ว่ามีใครทราบไหมว่าทำไมถึงเจอความไม่เสถียรที่รู้สึกได้ขนาดนั้น

    • ผมอยากรู้รายละเอียดเพิ่มมาก ๆ คิดว่าผู้ดูแล NATS ก็น่าจะเหมือนกัน
      สถาปัตยกรรมของ NATS นั้นเข้าใจง่ายและน่าสนใจ แต่ก็ชวนสงสัยว่ามันพลาดตรงไหน JetStream มีพารามิเตอร์ที่ปรับจูนได้เยอะมาก
      เช่น memory stream ที่มีหน้าต่างตรวจจับข้อมูลซ้ำตามเวลา, รูปแบบ push/pull, รวมถึงการตั้งค่านโยบาย retry และ acknowledgment
      แต่ก็อาจไม่เหมาะกับ การเชื่อมต่อข้อความเดี่ยวแบบชั่วครั้งชั่วคราว ไม่ว่าจะอย่างไร ถ้ามีรายละเอียดที่เฉพาะเจาะจงกว่านี้ก็น่าจะมีประโยชน์มาก
    • ไม่ได้จะด่า NATS นะ มีโอกาสมากที่เราจะใช้งานมันผิดเอง
      แต่สุดท้ายเราก็ไม่ได้ต้องการมัน เพราะชั้นข้อความไม่ได้เพิ่ม expressive power ให้เรา กลับมีแต่ทำให้การทดสอบและการมอนิเตอร์ยากขึ้น
    • ถ้าใช้ core NATS เท่าที่ผมรู้ มันไม่ใช่ JetStream ดังนั้น จะไม่มีตัวเลือกการส่งซ้ำเลย
  • ส่วนที่บอกว่า “เราติดตั้ง peer ราวกับว่าเราเป็น initiator และให้ flyctl เป็น responder จากนั้น Linux kernel จะเริ่มการเชื่อมต่อ WireGuard ใหม่ไปทางฝั่ง flyctl” นี่เท่ากับเพิ่ม half-RTT ให้กับ handshake โดยพฤตินัยหรือเปล่า?
    ตัวอย่างเช่นเป็นลำดับแบบ 1) flyctl ส่ง Initiation, 2) เพิ่ม peer ผ่าน netlink แล้วส่ง Initiation ใหม่, 3) flyctl ส่ง Response กลับมา อะไรทำนองนั้นหรือเปล่า

    • เท่าที่ผมอ่านคือทั้งสองฝั่งต่างก็ “คิด” ว่าตัวเองเป็นฝ่ายเริ่ม แต่ในทางปฏิบัติดูเหมือนจะไม่สำคัญ
      กล่าวคืออาจไม่ต้องมีหรือไม่ต้องรอขั้นตอนที่ 3 ก็ได้ และถ้ากันไม่ให้มีการ initiate ใหม่ในขั้นตอนที่ 2 ก็น่าจะเป็นแบบนั้นแน่
    • โดยรวมก็ใช่ ลองคิดว่า “Bob” มีนโยบายว่าจะคุยโทรศัพท์ได้เฉพาะกับเบอร์ที่อยู่ในสมุดรายชื่อเท่านั้น
      1. Alice โทรหา Bob
        1.a) Bob ไม่รับสาย แต่เพิ่มเบอร์จาก caller ID ลงในสมุดรายชื่อ
      2. Bob โทรกลับไปที่เบอร์นั้น คือ Alice
      3. Alice รับ แล้วทั้งคู่ก็คุยกันอย่างมีความสุข
  • ไม่ค่อยเข้าใจว่าประโยคที่ว่า “ทุกครั้งที่รัน flyctl, CLI อันน่ารักและขนาดมหึมาของเราจะสร้าง TCP/IP stack ขึ้นมาจากอากาศ มี IPv6 address ของตัวเอง และคุยกับ Fly Machines ที่รันอยู่ในเครือข่ายของเราได้โดยตรง” หมายถึงอะไร

    • โดยพื้นฐานแล้วหมายถึงใช้ WireGuard ใน user space แบบ implementation ของ Go อะไรทำนองนั้น ซึ่งตรงข้ามกับ WireGuard ใน kernel
      ที่พูดว่า “สร้าง TCP/IP stack ขึ้นมาจากอากาศ” เพราะปกติแล้วระบบปฏิบัติการจะให้ TCP/IP stack มาเป็นส่วนหนึ่งของ kernel
      แต่ใน wireguard-go นั้น TCP/IP stack รันอยู่ใน user space เลยสร้างมันขึ้นมาใน process user space ทั่วไปอย่าง command-line interface ของ flyctl ได้
      สำหรับคนที่ทำงานกับระบบมานาน มันอาจดูเหมือนเวทมนตร์พอสมควร จริง ๆ แล้ว TCP/IP stack แบบ user space ที่ใช้งานได้จริงภายใน process เพิ่งถือว่าใหม่และแปลกใหม่พอสมควร
    • มีเขียนถึงเรื่องนี้แยกทั้งบทความ: https://fly.io/blog/our-user-mode-wireguard-year/
    • หมายถึงมันใช้ WireGuard
    • ยังนึกภาพ CLI ที่ทั้งมหึมาและน่ารักไม่ค่อยออก
  • สงสัยว่าอะไรเป็นตัวกันไม่ให้ inject แพ็กเก็ต handshake แรกกลับเข้าไปใน network stack แบบนั้นก็น่าจะไม่มี packet loss
    แล้วก็สงสัยด้วยว่าจุดประสงค์ของการเช็ก udp[8] = 1 ใน eBPF filter คืออะไร

    • ไม่มีอะไรห้าม เป็นไอเดียที่ดี
      อย่างที่บอกในคอมเมนต์ข้าง ๆ BPF filter แค่ดักจับ initiation packet ซึ่งนั่นคือพฤติกรรมที่ต้องการ เป็นเวอร์ชันของ WireGuard ที่เทียบได้กับการ sniff SYN เพื่อดูการเริ่มต้นของ TCP connection
    • udp[8] = 1 ใช้ กรองเฉพาะแพ็กเก็ต handshake ถ้าไม่มีเงื่อนไขนี้ data packet ก็จะถูกส่งไปยัง daemon ใน user space ด้วย
      ไม่แน่ใจว่าจะ replay handshake แรกได้ไหม แต่เนื่องจาก WireGuard จะเมิน client ที่ไม่รู้จัก มันก็อาจทำได้
    • ฟังดูเหมือน NFQUEUE helper ที่ปล่อยแพ็กเก็ตผ่านหลังจากเพิ่ม key แล้ว
  • น่าสนใจที่ค่าเริ่มต้นคือทำ tunnel WireGuard บน WebSocket ถึงจะไม่ดีต่อประสิทธิภาพนัก แต่ก็น่าจะโอเคสำหรับงานสาย DevOps ที่ flyctl ใช้
    ตอนคิดถึงอนาคตของ QUIC/HTTP3 ผมก็สงสัยเรื่องทำนองนี้เหมือนกัน ว่าผู้ดูแลเครือข่ายอาจไม่ได้จัดการ UDP 443 ให้ดี แต่อาจเลือกบล็อกไปเลยก็ไม่ใช่ว่าจะเป็นไปไม่ได้

    • ใช้ WireGuard แบบ native ได้แน่นอน และใน flyctl ก็มีตัวเลือกตั้งค่า
      ถ้า UDP ใช้ไม่ได้ มันก็คือใช้ไม่ได้เลยและ debug ก็ยาก ดังนั้นค่าเริ่มต้นเลยตั้งเป็นทางที่เรารู้แน่ว่าทำงานได้
      แอบขมขื่นนิดหน่อยที่ผมแพ้ในข้อถกเถียงเรื่องจะเลือกค่าเริ่มต้นแบบไหน
  • สตาร์ทอัพของผมใช้ Fly มาเกือบ 1 ปี ฟีเจอร์หลักที่ทำให้โค้ดกลายเป็นโค้ดที่ deploy แล้วได้ภายในไม่ถึง 1 นาทีนั้นสวยงามจริง ๆ
    การยก node ใหม่ขึ้นมาแล้วปิดลงเพื่อทำ backfill ก็ใช้เวลาแค่ไม่กี่วินาที
    แต่ตัวบริษัทเองให้ความรู้สึกว่ายังไม่ค่อยนิ่งนัก ครั้งหนึ่ง API server เข้าใช้งานจาก Fly ไม่ได้อยู่ 48 ชั่วโมง และผมก็ไม่แน่ใจว่านั่นเป็นเพราะตั้งค่าพลาดเองหรือเป็น outage แบบ “เงียบ ๆ” อีกครั้ง
    มีผลิตภัณฑ์ “db” แต่ก็ออกแนว “ไม่ใช่ managed Postgres นะ” และฝั่งนั้นก็มีอาการสะดุดอยู่เรื่อย ๆ
    รู้สึกแปลกที่เอา Postgres มาใส่เป็นคำนามระดับบนสุดใน CLI แต่กลับจำกัดขอบเขตฟีเจอร์ที่รองรับ
    แม้แต่การเข้าถึง service API หลักก็ล่มบ่อยจนต้องรอ deploy แก้ service ใหม่
    ผมคิดถึงประสบการณ์การ deploy แบบนั้นอยู่ แต่พูดตามตรงตอนนี้พอใจกับ GCP Cloud Run มากกว่า เพราะมี “เรื่องให้ประหลาดใจ” น้อยกว่ามาก และเอกสารก็สมบูรณ์กว่ามาก

    • ประสบการณ์การ deploy นั้นยอดเยี่ยม แต่สำหรับผมฟีเจอร์เด็ดของ Fly.io คือ เครือข่าย Anycast กับความสามารถอย่าง FLY_REPLAY และ LiteFS สิ่งพวกนี้ทำให้การทำคลัสเตอร์ง่ายมาก
      น่าแปลกที่ผู้ให้บริการ VPS แทบไม่มีการช่วยลด latency ของ backend service ให้ผู้ใช้เลย ไม่มีเจ้าไหนรองรับ Anycast และตัวเลือก GeoDNS ก็มีน้อยมาก
      แต่ GeoDNS ก็เพิ่มความซับซ้อนอีกแบบหนึ่ง
      ผมอยากให้ค่า data transfer ของ Fly.io ถูกกว่านี้ ตอนนี้เลยตกอยู่ในสถานการณ์ที่ต้องค่อย ๆ ทำฟีเจอร์จำนวนมากของ Fly.io ขึ้นใหม่แบบงุ่มง่ามในบริการคล้าย ngrok ที่กำลังทำอยู่
      [0]: https://lastlogin.io
      [1]: โค้ดเฉพาะของ Fly ที่ต้องใช้เพื่อรัน LastLogin แบบกระจายทั่วโลกมีประมาณนี้: https://github.com/lastlogin-io/obligator/blob/37f75cc861f1b...
    • Fly ดูดีนะ แต่ยังไม่เคยมีโอกาสได้ใช้เอง อย่างไรก็ตาม Cloud Run ของ GCP เป็นหนึ่งในสามเครื่องมือ infra/deployment ที่ผมชอบที่สุดอยู่แล้ว ก็เลยถือว่าตั้งมาตรฐานไว้ค่อนข้างสูง
    • ผมมีประสบการณ์เกือบเหมือนกันเลย ใช้ Fly มา 1 ปีแล้วเพิ่งย้ายไป GCP เมื่อเดือนสองเดือนก่อน และในกรณีของเราเลือก GKE ด้วยเหตุผลบางอย่าง
      ตอนที่มันทำงานดีมันลื่นมากจริง ๆ แต่ความถี่ของช่วงเวลาดี ๆ นั้นยังไม่มากพอ
  • อยากถือโอกาสนี้แนะนำ Netmaker[0]
    ไม่ได้เป็นผู้เกี่ยวข้องอะไร แค่เป็นคนที่ใช้อย่างพอใจเพราะต้องการเข้าถึง AWS VPC แบบ private ข้ามหลายแอ็กเคานต์ และอยากให้มีการนำไปใช้กันแพร่หลายกว่านี้
    [0] https://www.netmaker.io/

    • Netmaker คล้าย Tailscale เหรอ? ดูจากเว็บไซต์แล้วก็ยังไม่ค่อยเข้าใจว่า จุดแตกต่าง คืออะไร
    • ดูเหมือนว่า Netmaker หรือเครื่องมือคล้าย ๆ กันจะช่วยจัดการคีย์แทนให้ ซึ่งถ้าเป็นแบบนั้นก็น่าจะทำให้ดูแลง่ายขึ้นมาก
      ที่ทำงานก่อนหน้านี้ผมเคยใช้ Ansible ตั้งค่าและดูแล wg บน Windows กับ Linux อยู่ไม่กี่เครื่อง ซึ่งก็โอเค แต่พอท้าย ๆ แล้วเริ่มเละนิดหน่อย
    • หรือว่าจะทำแบบ เนทีฟของ AWS ด้วย private link หรือ VPC peering ไม่ได้เหรอ? ผมไม่ค่อยรู้ด้านนี้ เลยยังไม่เข้าใจข้อดีของ Netmaker
    • มันเป็นแพลตฟอร์ม VPN ทั่วไปหรือเปล่า? อยากรู้ว่าคล้ายกับ Tailscale ไหม
      เว็บไซต์คลุมเครือเกินไป
  • ตรงที่บอกว่า “เกตเวย์ที่มี peer หลายแสนตัว ซึ่งในนั้นมี peer ที่จะไม่มีวันถูกใช้อีก” นี่คือสิ่งที่ผมนึกขึ้นได้พอดีตอนอ่านย่อหน้าแรก ๆ
    ไอเดียที่ว่า “ไม่มี API call สำหรับ subscribe อีเวนต์การพยายามเชื่อมต่อขาเข้า ไม่เป็นไร เราสร้างอีเวนต์เองก็ได้ คำขอเชื่อมต่อ WireGuard เป็นแพ็กเก็ตและระบุได้ง่าย ดังนั้นเราสามารถดักจับมันได้อย่างมีประสิทธิภาพด้วย BPF filter และ packet socket” ก็ดีเหมือนกัน
    เขาบอกว่าเมื่อได้รับข้อความเริ่มต้นขาเข้า ก็จะได้ ที่อยู่แบบ 4-tuple ของการเชื่อมต่อที่ต้องการ รวมถึง source port ชั่วคราวที่ flyctl ใช้ด้วย แล้วติดตั้ง peer ราวกับว่าเราเป็น initiator และ flyctl เป็น responder; เลยสงสัยว่านี่ทำงานได้แม้อยู่หลัง NAT ไหม

    • ทำงานได้ เพราะ UDP NAT รู้แค่ 4-tuple เท่านั้น เช่นในรูปแบบ {wggwd.fly.io, 12345, clientIP, 23456}
      ไม่ว่าจะเป็นแพ็กเก็ต UDP “initiator” ตัวใหม่ หรือเป็นการตอบกลับต่อข้อความเริ่มต้นขาออก มันก็ดูเหมือนกันเป๊ะสำหรับ UDP NAT ที่อยู่ตามเส้นทาง
      เพราะข้อมูลที่ใช้ตัดสินมีแค่ 4-tuple และ 4-tuple นั้นก็เหมือนกัน
    • ถ้าแพ็กเก็ตย้อนกลับมาที่ IP/พอร์ต เดียวกัน และถูกสร้างจาก IP/พอร์ต เดียวกัน ก็จะ ผ่าน NAT และทำงานได้