WireGuard ทำงานอย่างไร: เข้าใจหลักการ Cryptokey Routing
เจาะลึกการทำงานของ WireGuard ผ่านกลไก Cryptokey Routing อธิบายว่าเหตุใด AllowedIPs จึงทำหน้าที่เป็นทั้งตารางเส้นทางและรายการควบคุมการเข้าถึง พร้อมวิธีอ่านค่าไฟล์ wg0.conf ให้เข้าใจง่าย
หลักการทำงานของ WireGuard ในแนวคิดเดียว
WireGuard ทำงานโดยการผูกแพ็กเก็ตทุกชุดเข้ากับ public key กลไกนี้มีชื่อเรียกว่า cryptokey routing และเป็นหัวใจสำคัญของการออกแบบทั้งหมด: บรรทัด AllowedIPs ที่อยู่ถัดจาก peer คือตารางเส้นทาง (routing table) สำหรับแพ็กเก็ตที่ออกจากเครื่องของคุณ และเป็นรายการควบคุมการเข้าถึง (access control list) สำหรับแพ็กเก็ตที่มาจาก peer นั้น การตั้งค่าเพียงหนึ่งเดียวแต่ทำหน้าที่สองอย่าง หากอ่านค่า AllowedIPs ด้วยวิธีนี้ ไฟล์คอนฟิกของ WireGuard ทุกไฟล์จะกลายเป็นเรื่องที่เข้าใจได้ง่าย
ระบบไม่มีตารางเซสชันที่อ้างอิงด้วย IP address และไม่มีฐานข้อมูลผู้ใช้ peer หนึ่งรายประกอบด้วย public key และชุดของ address ที่ key นั้นได้รับอนุญาตให้ใช้งาน การทำ handshake และตัวจับเวลา (timers) มีไว้เพื่อรักษาความถูกต้องของการผูกข้อมูลนี้ในขณะที่เครือข่ายเบื้องล่างมีการเปลี่ยนแปลง หากคุณต้องการใช้งาน tunnel ให้สำเร็จก่อนที่จะศึกษาทฤษฎี ให้สร้างขึ้นมาด้วย การทำ self-hosted WireGuard VPN บน VPS ของคุณเอง แล้วค่อยกลับมาที่นี่เมื่อพบกับบรรทัดคอนฟิกที่น่าสงสัย
AllowedIPs คือตารางเส้นทางและรายการควบคุมการเข้าถึง
ให้พิจารณาขาออกก่อน เคอร์เนลของคุณจะส่งแพ็กเก็ตไปยังอุปกรณ์ wg0 ตามปกติผ่านตารางเส้นทางหลัก จากนั้น WireGuard จะนำที่อยู่ ปลายทาง ของแพ็กเก็ตนั้นไปเทียบกับตารางที่เก็บ prefix ที่อนุญาตของ peer ทุกตัว โดยใช้หลักการ prefix ที่ยาวที่สุดก่อน หากพบการจับคู่จะระบุตัว peer ได้ ซึ่งจะระบุ public key, session key และ UDP endpoint ที่เกี่ยวข้อง แพ็กเก็ตจะถูกเข้ารหัสสำหรับ peer นั้นและส่งออกไป
หากไม่มี AllowedIPs ของ peer ใดครอบคลุมปลายทางนั้น แพ็กเก็ตจะไม่ถูกส่งออกไป เนื่องจากไม่มีคีย์สำหรับใช้ในการส่ง
ping: sendmsg: Required key not availableข้อผิดพลาดนั้นหมายความว่า: ที่อยู่ที่คุณพยายามเข้าถึงไม่ได้ระบุไว้ภายใต้ peer ใดเลย ส่วนข้อผิดพลาดอื่นอย่าง ping: sendmsg: Destination address required หมายความว่ามีการจับคู่กับ peer ได้ แต่ WireGuard ไม่มี endpoint สำหรับ peer นั้น เนื่องจากไม่ได้มีการตั้งค่าไว้และยังไม่ได้รับข้อมูล endpoint มา
ตอนนี้มาดูขาเข้าบ้าง แพ็กเก็ต UDP เดินทางมาถึงพอร์ตที่เปิดรับ WireGuard จะค้นหา session จาก receiver index ในส่วนหัวของแพ็กเก็ต ตรวจสอบตัวนับเทียบกับ sliding replay window จากนั้นจึงถอดรหัสและตรวจสอบความถูกต้องของ payload หลังจากนั้นจึงจะอ่านแพ็กเก็ตภายใน และที่อยู่ ต้นทาง ของแพ็กเก็ตภายในนั้นจะต้องอยู่ภายใน AllowedIPs ของ peer ที่ส่งมา หากไม่อยู่ในเงื่อนไข แพ็กเก็ตจะถูกทิ้ง หากเปิดใช้งาน dynamic debug เคอร์เนลจะพิมพ์สาเหตุออกมาในบรรทัดลักษณะนี้:
wg0: Packet has unallowed src IP (10.8.0.9) from peer 2 (203.0.113.10:51820)นี่คือเหตุผลที่ peer ฝั่งเซิร์ฟเวอร์ได้รับ /32 peer ที่ตั้งค่าด้วย AllowedIPs = 10.8.0.2/32 จะสามารถส่งแพ็กเก็ตจาก 10.8.0.2 ได้เท่านั้นและห้ามส่งจากที่อยู่อื่น หากคุณเขียน 0.0.0.0/0 ลงไปแทน ไคลเอนต์ตัวนั้นจะได้รับอนุญาตให้ส่งแพ็กเก็ตโดยอ้างที่อยู่ต้นทางใดก็ได้ภายในอุโมงค์ของคุณ รวมถึงที่อยู่ของไคลเอนต์ตัวอื่นด้วย
prefix ที่ซ้อนทับกันจะถูกแก้ไขด้วยความเฉพาะเจาะจง เนื่องจากเป็นการค้นหาแบบ longest prefix match หากมี prefix ที่เหมือนกันใน peer สองตัว พฤติกรรมจะต่างออกไป: รายการจะย้ายไปอยู่ที่ peer ที่ถูกตั้งค่าล่าสุด และ peer ตัวแรกจะหยุดรับ traffic นั้นโดยไม่มีข้อความแจ้งเตือนใดๆ wg show wg0 allowed-ips จะแสดงตารางที่ใช้งานจริงในเคอร์เนล ซึ่งเป็นสิ่งที่สำคัญที่สุดในกรณีที่ไฟล์บนดิสก์และสถานะการทำงานจริงไม่ตรงกัน
การอ่านไฟล์คอนฟิกโดยคำนึงถึงการกำหนดเส้นทางด้วย cryptokey
ฝั่งเซิร์ฟเวอร์:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server private key>
[Peer]
PublicKey = <laptop public key>
AllowedIPs = 10.8.0.2/32ฝั่งไคลเอนต์:
[Interface]
Address = 10.8.0.2/32
PrivateKey = <laptop private key>
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25คีย์เวิร์ดเดียวกันมีผลลัพธ์ตรงกันข้ามในทั้งสองฝั่ง ฝั่งไคลเอนต์หมายถึง "ส่งทุกปลายทางไปยัง peer นี้" ส่วนฝั่งเซิร์ฟเวอร์หมายถึง "ยอมรับเฉพาะที่อยู่นี้ที่มาจาก peer นี้เท่านั้น" ความไม่สมมาตรนี้อยู่ที่ค่าที่กำหนด ไม่ใช่ที่บทบาทของฝั่งใดฝั่งหนึ่ง
คีย์เหล่านี้ครึ่งหนึ่งไม่ใช่ส่วนหนึ่งของโปรโตคอล Address, DNS, MTU, PostUp และ SaveConfig เป็นของ wg-quick ซึ่งเป็นเชลล์สคริปต์ที่ทำหน้าที่เปิดใช้งานอินเทอร์เฟซ เคอร์เนลจะไม่เห็นคีย์เหล่านี้ wg-quick strip wg0 จะแสดงคอนฟิกที่ลดทอนแล้วซึ่งเครื่องมือ wg โหลดใช้งานจริง ซึ่งเป็นวิธีที่เร็วที่สุดในการตรวจสอบการแยกส่วนนี้
สิ่งที่ handshake ทำงานจริง
handshake ของ WireGuard คือ Noise_IKpsk2 ซึ่งมาจาก Noise Protocol Framework ส่วน IK คือส่วนที่เป็นประโยชน์สำหรับผู้ดูแลระบบ: public key แบบ static ของฝั่งตอบรับ (responder) เป็นสิ่งที่ฝั่งเริ่มต้น (initiator) ทราบอยู่แล้ว เนื่องจากเป็นค่า PublicKey ในบล็อก [Peer] ของคุณ และฝั่งเริ่มต้นจะส่ง public key แบบ static ของตนเองไปในข้อความแรกโดยมีการเข้ารหัสไว้ ดังนั้นจึงไม่มีการแลกเปลี่ยน certificate และไม่มีการโต้ตอบเพื่อยืนยันตัวตน ผู้สังเกตการณ์ภายนอกจะไม่สามารถทราบได้ว่า key ใดกำลังเรียกเข้ามา เว้นแต่จะถือครอง private key ของฝั่งตอบรับไว้
ต้นทุนที่ต้องจ่ายคือการโต้ตอบหนึ่งรอบ ข้อความเริ่มต้นมีขนาด 148 bytes ข้อความตอบกลับมีขนาด 92 bytes และข้อมูลจะเริ่มส่งได้ทันทีหลังจากนั้น แต่ละฝั่งจะสร้างคู่ key แบบ ephemeral ของ Curve25519 ขึ้นมาใหม่ในทุกการทำ handshake และ session key จะได้มาจากสายโซ่ของผลลัพธ์ Diffie-Hellman ที่ผสมผสานระหว่าง static key และ ephemeral key เข้าด้วยกัน จากนั้น private key แบบ ephemeral จะถูกทำลายทิ้ง ซึ่งให้คุณสมบัติ forward secrecy: ผู้ที่บันทึก traffic ของคุณในวันนี้และขโมย private key ของเซิร์ฟเวอร์ไปในปีหน้า ก็ยังไม่สามารถอ่านข้อมูลที่บันทึกไว้ได้
การเริ่มต้น handshake จะแนบ timestamp แบบ TAI64N ไปด้วย และ peer แต่ละฝั่งจะจดจำค่า timestamp สูงสุดที่เคยได้รับจากอีกฝั่งไว้ ดังนั้นการส่ง handshake ซ้ำ (replay) จะถูกปฏิเสธ แพ็กเก็ตข้อมูลจะใช้ตัวนับขนาด 64-bit เป็น nonce และฝั่งรับจะเก็บ sliding window ของตัวนับที่เพิ่งได้รับไว้ ทำให้สามารถจัดการกับการส่งซ้ำและการเรียงลำดับที่ผิดพลาดได้โดยไม่ต้องมีสถานะการเชื่อมต่อแบบ TCP
Session key มีอายุการใช้งานไม่นาน และตัวจับเวลาถูกกำหนดไว้ในโค้ด (compiled in) ไม่สามารถปรับแต่งได้
The data behind this chart
[
{
"label": "REKEY_TIMEOUT",
"seconds": 5,
"notes": "resend a handshake initiation that got no answer"
},
{
"label": "KEEPALIVE_TIMEOUT",
"seconds": 10,
"notes": "send a keepalive after receiving data and sending none back"
},
{
"label": "REKEY_ATTEMPT_TIME",
"seconds": 90,
"notes": "give up on the handshake and report the peer as down"
},
{
"label": "REKEY_AFTER_TIME",
"seconds": 120,
"notes": "sender begins a fresh handshake for a new session key"
},
{
"label": "REJECT_AFTER_TIME",
"seconds": 180,
"notes": "the old session key is refused and traffic stops"
}
]ค่าเหล่านี้เป็นค่าคงที่จากข้อกำหนดของโปรโตคอล ไม่ใช่ค่าที่วัดได้ 5 ของค่าเหล่านี้เป็นตัวขับเคลื่อนวงจรชีวิตของ session ทั้งหมด หลังจากใช้งานไป 120 วินาที ฝั่งส่งจะเริ่มทำ handshake ใหม่ และหลังจาก 180 วินาที key เก่าจะถูกปฏิเสธทันที ทำให้ traffic หยุดลงจนกว่า handshake ใหม่จะเสร็จสมบูรณ์ การเริ่มต้นที่ไม่มีการตอบกลับจะถูกส่งซ้ำทุกๆ 5 วินาที และจะยกเลิกไปหลังจาก 90 วินาที นี่คือเหตุผลที่ wg show แสดงผล latest handshake เป็นอายุสัมพัทธ์ และเหตุผลที่ tunnel ที่ใช้งานปกติและมีสถานะดีจะรักษาค่าอายุนี้ให้ต่ำอยู่เสมอ หากค่าอายุเพิ่มขึ้นในขณะที่คุณกำลังส่งข้อมูล แสดงว่า handshake กำลังล้มเหลว ไม่ใช่เพราะ tunnel กำลังว่างงาน
เหตุใด peer จึงไม่มีบทบาทเป็น client หรือ server
ทั้งสองฝั่งใช้โค้ดชุดเดียวกันและรูปแบบการตั้งค่าเดียวกัน ไม่มีการกำหนดโหมด server ความไม่สมมาตรที่คุณรู้สึกเกิดจาก Endpoint และ Endpoint เป็นเพียงตัวเลือกเสริมเท่านั้น
peer ที่มีการตั้งค่า endpoint ไว้สามารถเริ่มการทำ handshake ได้ ส่วน peer ที่ไม่มี endpoint จะรอจนกว่าจะได้รับ packet แรกที่ผ่านการตรวจสอบสิทธิ์อย่างถูกต้อง จึงจะทราบที่อยู่และพอร์ตของอีกฝั่ง endpoint ที่ได้รับมานี้จะถูกจัดเก็บไว้และอัปเดตทุกครั้งที่มี packet ที่ถูกต้องส่งมาจากที่อยู่ใหม่ นี่คือกลไกการทำงานของ roaming เช่น แล็ปท็อปที่ย้ายจากการเชื่อมต่อ Wi-Fi ไปยังเครือข่ายมือถือจะยังคงรักษา tunnel เดิมไว้ได้ เนื่องจาก session ถูกระบุด้วย key และ index ไม่ใช่ด้วย IP address จึงไม่มีการเชื่อมต่อใหม่เกิดขึ้น เพราะไม่มีการเชื่อมต่อใดในความหมายของ TCP ตั้งแต่แรก
กลไกเดียวกันนี้สร้างข้อเท็จจริงที่ควรทราบคือ peer ที่มี public address จะถือ public IP ล่าสุดของอีกฝั่งไว้เสมอ และ wg show จะแสดงข้อมูลดังกล่าวออกมา
กำหนด primitives ไว้ตายตัวและไม่มีการเจรจาต่อรอง
WireGuard ไม่มีรายการ ciphersuite ให้เลือกใช้งาน โดยใช้ ChaCha20-Poly1305 สำหรับการเข้ารหัสแบบยืนยันความถูกต้อง (authenticated encryption), Curve25519 สำหรับการตกลงกุญแจ (key agreement), BLAKE2s สำหรับการทำ hashing และ HKDF สำหรับการสร้างกุญแจ (key derivation) ทุกการติดตั้งใช้งานชุดเดียวกันนี้ทั้งหมด จึงไม่มีขั้นตอนการเจรจาต่อรอง (negotiation phase) ให้ต้องประมวลผล และไม่มีช่องทางลดระดับความปลอดภัย (downgrade path) ไปใช้ตัวเลือกที่อ่อนแอกว่า ผลลัพธ์ที่แลกมานั้นชัดเจน คือหาก primitives ตัวใดตัวหนึ่งถูกเจาะได้ การแก้ไขจะต้องทำผ่านการออกโปรโตคอลเวอร์ชันใหม่และอัปเดตทั้งสองฝั่ง ไม่ใช่การปรับเปลี่ยนค่าคอนฟิก การตัดสินใจเพียงจุดเดียวนี้ช่วยลดโค้ดส่วนใหญ่และลดรูปแบบความล้มเหลวที่มักพบในอุโมงค์ข้อมูลแบบ TLS ซึ่งเป็นประเด็นหลักของการเปรียบเทียบใน WireGuard เทียบกับ OpenVPN
เหตุใดพอร์ตจึงไม่ตอบสนองต่อการสแกน
ข้อความ handshake ทุกข้อความจะมีฟิลด์ที่เรียกว่า mac1 ซึ่งเป็น MAC (message authentication code) ที่คำนวณจากข้อความโดยใช้คีย์ที่ได้มาจาก public key แบบ static ของ ผู้ตอบ ผู้ส่งที่ไม่ทราบ public key ดังกล่าวจะไม่สามารถสร้าง mac1 ที่ถูกต้องได้ และผู้รับจะทิ้งแพ็กเก็ตนั้นไปโดยไม่มีการตอบกลับใดๆ ทั้งสิ้น ไม่มีการแจ้งข้อผิดพลาด ไม่มีการรีเซ็ต และไม่มีข้อความ ICMP
ผลลัพธ์ที่ปรากฏคือการสแกน UDP ที่ไม่ได้รับข้อมูลใดๆ กลับมา
sudo nmap -sU -p 51820 vpn.example.comnmap จะรายงานผลเป็น open|filtered ซึ่งเป็นคำตอบเดียวกับที่ได้รับเมื่อพอร์ตถูกไฟร์วอลล์ทิ้งแพ็กเก็ตโดยไม่แจ้งเตือน พอร์ตจะมีพฤติกรรมเหมือนกันไม่ว่า WireGuard จะกำลังทำงานอยู่หรือไม่ก็ตาม อย่างน้อยก็สำหรับผู้ที่ไม่มี public key ของคุณอยู่ในครอบครอง
ฟิลด์ที่สองคือ mac2 ทำหน้าที่จัดการกับแรงกดดันจากการโจมตีแบบ denial of service เมื่อผู้รับอยู่ภายใต้ภาระงานหนัก ระบบจะตอบกลับการเริ่มต้นการเชื่อมต่อที่ถูกต้องด้วย cookie ขนาด 64 ไบต์ที่ผูกกับที่อยู่ต้นทางของผู้ส่ง และจะปฏิเสธการประมวลผล public key ที่ใช้ทรัพยากรสูงจนกว่าผู้ส่งจะส่ง cookie นั้นกลับมา วิธีนี้เป็นการพิสูจน์ว่าที่อยู่ต้นทางเป็นของจริงก่อนที่จะเสียทรัพยากร CPU ไปกับการประมวลผล และกลไกนี้จะทำงานเฉพาะเมื่อระบบอยู่ภายใต้ภาระงานหนักเท่านั้น
เหตุใด 0.0.0.0/0 จึงเปลี่ยน peer ให้กลายเป็น default route ของคุณ
เนื่องจาก AllowedIPs คือตารางเส้นทาง (routing table) ค่า AllowedIPs = 0.0.0.0/0, ::/0 จึงเป็นการอ้างสิทธิ์ปลายทางทุกแห่งสำหรับ peer นั้น ซึ่งเป็นการตั้งค่า full tunnel โดยสมบูรณ์
กลไกการ routing ที่ทำให้สิ่งนี้ทำงานได้นั้นน่าสนใจกว่าตัวบรรทัดคำสั่งเอง การตั้งค่า default route แบบปกติผ่าน wg0 จะทำให้เกิดการวนลูป (loop) เนื่องจากแพ็กเก็ต UDP ที่เข้ารหัสซึ่งบรรทุกข้อมูลของคุณจะต้องออกจากเครื่องเช่นกัน และมันจะไปตรงกับ default route ของตัวมันเอง wg-quick หลีกเลี่ยงปัญหานี้ด้วยการใช้ policy routing โดยจะทำเครื่องหมายแพ็กเก็ตขาออกของ WireGuard ด้วย fwmark, นำ default route ของ tunnel ไปไว้ในตารางเส้นทางแยกต่างหาก และเพิ่มกฎเพื่อให้เฉพาะทราฟฟิกที่ไม่ได้ทำเครื่องหมายเท่านั้นที่ผ่านไปยัง tunnel นั้นได้ เมื่อรัน ip rule show คุณจะเห็นผลลัพธ์ดังนี้:
32764: from all lookup main suppress_prefixlength 0
32765: not from all fwmark 0xca6c lookup 51820
32766: from all lookup main0xca6c คือ 51820 ในรูปแบบเลขฐานสิบหก และ 51820 ก็คือหมายเลขตารางเส้นทางด้วยเช่นกัน กฎ suppress_prefixlength 0 ทำให้ตาราง main ข้าม default route ของตัวมันเองไป เพื่อให้เส้นทางที่เฉพาะเจาะจง เช่น subnet ภายในเครื่องของคุณยังคงมีความสำคัญสูงสุด ในขณะที่ทราฟฟิกส่วนที่เหลือทั้งหมดจะถูกส่งต่อไปยังตารางของ tunnel สำหรับ split tunnel นั้นไม่จำเป็นต้องใช้กลไกเหล่านี้เลย เพราะรายการที่แคบกว่าอย่าง AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 จะกลายเป็นเส้นทางปกติในตาราง main แทน
สิ่งหนึ่งที่ full tunnel ไม่ได้แก้ไขด้วยตัวมันเองคือการแปลงชื่อโดเมน (name resolution) เนื่องจากตัว resolver ที่ไคลเอนต์ของคุณได้รับมาจากเครือข่ายท้องถิ่นมักจะยังคงทำงานอยู่และมีเส้นทางที่เฉพาะเจาะจงกว่า ซึ่งเป็นงานแยกต่างหากที่ครอบคลุมอยู่ใน DNS ที่รั่วไหลออกนอก WireGuard tunnel
จุดประสงค์ที่แท้จริงของ PersistentKeepalive
WireGuard จะไม่ส่งข้อมูลใดๆ ออกไปหากไม่มีการรับส่งข้อมูลเกิดขึ้น ไม่มีการส่ง heartbeat ไม่มีการรีเฟรชเซสชัน และไม่มีสัญญาณใดๆ บนสายสื่อสาร ความเงียบนี้ช่วยประหยัดพลังงานแบตเตอรี่และช่วยในกรณีการสแกนที่กล่าวถึงข้างต้น แต่ก็ทำให้การตั้งค่าบางรูปแบบใช้งานไม่ได้
Peer ที่อยู่หลัง NAT (network address translation) หรือหลัง stateful firewall จะสามารถติดต่อจากภายนอกได้ก็ต่อเมื่อมีการสร้าง mapping ไว้ในอุปกรณ์นั้นๆ ซึ่ง mapping ดังกล่าวจะถูกสร้างขึ้นโดยแพ็กเก็ตที่ส่งออกไป โดยทั่วไปอายุการใช้งานของ UDP mapping จะเริ่มต้นที่ประมาณ 30 วินาที เมื่อ mapping หมดอายุ แพ็กเก็ตจากฝั่งสาธารณะจะถูกทิ้งโดย middlebox และอุโมงค์เชื่อมต่อจะดูเหมือนใช้งานไม่ได้จนกว่า peer ที่อยู่หลัง NAT จะส่งข้อมูลบางอย่างออกมา PersistentKeepalive = 25 จะส่งแพ็กเก็ตเปล่าที่ผ่านการตรวจสอบสิทธิ์ทุกๆ 25 วินาที ซึ่งเป็นระยะเวลาที่สั้นกว่าอายุการใช้งานทั่วไปที่สั้นที่สุด ทำให้ mapping ยังคงเปิดอยู่เสมอ
ให้ตั้งค่านี้บน peer ที่อยู่หลัง NAT เท่านั้น เซิร์ฟเวอร์ที่มี public address และเปิดพอร์ต UDP ไว้ไม่จำเป็นต้องใช้การตั้งค่านี้ และการตั้งค่าบนเซิร์ฟเวอร์จะทำให้เกิดทราฟฟิกโดยไม่จำเป็น อย่าสับสนกับ automatic keepalive ซึ่งจะทำงานหลังจาก peer ได้รับข้อมูลและไม่มีข้อมูลของตนเองที่จะส่งกลับเป็นเวลา 10 วินาที ฟังก์ชันดังกล่าวจะเปิดใช้งานอยู่ตลอดเวลาและไม่สามารถกำหนดค่าได้
การกำหนดเส้นทาง LAN ผ่าน tunnel ไม่ใช่ฟีเจอร์ของ WireGuard
สมมติว่า peer B อยู่ในเครือข่ายภายในบ้าน 192.168.50.0/24 และ peer A ต้องการเชื่อมต่อไปยังเครือข่ายนั้น ระบบสองส่วนที่แยกจากกันต้องทำงานสอดประสานกัน โดยมีเพียงส่วนเดียวที่เป็น WireGuard
ส่วนของ WireGuard: เพิ่ม 192.168.50.0/24 ลงใน AllowedIPs ของ B บนเครื่อง A สิ่งนี้จะทำให้ A กำหนดเส้นทางของ prefix นั้นไปยัง B และทำให้ A ยอมรับแพ็กเก็ตที่มี source address เหล่านั้นจาก B หากไม่มีการตั้งค่านี้ cryptokey routing จะไม่มีคีย์สำหรับปลายทางและไม่มีสิทธิ์สำหรับต้นทาง
ส่วนของ kernel: บนเครื่อง B ค่า net.ipv4.ip_forward จะต้องเป็น 1 มิฉะนั้น kernel จะทิ้งแพ็กเก็ตที่ถอดรหัสแล้วทุกฉบับที่ไม่ได้จ่าหน้าถึงตัว B เอง นอกจากนี้ forward chain ของ firewall บน B จะต้องอนุญาตให้ทราฟฟิกผ่านได้ โฮสต์ต่างๆ ใน LAN จำเป็นต้องมีเส้นทางกลับไปยัง 10.8.0.0/24 หรือ B จะต้องทำ source NAT เพื่อให้การตอบกลับส่งผ่าน B กลับมา
หน้าที่ของ WireGuard สิ้นสุดเมื่อส่ง packet ที่ถอดรหัสแล้วให้ kernel หลังจากนั้นทั้งหมดเป็นการ routing และ filtering ตามปกติของ Linux นี่จึงเป็นสาเหตุที่ปัญหานี้ปรากฏใน counters ของ nft list ruleset หรือใน ip -s link show wg0 ไม่ใช่ใน wg show หากต้องการจัดการ peer ผ่าน web interface การ รัน wg-easy ใน Docker จะสร้างรายการ peer ให้โดยอัตโนมัติ แต่ forwarding rules ยังคงเป็นหน้าที่ของ host
การแยกหน้าที่แบบเดียวกันนี้ยังคงมีอยู่เมื่อ coordination layer เป็นผู้กระจาย prefix ให้ การ ประกาศ private network จาก VPS ด้วย Tailscale subnet router จะเข้ามาแทนการแก้ไข AllowedIPs ด้วยตนเองบน peer แต่ละเครื่อง อย่างไรก็ตาม forwarding sysctl และ firewall rules บน router เองยังเป็นสิ่งที่คุณต้องตั้งค่าเอง
เหตุผลที่ WireGuard ทำงานในระดับ kernel
wg0 เป็นไดรเวอร์อุปกรณ์เครือข่าย แพ็กเก็ตจะเดินทางผ่าน stack การกำหนดเส้นทางตามปกติ จากนั้นจะถูกเข้ารหัสในบริบทของ softirq และส่งออกผ่าน UDP socket โดยไม่ต้องข้ามไปยัง userspace เลย นี่คือที่มาของประสิทธิภาพการรับส่งข้อมูลที่สูง และยังเป็นเหตุผลที่โมดูลนี้มีขนาดเพียงประมาณสี่พันบรรทัด ซึ่งเล็กพอที่จะตรวจสอบและรวมเข้ากับ mainline Linux 5.6 ได้ในเดือนมีนาคม 2020 ทั้ง Ubuntu 24.04 และ Debian 13 ได้รวมโมดูลนี้มาให้แล้ว ดังนั้นจึงขาดเพียงแพ็กเกจ wireguard-tools เท่านั้น
การเป็นอินเทอร์เฟซตามปกติมีผลในทางปฏิบัติ tcpdump -ni wg0 จะแสดงแพ็กเก็ตภายในที่เป็นข้อความธรรมดา (plaintext) ในขณะที่ tcpdump -ni eth0 udp port 51820 จะแสดงแพ็กเก็ตภายนอกที่ถูกเข้ารหัส การเปรียบเทียบทั้งสองอย่างจะช่วยให้ทราบได้ทันทีว่าทิศทางใดที่เกิดปัญหา ทั้ง netfilter และการจัดการ traffic shaping จะปฏิบัติต่อ wg0 เหมือนกับลิงก์เครือข่ายอื่นๆ ในกรณีที่โมดูล kernel ไม่สามารถใช้งานได้ เช่น ในการทำ container virtualisation ที่ใช้ kernel ร่วมกับโฮสต์ wireguard-go จะทำหน้าที่นำโปรโตคอลเดียวกันนี้ไปใช้งานในระดับ userspace ผ่านอุปกรณ์ TUN ซึ่งส่งผลต่อประสิทธิภาพการรับส่งข้อมูลอย่างแท้จริง เนื่องจากทุกแพ็กเก็ตจะต้องข้ามขอบเขตของ kernel ถึงสองครั้ง
สิ่งที่ WireGuard ไม่สามารถป้องกันคุณได้
รูปแบบภัยคุกคาม (threat model) ของ WireGuard ถูกจำกัดไว้อย่างตั้งใจ และโปรโตคอลที่ทำงานเงียบเชียบเช่นนี้มักนำไปสู่ความเข้าใจที่คลาดเคลื่อน จึงขอสรุปประเด็นให้ชัดเจนดังนี้:
- ไม่สามารถปกปิดการใช้งาน WireGuard ได้ ข้อความ handshake มีขนาดคงที่ ไบต์แรกระบุประเภทของข้อความ และใช้โปรโตคอล UDP ในการรับส่งข้อมูล การตรวจสอบแพ็กเก็ตเชิงลึก (Deep packet inspection) สามารถระบุตัวตนได้ง่าย และเครือข่ายที่ไม่ต้องการให้ใช้งาน VPN สามารถบล็อกการเชื่อมต่อนี้ได้ การทำ obfuscation ถูกตัดออกไปโดยการออกแบบ
- ไม่สามารถปกปิดปริมาณหรือช่วงเวลาการรับส่งข้อมูลได้ ข้อมูล payload จะถูกเติม (padding) ให้มีขนาดเป็นพหุคูณของ 16 ไบต์เท่านั้น ผู้สังเกตการณ์จึงยังคงเห็นได้ว่าคุณส่งข้อมูลเมื่อใดและมีปริมาณโดยประมาณเท่าใด
- มีการเก็บ endpoint ล่าสุดไว้เสมอ ฝั่ง peer ที่มี public address จะจัดเก็บ IP สาธารณะปัจจุบันของอีกฝั่งไว้ และ
wg showจะแสดงข้อมูลดังกล่าว เมื่อรวมกับ tunnel address ที่กำหนดไว้ตายตัวในไฟล์ config ข้อมูลนี้จึงเป็นตัวระบุตัวตนที่คงที่ซึ่งติดตามผู้ใช้ไปในทุกเครือข่าย สำหรับการใช้งานบน VPS ส่วนตัวถือว่าไม่มีปัญหา แต่เหตุผลนี้เองที่ทำให้บริการเชิงพาณิชย์ต้องเพิ่มเลเยอร์ซ้อนทับเหนือโปรโตคอลนี้ - เป็นการยืนยันตัวตนด้วยกุญแจ ไม่ใช่ตัวบุคคล ใครก็ตามที่ถือไฟล์ private key ก็คือ peer นั้นๆ ควรตั้งค่า
/etc/wireguardให้เป็นโหมด 700 และไฟล์กุญแจให้เป็น 600 - ไม่มีรายการเพิกถอน (revocation list) และไม่มีวันหมดอายุ สิทธิ์การเข้าถึงจะสิ้นสุดลงเมื่อคุณลบรายการ peer ออกจากทุกเซิร์ฟเวอร์ที่เก็บข้อมูลนั้นไว้ และกุญแจแบบ static จะคงอยู่จนกว่าคุณจะลบออกไปเอง
ประเด็นเหล่านี้ไม่ได้ทำให้ WireGuard อ่อนแอ แต่ทำให้มันมีขนาดเล็ก ซึ่งนั่นคือจุดประสงค์หลัก คือการยืนยันตัวตนและเข้ารหัสข้อมูล โดยปล่อยให้การจัดการตัวตนและการจัดสรรที่อยู่เป็นหน้าที่ของระบบที่คุณสร้างขึ้นทับซ้อนด้านบน เลเยอร์การประสานงานในลักษณะที่อธิบายไว้ใน การเปรียบเทียบ WireGuard กับ Tailscale มีไว้เพื่อเติมเต็มช่องว่างดังกล่าวโดยใช้ data plane เดียวกับที่คุณเพิ่งอ่านไป เมื่อมีเลเยอร์ดังกล่าวแล้ว การตัดสินใจถัดไปคือใครบ้างที่สามารถเข้าถึงบริการที่คุณรันไว้ภายใน tunnel ซึ่งเป็นสิ่งที่ การเลือกระหว่าง Tailscale serve และ funnel จะช่วยจัดการให้สำหรับพอร์ตเดียว
FAQ
Cryptokey routing ใน WireGuard คืออะไร
Cryptokey routing คือกฎที่ผูกแพ็กเก็ตทุกชุดเข้ากับ public key รายการ peer แต่ละรายการจะมีรายการ prefix อยู่ใน AllowedIPs ในขาออก WireGuard จะเลือก peer โดยการจับคู่ปลายทางของแพ็กเก็ตกับรายการของ peer แต่ละราย โดยยึดตาม prefix ที่ยาวที่สุดก่อน รายการนี้จึงทำหน้าที่เสมือนตารางเส้นทาง (routing table) ในขาเข้า เมื่อแพ็กเก็ตถูกถอดรหัสและยืนยันตัวตนแล้ว ที่อยู่ต้นทางภายในแพ็กเก็ตจะต้องอยู่ในรายการของ peer รายนั้น มิฉะนั้นแพ็กเก็ตจะถูกทิ้ง รายการนี้จึงทำหน้าที่เสมือนรายการควบคุมการเข้าถึง (access control list) WireGuard ไม่มี config สำหรับ routing แยกต่างหากและไม่มี firewall ภายในแยกต่างหาก เพราะรายการเดียวนั้นทำหน้าที่ทั้งสองอย่าง
ฉันจำเป็นต้องตั้งค่า PersistentKeepalive บน peer ทั้งสองฝั่งหรือไม่
ไม่จำเป็น ให้ตั้งค่าเฉพาะฝั่งที่อยู่หลัง NAT (network address translation) หรือหลัง stateful firewall ซึ่งโดยปกติคือฝั่ง client WireGuard จะไม่ส่งข้อมูลใดๆ ในขณะที่ไม่มีการใช้งาน ดังนั้น mapping ที่ทำให้ฝั่งตรงข้ามติดต่อเข้ามาได้จะหมดอายุลง ซึ่งมักเกิดขึ้นภายในหนึ่งนาที ทำให้ tunnel ดูเหมือนใช้งานไม่ได้ในทิศทางนั้น PersistentKeepalive = 25 จะส่งแพ็กเก็ตเปล่าที่ผ่านการยืนยันตัวตนทุกๆ 25 วินาทีเพื่อรักษา mapping นั้นไว้ ฝั่งที่มี public address และเปิดพอร์ต UDP ไว้ไม่จำเป็นต้องตั้งค่านี้
ทำไมการ ping ผ่าน tunnel ถึงแจ้งว่า "Required key not available"
เพราะที่อยู่ปลายทางไม่ได้อยู่ใน AllowedIPs ของ peer ใดเลย ทำให้ cryptokey routing ไม่พบ key สำหรับเข้ารหัสแพ็กเก็ตและ kernel ปฏิเสธที่จะส่งข้อมูล ให้รันคำสั่ง wg show wg0 allowed-ips แล้วเปรียบเทียบผลลัพธ์กับที่อยู่ที่คุณกำลัง ping ข้อผิดพลาดที่คล้ายกันคือ Destination address required ซึ่งเป็นปัญหาที่ต่างออกไป คือมีการจับคู่ peer ได้แล้ว แต่ WireGuard ไม่มี endpoint สำหรับ peer นั้น เนื่องจากไม่ได้กำหนดค่าไว้และยังไม่มีแพ็กเก็ตที่ผ่านการยืนยันตัวตนส่งมาจาก peer นั้นเลย
Firewall สามารถตรวจจับและบล็อก WireGuard ได้หรือไม่
ได้ WireGuard ยืนยันตัวตนและเข้ารหัสทราฟฟิกของคุณ และไม่ได้พยายามอำพรางตัวเอง ข้อความ handshake มีขนาดคงที่คือ 148 และ 92 bytes ไบต์แรกของทุกข้อความจะระบุประเภทของมัน และการรับส่งข้อมูลใช้ UDP ดังนั้นการตรวจสอบแพ็กเก็ตเชิงลึก (deep packet inspection) จึงระบุโปรโตคอลนี้ได้โดยไม่มีปัญหา เครือข่ายที่บล็อก UDP หรือตรวจสอบลายนิ้วมือของโปรโตคอลจะหยุดการทำงานของมัน การซ่อน tunnel ต้องใช้วิธีห่อหุ้ม (wrap) ด้วยวิธีอื่น ซึ่งเป็นเครื่องมือแยกต่างหาก ไม่ใช่การตั้งค่าภายใน WireGuard