/static/codemoomoo2.png

WireGuard ฉบับสมบูรณ์ทุกกรณีการใช้งาน (Complete WireGuard Guide for Every Use Case)

Complete WireGuard Guide: From Cryptokey Routing to Production Deployments

คู่มือฉบับสมบูรณ์เกี่ยวกับ WireGuard VPN ตั้งแต่หลักการเข้ารหัส (Noise Protocol) การติดตั้งทุกแพลตฟอร์ม การตั้งค่า Network/Firewall/DNS ไปจนถึง 18 กรณีการใช้งานจริง การ Hardening การ Monitoring การหลบเลี่ยงการบล็อก และแล็บปฏิบัติ 8 ชุด พร้อมไฟล์ข้อมูลตัวอย่าง (wireguard-sample-data.md) สำหรับทดลองได้จริง


วิธีใช้เอกสารนี้ (How to use this document)


ส่วนที่ 1 พื้นฐานและหลักการทำงาน (Fundamentals and Operating Principles)

1.1 บทนำ (Introduction)

1.1.1 WireGuard คืออะไร

WireGuard คือ VPN (Virtual Private Network) แบบ Layer 3 tunnel ที่ออกแบบให้เรียบง่าย รวดเร็ว และปลอดภัยสมัยใหม่ ทำงานโดยห่อหุ้ม (encapsulate) แพ็กเก็ต IP ด้วยการเข้ารหัส แล้วส่งผ่าน UDP ไปยังปลายทาง ผู้ใช้สร้าง network interface เสมือน (เช่น wg0) ขึ้นมา แล้วตั้งค่า IP address และ route ให้ traffic ที่ต้องการวิ่งผ่าน interface นั้น

ลักษณะเด่นที่ทำให้ WireGuard ต่างจาก VPN รุ่นก่อน:

1.1.2 ปัญหาของ VPN รุ่นเก่า

ปัญหา (Problem) อธิบาย ตัวอย่าง
ความซับซ้อน (Complexity) มี protocol หลายชั้น หลาย mode หลายตัวเลือกที่ต้องเข้าใจ IPsec: IKE, ESP, AH, Phase 1/2, Transport/Tunnel mode
Code base ใหญ่ โค้ดจำนวนมากหมายถึงพื้นที่โจมตี (attack surface) ที่กว้างและ audit ยาก OpenVPN + OpenSSL มีโค้ดหลายแสนบรรทัดรวมไลบรารี
Config ยาก ตัวเลือกมาก ผิดพลาดง่าย ผลลัพธ์คือ config ที่ไม่ปลอดภัยโดยไม่รู้ตัว การเลือก cipher suite ที่อ่อนแอเพื่อความเข้ากันได้
Cipher negotiation เปิดช่องโจมตีแบบ downgrade attack การบังคับให้ใช้ cipher ที่อ่อนกว่า
การเชื่อมต่อช้า handshake หลายรอบ (round trip) IKEv2 + EAP ใช้หลาย message
Roaming ไม่ดี เปลี่ยนเครือข่ายแล้วต้อง reconnect มือถือสลับ Wi-Fi/4G

1.1.3 ประวัติและการพัฒนา (History)

ผู้ออกแบบและพัฒนาหลักคือ Jason A. Donenfeld (นักวิจัยด้านความปลอดภัย ผู้ก่อตั้ง Edge Security) เริ่มพัฒนาเป็นโมดูล Linux ราวปี 2015–2016 เผยแพร่เอกสารวิชาการ (whitepaper) ในงาน NDSS 2017 และถูกรวมเข้า Linux kernel 5.6 (ออกเมื่อเดือนมีนาคม 2020) ทำให้ผู้ใช้ Linux จำนวนมากได้ WireGuard ติดมากับ kernel โดยไม่ต้องติดตั้งโมดูลเพิ่ม และมีการ backport ไปยัง kernel LTS ที่เก่ากว่าในหลาย distribution

flowchart LR
    subgraph ERA1["ยุค VPN ดั้งเดิม (Classic VPN Era) 1990s-2000s"]
        A1["IPsec มาตรฐาน RFC 2401
ปี 1998 (IPsec standardized)"] A2["OpenVPN เผยแพร่
ปี 2001 (OpenVPN released)"] A3["IKEv2 RFC 4306
ปี 2005 (IKEv2 introduced)"] A1 --> A2 --> A3 end subgraph ERA2["ยุคกำเนิด WireGuard (Origin Era) 2015-2018"] B1["Donenfeld เริ่มพัฒนาโมดูล
ราวปี 2015-2016 (Initial development)"] B2["เผยแพร่ whitepaper ที่ NDSS
ปี 2017 (NDSS paper)"] B3["แอปทางการ Android/iOS
ปี 2018 (Official mobile apps)"] B1 --> B2 --> B3 end subgraph ERA3["ยุคเข้าสู่กระแสหลัก (Mainstream Era) 2019-2021"] C1["Tailscale และ mesh VPN
เริ่มเติบโต (Mesh VPN growth)"] C2["รวมเข้า Linux 5.6
มีนาคม 2020 (Merged in Linux 5.6)"] C3["WireGuardNT สำหรับ Windows
ปี 2021 (Windows kernel driver)"] C1 --> C2 --> C3 end subgraph ERA4["ยุคระบบนิเวศ (Ecosystem Era) 2022-ปัจจุบัน"] D1["Router/Firewall รองรับกว้างขวาง
(OpenWrt, OPNsense, MikroTik)"] D2["เครื่องมือ Post-Quantum และ Obfuscation
(Rosenpass, AmneziaWG)"] D1 --> D2 end ERA1 --> ERA2 --> ERA3 --> ERA4

หมายเหตุ: ปีที่ระบุเป็นหมุดหมายโดยประมาณจากเอกสารสาธารณะ ท่านควรตรวจสอบกับ wireguard.com สำหรับรายละเอียดการออกรุ่นล่าสุด

1.1.4 ปรัชญาการออกแบบ (Design Philosophy)

  1. เรียบง่าย (Simplicity): ถ้าลดได้ ให้ลด ไม่มีฟีเจอร์เกินจำเป็นใน protocol
  2. Code base เล็ก (Small code base): ยิ่งเล็กยิ่งตรวจสอบง่าย ลดช่องโหว่
  3. Opinionated cryptography: ผู้ออกแบบเลือก primitive ให้ ผู้ใช้ไม่ต้องตัดสินใจ ลดโอกาส config ผิด
  4. ไม่มี state ที่ผู้ใช้ต้องจัดการ: ไม่มี session ที่ต้อง login/logout มี key และ AllowedIPs เท่านั้น
  5. เงียบต่อคนแปลกหน้า (Silent to strangers): ไม่ตอบกลับแพ็กเก็ตที่ไม่ผ่านการพิสูจน์ตัวตน จึงสแกนพบได้ยาก

1.1.5 ภาพรวมการใช้งาน (Adoption Overview)

🧪 ตัวอย่าง 1.1: สคริปต์ตรวจความพร้อมของเครื่อง (check-wg-ready.sh) ดูในไฟล์ตัวอย่าง


1.2 เปรียบเทียบกับ VPN อื่น (Comparison with Other VPNs)

1.2.1 ตารางเปรียบเทียบ

หัวข้อ (Criteria) WireGuard OpenVPN IPsec (IKEv2) SSL VPN อื่น ๆ (เช่น AnyConnect, SSTP)
ขนาด code (โดยประมาณ) ~4 พันบรรทัด (kernel) หลายหมื่นบรรทัด + OpenSSL หลายแสนบรรทัด (kernel stack + IKE daemon) แตกต่างตามผลิตภัณฑ์ มักเป็นระบบปิด
Transport UDP เท่านั้น UDP หรือ TCP UDP 500/4500 + ESP TCP 443 เป็นหลัก
ทำงานที่ Kernel (Linux/Windows) Userspace Kernel + userspace daemon Userspace
ประสิทธิภาพ สูงมาก ปานกลาง สูง (มี hardware offload) ปานกลาง
Latency ของ handshake ต่ำ (1-RTT) สูง (TLS handshake) ปานกลาง-สูง สูง
ความง่ายในการตั้งค่า ง่ายมาก ปานกลาง (ต้องมี PKI) ยาก ขึ้นกับผลิตภัณฑ์
Cryptographic agility ไม่มี (ตั้งใจ) ต้องเปลี่ยนเวอร์ชัน protocol มีมาก มีมาก มีมาก
Authentication Public key (ไม่มี user/password ในตัว) Certificate, user/pass, MFA Certificate, PSK, EAP User/pass, SSO, MFA
Roaming ดีเยี่ยม พอใช้ ดี (MOBIKE) พอใช้
การผ่านไฟร์วอลล์เข้มงวด ยาก (UDP เท่านั้น) ง่าย (TCP 443) ปานกลาง ง่ายมาก
รองรับ platform Linux, Windows, macOS, iOS, Android, BSD ทุก platform ทุก platform (built-in ในหลาย OS) ทุก platform
การ audit ง่าย ยาก ยากมาก ยาก/ทำไม่ได้ (ปิด)

1.2.2 กราฟเปรียบเทียบเชิงแนวคิด (Conceptual Comparison)

ตารางต่อไปนี้เป็น ค่าเชิงแนวคิดแบบสัมพัทธ์ (relative, ไม่ใช่ผลวัดจริง) เพื่อให้เห็นแนวโน้ม ผลจริงขึ้นกับ CPU, NIC, kernel และขนาด MTU ท่านควรวัดเองด้วย iperf3 (ดูหัวข้อ 9.1)

Protocol Throughput (สัมพัทธ์) Latency เพิ่มขึ้น (สัมพัทธ์) ความเสถียรเมื่อ roam
ไม่ใช้ VPN (baseline) ██████████ 100% ▏ 0 -
WireGuard (kernel) █████████░ ~85-95% ▎ ต่ำ ดีเยี่ยม
IPsec (AES-NI, kernel) ████████░░ ~75-90% ▎ ต่ำ ดี
OpenVPN (UDP) ████░░░░░░ ~30-55% ▌ ปานกลาง พอใช้
OpenVPN (TCP) ███░░░░░░░ ~25-45% ▊ สูง พอใช้

1.2.3 เมื่อไรควรเลือกและไม่ควรเลือก WireGuard

ควรเลือก WireGuard เมื่อ:

ไม่ควรเลือก (หรือต้องเสริมเครื่องมืออื่น) เมื่อ:

flowchart TD
    Start["ต้องการ VPN
(Need a VPN)"] --> Q1{"เครือข่ายอนุญาต UDP หรือไม่?
(UDP allowed?)"} Q1 -- "ไม่ (No)" --> R1["ใช้ OpenVPN TCP 443 หรือ
WireGuard + obfuscation"] Q1 -- "ใช่ (Yes)" --> Q2{"ต้องใช้ algorithm ตามมาตรฐาน FIPS?
(FIPS required?)"} Q2 -- "ใช่ (Yes)" --> R2["ใช้ IPsec ที่ผ่านการรับรอง
(Certified IPsec)"] Q2 -- "ไม่ (No)" --> Q3{"ต้องมี SSO/MFA ในตัว?
(Built-in SSO/MFA?)"} Q3 -- "ใช่ (Yes)" --> R3["WireGuard + control plane
(Tailscale, Firezone) หรือ SSL VPN"] Q3 -- "ไม่ (No)" --> R4["WireGuard ล้วน
(Plain WireGuard)"]

🧪 ตัวอย่าง 1.2: สคริปต์วัด throughput เปรียบเทียบด้วย iperf3 ดูในไฟล์ตัวอย่าง


1.3 โมเดลแนวคิดหลัก (Core Conceptual Model)

1.3.1 Interface และ Peer

1.3.2 Cryptokey Routing

Cryptokey Routing คือหัวใจของ WireGuard: การผูก public key ของ peer เข้ากับ รายการ IP ที่อนุญาต (AllowedIPs) ตารางนี้ทำหน้าที่สองอย่างพร้อมกัน

  1. ขาออก (Outbound) = Routing table: เมื่อมีแพ็กเก็ตจะส่งออก WireGuard ค้นหา peer ที่ AllowedIPs ตรงกับ IP ปลายทาง (แบบ longest prefix match) แล้วเข้ารหัสด้วย key ของ peer นั้น
  2. ขาเข้า (Inbound) = Access control list: เมื่อรับแพ็กเก็ตที่ถอดรหัสได้ WireGuard ตรวจว่า source IP ของแพ็กเก็ตชั้นใน อยู่ใน AllowedIPs ของ peer ที่ส่งมาหรือไม่ ถ้าไม่ใช่จะ ทิ้งทันที (drop)

ตัวอย่างตาราง Cryptokey Routing ของ interface หนึ่ง:

Public Key (ย่อ) AllowedIPs ผลเมื่อส่งไป 10.8.0.2 ผลเมื่อรับแพ็กเก็ต src=10.8.0.9 จาก key นี้
PeerA...= 10.8.0.2/32 เข้ารหัสด้วย key ของ PeerA drop (ไม่อยู่ใน AllowedIPs)
PeerB...= 10.8.0.3/32, 192.168.20.0/24 ไม่ตรง ไม่ตรง
PeerC...= 10.8.0.9/32 ไม่ตรง ยอมรับ ถ้า src=10.8.0.9 มาจาก PeerC

1.3.3 WireGuard เป็น Layer 3 Tunnel

1.3.4 ความรู้สึกแบบ Stateless (Stateless-feel)

flowchart LR
    subgraph HostA["เครื่อง A (Host A)"]
        AppA["แอปพลิเคชัน
(Application)"] RTA["ตาราง route ของระบบ
(System routing table)
10.8.0.0/24 dev wg0"] WGA["Interface wg0
Private key A
10.8.0.1/24"] CRA["Cryptokey Routing
Peer B = 10.8.0.2/32"] AppA --> RTA --> WGA --> CRA end subgraph Net["อินเทอร์เน็ต (Internet)"] UDP["UDP datagram ที่เข้ารหัสแล้ว
(Encrypted UDP)"] end subgraph HostB["เครื่อง B (Host B)"] WGB["Interface wg0
Private key B
10.8.0.2/24"] CRB["Cryptokey Routing
Peer A = 10.8.0.1/32"] AppB["แอปพลิเคชัน
(Application)"] WGB --> CRB --> AppB end CRA --> UDP --> WGB

🧪 ตัวอย่าง 1.3: สคริปต์ show-cryptokey-table.sh แสดง peer และ AllowedIPs ในรูปตาราง ดูในไฟล์ตัวอย่าง


1.4 หลักการเข้ารหัสที่ใช้ (Cryptographic Principles)

1.4.1 Noise Protocol Framework

WireGuard ใช้ Noise Protocol Framework (ออกแบบโดย Trevor Perrin) รูปแบบ handshake ที่ใช้คือ Noise_IKpsk2 โดยชื่อเต็มของชุดที่ใช้ในเอกสารต้นฉบับคือ Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s

1.4.2 ชุด Primitive ที่ใช้

หน้าที่ (Role) Algorithm อธิบาย
Key exchange (ECDH) Curve25519 แลกกุญแจแบบ elliptic curve ความยาวกุญแจ 32 byte ความปลอดภัยราว 128 bit
Authenticated encryption (AEAD) ChaCha20-Poly1305 เข้ารหัส stream cipher + ตรวจสอบความถูกต้อง (tag 16 byte) เร็วแม้ไม่มี AES-NI
Hash BLAKE2s hash ที่เร็วกว่า SHA-2 ในซอฟต์แวร์ ใช้ทั้ง hash และ keyed MAC
Hashtable key SipHash24 ใช้ใน hashtable ภายใน kernel ป้องกัน hash-flooding
Key derivation HKDF สร้างกุญแจย่อย (session keys) จาก shared secret
Cookie encryption XChaCha20-Poly1305 ใช้กับ cookie reply (nonce ยาวขึ้น)

1.4.3 ทำไมไม่มี Cipher Negotiation

Cipher negotiation คือการที่สองฝั่งเจรจาเลือก algorithm ร่วมกัน ซึ่งเป็นต้นเหตุของช่องโหว่ในหลาย protocol (downgrade attack, config ผิดพลาด, ซับซ้อน)

WireGuard เลือก ไม่มี negotiation:

1.4.4 Perfect Forward Secrecy และการ Rekey

Perfect Forward Secrecy (PFS) หมายถึง การที่กุญแจเซสชันถูกสร้างจาก ephemeral key ซึ่งถูกทิ้งหลังใช้ ดังนั้นแม้ภายหลัง private key ระยะยาวรั่วไหล ผู้โจมตีก็ถอดรหัส traffic ในอดีตที่บันทึกไว้ไม่ได้

WireGuard จะ rekey (เจรจา session key ใหม่) อัตโนมัติ เมื่อ:

ตัวอย่างการคำนวณ: จำนวน handshake ต่อวันเมื่อ tunnel ใช้งานตลอดเวลา

Nhs = Tday Trekey

แทนค่า: Nhs = 86,400 ÷ 120 = 720 ครั้งต่อวัน (ประมาณ 1 ครั้งทุก 2 นาที) แต่ละ handshake ใช้ข้อมูลรวมประมาณ 148 + 92 = 240 byte (ไม่รวม header IP/UDP) ดังนั้น overhead ของ rekey ต่อวัน ≈ 720 × 240 = 172,800 byte ≈ 169 KiB ซึ่งน้อยมาก

ตัวอย่างการคำนวณ: เหตุใด limit จำนวนแพ็กเก็ตแทบไม่เป็นปัญหา

t = 260 R×31,557,600

แทนค่า: ถ้า R = 1,000,000 แพ็กเก็ต/วินาที จะได้ t ≈ 1.15×1018 ÷ 3.16×107 ≈ 36,500 ปี ดังนั้นในทางปฏิบัติ rekey จะถูกกระตุ้นด้วยเวลา (120 วินาที) ก่อนเสมอ

1.4.5 Pre-shared Key (PSK)

PSK คือกุญแจลับสมมาตรขนาด 32 byte ที่ทั้งสองฝั่งมีร่วมกัน ถูกผสมเข้าใน handshake เป็นชั้นป้องกันเพิ่ม ประโยชน์คือ:

🧪 ตัวอย่าง 1.4: สคริปต์สร้างชุดกุญแจและ PSK พร้อมแสดงขนาดและรูปแบบ (demo-keys.sh) ดูในไฟล์ตัวอย่าง


1.5 Handshake และการส่งข้อมูล (Handshake and Data Transport)

1.5.1 ข้อความทั้ง 4 ชนิด (Message Types)

Type ชื่อ ขนาด (byte) ผู้ส่ง หน้าที่
1 Handshake Initiation 148 Initiator เริ่มเจรจา: ephemeral key, static key (เข้ารหัส), timestamp (เข้ารหัส), mac1, mac2
2 Handshake Response 92 Responder ตอบกลับ: ephemeral key, ยืนยัน (empty encrypted), mac1, mac2
3 Cookie Reply 64 Responder ใช้เมื่อ responder ถูกโหลดหนัก ส่ง cookie ให้ initiator ใช้ใน mac2
4 Transport Data 32 + payload ทั้งสองฝั่ง ข้อมูลที่เข้ารหัสด้วย session key (header 16 byte + payload + tag 16 byte)

1.5.2 ลำดับการ Handshake

sequenceDiagram
    participant I as Initiator (ผู้เริ่ม)
    participant R as Responder (ผู้ตอบ)
    Note over I: มีแพ็กเก็ตต้องส่งแต่ยังไม่มี session
(Packet queued, no session yet) I->>R: Handshake Initiation (type 1, 148 B) Note over R: ตรวจ mac1 และ timestamp
ถ้าโหลดสูงอาจตอบ Cookie Reply R-->>I: Handshake Response (type 2, 92 B) Note over I,R: ทั้งสองฝั่งคำนวณ session keys ด้วย HKDF
(Derive transport keys) I->>R: Transport Data แรก (type 4) Note over R: ยืนยัน key ครบวงจร (Key confirmation) R->>I: Transport Data (type 4) Note over I,R: ทุก ~120 วินาที: rekey ด้วย handshake ใหม่

ขั้นตอนโดยย่อ:

  1. Initiator สร้าง ephemeral key pair แล้วส่ง Initiation ที่มี static public key และ timestamp (TAI64N) ซึ่งเข้ารหัสไว้
  2. Responder ตรวจ mac1 (keyed hash ด้วย public key ของ responder) ถ้าไม่ถูกต้องจะ ไม่ตอบอะไรเลย (ทำให้พอร์ต "เงียบ")
  3. Responder ตรวจ timestamp ว่าใหม่กว่าค่าที่เคยเห็นจาก peer นี้ (กัน replay ของ handshake)
  4. Responder ส่ง Response พร้อม ephemeral key ของตน
  5. ทั้งสองฝั่งสร้าง transport keys (ส่งหนึ่งชุด รับหนึ่งชุด)
  6. Initiator ส่งข้อมูลแรก (หรือ keepalive) เพื่อยืนยัน

1.5.3 Transport Data และ Counter กัน Replay

เมื่อ responder ตรวจพบว่าถูกโหลดหนัก (มี handshake เข้ามามากผิดปกติ) จะไม่ประมวลผลการคำนวณ Curve25519 ที่หนัก แต่ส่ง Cookie Reply กลับไปแทน:

  1. cookie ผูกกับ IP address ต้นทาง ของ initiator (ผ่าน MAC ที่มี secret หมุนเวียนทุก 2 นาที)
  2. initiator ต้องส่ง Initiation ใหม่พร้อม mac2 ที่คำนวณจาก cookie
  3. ผู้โจมตีที่ปลอม IP ต้นทาง (IP spoofing) จะไม่ได้รับ cookie จึงไม่ผ่านด่านนี้

1.5.5 Roaming

Roaming คือความสามารถในการเปลี่ยน IP/port ของ peer ได้โดยไม่ต้องตั้งค่าใหม่:

1.5.6 PersistentKeepalive และ NAT

อุปกรณ์ NAT/stateful firewall จะลบ mapping ของ UDP ที่เงียบเกินเวลาหนึ่ง (มักราว 30-120 วินาที) ทำให้ฝั่งนอก NAT ส่งหาฝั่งใน NAT ไม่ได้ PersistentKeepalive = N ทำให้ WireGuard ส่ง keepalive ทุก N วินาทีเพื่อรักษา mapping

ตัวอย่างการคำนวณ: ต้นทุนของ keepalive 25 วินาที

Bday = 86,400 K × Spkt

แทนค่า: Bday = (86,400 ÷ 25) × 60 = 3,456 × 60 = 207,360 byte ≈ 0.2 MB ต่อวัน (ประมาณ 19.2 bit/s) จึงเหมาะกับมือถือและอุปกรณ์ที่ใช้พลังงานจำกัดเมื่อตั้งค่าเหมาะสม แต่ต้องชั่งน้ำหนักเรื่องการปลุกวิทยุของมือถือ

ข้อแนะนำ: ใช้ 25 วินาทีเป็นค่าเริ่มต้น และเพิ่มได้ถ้า NAT ของท่านมี timeout นาน ฝั่งที่มี public IP และไม่อยู่หลัง NAT ไม่จำเป็น ต้องตั้ง keepalive

🧪 ตัวอย่าง 1.5: ไฟล์ตัวอย่างวิเคราะห์ handshake ด้วย tcpdump พร้อมสคริปต์จำแนก message type จากขนาด ดูในไฟล์ตัวอย่าง


1.6 สถาปัตยกรรม Implementation (Implementation Architecture)

1.6.1 Implementation หลัก

Implementation ภาษา ทำงานที่ หมายเหตุ
Linux kernel module C Kernel space ประสิทธิภาพสูงสุดบน Linux; รวมใน mainline ตั้งแต่ 5.6
wireguard-go Go Userspace (TUN) อ้างอิงอย่างเป็นทางการสำหรับระบบที่ไม่มี kernel module; ใช้บน macOS, BSD, iOS/Android (ในแอป)
BoringTun Rust Userspace พัฒนาโดย Cloudflare ใช้ในผลิตภัณฑ์ของตน เหมาะฝังในแอปและ container
WireGuardNT C Windows kernel driver ประสิทธิภาพสูงกว่า wireguard-go บน Windows
BSD kernel implementation C Kernel (FreeBSD, OpenBSD) OpenBSD มี wg(4); FreeBSD มี if_wg (ขึ้นกับเวอร์ชัน)
อื่น ๆ หลากหลาย - เช่น implementation ในอุปกรณ์เครือข่ายเชิงพาณิชย์ หรือ userspace บน Smoltcp, netstack

1.6.2 Kernel เทียบกับ Userspace

flowchart TB
    subgraph KERNEL["โหมด Kernel (Kernel mode)"]
        K1["แอปพลิเคชัน
(Application)"] --> K2["TCP/IP stack"] K2 --> K3["โมดูล WireGuard ใน kernel
(In-kernel WireGuard)"] K3 --> K4["UDP socket / NIC
(Network card)"] end subgraph USER["โหมด Userspace (Userspace mode)"] U1["แอปพลิเคชัน
(Application)"] --> U2["TCP/IP stack"] U2 --> U3["อุปกรณ์ TUN
(TUN device)"] U3 --> U4["โปรเซส wireguard-go หรือ BoringTun
(Userspace process)"] U4 --> U5["UDP socket / NIC
(Network card)"] end

1.6.3 การรองรับ Windows และ BSD

ตัวอย่างการคำนวณ: ประเมินความเร็วสูงสุดเชิงทฤษฎีของ tunnel เมื่อ CPU เป็นคอขวด

T = f p × 8

แทนค่า: ให้ f = 3.0×109 Hz และ p = 3 cycles/byte (ค่าสมมติเพื่อการเรียนรู้) จะได้ T = (3.0×109 ÷ 3) × 8 = 8×109 bit/s = 8 Gbit/s ต่อ core ค่าจริงจะต่ำกว่านี้เพราะมี overhead ของ network stack การคัดลอกหน่วยความจำ และ handshake ดังนั้นต้องวัดจริงด้วย iperf3

🧪 ตัวอย่าง 1.6: สคริปต์ตรวจว่าเครื่องใช้ kernel module หรือ userspace และติดตั้ง wireguard-go ดูในไฟล์ตัวอย่าง


ส่วนที่ 2 การติดตั้งบนทุกแพลตฟอร์ม (Installation on All Platforms)

flowchart TD
    S["เลือกแพลตฟอร์ม
(Choose platform)"] --> L["Linux"] S --> W["Windows"] S --> M["macOS"] S --> P["Android / iOS"] S --> B["BSD / pfSense / OPNsense"] S --> C["Container (Docker)"] L --> L1{"kernel เวอร์ชัน 5.6 ขึ้นไป?
(kernel >= 5.6?)"} L1 -- "ใช่ (Yes)" --> L2["ติดตั้ง wireguard-tools อย่างเดียว
(tools only)"] L1 -- "ไม่ (No)" --> L3["ติดตั้ง DKMS module + tools
(DKMS + tools)"] W --> W1["ติดตั้งแอป WireGuard (WireGuardNT)"] M --> M1["App Store หรือ Homebrew (wireguard-go)"] P --> P1["แอปทางการ + สแกน QR code"] B --> B1["Kernel driver หรือ wireguard-go"] C --> C1["NET_ADMIN + SYS_MODULE หรือ userspace"]

2.1 Linux

2.1.1 ตรวจสอบ kernel support

# ดูเวอร์ชัน kernel (Kernel 5.6 ขึ้นไปมี WireGuard ในตัว)
uname -r

# ตรวจว่า kernel มีโมดูล WireGuard หรือไม่ (ทั้งแบบ built-in และ module)
modinfo wireguard 2>/dev/null | head -n 5 || echo "ไม่พบโมดูล wireguard (module not found)"

# ทดสอบสร้าง interface ชนิด wireguard จริง (ต้องใช้ root)
sudo ip link add dev wgtest type wireguard && echo "kernel รองรับ (supported)" ; sudo ip link del dev wgtest 2>/dev/null

# ตัวอย่างผลลัพธ์ที่คาดหวัง:
#   5.15.0-91-generic
#   filename:       /lib/modules/5.15.0-91-generic/kernel/drivers/net/wireguard/wireguard.ko
#   kernel รองรับ (supported)

2.1.2 ติดตั้ง wireguard-tools ตาม Distribution

Distribution คำสั่งติดตั้ง (Install command) หมายเหตุ
Debian / Ubuntu sudo apt update && sudo apt install -y wireguard แพ็กเกจ wireguard ดึง tools มาด้วย; เพิ่ม resolvconf หรือใช้ systemd-resolved สำหรับ DNS=
RHEL / Rocky / AlmaLinux 9 sudo dnf install -y wireguard-tools kernel 5.14 มีโมดูลมาให้แล้ว
RHEL / Rocky / AlmaLinux 8 เปิด EPEL แล้ว sudo dnf install -y wireguard-tools + โมดูลจาก ELRepo (kmod-wireguard) หรือใช้ DKMS kernel 4.18 ไม่มีในตัว (ตรวจตามรุ่น minor)
Fedora sudo dnf install -y wireguard-tools kernel ใหม่ มีในตัว
Arch Linux sudo pacman -S wireguard-tools ตั้ง resolvconf ผ่าน openresolv ถ้าต้องการ
Alpine Linux sudo apk add wireguard-tools ใช้ OpenRC; เพิ่ม iptables/nftables ตามต้องการ
openSUSE sudo zypper install wireguard-tools Leap/Tumbleweed kernel ใหม่มีในตัว

2.1.3 กรณี kernel เก่า: DKMS module

DKMS (Dynamic Kernel Module Support) คือกลไกคอมไพล์โมดูลให้ตรงกับ kernel ที่ใช้อยู่

# Debian/Ubuntu: ติดตั้ง header ของ kernel และโมดูลแบบ DKMS
sudo apt install -y linux-headers-$(uname -r) wireguard-dkms wireguard-tools

# RHEL-family (หลังเปิด EPEL): ติดตั้งเครื่องมือคอมไพล์และ DKMS
sudo dnf install -y epel-release
sudo dnf install -y kernel-devel-$(uname -r) dkms
# แล้วติดตั้งแพ็กเกจ wireguard-dkms จากคลังที่ใช้ (ชื่อแพ็กเกจแตกต่างตามคลัง)

# ตรวจสอบสถานะการ build ของ DKMS
dkms status | grep -i wireguard

2.1.4 ตรวจสอบการติดตั้ง

# โหลดโมดูล (ถ้า built-in จะไม่มี error)
sudo modprobe wireguard && echo "โหลดโมดูลสำเร็จ (module loaded)"

# ดูว่าโมดูลถูกโหลด
lsmod | grep wireguard

# ดูเวอร์ชันของเครื่องมือ
wg --version
wg-quick --help 2>&1 | head -n 3

# ตัวอย่างผลลัพธ์:
#   wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/

🧪 ตัวอย่าง 2.1: สคริปต์ install-wireguard.sh ติดตั้งอัตโนมัติตาม distribution และตรวจสอบผลลัพธ์ ดูในไฟล์ตัวอย่าง


2.2 Windows

2.2.1 ติดตั้งแอปอย่างเป็นทางการ

  1. ดาวน์โหลดตัวติดตั้งจากหน้า Install ของ wireguard.com (ตรวจ digital signature ของไฟล์ก่อนรัน)
  2. ติดตั้ง ต้องใช้สิทธิ์ Administrator
  3. แอปใช้ไดรเวอร์ WireGuardNT (kernel driver) ซึ่งเร็วกว่าแบบ userspace เดิม

2.2.2 การนำเข้า config และ Tunnel Service

# ติดตั้ง tunnel เป็น service จากไฟล์ config (ใช้ PowerShell แบบ Administrator)
& "C:\Program Files\WireGuard\wireguard.exe" /installtunnelservice "C:\wg\wg0.conf"

# ตรวจสถานะ service (ใช้ single quote เพราะชื่อมีเครื่องหมาย $)
Get-Service -Name 'WireGuardTunnel$wg0'

# ดูสถานะ tunnel ด้วย wg.exe (มาพร้อมแอป)
& "C:\Program Files\WireGuard\wg.exe" show

# ถอน tunnel service
& "C:\Program Files\WireGuard\wireguard.exe" /uninstalltunnelservice wg0

# ตัวอย่างผลลัพธ์ที่คาดหวัง:
#   Status   Name               DisplayName
#   Running  WireGuardTunnel$wg0 WireGuard Tunnel: wg0

2.2.3 ข้อควรระวัง

# ดูหมวดหมู่เครือข่ายของ interface wg0
Get-NetConnectionProfile -InterfaceAlias wg0

# เปลี่ยนเป็น Private เพื่อให้กฎ Private profile ใช้งาน (ต้องพิจารณาความเสี่ยงก่อน)
Set-NetConnectionProfile -InterfaceAlias wg0 -NetworkCategory Private

# เปิดกฎให้ ping จาก subnet ของ tunnel ได้ (ตัวอย่าง)
New-NetFirewallRule -DisplayName "Allow ICMPv4 from WG" -Protocol ICMPv4 -IcmpType 8 `
  -RemoteAddress 10.8.0.0/24 -Action Allow

🧪 ตัวอย่าง 2.2: ไฟล์ windows-client.conf พร้อมสคริปต์ PowerShell ติดตั้ง/ถอน ดูในไฟล์ตัวอย่าง


2.3 macOS

2.3.1 App Store เทียบกับ Homebrew

วิธี ข้อดี ข้อจำกัด
แอป WireGuard (App Store) ใช้ง่าย มี GUI, on-demand rules, ใช้ Network Extension ของ Apple จัดการผ่าน CLI ไม่สะดวก, ผูกกับ Apple ID
Homebrew wireguard-tools ใช้ wg/wg-quick ได้ เหมาะ automation ใช้ wireguard-go (userspace), ต้องรันด้วย sudo, ไม่มี on-demand

2.3.2 การใช้ wg-quick บน macOS

# ติดตั้งเครื่องมือ (wg-quick ต้องการ bash เวอร์ชัน 4 ขึ้นไป)
brew install wireguard-tools bash

# ตำแหน่ง config: Apple Silicon = /opt/homebrew/etc/wireguard , Intel = /usr/local/etc/wireguard
sudo mkdir -p /opt/homebrew/etc/wireguard
sudo cp wg0.conf /opt/homebrew/etc/wireguard/
sudo chmod 600 /opt/homebrew/etc/wireguard/wg0.conf

# เปิด tunnel (macOS จะสร้าง interface ชื่อ utunN และเก็บชื่อ wg0 ไว้ใน /var/run/wireguard)
sudo wg-quick up wg0

# ตรวจสอบสถานะ
sudo wg show

# ปิด tunnel
sudo wg-quick down wg0

หมายเหตุ: บน macOS ชื่อ interface จริงเป็น utunN ไม่ใช่ wg0 ให้ดูชื่อจริงจาก sudo wg show interfaces หรือไฟล์ /var/run/wireguard/wg0.name

🧪 ตัวอย่าง 2.3: สคริปต์ macos-wgctl.sh สำหรับเปิด/ปิด/ตรวจสถานะ ดูในไฟล์ตัวอย่าง


2.4 Android และ iOS

2.4.1 แอปทางการและการสแกน QR code

2.4.2 On-Demand Activation

2.4.3 Per-app Tunneling (Android Split Tunneling)

Android แอปรองรับการเลือกแอปที่ใช้/ไม่ใช้ tunnel ในหน้าแก้ไข tunnel:

# ตัวอย่าง config สำหรับ Android (ส่วนนี้ใช้ได้เฉพาะแอป Android)
[Interface]
PrivateKey = <ANDROID_PRIVATE_KEY>
Address = 10.8.0.10/32
DNS = 10.8.0.1
# ไม่ให้แอปเหล่านี้ผ่าน VPN (excluded applications)
ExcludedApplications = com.android.chrome, com.example.bankapp

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25

2.4.4 ข้อจำกัดด้าน Battery และ Background

🧪 ตัวอย่าง 2.4: สคริปต์สร้าง QR code ของ config มือถือ (ใช้ qrencode) ดูในไฟล์ตัวอย่าง


2.5 BSD และระบบอื่น (BSD and Other Systems)

2.5.1 FreeBSD

# ติดตั้ง tools
pkg install -y wireguard-tools

# โหลดโมดูล kernel (บนรุ่นที่มี if_wg) และให้โหลดตอนบูต
kldload if_wg
sysrc kld_list+="if_wg"

# ตั้งค่า interface ผ่าน rc.conf (ตัวอย่าง)
sysrc wireguard_enable="YES"
sysrc wireguard_interfaces="wg0"

# เก็บ config ไว้ที่ /usr/local/etc/wireguard/wg0.conf แล้วเริ่ม
service wireguard start wg0

2.5.2 OpenBSD

OpenBSD มี wg(4) ใน base system ตั้งค่าด้วย ifconfig / hostname.if(5)

# ไฟล์ /etc/hostname.wg0  (ต้อง chmod 640 หรือ 600, owner root)
wgkey <OPENBSD_PRIVATE_KEY>
wgport 51820
wgpeer <PEER_PUBLIC_KEY> wgendpoint 203.0.113.10 51820 wgaip 10.8.0.0/24
inet 10.8.0.2 255.255.255.0
up
# นำ config ไปใช้ทันทีโดยไม่ต้องรีบูต
sh /etc/netstart wg0

# ดูสถานะ
ifconfig wg0

2.5.3 NetBSD

ใช้ผ่าน pkgsrc: net/wireguard-go และ net/wireguard-tools (userspace implementation) ตั้งค่า TUN device แล้วรัน wireguard-go wg0

2.5.4 pfSense และ OPNsense (แนะนำเบื้องต้น)

🧪 ตัวอย่าง 2.5: ไฟล์ config ตัวอย่างสำหรับ FreeBSD และ OpenBSD ดูในไฟล์ตัวอย่าง


2.6 การรันใน Container (Running in Containers)

2.6.1 Docker: capability ที่ต้องใช้

Capability / ค่าตั้ง เหตุผล
NET_ADMIN สร้าง/ตั้งค่า network interface และ route ภายใน container
SYS_MODULE โหลดโมดูล wireguard ของ host (จำเป็นเฉพาะเมื่อ host ยังไม่ได้โหลดโมดูล)
sysctls: net.ipv4.conf.all.src_valid_mark=1 ให้ wg-quick ตั้ง policy routing ด้วย fwmark ได้
net.ipv4.ip_forward=1 ถ้า container ต้องทำหน้าที่ forward ให้เครื่องอื่น
mount /lib/modules (read-only) ให้ container ตรวจ/โหลดโมดูลของ host ได้
# docker-compose.yml: WireGuard server อย่างง่าย (ใช้ image ของ linuxserver.io)
services:
  wireguard:
    image: lscr.io/linuxserver/wireguard:latest
    container_name: wireguard
    cap_add:
      - NET_ADMIN        # จำเป็น: จัดการ interface
      - SYS_MODULE       # จำเป็นเฉพาะกรณี host ยังไม่โหลดโมดูล
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Asia/Bangkok
      - SERVERURL=vpn.example.com   # ชื่อ/IP สาธารณะของเซิร์ฟเวอร์
      - SERVERPORT=51820
      - PEERS=3                     # สร้าง client 3 ชุดอัตโนมัติ
      - PEERDNS=auto
      - INTERNAL_SUBNET=10.13.13.0
    volumes:
      - ./config:/config
      - /lib/modules:/lib/modules:ro
    ports:
      - 51820:51820/udp
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    restart: unless-stopped

# วิธีใช้:
#   docker compose up -d
#   docker compose logs wireguard | tail      # ดู QR code ของ peer1 ใน log
#   cat config/peer_peer1/peer_peer1.conf     # ไฟล์ config ของ client

2.6.2 ข้อควรระวัง Kernel Module กับ Host

2.6.3 Rootless / Userspace Mode ด้วย wireguard-go

# รัน container แบบ userspace ด้วย wireguard-go (ไม่ต้องมีโมดูล kernel)
# ต้องมี /dev/net/tun และ capability NET_ADMIN
docker run -d --name wg-userspace \
  --cap-add NET_ADMIN \
  --device /dev/net/tun:/dev/net/tun \
  -e WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard-go \
  -v "$PWD/wg0.conf:/etc/wireguard/wg0.conf:ro" \
  -p 51820:51820/udp \
  --entrypoint sh \
  alpine:3.20 -c "apk add --no-cache wireguard-tools wireguard-go iptables && wg-quick up wg0 && sleep infinity"

# ตรวจสอบผลลัพธ์
docker exec wg-userspace wg show
# ตัวอย่างผลลัพธ์: interface: wg0, public key: ..., listening port: 51820

🧪 ตัวอย่าง 2.6: ไฟล์ docker-compose.yml และ userspace-run.sh ดูในไฟล์ตัวอย่าง


ส่วนที่ 3 การตั้งค่าพื้นฐาน (Basic Configuration)

3.1 การสร้างกุญแจ (Key Generation)

3.1.1 คำสั่งที่ใช้

คำสั่ง หน้าที่ ผลลัพธ์
wg genkey สร้าง private key ใหม่ สตริง base64 ยาว 44 ตัวอักษร (32 byte)
wg pubkey คำนวณ public key จาก private key ที่รับทาง stdin สตริง base64 ยาว 44 ตัวอักษร
wg genpsk สร้าง pre-shared key สตริง base64 ยาว 44 ตัวอักษร
# ตั้ง umask ให้ไฟล์ที่สร้างอ่านได้เฉพาะเจ้าของ (permission 600)
umask 077

# สร้าง private key แล้วคำนวณ public key ในคำสั่งเดียว
wg genkey | tee server.key | wg pubkey > server.pub

# สร้าง pre-shared key สำหรับคู่ peer หนึ่ง ๆ (ไม่บังคับ)
wg genpsk > server-client1.psk

# ตรวจสอบ permission และขนาด
ls -l server.key server.pub server-client1.psk
wc -c server.key     # ควรได้ 45 (44 ตัวอักษร + newline)

# วิธีใช้: อ่านค่าไปใส่ config
echo "PrivateKey = $(cat server.key)"
echo "PublicKey  = $(cat server.pub)"

3.1.2 การจัดเก็บ Private Key อย่างปลอดภัย

# ตั้ง permission ของโฟลเดอร์และไฟล์ที่เหมาะสม
sudo install -d -m 700 /etc/wireguard
sudo install -m 600 wg0.conf /etc/wireguard/wg0.conf
sudo ls -ld /etc/wireguard && sudo ls -l /etc/wireguard

3.1.3 แนวปฏิบัติ: 1 Key ต่อ 1 อุปกรณ์

🧪 ตัวอย่าง 3.1: สคริปต์ gen-keys.sh สร้างกุญแจของทุก peer ในครั้งเดียวและตั้ง permission ดูในไฟล์ตัวอย่าง


3.2 โครงสร้างไฟล์ config (Configuration File Structure)

3.2.1 ภาพรวม

ไฟล์ config ของ wg-quick เป็นรูปแบบ INI มี 2 ส่วนหลัก คือ [Interface] (หนึ่งส่วน) และ [Peer] (ศูนย์ถึงหลายส่วน)

flowchart TD
    CF["ไฟล์ wg0.conf
(Config file)"] --> IF["[Interface]
ตัวตนของเครื่องนี้ (This host)"] CF --> P1["[Peer] ที่ 1
เครื่องปลายทาง (Remote peer)"] CF --> P2["[Peer] ที่ 2 ... n"] IF --> I1["PrivateKey / ListenPort"] IF --> I2["Address / DNS / MTU"] IF --> I3["Table / FwMark"] IF --> I4["PreUp / PostUp / PreDown / PostDown / SaveConfig"] P1 --> Q1["PublicKey / PresharedKey"] P1 --> Q2["AllowedIPs / Endpoint"] P1 --> Q3["PersistentKeepalive"]

3.2.2 ส่วน [Interface]

ตัวเลือก ความหมาย หมายเหตุ
PrivateKey private key ของ interface นี้ (จำเป็น) base64 32 byte
Address IP address ของ interface ใน tunnel ใส่ได้หลายค่า (IPv4 + IPv6) เช่น 10.8.0.1/24, fd00:8::1/64 ส่วนขยายของ wg-quick
ListenPort พอร์ต UDP ที่รับ ถ้าไม่ตั้ง ระบบสุ่มพอร์ต ฝั่ง server ควรกำหนดค่าคงที่
DNS DNS server (และ search domain) ที่ใช้เมื่อ tunnel ขึ้น ส่วนขยายของ wg-quick ต้องมี resolvconf/systemd-resolved
MTU ขนาด MTU ของ interface ค่าเริ่มต้น wg-quick เลือกจาก interface ของ route ปลายทาง มักได้ 1420
Table ตาราง routing ที่ใส่ route auto (ค่าเริ่มต้น), off (ไม่ยุ่งกับ route), หรือเลขตาราง
FwMark fwmark ที่ใส่ให้แพ็กเก็ต UDP ที่เข้ารหัสแล้ว ใช้ทำ policy routing (กัน routing loop)
PreUp / PostUp คำสั่งที่รันก่อน/หลัง interface ขึ้น ใช้ตั้ง firewall/NAT
PreDown / PostDown คำสั่งที่รันก่อน/หลัง interface ลง ใช้ล้างกฎที่ตั้งไว้
SaveConfig true = เขียนสถานะ runtime กลับไฟล์เมื่อ down ระวัง: จะเขียนทับไฟล์ที่แก้ด้วยมือ

3.2.3 ส่วน [Peer]

ตัวเลือก ความหมาย หมายเหตุ
PublicKey public key ของ peer (จำเป็น) ใช้ระบุตัว peer
PresharedKey PSK สำหรับคู่นี้ ต้องเหมือนกันทั้งสองฝั่ง
AllowedIPs รายการ IP/subnet ที่ route ไป peer นี้ และอนุญาตเป็น source คั่นด้วย comma
Endpoint host:port ของ peer ใส่เมื่อเราเป็นฝ่ายเริ่มติดต่อ; รองรับชื่อ DNS (resolve ตอน up)
PersistentKeepalive ส่ง keepalive ทุก N วินาที 0 = ปิด (ค่าเริ่มต้น); 25 เหมาะกับหลัง NAT

3.2.4 Hook: PreUp, PostUp, PreDown, PostDown, SaveConfig

[Interface]
PrivateKey = <SERVER_PRIVATE_KEY>
Address = 10.8.0.1/24
ListenPort = 51820
# เปิด NAT เมื่อ interface ขึ้น และล้างเมื่อลง (eth0 = interface ขาออกสู่อินเทอร์เน็ต)
PostUp   = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

3.2.5 อธิบาย AllowedIPs ให้ชัด

AllowedIPs ทำหน้าที่ สองอย่างในที่เดียว และเป็นแหล่งความสับสนอันดับหนึ่งของผู้เริ่มต้น

มิติ บทบาท ตัวอย่าง
ขาออก (Outbound) เป็น routing table: "แพ็กเก็ตที่จะไป IP นี้ ให้เข้ารหัสแล้วส่งไป peer นี้" AllowedIPs = 192.168.20.0/24 → ส่ง traffic ไป 192.168.20.x ผ่าน peer นี้
ขาเข้า (Inbound) เป็น ACL: "รับเฉพาะแพ็กเก็ตที่ source IP อยู่ในช่วงนี้ จาก peer นี้" แพ็กเก็ตจาก peer นี้ที่ src ไม่อยู่ใน 192.168.20.0/24 จะถูกทิ้ง

กฎสำคัญ:

  1. ช่วง AllowedIPs ของ peer ต่าง ๆ ซ้อนทับกันไม่ได้อย่างสมบูรณ์: ถ้า IP หนึ่งปรากฏใน AllowedIPs ของสอง peer จะถูกย้ายไปผูกกับ peer ที่ตั้งค่าล่าสุด
  2. 0.0.0.0/0 หมายถึง "ส่งทุกอย่างผ่าน peer นี้" (default route) ควรมี peer เดียว เท่านั้นที่มีค่านี้
  3. ใน config ของ client AllowedIPs ของ server คือ "สิ่งที่ client จะส่งผ่าน VPN"; ใน config ของ server AllowedIPs ของ client คือ "IP ที่ client คนนั้นมีสิทธิ์ใช้เป็น source" (ปกติคือ /32 ของ client)

🧪 ตัวอย่าง 3.2: ไฟล์ config อ้างอิงที่ใส่ comment ทุกตัวเลือก (reference.conf) ดูในไฟล์ตัวอย่าง


3.3 Tunnel แรกแบบ Point-to-Point (First Point-to-Point Tunnel)

3.3.1 สถานการณ์และสถาปัตยกรรม

เชื่อมสองเครื่อง คือ Node A (มี public IP 203.0.113.10) และ Node B (อยู่หลัง NAT ได้) ผ่าน tunnel network 10.8.0.0/24

flowchart LR
    subgraph A["Node A (เซิร์ฟเวอร์ Server)"]
        A1["eth0: 203.0.113.10"]
        A2["wg0: 10.8.0.1/24
ListenPort 51820"] end subgraph B["Node B (ไคลเอนต์ Client)"] B1["eth0: 192.168.1.50 (หลัง NAT)"] B2["wg0: 10.8.0.2/24"] end A2 <-- "UDP 51820 (เข้ารหัส Encrypted)" --> B2 A1 --- A2 B1 --- B2

3.3.2 การออกแบบ Addressing ของ Tunnel Network

ควรเลือกช่วง private ที่ ไม่ชนกับเครือข่ายที่เกี่ยวข้อง (LAN ที่บ้าน, ออฟฟิศ, Wi-Fi สาธารณะที่มักใช้ 192.168.0.0/24, 192.168.1.0/24, 10.0.0.0/24) จึงนิยมเลือกช่วงที่ไม่ค่อยถูกใช้ เช่น 10.8.0.0/24, 10.77.0.0/24, 172.30.50.0/24

จำนวนที่อยู่ที่ใช้งานได้ในเครือข่าย IPv4 prefix /p:

Husable = 232−p − 2
Prefix คำนวณ Husable เหมาะกับ
/30 22 − 2 2 point-to-point 2 เครื่อง
/29 23 − 2 6 ทีมเล็ก
/26 26 − 2 62 ทีมกลาง
/24 28 − 2 254 ใช้บ่อยที่สุด
/22 210 − 2 1,022 องค์กรขนาดกลาง

ตัวอย่างการคำนวณ: ต้องรองรับพนักงาน 40 คน คนละ 2 อุปกรณ์ = 80 peer + server 1 + สำรอง 20% ≈ 100 address ต้องการ H ≥ 100 ดังนั้น 232−p − 2 ≥ 100 → 232−p ≥ 102 → 32−p ≥ 7 (เพราะ 27 = 128) → p ≤ 25 เลือก /25 (126 address) หรือ /24 เพื่อเผื่อโต

หมายเหตุ: บน interface ของ WireGuard ปกติตั้ง client เป็น /32 ในฝั่ง server (AllowedIPs) และตั้ง interface ของ client เป็น /24 หรือ /32 ตามรูปแบบการใช้งาน

3.3.3 ขั้นตอนทีละขั้น

ขั้นที่ 1: สร้างกุญแจบนทั้งสองเครื่อง

# บน Node A
umask 077 && wg genkey | tee /etc/wireguard/a.key | wg pubkey > /etc/wireguard/a.pub
# บน Node B
umask 077 && wg genkey | tee /etc/wireguard/b.key | wg pubkey > /etc/wireguard/b.pub
# คัดลอกเฉพาะ "public key" ข้ามเครื่อง (ห้ามส่ง private key)

ขั้นที่ 2: เขียน config บน Node A (/etc/wireguard/wg0.conf)

[Interface]
# Node A: ฝั่งที่มี public IP คอยรับ connection
PrivateKey = <NODE_A_PRIVATE_KEY>
Address = 10.8.0.1/24
ListenPort = 51820

[Peer]
# Node B
PublicKey = <NODE_B_PUBLIC_KEY>
# อนุญาตเฉพาะ IP ของ Node B ใน tunnel
AllowedIPs = 10.8.0.2/32

ขั้นที่ 3: เขียน config บน Node B

[Interface]
# Node B: ฝั่งที่เริ่มติดต่อ (อยู่หลัง NAT ได้)
PrivateKey = <NODE_B_PRIVATE_KEY>
Address = 10.8.0.2/24

[Peer]
# Node A
PublicKey = <NODE_A_PUBLIC_KEY>
AllowedIPs = 10.8.0.1/32
Endpoint = 203.0.113.10:51820
# อยู่หลัง NAT ให้ส่ง keepalive เพื่อรักษา mapping
PersistentKeepalive = 25

ขั้นที่ 4: เปิด tunnel และเปิดพอร์ต

# เปิดพอร์ตบน Node A (ตัวอย่างใช้ ufw)
sudo ufw allow 51820/udp

# เปิด tunnel บนทั้งสองเครื่อง
sudo wg-quick up wg0

ขั้นที่ 5: ทดสอบ

# บน Node B: ping Node A ผ่าน tunnel
ping -c 3 10.8.0.1

# ดูสถานะ (ต้องเห็น latest handshake และ transfer)
sudo wg show

# ตัวอย่างผลลัพธ์ที่คาดหวังของ wg show บน Node B:
#   interface: wg0
#     public key: (public key ของ B)
#     private key: (hidden)
#     listening port: 49321
#
#   peer: (public key ของ A)
#     endpoint: 203.0.113.10:51820
#     allowed ips: 10.8.0.1/32
#     latest handshake: 4 seconds ago
#     transfer: 1.20 KiB received, 1.05 KiB sent
#     persistent keepalive: every 25 seconds

3.3.4 ข้อควรระวัง

🧪 ตัวอย่าง 3.3: สคริปต์ทดลองบนเครื่องเดียวด้วย network namespace (lab-p2p-netns.sh) พร้อมผลลัพธ์ที่คาดหวัง ดูในไฟล์ตัวอย่าง


3.4 เครื่องมือจัดการ (Management Tools)

3.4.1 wg เทียบกับ wg-quick

เครื่องมือ ระดับ ทำอะไร ไม่ทำอะไร
wg Low-level ตั้งค่า WireGuard ภายใน interface: key, peer, AllowedIPs, endpoint; แสดงสถานะ ไม่สร้าง interface, ไม่ตั้ง IP, ไม่ตั้ง route, ไม่ตั้ง DNS
wg-quick High-level (shell script) อ่าน .conf แล้วสร้าง interface, ตั้ง IP, route, DNS, MTU, รัน hook ไม่ใช่ daemon (รันครั้งเดียวแล้วจบ)
# คำสั่งที่ใช้บ่อยของ wg
sudo wg show                  # สถานะทุก interface
sudo wg show wg0 latest-handshakes   # เวลา handshake ล่าสุดของทุก peer (epoch)
sudo wg showconf wg0          # แสดง config ปัจจุบัน (รูปแบบที่ wg เข้าใจ)
sudo wg set wg0 peer <PUBKEY> allowed-ips 10.8.0.5/32   # เพิ่ม/แก้ peer แบบสด (ไม่ต้อง restart)
sudo wg set wg0 peer <PUBKEY> remove                    # ลบ peer สด ๆ

# โหลด config ใหม่โดยไม่ตัด tunnel ที่ใช้อยู่ (hot reload)
sudo wg syncconf wg0 <(sudo wg-quick strip wg0)

3.4.2 ตั้งค่า Manual ด้วย ip (ไม่ใช้ wg-quick)

# 1) สร้าง interface ชนิด wireguard
sudo ip link add dev wg0 type wireguard

# 2) กำหนด IP address ให้ interface
sudo ip address add 10.8.0.2/24 dev wg0

# 3) ตั้งค่า WireGuard (key, peer) ด้วย wg ; ไฟล์ b.key ต้องมี permission 600
sudo wg set wg0 \
    private-key /etc/wireguard/b.key \
    peer <NODE_A_PUBLIC_KEY> \
    allowed-ips 10.8.0.1/32 \
    endpoint 203.0.113.10:51820 \
    persistent-keepalive 25

# 4) เปิด interface
sudo ip link set up dev wg0

# 5) (ถ้าต้องการ) เพิ่ม route ไปยังเครือข่ายหลัง peer
sudo ip route add 192.168.20.0/24 dev wg0

# ปิดและลบ
sudo ip link set down dev wg0 && sudo ip link del dev wg0

# หมายเหตุ: AllowedIPs ไม่สร้าง route ให้ใน kernel โดยอัตโนมัติ (wg-quick เป็นผู้เพิ่ม route ให้)

3.4.3 เปิด/ปิด Tunnel และ systemd

# เปิด/ปิดแบบชั่วคราว
sudo wg-quick up wg0
sudo wg-quick down wg0

# ให้ tunnel เริ่มอัตโนมัติเมื่อบูต (unit template ชื่อ wg-quick@<interface>)
sudo systemctl enable --now wg-quick@wg0

# ตรวจสถานะ และดู log
systemctl status wg-quick@wg0
journalctl -u wg-quick@wg0 --no-pager | tail -n 20

# รีโหลด config โดยไม่ตัด tunnel (แนะนำใช้กับ production)
sudo systemctl reload wg-quick@wg0     # ในบางแพ็กเกจมี ExecReload ที่ใช้ wg syncconf

3.4.4 การรัน Tunnel อัตโนมัติเมื่อบูต

  1. วาง config ที่ /etc/wireguard/wg0.conf (permission 600)
  2. systemctl enable wg-quick@wg0
  3. ตรวจให้ network พร้อมก่อน: unit template มี After=network-online.target อยู่แล้ว แต่หากใช้ Endpoint เป็นชื่อโดเมน ต้องแน่ใจว่า DNS ใช้ได้ตอนบูต
  4. ทดสอบด้วยการรีบูต แล้วตรวจ wg show

🧪 ตัวอย่าง 3.4: ไฟล์ wg0.conf + สคริปต์ตั้งค่า manual (manual-setup.sh) ดูในไฟล์ตัวอย่าง


3.5 Network Manager Integration

3.5.1 NetworkManager

# นำเข้าไฟล์ config เป็น connection ของ NetworkManager
sudo nmcli connection import type wireguard file /etc/wireguard/wg0.conf

# เปิด/ปิด connection
nmcli connection up wg0
nmcli connection down wg0

# ให้เชื่อมต่ออัตโนมัติ
nmcli connection modify wg0 connection.autoconnect yes

# ดูรายละเอียด
nmcli connection show wg0 | grep -i wireguard

ข้อสังเกต: NetworkManager จัดการ route และ DNS เอง ไม่รัน PostUp/PostDown ของ wg-quick ดังนั้น config ที่พึ่ง hook (เช่น NAT) ควรใช้ wg-quick หรือ systemd unit แยก

3.5.2 systemd-networkd

สร้างสองไฟล์: .netdev (ประกาศ interface) และ .network (ตั้ง address/route)

# /etc/systemd/network/99-wg0.netdev
[NetDev]
Name=wg0
Kind=wireguard
Description=WireGuard tunnel wg0

[WireGuard]
# ใช้ไฟล์กุญแจแยก เพื่อไม่ฝัง secret ในไฟล์ .netdev
PrivateKeyFile=/etc/systemd/network/wg0.key
ListenPort=51820

[WireGuardPeer]
PublicKey=<PEER_PUBLIC_KEY>
AllowedIPs=10.8.0.2/32
# Endpoint=203.0.113.10:51820      # ใส่เมื่อเป็นฝ่ายเริ่มติดต่อ
# PersistentKeepalive=25
# /etc/systemd/network/99-wg0.network
[Match]
Name=wg0

[Network]
Address=10.8.0.1/24

# เพิ่ม route ไปเครือข่ายหลัง peer (ถ้ามี)
[Route]
Destination=192.168.20.0/24
# ตั้งเจ้าของไฟล์กุญแจให้ systemd-networkd อ่านได้ แต่ผู้ใช้อื่นอ่านไม่ได้
sudo install -m 0640 -o root -g systemd-network wg0.key /etc/systemd/network/wg0.key

# นำไปใช้
sudo networkctl reload
networkctl status wg0

3.5.3 netplan

# /etc/netplan/90-wireguard.yaml  (ตรวจเวอร์ชัน netplan ที่รองรับ key แบบไฟล์)
network:
  version: 2
  tunnels:
    wg0:
      mode: wireguard
      addresses: [10.8.0.1/24]
      port: 51820
      key:
        private: /etc/wireguard/server.key   # path ของไฟล์หรือค่า key (แนะนำให้ใช้ไฟล์)
      peers:
        - keys:
            public: <PEER_PUBLIC_KEY>
          allowed-ips: [10.8.0.2/32]
          # endpoint: 203.0.113.10:51820
sudo chmod 600 /etc/netplan/90-wireguard.yaml
sudo netplan generate && sudo netplan apply

3.5.4 ifupdown (Debian แบบดั้งเดิม)

# /etc/network/interfaces.d/wg0
# หมายเหตุ: ไฟล์ที่ใช้กับ "wg setconf" ต้องเป็นรูปแบบ pure WireGuard
# (ไม่มี Address, DNS, PostUp ฯลฯ ซึ่งเป็นส่วนขยายของ wg-quick)
auto wg0
iface wg0 inet static
    address 10.8.0.1/24
    pre-up ip link add dev wg0 type wireguard
    pre-up wg setconf wg0 /etc/wireguard/wg0-pure.conf
    post-down ip link del dev wg0
# สร้างไฟล์ pure config จาก wg0.conf ที่มีส่วนขยายของ wg-quick
sudo sh -c 'wg-quick strip wg0 > /etc/wireguard/wg0-pure.conf' && sudo chmod 600 /etc/wireguard/wg0-pure.conf
sudo ifup wg0

3.5.5 เปรียบเทียบวิธีจัดการ

วิธี ข้อดี ข้อเสีย เหมาะกับ
wg-quick + systemd ง่าย รองรับ hook ครบ สคริปต์ shell Server ทั่วไป
NetworkManager เข้ากับ desktop/laptop, GUI ไม่รัน hook Laptop ผู้ใช้ปลายทาง
systemd-networkd ประกาศแบบ declarative ไม่มี hook แบบ wg-quick Server/embedded ที่ใช้ networkd
netplan รวมกับ config เครือข่ายของ Ubuntu ฟีเจอร์ WireGuard จำกัดตามเวอร์ชัน Ubuntu Server/Cloud
ifupdown ใช้กับ Debian เก่า ต้องทำ pure config เอง ระบบเดิมที่ยังใช้ ifupdown

🧪 ตัวอย่าง 3.5: ชุดไฟล์ .netdev/.network, netplan และ ifupdown ดูในไฟล์ตัวอย่าง


ส่วนที่ 4 Network, Firewall และ DNS (Networking, Firewall and DNS)

4.1 IP Forwarding และ NAT (IP Forwarding and NAT)

4.1.1 เปิด IP Forwarding

เครื่องที่ทำหน้าที่เป็น gateway (ส่งต่อแพ็กเก็ตระหว่าง wg0 กับ interface อื่น) ต้องเปิด IP forwarding ใน kernel

# เปิดแบบชั่วคราว (หายเมื่อรีบูต)
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1

# เปิดถาวร: เขียนไฟล์ใน /etc/sysctl.d/
sudo tee /etc/sysctl.d/99-wireguard.conf >/dev/null <<'EOF'
# เปิดการส่งต่อแพ็กเก็ต IPv4 และ IPv6 สำหรับ WireGuard gateway
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sudo sysctl --system

# ตรวจสอบ (ต้องได้ค่า 1)
sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding

4.1.2 Masquerade ด้วย iptables / nftables

Masquerade คือการ NAT แบบแปลง source address ของแพ็กเก็ตเป็น address ของ interface ขาออก (เหมาะกับ IP ที่เปลี่ยนได้)

# แบบ iptables (eth0 = ขาออกสู่อินเทอร์เน็ต, 10.8.0.0/24 = tunnel network)
sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
sudo iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
sudo iptables -A FORWARD -i eth0 -o wg0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# แบบ nftables: /etc/nftables.d/wireguard-nat.nft
table inet wg_filter {
    chain forward {
        type filter hook forward priority 0; policy drop;
        # อนุญาตการตอบกลับของ connection ที่มีอยู่
        ct state established,related accept
        # อนุญาตจาก tunnel ออกอินเทอร์เน็ต
        iifname "wg0" oifname "eth0" accept
    }
}
table ip wg_nat {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        # แปลง source ของ traffic จาก tunnel ที่ออก eth0
        ip saddr 10.8.0.0/24 oifname "eth0" masquerade
    }
}
# วิธีใช้:
#   sudo nft -f /etc/nftables.d/wireguard-nat.nft
#   sudo nft list ruleset | grep -A3 masquerade

4.1.3 เมื่อใดต้อง NAT และเมื่อใดควร Route ตรง ๆ

สถานการณ์ ควรทำ เหตุผล
Client ออกอินเทอร์เน็ตผ่าน server (full tunnel) NAT (masquerade) อินเทอร์เน็ตไม่รู้จัก 10.8.0.0/24 ต้องแปลงเป็น IP ของ server
Client เข้าถึง LAN หลัง server (เช่น 192.168.1.0/24) ขึ้นกับสถานการณ์: NAT ง่ายที่สุด; route ตรงเห็น IP จริงของ client ถ้าไม่ NAT ต้องให้ LAN มี route กลับ 10.8.0.0/24 ผ่าน server
Site-to-site สองสาขา Route ตรง (ไม่ NAT) ต้องการเห็น IP ต้นทางจริง และไม่ให้ subnet ชนกัน
Server ต้องเก็บ log/ACL ตาม IP ของ client Route ตรง NAT ทำให้ปลายทางเห็นแต่ IP ของ gateway

4.2 Firewall

4.2.1 เปิดพอร์ต UDP ของ WireGuard

เปิดเฉพาะ UDP ตาม ListenPort (ค่า convention คือ 51820) ที่ฝั่งซึ่งต้องรับ connection

# ufw
sudo ufw allow 51820/udp comment 'WireGuard'
sudo ufw route allow in on wg0 out on eth0     # อนุญาตการ forward จาก tunnel สู่อินเทอร์เน็ต
sudo ufw status verbose

# firewalld
sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --permanent --zone=trusted --add-interface=wg0   # หรือสร้าง zone เฉพาะ
sudo firewall-cmd --permanent --add-masquerade                     # ถ้าต้องการ NAT
sudo firewall-cmd --reload

# iptables
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT

# nftables (ในตาราง inet filter ที่มีอยู่)
sudo nft add rule inet filter input udp dport 51820 accept

4.2.2 ภาพรวม Packet Flow ผ่าน Netfilter Hooks

flowchart TD
    IN["แพ็กเก็ตเข้า (Packet in)
UDP 51820 จาก eth0"] --> PRE["PREROUTING
(raw, mangle, nat)"] PRE --> RT1{"ปลายทางคือเครื่องนี้?
(Local destination?)"} RT1 -- "ใช่ (Yes)" --> INPUT["INPUT
(กฎ UDP 51820 อนุญาตที่นี่)"] INPUT --> WGD["WireGuard ถอดรหัส
(Decrypt)"] WGD --> INNER["แพ็กเก็ตชั้นใน src ใน AllowedIPs
(Inner packet)"] INNER --> PRE2["PREROUTING (ผ่าน wg0)"] PRE2 --> RT2{"ส่งต่อไปที่อื่น?
(Forward?)"} RT2 -- "ใช่ (Yes)" --> FWD["FORWARD
(นโยบายระหว่าง peer ตั้งที่นี่)"] FWD --> POST["POSTROUTING
(MASQUERADE ตั้งที่นี่)"] POST --> OUT["ออกจาก eth0 หรือ interface อื่น
(Packet out)"] RT2 -- "ไม่ (No)" --> LOCALIN["INPUT ของ wg0
(บริการบนเครื่องนี้ เช่น DNS)"]

4.2.3 ตัวอย่างกฎ: จำกัดสิทธิ์ระหว่าง Peer ด้วย Firewall

อย่าพึ่ง AllowedIPs เพียงอย่างเดียว เพราะ AllowedIPs ควบคุมแค่ "ใครใช้ source IP อะไรได้" ไม่ได้ควบคุม "ใครเข้าถึงพอร์ตใดของใคร" ควรใช้ firewall ร่วมด้วย (defense in depth)

# /etc/nftables.d/wg-policy.nft : นโยบายเข้มงวดสำหรับ hub
# - client (10.8.0.0/24) ห้ามคุยกันเอง (client isolation)
# - ผู้ดูแล (10.8.0.2) เข้าถึง client ทุกเครื่องได้
# - ทุกเครื่องเข้าถึงเฉพาะเซิร์ฟเวอร์ภายใน 192.168.10.5 พอร์ต 443
table inet wg_policy {
    set admins { type ipv4_addr; elements = { 10.8.0.2 } }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept

        # ผู้ดูแลเข้าถึงทุกเครื่องใน tunnel ได้
        iifname "wg0" oifname "wg0" ip saddr @admins accept

        # ห้าม client คุยกันเอง (traffic ที่เข้าและออก wg0 ตัวเดียวกัน)
        iifname "wg0" oifname "wg0" counter drop

        # client ใช้ได้เฉพาะ HTTPS ไปเซิร์ฟเวอร์ภายใน
        iifname "wg0" oifname "eth1" ip daddr 192.168.10.5 tcp dport 443 accept
    }
}
# วิธีใช้:
#   sudo nft -f /etc/nftables.d/wg-policy.nft
#   sudo nft list chain inet wg_policy forward
#   ทดสอบจาก client: curl -k https://192.168.10.5 (ผ่าน) / ping 10.8.0.3 (ถูก drop)

🧪 ตัวอย่าง 4.2: ชุดกฎ ufw, firewalld, nftables และสคริปต์ทดสอบ (test-policy.sh) ดูในไฟล์ตัวอย่าง


4.3 Routing ขั้นสูง (Advanced Routing)

4.3.1 Full Tunnel เทียบกับ Split Tunnel

ประเด็น Full tunnel Split tunnel
AllowedIPs ฝั่ง client 0.0.0.0/0, ::/0 เฉพาะ subnet ที่ต้องการ เช่น 10.8.0.0/24, 192.168.1.0/24
Traffic อินเทอร์เน็ต ผ่าน VPN ทั้งหมด ออกตามปกติ
ความเป็นส่วนตัวบน Wi-Fi สาธารณะ สูง ต่ำกว่า (traffic ภายนอก VPN ไม่ถูกปกป้อง)
ภาระของ server สูง (แบนด์วิดท์และ NAT) ต่ำ
ความเสี่ยง DNS/IPv6 leak ต้องระวัง ต้องระวังเรื่อง split DNS

4.3.2 wg-quick กับ Policy Routing

เมื่อ AllowedIPs เป็น 0.0.0.0/0 (default route) การใส่ default route ลง wg0 ตรง ๆ จะทำให้ UDP ที่เข้ารหัสแล้ว วนกลับเข้า wg0 อีก (routing loop) เพราะแพ็กเก็ตไปยัง Endpoint ก็ตรงกับ 0.0.0.0/0 ด้วย wg-quick แก้ปัญหานี้ด้วย fwmark และ routing table แยก

flowchart TD
    A["แอปส่งแพ็กเก็ตไปที่ 1.1.1.1
(App sends to 1.1.1.1)"] --> R1{"มี fwmark 51820?
(Has fwmark 51820?)"} R1 -- "ไม่มี (No)" --> T1["ใช้ตาราง 51820
default dev wg0"] T1 --> W["WireGuard เข้ารหัส
แล้วส่ง UDP ไป Endpoint
พร้อมติด fwmark 51820"] W --> R2{"มี fwmark 51820?"} R2 -- "มี (Yes)" --> T2["ข้ามตาราง 51820
ใช้ main table: default via eth0"] T2 --> NET["ออกอินเทอร์เน็ตทาง eth0
(No loop)"]

คำสั่งที่ wg-quick ใช้เบื้องหลัง (เขียนเองได้เมื่ออยากควบคุมละเอียด):

# 1) ให้ WireGuard ติด fwmark ที่แพ็กเก็ต UDP ขาออกของตัวเอง
sudo wg set wg0 fwmark 51820

# 2) สร้าง default route ใน "ตารางแยก" (table 51820) ชี้ไป wg0
sudo ip -4 route add 0.0.0.0/0 dev wg0 table 51820

# 3) แพ็กเก็ตที่ "ไม่มี" fwmark นี้ ให้ใช้ตาราง 51820 (ส่งเข้า VPN)
sudo ip -4 rule add not fwmark 51820 table 51820

# 4) route ที่เจาะจงกว่า default ใน main table ยังใช้ได้ (เช่น LAN) โดยไม่ถูกบังคับเข้า VPN
sudo ip -4 rule add table main suppress_prefixlength 0

# ตรวจสอบผลลัพธ์
ip -4 rule show
ip -4 route show table 51820
# ตัวอย่างผลลัพธ์:
#   0:      from all lookup local
#   32764:  from all lookup main suppress_prefixlength 0
#   32765:  not from all fwmark 0xca6c lookup 51820
#   32766:  from all lookup main
#   default dev wg0 scope link

หมายเหตุ: 0xca6c คือเลขฐานสิบหกของ 51820 (51,820 = 0xCA6C) และ IPv6 ใช้คำสั่งชุดเดียวกันด้วย ip -6

4.3.3 Policy-based Routing และหลาย Routing Table

Policy-based routing (PBR) คือการเลือกเส้นทางตามเงื่อนไขอื่นนอกจาก IP ปลายทาง (เช่น source IP, fwmark, interface) ผ่านคำสั่ง ip rule

# ตัวอย่าง: ให้เครื่อง 192.168.1.50 (ใน LAN) ออกอินเทอร์เน็ตผ่าน VPN เท่านั้น
# คนอื่นใน LAN ออกทาง ISP ตามปกติ
# 1) ตั้ง Table = off ใน [Interface] เพื่อไม่ให้ wg-quick แตะ routing เอง
# 2) สร้างตาราง 100 และกำหนดกฎ
sudo ip route add default dev wg0 table 100
sudo ip rule add from 192.168.1.50/32 table 100 priority 1000

# ตรวจสอบ
ip rule show | grep 1000
ip route show table 100

# ลบกฎ
sudo ip rule del from 192.168.1.50/32 table 100 priority 1000

4.3.4 การกัน Routing Loop ของ Endpoint

อาการ: เปิด full tunnel แล้ว handshake ไม่สำเร็จ หรือหลุดทันที เพราะแพ็กเก็ตที่ไปยัง Endpoint ถูกส่งเข้า wg0

วิธี รายละเอียด
ใช้ wg-quick (fwmark อัตโนมัติ) วิธีที่ง่ายที่สุดบน Linux
เพิ่ม route เฉพาะไปยัง endpoint ผ่าน gateway เดิม ip route add <ENDPOINT_IP>/32 via <GW> dev eth0 (ใช้เมื่อ Table=off หรือใช้ AllowedIPs แบบ 0.0.0.0/1, 128.0.0.0/1)
ตั้ง FwMark ใน [Interface] ทำให้ policy routing ใช้ mark เดียวกันทุกครั้ง
ใช้ 0.0.0.0/1 + 128.0.0.0/1 ครอบคลุม IPv4 ทั้งหมดโดยเจาะจงกว่า /0 เดิมของระบบ แต่ยังต้องกัน endpoint

🧪 ตัวอย่าง 4.3: สคริปต์ pbr-demo.sh ทดลอง policy routing ด้วย namespace ดูในไฟล์ตัวอย่าง


4.4 DNS

4.4.1 การตั้ง DNS = และผลต่อ resolvconf / systemd-resolved

เมื่อกำหนด DNS = 10.8.0.1 ใน [Interface] wg-quick จะเรียก resolvconf -a wg0 -m 0 -x (หรือ resolvectl ถ้ามี systemd-resolved) เพื่อเพิ่ม DNS server ของ tunnel และลบเมื่อ tunnel ลง

# ตรวจว่าระบบใช้ตัวจัดการ DNS ใด
ls -l /etc/resolv.conf       # ถ้าชี้ไป ../run/systemd/resolve/stub-resolv.conf = systemd-resolved
which resolvconf resolvectl

# ดูสถานะ DNS ต่อ interface หลังเปิด tunnel
resolvectl status wg0

# ทดสอบ DNS ผ่าน tunnel โดยเจาะจง server
dig @10.8.0.1 example.com +short

4.4.2 Split DNS (เฉพาะโดเมนภายใน)

Split DNS คือการส่งคำถาม DNS เฉพาะโดเมนภายใน (เช่น corp.example.com) ไปยัง DNS ของ tunnel ส่วนโดเมนอื่นใช้ DNS ปกติ

# ตัวอย่างบน client ที่ใช้ systemd-resolved (ไม่ใส่ DNS= ใน [Interface] เพื่อกันการแย่ง DNS ทั้งหมด)
[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.8.0.2/24
# ตั้ง DNS เฉพาะ interface wg0 เมื่อ tunnel ขึ้น: "~" นำหน้า = routing-only domain
PostUp   = resolvectl dns %i 10.8.0.1; resolvectl domain %i ~corp.example.com ~internal.lan
PostDown = resolvectl revert %i

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
AllowedIPs = 10.8.0.0/24, 192.168.10.0/24
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25
# วิธีทดสอบ
resolvectl query fileserver.corp.example.com    # ควรถูก resolve ผ่าน wg0
resolvectl query www.example.org                # ควรใช้ DNS เดิม (ไม่ผ่าน wg0)

4.4.3 DNS Leak และการป้องกัน

DNS leak คือการที่คำถาม DNS ถูกส่งออกนอก tunnel (ไปยัง DNS ของ ISP/Wi-Fi) ทั้งที่ผู้ใช้คิดว่าใช้ full tunnel อยู่ ทำให้เปิดเผยโดเมนที่เข้าถึง

สาเหตุที่พบบ่อย:

  1. ไม่ได้ตั้ง DNS = ใน full tunnel
  2. ระบบมี DNS หลายตัว และใช้ตัวของ interface เดิมก่อน (ลำดับ/metric)
  3. IPv6 DNS ยังรั่วเพราะ AllowedIPs ไม่มี ::/0
  4. แอป/เบราว์เซอร์ใช้ DNS-over-HTTPS ของตนเองโดยไม่ผ่านระบบ

แนวทางป้องกัน:

4.4.4 ใช้ร่วมกับ Pi-hole / AdGuard Home / Unbound

ให้บริการ DNS ฟังที่ address ของ tunnel (10.8.0.1) แล้วให้ client ใช้ DNS = 10.8.0.1

# ตัวอย่าง Unbound: /etc/unbound/unbound.conf.d/wg.conf
server:
    interface: 10.8.0.1              # ฟังเฉพาะบน address ของ tunnel
    access-control: 10.8.0.0/24 allow   # อนุญาตเฉพาะ client ใน tunnel
    access-control: 0.0.0.0/0 refuse
    hide-identity: yes
    hide-version: yes
    prefetch: yes
sudo systemctl restart unbound
dig @10.8.0.1 example.com +short       # ทดสอบจาก client

# Pi-hole: ตั้ง "Interface settings" ให้ฟังที่ wg0 (Allow only local requests หรือเลือก wg0)
# AdGuard Home: ตั้ง Listen interfaces ที่ wg0 / 10.8.0.1 ใน Settings > DNS settings

🧪 ตัวอย่าง 4.4: ไฟล์ตัวอย่าง Unbound และสคริปต์ทดสอบ DNS leak (dns-leak-check.sh) ดูในไฟล์ตัวอย่าง


4.5 MTU และ MSS

4.5.1 ที่มาของ Overhead

MTU (Maximum Transmission Unit) คือขนาดแพ็กเก็ต IP ที่ใหญ่ที่สุดที่ส่งได้โดยไม่ต้อง fragment เมื่อ WireGuard ห่อแพ็กเก็ต จะเพิ่มส่วนหัว จึงต้องลด MTU ของ wg0 ลง

ส่วนประกอบ IPv4 transport IPv6 transport
IP header ชั้นนอก (Outer IP) 20 byte 40 byte
UDP header 8 byte 8 byte
WireGuard header (type + reserved + receiver + counter) 16 byte 16 byte
Poly1305 authentication tag 16 byte 16 byte
รวม overhead 60 byte 80 byte
MTUwg = MTUlink − ( Hip + Hudp + Hwg + Htag )

ตัวอย่างการคำนวณ:

กรณี MTUlink Overhead MTUwg สูงสุด
Ethernet + IPv4 transport 1500 60 1440
Ethernet + IPv6 transport 1500 80 1420
PPPoE (DSL/Fiber บางเจ้า) + IPv4 1492 60 1432
PPPoE + IPv6 1492 80 1412
มือถือ/เครือข่ายที่มี MTU 1400 + IPv6 1400 80 1320

ค่าเริ่มต้น 1420 ปลอดภัยสำหรับกรณีที่ transport เป็น IPv6 บน Ethernet (และ IPv4 ด้วย) ส่วน 1280 คือ MTU ขั้นต่ำของ IPv6 หากตั้งต่ำกว่านี้ IPv6 จะใช้งานบน interface นั้นไม่ได้

4.5.2 อาการของ MTU ผิด

# ทดสอบหา MTU สูงสุดที่ผ่านจริงด้วยการห้าม fragment (-M do)
# ขนาด payload สูงสุด = MTU - 28 (IP 20 + ICMP 8)
# ลองที่ MTU 1420 -> payload 1392
ping -c 3 -M do -s 1392 10.8.0.1       # ถ้าผ่าน MTU ของเส้นทาง >= 1420
ping -c 3 -M do -s 1393 10.8.0.1       # ถ้าไม่ผ่านแปลว่า 1420 เป็นเพดานพอดี
# ตัวอย่างข้อความเมื่อเกิน: "ping: local error: message too long, mtu=1420"

ตัวอย่างการคำนวณ: ถ้า ping -s 1372 ผ่านแต่ -s 1373 ไม่ผ่าน → path MTU = 1372 + 28 = 1400 → ตั้ง MTU ของ wg0 ให้พอดี 1400 − 80 = 1320 (กรณี IPv6 transport) หรือ 1400 − 60 = 1340 (กรณี IPv4 transport)

4.5.3 MSS Clamping

MSS (Maximum Segment Size) คือขนาด payload TCP สูงสุดต่อ segment ในการจับมือ TCP แต่ละฝั่งประกาศ MSS ของตน ถ้า MSS ใหญ่เกิน MTU ของ tunnel จะเกิดปัญหาเมื่อ ICMP "Fragmentation needed" ถูกบล็อก MSS clamping คือการให้ router แก้ค่า MSS ใน SYN ให้พอดีกับ MTU ของ tunnel

MSS = MTU − Hip − Htcp

แทนค่า: MTU = 1420 → MSS (IPv4) = 1420 − 20 − 20 = 1380; MSS (IPv6) = 1420 − 40 − 20 = 1360

# iptables: ปรับ MSS ให้พอดีกับ PMTU ของ interface ขาออกอัตโนมัติ
sudo iptables -t mangle -A FORWARD -o wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
sudo iptables -t mangle -A FORWARD -i wg0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

# แบบกำหนดค่าตายตัว (ถ้า MTU = 1420, IPv4)
sudo iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 1380
# nftables: ปรับ MSS ตาม MTU ของ route
table inet wg_mss {
    chain forward {
        type filter hook forward priority mangle; policy accept;
        tcp flags syn tcp option maxseg size set rt mtu
    }
}

🧪 ตัวอย่าง 4.5: สคริปต์ find-mtu.sh ค้นหา MTU ที่เหมาะสมแบบอัตโนมัติ ดูในไฟล์ตัวอย่าง


4.6 IPv6 และ Dual Stack

4.6.1 Address แบบ Dual Stack

ใช้ ULA (Unique Local Address) ในช่วง fd00::/8 สำหรับ tunnel ภายใน (คล้าย private IPv4) หรือใช้ prefix สาธารณะที่ได้รับจัดสรร

# ตัวอย่าง server แบบ dual stack
[Interface]
PrivateKey = <SERVER_PRIVATE_KEY>
# ใส่ได้หลาย address คั่นด้วย comma
Address = 10.8.0.1/24, fd42:8:0::1/64
ListenPort = 51820
PostUp   = sysctl -w net.ipv6.conf.all.forwarding=1
# กรณีต้องการให้ client ออกอินเทอร์เน็ต IPv6 ผ่าน NAT66 (ถ้า ISP ไม่ให้ prefix เพิ่ม)
PostUp   = ip6tables -t nat -A POSTROUTING -s fd42:8:0::/64 -o eth0 -j MASQUERADE
PostDown = ip6tables -t nat -D POSTROUTING -s fd42:8:0::/64 -o eth0 -j MASQUERADE

[Peer]
PublicKey = <CLIENT_PUBLIC_KEY>
# กำหนดทั้ง IPv4 และ IPv6 ของ client
AllowedIPs = 10.8.0.2/32, fd42:8:0::2/128

หมายเหตุ: ถ้า ISP ให้ prefix IPv6 สาธารณะ (เช่น /56 หรือ /64 ที่ route มาให้) ควร route แทน NAT66 เพื่อคงหลักการ end-to-end

4.6.2 Tunnel ที่ใช้ IPv6 เป็น Transport

4.6.3 IPv6-only Network และ NAT64 (โดยสังเขป)

ถ้าเครือข่ายเป็น IPv6-only แต่ต้องการเข้าถึงบริการ IPv4: ใช้ NAT64 + DNS64 (เช่น Tayga หรือ Jool บน gateway) แปลง IPv6 ที่อยู่ในช่วง 64:ff9b::/96 ไปเป็น IPv4 ส่วน WireGuard ทำหน้าที่เป็นเส้นทางที่ปลอดภัยมายัง gateway นั้น (ไม่ได้แปลง protocol เอง)

🧪 ตัวอย่าง 4.6: config แบบ dual stack พร้อมคำสั่งทดสอบ ping -6 และ curl -6 ดูในไฟล์ตัวอย่าง


ส่วนที่ 5 กรณีการใช้งาน (Use Cases)

ส่วนนี้คือหัวใจของบทความ แต่ละกรณีมีโครงสร้างเดียวกัน: สถานการณ์ → สถาปัตยกรรม → config ตัวอย่าง → ข้อควรระวัง → การทดสอบ ค่า IP ทั้งหมดเป็นค่าสมมติ ใช้ <..._PRIVATE_KEY> / <..._PUBLIC_KEY> แทนกุญแจ ให้สร้างของจริงด้วย wg genkey (หัวข้อ 3.1)

กรณี ลักษณะ Topology ความยาก
5.1 Remote Access Star (หลาย client – 1 server) ★☆☆
5.2 Full Tunnel Client – Server ★★☆
5.3 Split Tunnel Client – Server ★★☆
5.4 Site-to-Site Point-to-Point (LAN–LAN) ★★☆
5.5 Hub-and-Spoke Star ★★☆
5.6 Full Mesh Mesh ★★★
5.7 หลัง NAT/CGNAT Star ผ่าน VPS ★★☆
5.8 Home Lab Gateway ★☆☆
5.9 Expose Service ผ่าน VPS Reverse tunnel ★★★
5.10 Commercial VPN Gateway + exit ★★☆
5.11 Egress/Exit Node Policy routing ★★★
5.12 Multi-tunnel/Failover Redundant ★★★
5.13 IoT/Embedded Star (หลายพันอุปกรณ์) ★★☆
5.14 Developer/DevOps Star + CI ★★☆
5.15 Remote Work ทีม Star + segmentation ★★★
5.16 Gaming/LAN Party Star/Mesh + L2 ★★☆
5.17 Mobile Roaming Client – Server ★☆☆
5.18 Cloud/Hybrid Gateway ต่อ Gateway ★★★

5.1 Remote Access (Road Warrior)

5.1.1 สถานการณ์

พนักงานหรือผู้ดูแลระบบอยู่นอกสถานที่ ต้องการเข้าถึงเครือข่ายองค์กร/บ้าน (192.168.10.0/24) อย่างปลอดภัยจากโน้ตบุ๊กและมือถือ Road Warrior คือผู้ใช้ที่เคลื่อนที่และมี IP เปลี่ยนไปเรื่อย ๆ

5.1.2 สถาปัตยกรรม

flowchart LR
    subgraph ROAD["ผู้ใช้เคลื่อนที่ (Road warriors)"]
        C1["โน้ตบุ๊ก Alice
10.8.0.10"] C2["มือถือ Alice
10.8.0.11"] C3["โน้ตบุ๊ก Bob
10.8.0.20"] end subgraph SRV["เซิร์ฟเวอร์ VPN (VPN server) 203.0.113.10"] S1["wg0: 10.8.0.1/24
UDP 51820"] end subgraph LAN["เครือข่ายองค์กร (Corporate LAN) 192.168.10.0/24"] L1["ไฟล์เซิร์ฟเวอร์
192.168.10.5"] L2["เว็บภายใน
192.168.10.6"] end C1 --> S1 C2 --> S1 C3 --> S1 S1 --> L1 S1 --> L2

การออกแบบ IP pool (10.8.0.0/24):

ช่วง การใช้งาน
10.8.0.1 Server
10.8.0.2–10.8.0.9 ผู้ดูแลระบบ
10.8.0.10–10.8.0.99 โน้ตบุ๊กพนักงาน (หนึ่งคนอาจใช้ 2 address: โน้ตบุ๊ก/มือถือ)
10.8.0.100–10.8.0.199 มือถือและอุปกรณ์พกพา
10.8.0.200–10.8.0.254 สำรอง/ผู้รับเหมาชั่วคราว

การจัดการหลายผู้ใช้: สร้าง [Peer] หนึ่งบล็อกต่ออุปกรณ์ใน server ใช้ AllowedIPs = <IP ของอุปกรณ์>/32 เสมอ และเขียน comment ระบุเจ้าของและวันที่ออก key

5.1.3 Config ตัวอย่าง

Server (/etc/wireguard/wg0.conf):

[Interface]
# เซิร์ฟเวอร์ Road Warrior
PrivateKey = <SERVER_PRIVATE_KEY>
Address = 10.8.0.1/24
ListenPort = 51820
# เปิด NAT เพื่อให้ client เข้าถึง LAN 192.168.10.0/24 ผ่าน eth0 (กรณี LAN ไม่มี route กลับ)
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT
PostUp   = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

# --- Alice (โน้ตบุ๊ก) ออกเมื่อ 2026-01-10 ---
[Peer]
PublicKey = <ALICE_LAPTOP_PUBLIC_KEY>
PresharedKey = <ALICE_LAPTOP_PSK>
AllowedIPs = 10.8.0.10/32

# --- Alice (มือถือ) ---
[Peer]
PublicKey = <ALICE_PHONE_PUBLIC_KEY>
PresharedKey = <ALICE_PHONE_PSK>
AllowedIPs = 10.8.0.11/32

# --- Bob (โน้ตบุ๊ก) ---
[Peer]
PublicKey = <BOB_LAPTOP_PUBLIC_KEY>
PresharedKey = <BOB_LAPTOP_PSK>
AllowedIPs = 10.8.0.20/32

Client (Alice โน้ตบุ๊ก):

[Interface]
PrivateKey = <ALICE_LAPTOP_PRIVATE_KEY>
Address = 10.8.0.10/32
DNS = 192.168.10.2                  # DNS ภายในองค์กร

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
PresharedKey = <ALICE_LAPTOP_PSK>
# ส่งเฉพาะ traffic ไป tunnel network และ LAN องค์กร (split tunnel)
AllowedIPs = 10.8.0.0/24, 192.168.10.0/24
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25

5.1.4 ข้อควรระวัง

5.1.5 การทดสอบ

# บน client: เปิด tunnel แล้วทดสอบ
sudo wg-quick up wg0
ping -c 3 10.8.0.1                 # ถึง server ผ่าน tunnel
ping -c 3 192.168.10.5             # ถึงไฟล์เซิร์ฟเวอร์ใน LAN
curl -sS http://192.168.10.6/ | head -n 3    # ถึงเว็บภายใน

# บน server: ดูว่ามี client เชื่อมกี่คน (handshake ภายใน 3 นาที = ใช้งานอยู่)
sudo wg show wg0 latest-handshakes | awk -v now="$(date +%s)" '{ if (now-$2 < 180) c++ } END { print c+0, "peer(s) active" }'

🧪 ตัวอย่าง 5.1: สคริปต์ add-client.sh สร้าง client config ใหม่พร้อม QR และเพิ่ม peer เข้า server แบบสด ดูในไฟล์ตัวอย่าง


5.2 Full Tunnel VPN (ส่ง traffic ทั้งหมดผ่าน VPN)

5.2.1 สถานการณ์

ผู้ใช้อยู่บน Wi-Fi สาธารณะ (ร้านกาแฟ สนามบิน โรงแรม) ต้องการเข้ารหัส traffic ทั้งหมดและซ่อน IP ต้นทางจริงจากเครือข่ายนั้น

5.2.2 สถาปัตยกรรม

flowchart LR
    U["ผู้ใช้บน Wi-Fi สาธารณะ
(User on public Wi-Fi)"] -- "ทุกอย่างเข้า wg0
(All traffic into wg0)" --> W["เครือข่ายท้องถิ่น (เห็นเฉพาะ UDP เข้ารหัส)
(Local network sees encrypted UDP only)"] W --> S["VPN Server
NAT ออกอินเทอร์เน็ต"] S --> I["อินเทอร์เน็ต
(Internet)"]

5.2.3 Config ตัวอย่าง

Client (full tunnel + kill switch):

[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.8.0.30/32, fd42:8:0::30/128
DNS = 10.8.0.1                       # ใช้ DNS ผ่าน tunnel ป้องกัน DNS leak
# Kill switch: ปฏิเสธทุกแพ็กเก็ตขาออกที่ "ไม่ผ่าน wg0" และ "ไม่ใช่ UDP ของ WireGuard เอง" (ไม่มี fwmark)
PostUp   = iptables  -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PostUp   = ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown  = iptables  -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown  = ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
PresharedKey = <CLIENT_PSK>
# ส่งทั้ง IPv4 และ IPv6 ผ่าน tunnel เพื่อกัน IPv6 leak
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25

Server (เพิ่ม NAT สำหรับ IPv4/IPv6):

[Interface]
PrivateKey = <SERVER_PRIVATE_KEY>
Address = 10.8.0.1/24, fd42:8:0::1/64
ListenPort = 51820
PostUp   = sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
PostUp   = iptables  -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostUp   = ip6tables -t nat -A POSTROUTING -s fd42:8:0::/64 -o eth0 -j MASQUERADE
PostDown = iptables  -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = ip6tables -t nat -D POSTROUTING -s fd42:8:0::/64 -o eth0 -j MASQUERADE

[Peer]
PublicKey = <CLIENT_PUBLIC_KEY>
PresharedKey = <CLIENT_PSK>
AllowedIPs = 10.8.0.30/32, fd42:8:0::30/128

5.2.4 ข้อควรระวัง

5.2.5 การทดสอบ

sudo wg-quick up wg0

# 1) IP สาธารณะต้องเป็น IP ของ server ไม่ใช่ IP ของ Wi-Fi
curl -4 -sS https://ifconfig.me ; echo
curl -6 -sS https://ifconfig.me ; echo      # ถ้า server ไม่รองรับ IPv6 ต้อง "ล้มเหลว" (ไม่รั่ว)

# 2) ทดสอบ DNS leak: คำถาม DNS ต้องออกที่ wg0 เท่านั้น (เปิด terminal ที่สองรัน tcpdump)
sudo tcpdump -ni any 'udp port 53' -c 5 &
dig example.com +short

# 3) ทดสอบ kill switch: ปิด tunnel ฝั่ง server (หรือ wg set wg0 peer <KEY> remove) แล้วลอง ping
ping -c 2 -W 2 1.1.1.1      # ต้อง "ไม่ผ่าน" (ถูก REJECT)

🧪 ตัวอย่าง 5.2: ไฟล์ fulltunnel-client.conf และสคริปต์ leak-test.sh ดูในไฟล์ตัวอย่าง


5.3 Split Tunnel

5.3.1 สถานการณ์

ต้องการให้เฉพาะ traffic ภายใน (เซิร์ฟเวอร์องค์กร, subnet ที่บ้าน) ผ่าน VPN ส่วน traffic อินเทอร์เน็ตทั่วไป (YouTube, อัปเดตระบบ) ออกตามปกติเพื่อประหยัดแบนด์วิดท์และลด latency

5.3.2 สถาปัตยกรรม

flowchart TD
    APP["แอปส่ง traffic
(Application traffic)"] --> D{"ปลายทางอยู่ใน AllowedIPs?
(Destination in AllowedIPs?)"} D -- "ใช่ (Yes): 10.8.0.0/24, 192.168.10.0/24" --> WG["เข้า wg0 (เข้ารหัส)"] D -- "ไม่ใช่ (No)" --> ETH["ออก eth0/wlan0 ตามปกติ
(Normal path)"]

5.3.3 การคำนวณ AllowedIPs แบบ "ทุกอย่างยกเว้น..."

บางครั้งต้องการ "ส่งทุกอย่างผ่าน VPN ยกเว้นเครือข่าย LAN ท้องถิ่น (192.168.0.0/16)" แต่ AllowedIPs ไม่มีตัวดำเนินการ "ยกเว้น" จึงต้องแตก 0.0.0.0/0 ออกเป็นหลายบล็อก CIDR ที่ ไม่ครอบคลุม ช่วงที่ยกเว้น

หลักการ: ถ้ายกเว้นบล็อกเดียวที่ prefix /p ออกจาก 0.0.0.0/0 จะได้บล็อกที่เหลือ p บล็อกพอดี (หนึ่งบล็อกต่อหนึ่งบิตของ prefix ที่ต่างจากบล็อกที่ยกเว้น)

Nblocks = p

ตัวอย่างการคำนวณ: ยกเว้น 192.168.0.0/16 (p = 16) → ได้ 16 บล็อก ดังนี้

0.0.0.0/1, 128.0.0.0/2, 192.0.0.0/9, 192.128.0.0/11, 192.160.0.0/13, 192.169.0.0/16,
192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12, 192.192.0.0/10, 193.0.0.0/8,
194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, 224.0.0.0/3

ตรวจสอบ: 192.168.x.x อยู่ระหว่าง 192.168.0.0 ถึง 192.168.255.255 ซึ่งอยู่ในช่องว่างระหว่าง 192.160.0.0/13 (ครอบ 192.160–192.167) กับ 192.169.0.0/16 พอดี จึงไม่ถูกครอบคลุมโดยบล็อกใดเลย

ถ้ายกเว้นทั้ง RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) จะได้ 31 บล็อก

0.0.0.0/5, 8.0.0.0/7, 11.0.0.0/8, 12.0.0.0/6, 16.0.0.0/4, 32.0.0.0/3, 64.0.0.0/2,
128.0.0.0/3, 160.0.0.0/5, 168.0.0.0/6, 172.0.0.0/12, 172.32.0.0/11, 172.64.0.0/10,
172.128.0.0/9, 173.0.0.0/8, 174.0.0.0/7, 176.0.0.0/4, 192.0.0.0/9, 192.128.0.0/11,
192.160.0.0/13, 192.169.0.0/16, 192.170.0.0/15, 192.172.0.0/14, 192.176.0.0/12,
192.192.0.0/10, 193.0.0.0/8, 194.0.0.0/7, 196.0.0.0/6, 200.0.0.0/5, 208.0.0.0/4, 224.0.0.0/3

ใช้สคริปต์ Python ต่อไปนี้คำนวณช่วงใด ๆ ได้เอง

#!/usr/bin/env python3
"""allowedips-calc.py : คำนวณ AllowedIPs แบบ 'ทุกอย่างยกเว้น...'
วิธีใช้: python3 allowedips-calc.py 192.168.0.0/16 10.0.0.0/8
         (ไม่ใส่อาร์กิวเมนต์ = ใช้ค่ายกเว้น RFC 1918 ทั้งสามช่วง)
"""
import ipaddress
import sys

def compute(base, excludes):
    """ตัดช่วง excludes ออกจาก base แล้วคืนรายการ CIDR ที่เหลือ (เรียงลำดับ)"""
    nets = [ipaddress.ip_network(base)]
    for ex in map(ipaddress.ip_network, excludes):
        remaining = []
        for n in nets:
            if n.version != ex.version:
                remaining.append(n)
            elif ex.subnet_of(n):
                remaining.extend(n.address_exclude(ex))   # แตกบล็อกที่มี ex อยู่ข้างใน
            elif n.subnet_of(ex):
                continue                                  # บล็อกนี้อยู่ใน ex ทั้งหมด -> ตัดทิ้ง
            else:
                remaining.append(n)                       # ไม่ทับกันเลย
        nets = remaining
    return sorted(ipaddress.collapse_addresses(nets))

if __name__ == "__main__":
    ex = sys.argv[1:] or ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
    result = compute("0.0.0.0/0", ex)
    print("AllowedIPs = " + ", ".join(map(str, result)))
    print(f"# จำนวนบล็อก: {len(result)}", file=sys.stderr)

5.3.4 Split Tunnel รายแอป (Android) และรายโดเมน

รายแอป (Android): ใช้ ExcludedApplications / IncludedApplications (ดูหัวข้อ 2.4.3)

รายโดเมน (Linux): ใช้ dnsmasq + ipset เพื่อรวบรวม IP ของโดเมนที่ต้องการ แล้วติด fwmark ให้ส่งเข้า tunnel ด้วย policy routing

# 1) สร้าง ipset สำหรับเก็บ IP ของโดเมนที่ต้องผ่าน VPN
sudo ipset create vpn_domains hash:ip timeout 3600

# 2) dnsmasq: ทุกครั้งที่ resolve โดเมนเหล่านี้ ให้เพิ่ม IP ที่ได้ลง ipset
#    /etc/dnsmasq.d/vpn-domains.conf
#       ipset=/example.com/internal.example.org/vpn_domains

# 3) ติด fwmark 0x64 ให้แพ็กเก็ตที่ไปยัง IP ใน ipset
sudo iptables -t mangle -A OUTPUT -m set --match-set vpn_domains dst -j MARK --set-mark 0x64

# 4) แพ็กเก็ตที่มี mark 0x64 ใช้ตาราง 100 (default ผ่าน wg0)
#    (ใน [Interface] ของ wg0 ตั้ง Table = off เพื่อไม่ให้ wg-quick แตะ routing)
sudo ip route add default dev wg0 table 100
sudo ip rule add fwmark 0x64 table 100 priority 1000

# ทดสอบ
dig example.com +short
sudo ipset list vpn_domains      # ต้องเห็น IP ของโดเมนอยู่ในเซต
curl -sS https://example.com -o /dev/null -w '%{remote_ip}\n'

ข้อจำกัด: IP ของโดเมนอาจเปลี่ยน (CDN) และการรวม ipset ทำงานเมื่อ client ใช้ resolver ที่เรากำหนดเท่านั้น (ไม่รองรับ DoH ของเบราว์เซอร์)

5.3.5 การทดสอบ

# ดูว่า route แยกกันถูกต้อง
ip route get 192.168.10.5        # ต้องผ่าน dev wg0
ip route get 8.8.8.8             # ต้องผ่าน interface ปกติ (ไม่ใช่ wg0)
curl -sS https://ifconfig.me     # ต้องเป็น IP จริงของท่าน (ไม่ผ่าน VPN)

🧪 ตัวอย่าง 5.3: allowedips-calc.py, config แบบ split tunnel, และสคริปต์ per-domain ดูในไฟล์ตัวอย่าง


5.4 Site-to-Site VPN

5.4.1 สถานการณ์

เชื่อมเครือข่ายสองสาขาเข้าด้วยกัน: Site A (กรุงเทพ, LAN 192.168.10.0/24, public IP คงที่ 203.0.113.10) และ Site B (สงขลา, LAN 192.168.20.0/24, public IP แบบ dynamic) ให้เครื่องใน LAN ทั้งสองคุยกันได้โดยตรง

5.4.2 สถาปัตยกรรม

flowchart LR
    subgraph SA["Site A กรุงเทพ (Bangkok)"]
        HA["Host A
192.168.10.50"] --- GA["Gateway A
LAN 192.168.10.1
wg0 10.200.0.1
Public 203.0.113.10"] end subgraph SB["Site B สงขลา (Songkhla)"] GB["Gateway B
LAN 192.168.20.1
wg0 10.200.0.2
Public dynamic"] --- HB["Host B
192.168.20.60"] end GA <-- "WireGuard UDP 51820" --> GB

ตารางการ route:

ที่ตั้ง ปลายทาง Next hop
Gateway A 192.168.20.0/24 dev wg0 (เพิ่มอัตโนมัติจาก AllowedIPs ของ wg-quick)
Gateway B 192.168.10.0/24 dev wg0
Host A 192.168.20.0/24 via 192.168.10.1 (ถ้า Gateway A เป็น default gateway ของ LAN ไม่ต้องเพิ่ม)
Host B 192.168.10.0/24 via 192.168.20.1 (เช่นเดียวกัน)

5.4.3 Config ตัวอย่าง

Gateway A (static IP):

[Interface]
PrivateKey = <GATEWAY_A_PRIVATE_KEY>
Address = 10.200.0.1/30                  # /30 พอสำหรับ point-to-point
ListenPort = 51820
# เปิด forwarding และอนุญาตเฉพาะ traffic ระหว่างสอง LAN (ไม่ NAT เพื่อคงเห็น IP จริง)
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -A FORWARD -i %i -o eth1 -s 192.168.20.0/24 -d 192.168.10.0/24 -j ACCEPT
PostUp   = iptables -A FORWARD -i eth1 -o %i -s 192.168.10.0/24 -d 192.168.20.0/24 -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o eth1 -s 192.168.20.0/24 -d 192.168.10.0/24 -j ACCEPT
PostDown = iptables -D FORWARD -i eth1 -o %i -s 192.168.10.0/24 -d 192.168.20.0/24 -j ACCEPT

[Peer]
# Gateway B (IP dynamic จึงไม่ใส่ Endpoint; Gateway B เป็นฝ่ายเริ่มติดต่อ)
PublicKey = <GATEWAY_B_PUBLIC_KEY>
PresharedKey = <SITE_PSK>
AllowedIPs = 10.200.0.2/32, 192.168.20.0/24

Gateway B (IP dynamic):

[Interface]
PrivateKey = <GATEWAY_B_PRIVATE_KEY>
Address = 10.200.0.2/30
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -A FORWARD -i %i -o eth1 -s 192.168.10.0/24 -d 192.168.20.0/24 -j ACCEPT
PostUp   = iptables -A FORWARD -i eth1 -o %i -s 192.168.20.0/24 -d 192.168.10.0/24 -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o eth1 -s 192.168.10.0/24 -d 192.168.20.0/24 -j ACCEPT
PostDown = iptables -D FORWARD -i eth1 -o %i -s 192.168.20.0/24 -d 192.168.10.0/24 -j ACCEPT

[Peer]
PublicKey = <GATEWAY_A_PUBLIC_KEY>
PresharedKey = <SITE_PSK>
AllowedIPs = 10.200.0.1/32, 192.168.10.0/24
Endpoint = 203.0.113.10:51820
# Gateway B อยู่หลัง NAT/dynamic IP: ส่ง keepalive เพื่อให้ A ส่งหาได้ตลอด
PersistentKeepalive = 25

กรณี static IP ทั้งสองฝั่ง: ใส่ Endpoint ทั้งสองฝั่ง และตั้ง ListenPort คงที่ทั้งสอง ไม่จำเป็นต้องมี keepalive (ยกเว้นอยู่หลัง NAT)

กรณีที่ Gateway ไม่ใช่ default gateway ของ LAN: เพิ่ม static route บน router หลักของ LAN เช่น ip route add 192.168.20.0/24 via 192.168.10.1 (ที่ router หลักของ Site A)

5.4.4 ข้อควรระวัง

5.4.5 การทดสอบ

# บน Gateway B
sudo wg-quick up wg0
ping -c 3 10.200.0.1              # ถึง Gateway A ผ่าน tunnel
ping -c 3 192.168.10.1            # ถึง LAN ฝั่ง A (ที่อยู่ eth1 ของ Gateway A)

# บน Host B (192.168.20.60)
ping -c 3 192.168.10.50           # ถึง Host A ข้ามไซต์
traceroute -n 192.168.10.50       # ต้องเห็น hop: 192.168.20.1 -> 10.200.0.1 -> 192.168.10.50

🧪 ตัวอย่าง 5.4: ชุด config site-a.conf / site-b.conf และสคริปต์ทดลองด้วย namespace สามเครื่อง ดูในไฟล์ตัวอย่าง


5.5 Hub-and-Spoke

5.5.1 สถานการณ์

มีหลายสาขาหรือหลายเครื่อง (spoke) ที่ต้องคุยกันเองได้ โดยให้ทุกเครื่องเชื่อมเฉพาะกับ hub กลาง (เช่น VPS หนึ่งเครื่อง) เหมาะกับกรณีที่ spoke หลายตัวอยู่หลัง NAT และไม่สามารถติดต่อกันโดยตรง

5.5.2 สถาปัตยกรรม

flowchart TD
    HUB["Hub 10.9.0.1
Public 203.0.113.10
เปิด forwarding (IP forwarding on)"] S1["Spoke 1
10.9.0.2
LAN 192.168.1.0/24"] S2["Spoke 2
10.9.0.3
LAN 192.168.2.0/24"] S3["Spoke 3
10.9.0.4
(เครื่องเดี่ยว Single host)"] S1 <--> HUB S2 <--> HUB S3 <--> HUB

การไหลของ traffic ระหว่าง spoke: Spoke 1 → Hub → Spoke 2 โดย Spoke 1 ต้องมี route ไปยัง subnet ของ Spoke 2 ผ่าน Hub

5.5.3 Config ตัวอย่าง

Hub:

[Interface]
PrivateKey = <HUB_PRIVATE_KEY>
Address = 10.9.0.1/24
ListenPort = 51820
# Hub ต้องส่งต่อแพ็กเก็ตระหว่าง spoke (ภายใน wg0 ตัวเดียว) จึงเปิด forwarding
PostUp   = sysctl -w net.ipv4.ip_forward=1
# อนุญาตเฉพาะการ forward ภายใน wg0 (ไม่ให้ออก eth0)
PostUp   = iptables -A FORWARD -i %i -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o %i -j ACCEPT

[Peer]
# Spoke 1 (และ LAN ของมัน)
PublicKey = <SPOKE1_PUBLIC_KEY>
AllowedIPs = 10.9.0.2/32, 192.168.1.0/24

[Peer]
# Spoke 2 (และ LAN ของมัน)
PublicKey = <SPOKE2_PUBLIC_KEY>
AllowedIPs = 10.9.0.3/32, 192.168.2.0/24

[Peer]
# Spoke 3
PublicKey = <SPOKE3_PUBLIC_KEY>
AllowedIPs = 10.9.0.4/32

Spoke 1 (ส่วนที่ชี้ไป Hub):

[Interface]
PrivateKey = <SPOKE1_PRIVATE_KEY>
Address = 10.9.0.2/24
PostUp = sysctl -w net.ipv4.ip_forward=1       # ถ้าทำหน้าที่เป็น gateway ของ LAN ตัวเอง

[Peer]
PublicKey = <HUB_PUBLIC_KEY>
# "ทุกอย่างในเครือข่ายของ hub" รวมถึง spoke อื่นและ LAN ของ spoke อื่น
AllowedIPs = 10.9.0.0/24, 192.168.2.0/24
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25

หลักการสำคัญ: AllowedIPs ใน Spoke 1 ของ peer "Hub" ครอบคลุม IP ของ spoke อื่น (10.9.0.0/24) จึงเข้ารหัสส่งไป Hub; ที่ Hub แพ็กเก็ตถูกถอดรหัส (ผ่านเพราะ src 10.9.0.2 อยู่ใน AllowedIPs ของ Spoke 1) แล้ว kernel ของ Hub ค้นหา route ไปยัง 10.9.0.3 พบว่าผ่าน wg0 จึงส่งเข้ารหัสต่อด้วย key ของ Spoke 2

5.5.4 ข้อดี/ข้อเสีย และการคำนวณ Latency

ประเด็น ข้อดี ข้อเสีย
ความซับซ้อน n − 1 tunnel สำหรับ n จุด; config น้อย Hub มี config ใหญ่ที่สุด
การผ่าน NAT spoke หลัง NAT ได้ทั้งหมด (ทุกตัวต่อออกไปหา hub) -
การควบคุม ตั้งนโยบาย firewall ที่จุดเดียว -
ความพร้อมใช้งาน - Hub เป็นจุดล้มเหลวเดียว (single point of failure)
ประสิทธิภาพ - Latency รวมสูงกว่าเส้นตรง, แบนด์วิดท์ของ Hub เป็นคอขวด
LAB = LAH + LHB + tproc

ตัวอย่าง: A–Hub = 20 ms, Hub–B = 30 ms, tproc ≈ 0.5 ms → LAB ≈ 50.5 ms ขณะที่เส้นทางตรง A–B (ถ้าเชื่อมได้) อาจเหลือ ~15 ms ดังนั้นควรวาง Hub ในภูมิภาคกลางระหว่าง spoke

แบนด์วิดท์ที่ Hub: ถ้ามีสอง spoke คุยกันที่ 100 Mbit/s ทิศทางเดียว Hub ต้องรับ 100 และส่ง 100 Mbit/s (รวม 200 Mbit/s ผ่าน NIC) และต้องเข้ารหัส/ถอดรหัสเท่ากัน

5.5.5 การทดสอบ

# จาก Spoke 1 ไป Spoke 2 (ผ่าน Hub)
ping -c 3 10.9.0.3
traceroute -n 10.9.0.3          # คาดหวัง: 10.9.0.1 (hub) -> 10.9.0.3

# บน Hub ดูว่า traffic ผ่านจริง
sudo wg show wg0 transfer
sudo tcpdump -ni wg0 icmp -c 6   # เห็น ICMP จาก 10.9.0.2 ไป 10.9.0.3 ทั้งขาเข้าและขาออก

🧪 ตัวอย่าง 5.5: ชุด config hub + 3 spoke และสคริปต์ทดลองด้วย namespace ดูในไฟล์ตัวอย่าง


5.6 Full Mesh

5.6.1 สถานการณ์

ต้องการให้ทุกเครื่องเชื่อมถึงกันโดยตรง (latency ต่ำสุด ไม่มี single point of failure) เหมาะกับคลัสเตอร์เซิร์ฟเวอร์ที่มี public IP หรือกลุ่มเล็ก

5.6.2 สถาปัตยกรรมและปัญหา Scaling

flowchart LR
    A["Node A"] --- B["Node B"]
    A --- C["Node C"]
    A --- D["Node D"]
    B --- C
    B --- D
    C --- D
Ntunnels = n(n−1) 2
จำนวน node (n) Tunnel รวม n(n−1)/2 Peer ต่อ node (n−1) บล็อก [Peer] ทั้งระบบ n(n−1)
4 6 3 12
5 10 4 20
10 45 9 90
50 1,225 49 2,450
100 4,950 99 9,900
500 124,750 499 249,500

ตัวอย่างการคำนวณ: n = 50 → N = 50 × 49 ÷ 2 = 1,225 tunnel; การเพิ่ม node ที่ 51 ต้องแก้ config ของ ทั้ง 50 เครื่องเดิม และสร้างใหม่ 50 tunnel (N กลายเป็น 1,275) นี่คือเหตุผลที่ต้องใช้เครื่องมือช่วยจัดการ

5.6.3 การจัดการด้วยเครื่องมือ

แนวทาง หมายเหตุ
สคริปต์สร้าง config เอง (ตัวอย่างด้านล่าง) ควบคุมเต็มที่ เหมาะ ≤ ไม่กี่สิบ node
Ansible/Terraform template เหมาะกับ infrastructure-as-code (หัวข้อ 11.1)
wg-meshconf และเครื่องมือชุมชนอื่น ๆ สร้าง config ทั้ง mesh จากไฟล์เดียว
Mesh VPN (Tailscale, Netbird, Netmaker, Headscale) จัดการ key, NAT traversal, ACL ให้อัตโนมัติ (หัวข้อ 7.2)
#!/usr/bin/env python3
"""mesh-gen.py : สร้าง config ทั้ง full mesh จากรายชื่อ node
วิธีใช้:  python3 mesh-gen.py nodes.csv outdir/
รูปแบบ nodes.csv (ไม่มี header):  ชื่อ,IP ใน tunnel,Endpoint (ว่างได้ถ้าอยู่หลัง NAT)
  node1,10.7.0.1,203.0.113.11:51820
  node2,10.7.0.2,203.0.113.12:51820
  node3,10.7.0.3,
"""
import csv, os, subprocess, sys

def sh(cmd, inp=None):
    """รันคำสั่งและคืนค่า stdout (ใช้เรียก wg genkey / pubkey / genpsk)"""
    return subprocess.run(cmd, input=inp, capture_output=True, text=True, check=True).stdout.strip()

def main(csv_path, outdir):
    os.makedirs(outdir, exist_ok=True)
    nodes = []
    for name, ip, endpoint in csv.reader(open(csv_path)):
        priv = sh(["wg", "genkey"])
        nodes.append({"name": name, "ip": ip, "ep": endpoint.strip(),
                      "priv": priv, "pub": sh(["wg", "pubkey"], priv)})
    # สร้าง PSK หนึ่งค่าต่อหนึ่งคู่ (i < j)
    psk = {(i, j): sh(["wg", "genpsk"]) for i in range(len(nodes)) for j in range(i + 1, len(nodes))}
    for i, me in enumerate(nodes):
        lines = ["[Interface]", f"PrivateKey = {me['priv']}", f"Address = {me['ip']}/24"]
        if me["ep"]:
            lines.append(f"ListenPort = {me['ep'].rsplit(':', 1)[1]}")
        for j, peer in enumerate(nodes):
            if i == j:
                continue
            lines += ["", "[Peer]", f"# {peer['name']}", f"PublicKey = {peer['pub']}",
                      f"PresharedKey = {psk[tuple(sorted((i, j)))]}", f"AllowedIPs = {peer['ip']}/32"]
            if peer["ep"]:
                lines.append(f"Endpoint = {peer['ep']}")
            else:
                pass  # peer หลัง NAT ไม่มี endpoint ให้ผู้ที่อยู่หลัง NAT เป็นฝ่ายเริ่ม
            if not me["ep"]:
                lines.append("PersistentKeepalive = 25")   # ตัวเราอยู่หลัง NAT
        path = os.path.join(outdir, f"{me['name']}.conf")
        with open(path, "w") as f:
            f.write("\n".join(lines) + "\n")
        os.chmod(path, 0o600)
        print("สร้างแล้ว:", path)

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2])

5.6.4 Hybrid: Mesh + Relay สำหรับ Peer หลัง NAT

เมื่อบาง node อยู่หลัง NAT (ไม่มี public endpoint) การเชื่อมตรงทุกคู่ทำไม่ได้ จึงใช้ relay node (node ที่มี public IP) ทำหน้าที่เหมือน hub สำหรับ node เหล่านั้น:

flowchart LR
    subgraph PUB["Node มี Public IP (Public nodes)"]
        P1["P1"] --- P2["P2"]
        P1 --- R["Relay R"]
        P2 --- R
    end
    subgraph NAT["Node หลัง NAT (Nodes behind NAT)"]
        N1["N1"]
        N2["N2"]
    end
    N1 -- "ต่อออกไปหา R" --> R
    N2 -- "ต่อออกไปหา R" --> R

5.6.5 การทดสอบ

python3 mesh-gen.py nodes.csv out/
# คัดลอก out/nodeX.conf ไปยังแต่ละเครื่อง แล้ว
sudo wg-quick up wg0

# บนแต่ละ node: ping ทุก node (ควรผ่านทั้งหมดเมื่อ handshake ครบ)
for ip in 10.7.0.1 10.7.0.2 10.7.0.3; do ping -c 1 -W 2 $ip >/dev/null && echo "$ip OK" || echo "$ip FAIL"; done
sudo wg show wg0 latest-handshakes     # ทุก peer ควรมีเวลา handshake ล่าสุด

🧪 ตัวอย่าง 5.6: nodes.csv และ mesh-gen.py ฉบับเต็ม พร้อมสคริปต์ทดลอง 4 namespace ดูในไฟล์ตัวอย่าง


5.7 เชื่อมต่ออุปกรณ์หลัง NAT / CGNAT

5.7.1 สถานการณ์

ISP ให้ IP แบบ CGNAT (Carrier-Grade NAT) หรือไม่เปิด public IP ให้ ทำให้เปิดพอร์ตเข้าบ้านไม่ได้ ตรวจสอบได้โดยเปรียบเทียบ IP ที่ router แสดงบนหน้า WAN กับ IP ที่เห็นจากอินเทอร์เน็ต ถ้าต่างกัน หรืออยู่ในช่วง 100.64.0.0/10 แสดงว่าอยู่หลัง CGNAT

5.7.2 สถาปัตยกรรม: ใช้ VPS เป็น Relay/Hub

flowchart LR
    subgraph HOME["บ้าน หลัง CGNAT (Home behind CGNAT)"]
        H["Home server
wg0 10.10.0.2"] end subgraph VPS["VPS (Public IP 203.0.113.10)"] V["wg0 10.10.0.1
ListenPort 51820"] end subgraph OUT["ผู้ใช้ภายนอก (Remote user)"] U["โน้ตบุ๊ก wg0 10.10.0.3"] end H -- "ต่อออกไปหา VPS
(outbound UDP)" --> V U -- "ต่อไปหา VPS" --> V

หลักการ: ทุกฝ่ายเป็นผู้ ต่อออกไปหา VPS (outbound) ซึ่งผ่าน CGNAT ได้เสมอ ส่วน VPS ส่งข้อมูลกลับมาตามเส้นทางของ mapping ที่เปิดไว้

5.7.3 UDP Hole Punching กับ WireGuard และข้อจำกัด

UDP hole punching คือเทคนิคให้สองฝั่งที่อยู่หลัง NAT ส่ง UDP หากันพร้อมกัน เพื่อให้ NAT ทั้งสองเปิด mapping และคุยตรงกันได้โดยไม่ผ่านตัวกลาง โดยมี server กลางช่วยแลก endpoint ที่สังเกตได้ (เช่น STUN)

5.7.4 Config ตัวอย่าง

VPS (hub/relay):

[Interface]
PrivateKey = <VPS_PRIVATE_KEY>
Address = 10.10.0.1/24
ListenPort = 51820
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -A FORWARD -i %i -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o %i -j ACCEPT

[Peer]
# Home server (หลัง CGNAT) ไม่มี Endpoint: VPS เรียนรู้จากแพ็กเก็ตที่ได้รับ
PublicKey = <HOME_PUBLIC_KEY>
AllowedIPs = 10.10.0.2/32, 192.168.1.0/24

[Peer]
# โน้ตบุ๊กผู้ใช้
PublicKey = <LAPTOP_PUBLIC_KEY>
AllowedIPs = 10.10.0.3/32

Home server:

[Interface]
PrivateKey = <HOME_PRIVATE_KEY>
Address = 10.10.0.2/24
# ทำหน้าที่เป็น gateway ให้ LAN ที่บ้าน เมื่อผู้ใช้ภายนอกเข้ามาที่ 192.168.1.0/24
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.10.0.0/24 -o eth0 -j MASQUERADE

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
AllowedIPs = 10.10.0.0/24
Endpoint = 203.0.113.10:51820
# สำคัญมาก: รักษา NAT mapping ไว้ ไม่ให้ VPS ส่งหา Home ไม่ได้
PersistentKeepalive = 25

โน้ตบุ๊กผู้ใช้:

[Interface]
PrivateKey = <LAPTOP_PRIVATE_KEY>
Address = 10.10.0.3/32

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
# เข้าถึง Home server และ LAN ที่บ้านผ่าน VPS
AllowedIPs = 10.10.0.0/24, 192.168.1.0/24
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25

5.7.5 PersistentKeepalive ที่เหมาะสม

ค่าที่เหมาะสมต้อง น้อยกว่า NAT timeout ของ UDP ที่สั้นที่สุดในเส้นทาง

K ≤ Tnat 2

ตัวอย่าง: NAT บางตัว timeout 30 วินาที → K ≤ 15 วินาที; NAT ทั่วไปที่ 60–120 วินาที → K = 25 ก็เพียงพอ ค่าที่ถี่เกินจำเป็นสิ้นเปลืองแบตเตอรี่และข้อมูล

5.7.6 ข้อควรระวังและการทดสอบ

# บน Home server
sudo wg-quick up wg0
sudo wg show wg0              # ต้องเห็น latest handshake กับ VPS ภายใน 25 วินาทีแรก

# จากโน้ตบุ๊กภายนอก: เข้าถึงบริการที่บ้านผ่าน VPS
ping -c 3 10.10.0.2
curl -sS http://192.168.1.20:8123/ -o /dev/null -w '%{http_code}\n'   # ตัวอย่าง Home Assistant (ต้องใช้ NAT ที่ home server)

🧪 ตัวอย่าง 5.7: ชุด config 3 ไฟล์ (vps.conf, home.conf, laptop.conf) ดูในไฟล์ตัวอย่าง


5.8 เข้าถึง Home Lab / Self-hosted Services

5.8.1 สถานการณ์

มี NAS, Home Assistant, Git server (Gitea/Forgejo), media server (Jellyfin) ในบ้าน ต้องการเข้าถึงจากภายนอกอย่างปลอดภัย โดยไม่ต้องเปิดพอร์ตของแต่ละบริการสู่อินเทอร์เน็ต

5.8.2 เปรียบเทียบ: เปิดพอร์ตตรง ๆ กับเข้าผ่าน VPN

ประเด็น เปิดพอร์ตตรง ๆ (Port forward) เข้าผ่าน WireGuard
พื้นที่โจมตี (Attack surface) ทุกบริการ ถูกสแกน/โจมตีจากทั่วโลก พอร์ต UDP เดียว และเงียบต่อผู้ไม่มี key
การยืนยันตัวตน ขึ้นกับแต่ละบริการ (หลายตัวอ่อนแอ) ผ่าน key ก่อนเห็นบริการใด ๆ
การเข้ารหัส ต้องตั้ง TLS/certificate เอง เข้ารหัสทั้ง tunnel อยู่แล้ว
ความสะดวกของผู้ใช้ภายนอก เข้าได้จาก browser ทันที ต้องมี client WireGuard
ใช้ได้เมื่ออยู่หลัง CGNAT ไม่ได้ ได้ (ผ่าน VPS)
การแชร์ให้คนทั่วไป ทำได้ง่าย ไม่เหมาะ (ใช้ reverse tunnel หัวข้อ 5.9)

5.8.3 การเข้า LAN ทั้งหมดผ่านเครื่อง Gateway เดียว

ตั้ง WireGuard บนเครื่องเดียวใน LAN (เช่น Raspberry Pi หรือ router) ให้เป็น gateway ของ client ภายนอก

flowchart LR
    R["ผู้ใช้ภายนอก
(Remote user)
10.8.0.2"] -- "WireGuard UDP 51820" --> RT["Router บ้าน
Port forward UDP 51820
ไปยัง Pi"] RT --> PI["Raspberry Pi (WG gateway)
192.168.1.5 / wg0 10.8.0.1"] PI --> NAS["NAS 192.168.1.10"] PI --> HA["Home Assistant 192.168.1.20"] PI --> GIT["Git server 192.168.1.30"] PI --> MED["Media server 192.168.1.40"]

Gateway (Raspberry Pi):

[Interface]
PrivateKey = <PI_PRIVATE_KEY>
Address = 10.8.0.1/24
ListenPort = 51820
# NAT ให้ client ดู LAN เหมือนมาจาก Pi (ไม่ต้องตั้ง route บน router บ้าน)
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

[Peer]
PublicKey = <PHONE_PUBLIC_KEY>
AllowedIPs = 10.8.0.2/32

Client (มือถือ/โน้ตบุ๊ก):

[Interface]
PrivateKey = <PHONE_PRIVATE_KEY>
Address = 10.8.0.2/32
DNS = 192.168.1.5          # ถ้ารัน Pi-hole/Unbound บน Pi

[Peer]
PublicKey = <PI_PUBLIC_KEY>
AllowedIPs = 10.8.0.0/24, 192.168.1.0/24
Endpoint = home.example.com:51820      # DDNS (หัวข้อ 9.5)
PersistentKeepalive = 25

5.8.4 ข้อควรระวัง

5.8.5 การทดสอบ

# จากนอกบ้าน (ปิด Wi-Fi บ้าน ใช้ 4G) เปิด tunnel แล้ว
curl -sS -o /dev/null -w 'NAS: %{http_code}\n' http://192.168.1.10:5000/
curl -sS -o /dev/null -w 'HA: %{http_code}\n'  http://192.168.1.20:8123/
nmap -Pn -p 22,80,443,8123 192.168.1.20      # ตรวจพอร์ตที่เปิด

🧪 ตัวอย่าง 5.8: ไฟล์ homelab-gateway.conf, homelab-client.conf และสคริปต์เช็กบริการ ดูในไฟล์ตัวอย่าง


5.9 Expose Service ผ่าน VPS (Reverse Tunnel Pattern)

5.9.1 สถานการณ์

ต้องการเปิดบริการที่รันที่บ้าน (เช่น เว็บ blog, Git server สาธารณะ) ให้คนทั่วไปเข้าถึงได้ผ่านโดเมน โดยไม่เปิดเผย IP บ้านและแม้อยู่หลัง CGNAT ใช้ VPS ที่มี public IP เป็น "ประตูหน้า" ส่งต่อ traffic ผ่าน WireGuard มายังเครื่องที่บ้าน

5.9.2 สถาปัตยกรรม

flowchart LR
    USER["ผู้ใช้ทั่วไป (Public user)
https://blog.example.com"] --> V1["VPS 203.0.113.10
Reverse proxy (Nginx/Caddy) หรือ DNAT"] V1 -- "WireGuard 10.20.0.1 -> 10.20.0.2" --> H1["Home server 10.20.0.2
บริการ blog พอร์ต 8080"]

มีสองรูปแบบ:

รูปแบบ ทำงานที่ ข้อดี ข้อเสีย
Reverse proxy (Nginx/Caddy/Traefik) Layer 7 (HTTP/HTTPS) จัดการ TLS/โดเมน/หลายเว็บบน IP เดียว, ใส่ header X-Forwarded-For ได้ รองรับเฉพาะ HTTP/HTTPS (หรือ stream ในบาง proxy)
DNAT / port forward (iptables/nftables) Layer 3-4 รองรับทุก TCP/UDP (SSH, game server) ปลายทางเห็น source IP เป็นของ VPS ถ้าไม่จัดการ routing ให้ถูกต้อง

5.9.3 Config ตัวอย่าง

VPS (wg0.conf) พร้อม DNAT:

[Interface]
PrivateKey = <VPS_PRIVATE_KEY>
Address = 10.20.0.1/24
ListenPort = 51820
PostUp   = sysctl -w net.ipv4.ip_forward=1
# DNAT: พอร์ต 2222 ของ VPS -> SSH (22) ที่ home server 10.20.0.2
PostUp   = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 2222 -j DNAT --to-destination 10.20.0.2:22
PostUp   = iptables -A FORWARD -i eth0 -o %i -p tcp -d 10.20.0.2 --dport 22 -j ACCEPT
PostUp   = iptables -A FORWARD -i %i -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 2222 -j DNAT --to-destination 10.20.0.2:22
PostDown = iptables -D FORWARD -i eth0 -o %i -p tcp -d 10.20.0.2 --dport 22 -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT

[Peer]
PublicKey = <HOME_PUBLIC_KEY>
AllowedIPs = 10.20.0.2/32

Home server (wg0.conf):

[Interface]
PrivateKey = <HOME_PRIVATE_KEY>
Address = 10.20.0.2/24

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
# AllowedIPs = 10.20.0.1/32 พอสำหรับ reverse proxy;
# ต้องเพิ่ม 0.0.0.0/0 (หรือช่วงผู้ใช้จริง) ถ้าต้อง "ตอบกลับ" ไปยัง IP ต้นทางจริงผ่านอุโมงค์ (กรณีรักษา source IP)
AllowedIPs = 10.20.0.1/32
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25

Nginx บน VPS (reverse proxy):

# /etc/nginx/conf.d/blog.conf
server {
    listen 80;
    server_name blog.example.com;

    location / {
        # ส่งต่อไปยังบริการที่บ้านผ่าน tunnel
        proxy_pass http://10.20.0.2:8080;
        # ส่ง IP จริงของผู้ใช้ให้ปลายทางรับรู้
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
# ขอ certificate TLS: sudo certbot --nginx -d blog.example.com

Caddy บน VPS (ทางเลือกที่ง่ายกว่า มี TLS อัตโนมัติ):

# /etc/caddy/Caddyfile
blog.example.com {
    # Caddy ขอและต่ออายุ certificate ให้เอง และใส่ X-Forwarded-For ให้อัตโนมัติ
    reverse_proxy 10.20.0.2:8080
}

5.9.4 การรักษา Source IP จริงของผู้ใช้

วิธี การทำงาน
HTTP header (X-Forwarded-For) reverse proxy ใส่ IP จริง บริการต้องไว้ใจ header เฉพาะจาก 10.20.0.1
PROXY protocol proxy ส่ง IP จริงต้นทางผ่านโปรโตคอลเสริม (รองรับใน Nginx stream, HAProxy, Caddy) ใช้กับ TCP ที่ไม่ใช่ HTTP
DNAT ไม่ SNAT + route กลับ ไม่ทำ masquerade บน VPS แล้วให้ Home ส่งตอบทุกอย่างกลับ VPS ด้วย AllowedIPs 0.0.0.0/0 และ policy routing (ซับซ้อน)
SNAT เป็น 10.20.0.1 ง่ายที่สุดแต่ปลายทางเห็นแต่ IP ของ VPS (log สูญเสียข้อมูลผู้ใช้)

ข้อควรระวัง: อย่าเชื่อ X-Forwarded-For จาก client ภายนอกโดยตรง ให้ reverse proxy เขียนทับ (overwrite) ค่า header ก่อนส่งต่อ

5.9.5 ข้อควรระวัง

5.9.6 การทดสอบ

# จากอินเทอร์เน็ตภายนอก
curl -I https://blog.example.com
ssh -p 2222 user@203.0.113.10           # ต้องเข้าถึง home server (DNAT)

# บน VPS: ตรวจ NAT และ conntrack
sudo iptables -t nat -L PREROUTING -n -v
sudo conntrack -L 2>/dev/null | grep 10.20.0.2 | head
# บน Home: ดูว่า log ของบริการเห็น IP ผู้ใช้จริง (จาก header)

🧪 ตัวอย่าง 5.9: ไฟล์ vps-dnat.conf, nginx-blog.conf, Caddyfile ดูในไฟล์ตัวอย่าง


5.10 ใช้บริการ Commercial VPN ผ่าน WireGuard

5.10.1 สถานการณ์

ผู้ให้บริการอย่าง Mullvad, ProtonVPN, IVPN ฯลฯ เปิดให้ดาวน์โหลดไฟล์ config WireGuard เพื่อใช้กับอุปกรณ์ใดก็ได้ (router, Raspberry Pi, Linux) โดยไม่ต้องใช้แอปของผู้ให้บริการ

5.10.2 การนำ config ไปใช้เอง

  1. เข้าเว็บผู้ให้บริการ สร้างไฟล์ WireGuard config (เลือกประเทศ/เซิร์ฟเวอร์)
  2. ตรวจ [Interface]: PrivateKey, Address (ผู้ให้บริการกำหนดให้), DNS
  3. ตรวจ [Peer]: PublicKey ของ server, Endpoint, AllowedIPs = 0.0.0.0/0, ::/0
  4. บันทึกเป็น /etc/wireguard/vpn-provider.conf (permission 600) แล้ว wg-quick up vpn-provider
# ตัวอย่างโครงสร้าง config ที่ผู้ให้บริการออกให้ (ค่าทั้งหมดสมมติ)
[Interface]
PrivateKey = <PROVIDER_ISSUED_PRIVATE_KEY>
Address = 10.66.12.34/32, fc00:bbbb:bbbb:bb01::1:c22/128
DNS = 10.64.0.1

[Peer]
PublicKey = <PROVIDER_SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 198.51.100.7:51820

5.10.3 สร้าง Gateway ที่ส่งทั้งเครือข่ายออกผ่านผู้ให้บริการ

ติดตั้งบน Raspberry Pi/เครื่อง Linux ให้ทำหน้าที่เป็น gateway ของอุปกรณ์ในบ้าน แล้วส่ง traffic ออกผ่าน commercial VPN

flowchart LR
    LAN["อุปกรณ์ใน LAN
(LAN devices)
192.168.1.0/24"] --> GW["Gateway (Linux)
eth0: 192.168.1.2"] GW -- "wg0 (VPN provider)" --> PROV["Commercial VPN server"] PROV --> NET["อินเทอร์เน็ต (Internet)"]
# /etc/wireguard/vpn-provider.conf บน gateway
[Interface]
PrivateKey = <PROVIDER_ISSUED_PRIVATE_KEY>
Address = 10.66.12.34/32
# ไม่ให้ wg-quick ใส่ default route เอง เพื่อเราจะทำ policy routing สำหรับ LAN
Table = off
PostUp   = sysctl -w net.ipv4.ip_forward=1
# ส่ง traffic จาก LAN เข้าตาราง 200 (default ผ่าน wg0)
PostUp   = ip route add default dev %i table 200
PostUp   = ip rule add from 192.168.1.0/24 table 200 priority 1000
# NAT traffic ของ LAN ที่ออก wg0
PostUp   = iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o %i -j MASQUERADE
# Kill switch: ห้าม LAN ออกอินเทอร์เน็ตทาง eth0 ตรง ๆ ถ้า VPN ล่ม
PostUp   = iptables -I FORWARD -s 192.168.1.0/24 -o eth0 -j REJECT
PostDown = iptables -D FORWARD -s 192.168.1.0/24 -o eth0 -j REJECT
PostDown = iptables -t nat -D POSTROUTING -s 192.168.1.0/24 -o %i -j MASQUERADE
PostDown = ip rule del from 192.168.1.0/24 table 200 priority 1000
PostDown = ip route del default dev %i table 200

[Peer]
PublicKey = <PROVIDER_SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0
Endpoint = 198.51.100.7:51820
PersistentKeepalive = 25

วิธีใช้: ตั้ง default gateway ของอุปกรณ์ใน LAN (หรือที่ DHCP) ให้ชี้มาที่ 192.168.1.2 แล้วทดสอบด้วย curl https://ifconfig.me บนอุปกรณ์ LAN

5.10.4 Multi-hop (Chaining หลาย Tunnel)

Multi-hop คือการส่ง traffic ผ่านหลาย server ต่อกัน (เข้า server A แล้วออกที่ server B) เพื่อกระจายความไว้วางใจ ผู้ให้บริการบางรายมีให้เลือกในตัว หรือทำเองได้โดยซ้อน tunnel: ส่ง UDP ของ tunnel ที่สองผ่าน tunnel แรก

# wg-entry.conf : hop 1 (ทางเข้า) ใช้ Table = off แล้วให้ route เฉพาะไปยัง endpoint ของ hop 2
[Interface]
PrivateKey = <ENTRY_PRIVATE_KEY>
Address = 10.66.1.2/32
Table = off
[Peer]
PublicKey = <ENTRY_SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0
Endpoint = 198.51.100.7:51820

# wg-exit.conf : hop 2 (ทางออก) endpoint ของ exit จะถูกส่งผ่าน wg-entry
[Interface]
PrivateKey = <EXIT_PRIVATE_KEY>
Address = 10.66.2.2/32
# ให้ UDP ที่ไป exit server ผ่านอุโมงค์ entry
PostUp = ip route add 192.0.2.20/32 dev wg-entry
[Peer]
PublicKey = <EXIT_SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0
Endpoint = 192.0.2.20:51820

ข้อแลกเปลี่ยน:

5.10.5 การทดสอบ

sudo wg-quick up vpn-provider
curl -sS https://ifconfig.me ; echo           # ต้องเป็น IP ของผู้ให้บริการ
curl -sS https://am.i.mullvad.net/connected 2>/dev/null || true   # หน้าตรวจของผู้ให้บริการบางราย
dig +short myip.opendns.com @resolver1.opendns.com                # ตรวจว่า DNS ออกทางผู้ให้บริการ

🧪 ตัวอย่าง 5.10: vpn-provider.conf (เทมเพลต), wg-entry.conf / wg-exit.conf ดูในไฟล์ตัวอย่าง


5.11 ทำ Egress Gateway / Exit Node

5.11.1 สถานการณ์

ต้องการให้เครื่องหนึ่ง (เช่น VPS ในต่างประเทศ) เป็น ทางออกอินเทอร์เน็ต ของเครื่องอื่น และเลือก "ตำแหน่ง" ทางออกได้ (เช่น ออกที่ญี่ปุ่นสำหรับบริการหนึ่ง ออกที่สิงคโปร์สำหรับอีกบริการ) รวมถึงแยกตามอุปกรณ์หรือ subnet

5.11.2 สถาปัตยกรรม

flowchart LR
    subgraph CLIENTS["อุปกรณ์ใน LAN (LAN devices)"]
        D1["TV 192.168.1.101"]
        D2["โน้ตบุ๊ก 192.168.1.102"]
        D3["เซิร์ฟเวอร์ 192.168.1.103"]
    end
    GW["Gateway
Policy routing"] D1 --> GW D2 --> GW D3 --> GW GW -- "wg-jp (ออกญี่ปุ่น)" --> JP["Exit JP 198.51.100.11"] GW -- "wg-sg (ออกสิงคโปร์)" --> SG["Exit SG 198.51.100.12"] GW -- "eth0 ปกติ (Direct)" --> ISP["ISP"]

5.11.3 Config ตัวอย่าง: แยกทางออกตามอุปกรณ์ด้วย Policy Routing

# /etc/wireguard/wg-jp.conf (Gateway -> Exit JP) ; wg-sg.conf ใช้โครงเดียวกันเปลี่ยน key/endpoint/ตาราง
[Interface]
PrivateKey = <GW_PRIVATE_KEY_FOR_JP>
Address = 10.31.0.2/32
Table = off                                 # เราควบคุม routing เอง
PostUp   = ip route add default dev %i table 101
PostUp   = ip rule add from 192.168.1.101/32 table 101 priority 1101
PostUp   = iptables -t nat -A POSTROUTING -s 192.168.1.101/32 -o %i -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 192.168.1.101/32 -o %i -j MASQUERADE
PostDown = ip rule del from 192.168.1.101/32 table 101 priority 1101
PostDown = ip route del default dev %i table 101

[Peer]
PublicKey = <EXIT_JP_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0
Endpoint = 198.51.100.11:51820
PersistentKeepalive = 25
# Exit node (ทั้ง JP และ SG ใช้โครงเดียวกัน): ทำ NAT ออกอินเทอร์เน็ต
[Interface]
PrivateKey = <EXIT_PRIVATE_KEY>
Address = 10.31.0.1/24
ListenPort = 51820
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -t nat -A POSTROUTING -s 10.31.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.31.0.0/24 -o eth0 -j MASQUERADE

[Peer]
PublicKey = <GW_PUBLIC_KEY>
AllowedIPs = 10.31.0.2/32

ตารางสรุปการแยก traffic:

อุปกรณ์ กฎ (ip rule) ตาราง ทางออก
TV 192.168.1.101 from 192.168.1.101/32 table 101 101 wg-jp (ญี่ปุ่น)
โน้ตบุ๊ก 192.168.1.102 from 192.168.1.102/32 table 102 102 wg-sg (สิงคโปร์)
เซิร์ฟเวอร์ 192.168.1.103 ไม่มีกฎ main ISP ตรง

5.11.4 ข้อควรระวังและการทดสอบ

# ตรวจ policy ที่ gateway
ip rule show | sort -n | head
ip route show table 101
# ทดสอบจาก TV หรือจำลองด้วย curl --interface (บน gateway)
curl --interface 192.168.1.2 -sS https://ifconfig.me          # ออก ISP
# บนอุปกรณ์ 192.168.1.101: curl https://ifconfig.me  ต้องได้ IP ของ Exit JP

🧪 ตัวอย่าง 5.11: wg-jp.conf, wg-sg.conf, exit-node.conf และสคริปต์ rules-table.sh ดูในไฟล์ตัวอย่าง


5.12 Multiple Tunnels, Failover และ Load Balancing

5.12.1 สถานการณ์

ต้องการความต่อเนื่องของ VPN: ถ้า server หลักล่ม ให้ย้ายไป server สำรองอัตโนมัติ หรือกระจายโหลดไปหลาย tunnel

5.12.2 สถาปัตยกรรม

flowchart TD
    C["Client / Gateway"] --> W1["wg-primary
Server 1 (metric 10)"] C --> W2["wg-backup
Server 2 (metric 20)"] W1 --> N["เครือข่ายปลายทาง (Target network)"] W2 --> N HC["Health check script
ตรวจ handshake และ ping
(Handshake and ping check)"] -.-> C

5.12.3 รันหลาย Interface พร้อมกัน

ใช้ไฟล์ wg-primary.conf และ wg-backup.conf ที่มี Table = off และจัดการ route เอง

# เปิดทั้งสอง interface
sudo wg-quick up wg-primary
sudo wg-quick up wg-backup

# สร้าง default route สองเส้นใน main table ด้วย metric ต่างกัน (metric ต่ำ = ใช้ก่อน)
sudo ip route add default dev wg-primary metric 10
sudo ip route add default dev wg-backup  metric 20
ip route show default
# ตัวอย่างผลลัพธ์:
#   default dev wg-primary scope link metric 10
#   default dev wg-backup scope link metric 20

5.12.4 Failover ระหว่าง Endpoint สำรอง

WireGuard รองรับ Endpoint ได้ตัวเดียวต่อ peer จึงทำ failover โดยสคริปต์ตรวจสุขภาพแล้วสลับ endpoint หรือสลับ route

#!/usr/bin/env bash
# wg-failover.sh : ตรวจสุขภาพ tunnel หลัก ถ้า handshake เก่าเกินกำหนดให้สลับ route ไปสำรอง
# วิธีใช้: sudo ./wg-failover.sh   (รันด้วย systemd timer ทุก 15 วินาที)
set -euo pipefail

PRIMARY="wg-primary"                 # interface หลัก
BACKUP="wg-backup"                   # interface สำรอง
PRIMARY_PEER="<PRIMARY_PEER_PUBLIC_KEY>"
PROBE_IP="10.31.0.1"                 # ที่อยู่ภายใน tunnel ใช้ ping ทดสอบ
MAX_AGE=180                          # handshake เก่ากว่านี้ (วินาที) ถือว่าล่ม

# เวลา handshake ล่าสุดของ peer หลัก (epoch; 0 = ยังไม่เคย)
last=$(wg show "$PRIMARY" latest-handshakes | awk -v k="$PRIMARY_PEER" '$1==k {print $2}')
now=$(date +%s)
age=$(( now - ${last:-0} ))

# ทดสอบ ping ผ่าน interface หลัก
if ping -c 2 -W 2 -I "$PRIMARY" "$PROBE_IP" >/dev/null 2>&1 && [ "$age" -lt "$MAX_AGE" ]; then
  # หลักปกติ: ให้ metric ของหลักต่ำสุด
  ip route replace default dev "$PRIMARY" metric 10
  logger -t wg-failover "primary OK (handshake age ${age}s)"
else
  # หลักล่ม: ให้ route ของสำรองเป็นหลัก
  ip route replace default dev "$BACKUP" metric 5
  logger -t wg-failover "primary DOWN (age ${age}s) -> using $BACKUP"
fi
# /etc/systemd/system/wg-failover.timer
[Unit]
Description=รัน wg-failover ทุก 15 วินาที
[Timer]
OnBootSec=30
OnUnitActiveSec=15
[Install]
WantedBy=timers.target
# และ /etc/systemd/system/wg-failover.service ให้ ExecStart=/usr/local/bin/wg-failover.sh
# เปิดใช้: sudo systemctl enable --now wg-failover.timer

5.12.5 Routing Metric, ECMP และ Health Check

# ตัวอย่าง ECMP สองเส้นทางที่น้ำหนักเท่ากัน
sudo ip route replace default \
  nexthop dev wg-a weight 1 \
  nexthop dev wg-b weight 1
ip route show default

# ปรับให้ hash ใช้ L4 (พอร์ต) ด้วยเพื่อกระจายได้ดีขึ้น
sudo sysctl -w net.ipv4.fib_multipath_hash_policy=1

ตัวอย่างการคำนวณ: ความพร้อมใช้งานรวมของสอง tunnel อิสระ

Atotal = 1 − ( 1 − A1 ) ( 1 − A2 )

แทนค่า: A1 = A2 = 0.99 (99%) → Atotal = 1 − (0.01)(0.01) = 1 − 0.0001 = 0.9999 (99.99%) เวลาล่มต่อปีลดจาก ≈ 87.6 ชั่วโมง (1% ของ 8,760) เหลือ ≈ 0.88 ชั่วโมง (≈ 53 นาที) แต่ต้องคิดเวลาตรวจจับและสลับด้วย (เช่น ทุก 15 วินาที + MAX_AGE)

5.12.6 การทดสอบ

# จำลอง primary ล่ม: บล็อก UDP ไปยัง server หลัก
sudo iptables -I OUTPUT -d <PRIMARY_SERVER_IP> -p udp --dport 51820 -j DROP
sleep 30; ip route show default            # ควรเห็น backup ถูกเลือก
journalctl -t wg-failover -n 5 --no-pager
# คืนค่า
sudo iptables -D OUTPUT -d <PRIMARY_SERVER_IP> -p udp --dport 51820 -j DROP

🧪 ตัวอย่าง 5.12: wg-primary.conf, wg-backup.conf, wg-failover.sh, unit ของ systemd ดูในไฟล์ตัวอย่าง


5.13 เข้าถึงอุปกรณ์ IoT / Embedded / Raspberry Pi

5.13.1 สถานการณ์

มีอุปกรณ์ภาคสนามจำนวนมาก (Raspberry Pi เก็บข้อมูลเซนเซอร์, gateway ในฟาร์ม, ตู้คอนโทรล) ที่อยู่หลัง SIM/4G แบบ NAT ต้องการเข้าไป SSH/ดึงข้อมูล/อัปเดตระยะไกล ESP-class (ESP32) บางรุ่นมีไลบรารี WireGuard ให้ใช้ แต่ทรัพยากรจำกัด จึงนิยมใช้ Raspberry Pi หรือ gateway ที่รัน Linux เป็นตัวกลาง ให้ MCU เชื่อมผ่าน LAN ท้องถิ่น

5.13.2 สถาปัตยกรรม

flowchart LR
    subgraph FIELD["ภาคสนาม (Field sites) หลัง 4G NAT"]
        P1["Pi-001
10.40.1.1"] P2["Pi-002
10.40.1.2"] GWF["Edge gateway
10.40.2.1
(ESP32/MCU หลายตัวต่อ LAN)"] end subgraph CORE["ศูนย์กลาง (Core) 203.0.113.10"] SRV["Collector/Hub
wg0 10.40.0.1/16
รับ connection จากทุกอุปกรณ์"] end OPS["ผู้ดูแล (Operator)
10.40.0.10"] P1 --> SRV P2 --> SRV GWF --> SRV OPS --> SRV

5.13.3 การจัดการ Key จำนวนมาก

การออกแบบ addressing สำหรับอุปกรณ์จำนวนมาก: ใช้ 10.40.0.0/16 (65,534 address) แบ่งตามชนิดหรือสาขา:

ช่วง การใช้งาน
10.40.0.0/24 ศูนย์กลาง/ผู้ดูแล
10.40.1.0/24 Raspberry Pi ภาคสนาม (254 เครื่อง)
10.40.2.0/24 Edge gateway
10.40.3.0/24 เซนเซอร์ที่รองรับ WireGuard โดยตรง (ESP-class)

ต้นทุนหน่วยความจำและการตั้งค่า: แต่ละ peer ใช้หน่วยความจำของ kernel เพียงเล็กน้อย (หลักหลายร้อย byte ถึงกิโลไบต์) การมี peer หลักพันถึงหมื่นรายการทำงานได้บน server ทั่วไป แต่การโหลดผ่าน wg-quick ช้าลง ควรใช้ wg set/wg syncconf เพิ่ม peer แทนการรีสตาร์ท interface

#!/usr/bin/env bash
# provision-devices.sh : ออก key และ config ให้อุปกรณ์จำนวนมากจากไฟล์รายชื่อ
# วิธีใช้:  ./provision-devices.sh devices.txt out/  (ไฟล์รายชื่อ: บรรทัดละ  "ชื่ออุปกรณ์ IP")
#   เช่น  pi-001 10.40.1.1
set -euo pipefail
LIST="$1"; OUT="$2"
SERVER_PUB="$(cat server.pub)"                 # public key ของ collector
ENDPOINT="vpn.example.com:51820"
umask 077; mkdir -p "$OUT"

: > "$OUT/server-peers.conf"                   # ไฟล์ [Peer] ทั้งหมดสำหรับ server
while read -r name ip; do
  [ -z "${name:-}" ] && continue
  key=$(wg genkey); pub=$(echo "$key" | wg pubkey)
  # ไฟล์ของอุปกรณ์ (ฝังลงอุปกรณ์ตอน flash/provision)
  cat > "$OUT/$name.conf" <<EOF
[Interface]
PrivateKey = $key
Address = $ip/32

[Peer]
PublicKey = $SERVER_PUB
AllowedIPs = 10.40.0.0/24
Endpoint = $ENDPOINT
PersistentKeepalive = 60
EOF
  # ส่วน [Peer] สำหรับ server (ผูกด้วย /32 ของอุปกรณ์เท่านั้น)
  printf '[Peer]\n# %s\nPublicKey = %s\nAllowedIPs = %s/32\n\n' "$name" "$pub" "$ip" >> "$OUT/server-peers.conf"
  echo "ออกให้ $name ($ip)"
done < "$LIST"
echo "นำ $OUT/server-peers.conf ไปต่อท้าย /etc/wireguard/wg0.conf ของ server"

5.13.4 ข้อจำกัดทรัพยากรและ Remote Maintenance

# /etc/systemd/system/wg-watchdog.service บนอุปกรณ์ภาคสนาม: รีสตาร์ท tunnel ถ้าติดต่อ collector ไม่ได้
[Unit]
Description=รีสตาร์ท WireGuard ถ้า handshake เก่าเกิน 5 นาที
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'age=$(( $(date +%%s) - $(wg show wg0 latest-handshakes | awk "{print \\$2}" | head -n1) )); [ "$age" -gt 300 ] && systemctl restart wg-quick@wg0 || true'

5.13.5 การทดสอบ

# บน collector: ดูอุปกรณ์ที่ออนไลน์ (handshake ภายใน 3 นาที) และจำนวนทั้งหมด
sudo wg show wg0 latest-handshakes | awk -v now="$(date +%s)" '
  { total++; if (now-$2 < 180) online++ } END { printf "online %d / total %d\n", online, total }'
ssh pi@10.40.1.1 'uptime; wg show wg0 | head -n 4'

🧪 ตัวอย่าง 5.13: provision-devices.sh พร้อมไฟล์ devices.txt ตัวอย่าง 5 เครื่อง ดูในไฟล์ตัวอย่าง


5.14 Developer และ DevOps Workflow

5.14.1 สถานการณ์

นักพัฒนาต้องเข้าถึง staging API, ฐานข้อมูล, Kubernetes API และเครื่องภายใน โดยไม่เปิดสู่อินเทอร์เน็ต เดิมใช้ bastion/jump host (SSH) ที่ต้องจัดการ key ของผู้ใช้บนเครื่องกลาง WireGuard ช่วยทดแทนได้ด้วยการให้เครื่องนักพัฒนาอยู่ใน private network โดยตรง

5.14.2 สถาปัตยกรรม

flowchart LR
    DEV["เครื่องนักพัฒนา (Developer)
10.50.0.10"] --> GW["WireGuard Gateway
10.50.0.1"] CI["CI/CD runner
10.50.0.200"] --> GW GW --> API["Staging API
172.20.1.10:8443"] GW --> DB["PostgreSQL
172.20.2.20:5432"] GW --> K8S["Kubernetes API
172.20.3.5:6443"]

5.14.3 Config ตัวอย่างและการกำหนดสิทธิ์

นโยบายแบบละเอียดด้วย nftables ที่ gateway:

# /etc/nftables.d/dev-access.nft
table inet dev_access {
    set devs { type ipv4_addr; elements = { 10.50.0.10-10.50.0.99 } }
    set ci   { type ipv4_addr; elements = { 10.50.0.200 } }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept

        # นักพัฒนา: เข้า API, K8s API และ DB แบบอ่านอย่างเดียวผ่านพอร์ตที่กำหนด
        ip saddr @devs ip daddr 172.20.1.10 tcp dport 8443 accept
        ip saddr @devs ip daddr 172.20.3.5  tcp dport 6443 accept
        ip saddr @devs ip daddr 172.20.2.20 tcp dport 5432 accept
        # CI runner: เข้า API และ K8s API เท่านั้น (ไม่แตะ DB)
        ip saddr @ci   ip daddr { 172.20.1.10, 172.20.3.5 } tcp dport { 8443, 6443 } accept
    }
}

Client ของนักพัฒนา (split tunnel เฉพาะ subnet ภายใน):

[Interface]
PrivateKey = <DEV_PRIVATE_KEY>
Address = 10.50.0.10/32
DNS = 172.20.0.2
PostUp   = resolvectl dns %i 172.20.0.2; resolvectl domain %i ~svc.internal
PostDown = resolvectl revert %i

[Peer]
PublicKey = <GATEWAY_PUBLIC_KEY>
AllowedIPs = 10.50.0.0/24, 172.20.0.0/16
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25

ใช้ร่วมกับ SSH, Git, kubectl:

# SSH ไปเครื่องภายในได้โดยตรง ไม่ต้อง ProxyJump
ssh deploy@172.20.1.10

# Git server ภายใน
git clone ssh://git@172.20.4.5/team/project.git

# kubectl ชี้ไปยัง API server ผ่าน tunnel (IP ภายใน)
kubectl --server=https://172.20.3.5:6443 get nodes

# ตัวอย่าง ~/.ssh/config : ใช้ชื่อสั้นและไม่ต้อง jump
# Host stg
#     HostName 172.20.1.10
#     User deploy

CI/CD runner: ติดตั้ง WireGuard ใน job ของ CI (เช่น GitHub Actions self-hosted runner หรือ container) โดยเก็บ private key เป็น secret ของ CI (ไม่ commit)

# ตัวอย่างขั้นตอนใน pipeline (เช่น GitHub Actions) ที่ต่อ VPN ก่อน deploy
# เก็บ WG_CONF (ทั้งไฟล์ config) เป็น secret ของ repository
steps:
  - name: ติดตั้ง WireGuard และเปิด tunnel
    run: |
      sudo apt-get update && sudo apt-get install -y wireguard-tools resolvconf
      echo "${{ secrets.WG_CONF }}" | sudo tee /etc/wireguard/ci.conf >/dev/null
      sudo chmod 600 /etc/wireguard/ci.conf
      sudo wg-quick up ci
  - name: ทดสอบการเข้าถึง
    run: curl -fsS https://172.20.1.10:8443/healthz -k
  - name: ปิด tunnel (ทำเสมอ แม้ขั้นตอนก่อนหน้าล้ม)
    if: always()
    run: sudo wg-quick down ci || true

5.14.4 ข้อควรระวังและการทดสอบ

nc -zv 172.20.2.20 5432 && psql "host=172.20.2.20 user=dev dbname=app" -c 'select 1'
kubectl --server=https://172.20.3.5:6443 get ns

🧪 ตัวอย่าง 5.14: ไฟล์ dev-access.nft, dev-client.conf และ workflow ของ CI ดูในไฟล์ตัวอย่าง


5.15 Remote Work สำหรับทีมขนาดเล็ก–กลาง

5.15.1 สถานการณ์

บริษัทมีทีมหลายแผนก (Engineering, Finance, HR, ผู้รับเหมา) ต้องการให้ทำงานจากที่บ้านโดยเข้าถึงเฉพาะทรัพยากรที่เกี่ยวข้อง ตามหลัก least privilege และมีขั้นตอน onboard/offboard ที่ชัดเจน

5.15.2 การออกแบบ Subnet ต่อทีม/บทบาท

flowchart TD
    GW["WireGuard Gateway
wg0 10.60.0.1/16"] GW --> ENG["Engineering 10.60.1.0/24
เข้าถึง: Git, CI, Staging"] GW --> FIN["Finance 10.60.2.0/24
เข้าถึง: ERP, ไฟล์บัญชี"] GW --> HR["HR 10.60.3.0/24
เข้าถึง: ระบบ HR"] GW --> CON["ผู้รับเหมา Contractors 10.60.9.0/24
เข้าถึง: เฉพาะโปรเจกต์ที่ได้รับสิทธิ์"] GW --> ADM["ผู้ดูแล Admins 10.60.0.0/28
เข้าถึง: ทุกอย่างผ่านการอนุมัติ"]
กลุ่ม Subnet ใน tunnel ทรัพยากรที่เข้าถึงได้
Admins 10.60.0.0/28 ทุก subnet ภายใน
Engineering 10.60.1.0/24 172.20.1.0/24 (staging), 172.20.4.0/24 (git/CI)
Finance 10.60.2.0/24 172.20.5.0/24 (ERP)
HR 10.60.3.0/24 172.20.6.0/24 (HR)
Contractors 10.60.9.0/24 เฉพาะ host ที่ระบุทีละเครื่อง

การเข้าถึงขึ้นกับ firewall ที่ gateway ไม่ใช่ AllowedIPs ของ client ซึ่งอาจตั้งกว้างได้เพราะ client ควบคุมเอง:

# /etc/nftables.d/team-policy.nft : นโยบาย least privilege ตามกลุ่ม
table inet team_policy {
    set eng { type ipv4_addr; flags interval; elements = { 10.60.1.0/24 } }
    set fin { type ipv4_addr; flags interval; elements = { 10.60.2.0/24 } }
    set hr  { type ipv4_addr; flags interval; elements = { 10.60.3.0/24 } }
    set con { type ipv4_addr; flags interval; elements = { 10.60.9.0/24 } }
    set adm { type ipv4_addr; flags interval; elements = { 10.60.0.0/28 } }

    chain forward {
        type filter hook forward priority 0; policy drop;
        ct state established,related accept
        ip saddr @adm accept
        ip saddr @eng ip daddr { 172.20.1.0/24, 172.20.4.0/24 } accept
        ip saddr @fin ip daddr 172.20.5.0/24 accept
        ip saddr @hr  ip daddr 172.20.6.0/24 accept
        ip saddr @con ip daddr 172.20.1.50 tcp dport { 22, 443 } accept
        # ห้ามคุยระหว่างกลุ่มและระหว่างเครื่องในกลุ่มเดียวกัน (isolation) -- ตกไปที่ policy drop
        log prefix "WG-DENY " counter drop
    }
}

5.15.3 การ Onboard / Offboard ผู้ใช้

flowchart LR
    REQ["ขอสิทธิ์ (Request)
ผ่านระบบ ticket"] --> APP["หัวหน้าอนุมัติ
(Approval)"] APP --> GEN["สร้าง key และ IP
ตามกลุ่ม (Provision)"] GEN --> SEND["ส่งไฟล์/QR ผ่านช่องทางปลอดภัย
(Secure delivery)"] SEND --> USE["ใช้งาน (Active)"] USE --> REV["ทบทวนทุกไตรมาส
(Quarterly review)"] REV --> OFF["Offboard: ลบ peer
และ revoke (Remove peer)"]
#!/usr/bin/env bash
# user-lifecycle.sh : onboard / offboard ผู้ใช้บน WireGuard server
# วิธีใช้:
#   sudo ./user-lifecycle.sh add alice-laptop 10.60.1.10 "Alice Eng laptop"
#   sudo ./user-lifecycle.sh remove alice-laptop
#   sudo ./user-lifecycle.sh list
set -euo pipefail
IF=wg0; DB=/etc/wireguard/peers.db        # รูปแบบ DB: ชื่อ|public key|IP|หมายเหตุ|วันที่ออก
cmd=${1:?usage}; name=${2:-}; ip=${3:-}; note=${4:-}
umask 077; touch "$DB"

case "$cmd" in
  add)
    key=$(wg genkey); pub=$(echo "$key" | wg pubkey); psk=$(wg genpsk)
    # เพิ่ม peer แบบสด (ไม่ตัด session ของคนอื่น)
    wg set "$IF" peer "$pub" preshared-key <(echo "$psk") allowed-ips "$ip/32"
    echo "$name|$pub|$ip|$note|$(date +%F)" >> "$DB"
    # เขียนลง config ถาวรด้วย
    printf '\n# %s (%s) ออกเมื่อ %s\n[Peer]\nPublicKey = %s\nPresharedKey = %s\nAllowedIPs = %s/32\n' \
      "$name" "$note" "$(date +%F)" "$pub" "$psk" "$ip" >> /etc/wireguard/$IF.conf
    # สร้าง config ให้ผู้ใช้
    cat > "/root/$name.conf" <<EOF
[Interface]
PrivateKey = $key
Address = $ip/32
DNS = 172.20.0.2
[Peer]
PublicKey = $(cat /etc/wireguard/server.pub)
PresharedKey = $psk
AllowedIPs = 10.60.0.0/16, 172.20.0.0/16
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25
EOF
    echo "สร้าง /root/$name.conf แล้ว (ส่งผ่านช่องทางปลอดภัย แล้วลบไฟล์นี้)"
    ;;
  remove)
    pub=$(awk -F'|' -v n="$name" '$1==n {print $2}' "$DB")
    [ -n "$pub" ] || { echo "ไม่พบ $name"; exit 1; }
    wg set "$IF" peer "$pub" remove                       # ตัดสิทธิ์ทันที
    sed -i "/^$name|/d" "$DB"
    echo "ลบ $name ออกจาก runtime แล้ว ต้องลบบล็อก [Peer] ที่ตรงกันใน /etc/wireguard/$IF.conf ด้วย"
    ;;
  list)
    column -t -s'|' "$DB"
    ;;
esac

5.15.4 ข้อควรระวัง

5.15.5 การทดสอบ

# จากเครื่อง Finance (10.60.2.x): ต้องเข้า ERP ได้ แต่เข้า Git ไม่ได้
nc -zv 172.20.5.10 443          # ผ่าน
nc -zv -w 3 172.20.4.5 22       # ต้องถูกปฏิเสธ/timeout
# บน gateway: ดู log การปฏิเสธ
sudo journalctl -k | grep WG-DENY | tail

🧪 ตัวอย่าง 5.15: team-policy.nft, user-lifecycle.sh และไฟล์ peers.db ตัวอย่าง ดูในไฟล์ตัวอย่าง


5.16 Gaming และ LAN Party เสมือน

5.16.1 สถานการณ์

เล่นเกมเก่าหรือเกมที่ออกแบบสำหรับ LAN (ค้นหาผู้เล่นด้วย broadcast) ร่วมกับเพื่อนผ่านอินเทอร์เน็ต เกมจำนวนมากต้องการให้ผู้เล่นอยู่ในเครือข่ายเดียวกันและใช้ IP ตรง ๆ

5.16.2 สถาปัตยกรรมและการเลือกโทโพโลยี

โทโพโลยี เหมาะกับ Latency หมายเหตุ
Hub (เซิร์ฟเวอร์เกมคือ hub) เกมที่มีโฮสต์ผู้เล่นคนเดียว ผู้เล่น ↔ โฮสต์ ตรง ง่ายที่สุด ให้โฮสต์เป็น server WireGuard
Hub-and-spoke บน VPS ผู้เล่นหลายคนอยู่หลัง NAT ผู้เล่น ↔ VPS ↔ ผู้เล่น เลือกที่ตั้ง VPS ให้อยู่กลางภูมิภาค
Full mesh เกม peer-to-peer (≤ 8 คน) ตรงที่สุด ต้องการ endpoint ถึงกันได้ (public IP/hole punch)

ผลต่อ Latency: WireGuard เพิ่มเวลาประมวลผลน้อยมาก (ไม่ถึง 1 ms ต่อทิศทางบนฮาร์ดแวร์ทั่วไป) ตัวกำหนดหลักคือเส้นทางเครือข่ายจริง การวนผ่าน VPS ไกลจะเพิ่ม RTT (ดูสูตรหัวข้อ 5.5.4)

ตัวอย่างการคำนวณ: ผู้เล่น A–VPS = 15 ms, VPS–B = 12 ms → RTT A–B ≈ 15 + 12 + ~1 = 28 ms ซึ่งเล่นเกมส่วนใหญ่ได้สบาย; ถ้าวาง VPS ไกลจนรวมเกิน 80–100 ms เกมแอ็กชันจะรู้สึกหน่วง

5.16.3 เกมที่ต้องใช้ Layer 2 / Broadcast

WireGuard เป็น Layer 3 จึงไม่ส่ง broadcast/multicast ข้ามอุโมงค์ ทางแก้:

  1. ใช้ IP ตรง: ให้เพื่อนใส่ IP ของโฮสต์ (เช่น 10.70.0.2) ในเมนู "Direct connect"
  2. ซ้อน tunnel L2 ใน WireGuard เช่น GRE-tap หรือ VXLAN แล้ว bridge (ขั้นสูง)
  3. ใช้ broadcast relay เช่น udp-broadcast-relay-redux ที่คัดลอก broadcast UDP พอร์ตของเกม ไปยัง interface ของ tunnel
# ตัวอย่างซ้อน VXLAN บน WireGuard เพื่อให้ได้ L2 segment ร่วมกัน (ฝั่งโฮสต์ 10.70.0.1 / ผู้เล่น 10.70.0.2)
# MTU ของ VXLAN ต้องลดอีก 50 byte จาก wg0 (1420 - 50 = 1370)
sudo ip link add vx0 type vxlan id 100 remote 10.70.0.2 local 10.70.0.1 dstport 4789 dev wg0
sudo ip link set vx0 mtu 1370 up
sudo ip addr add 192.168.100.1/24 dev vx0
# ฝั่งผู้เล่น: สลับ local/remote และใช้ 192.168.100.2/24
# ต่อด้วย: ip link add br0 type bridge && ip link set vx0 master br0  (ถ้าต้องการ bridge)
# ตัวอย่าง broadcast relay (ต้องติดตั้งเครื่องมือ) ส่งต่อ UDP 4445 ระหว่าง LAN และ wg0
udp-broadcast-relay-redux --id 1 --port 4445 --dev eth0 --dev wg0 --multicast 224.0.0.251 -s 0.0.0.0

5.16.4 Config ตัวอย่าง (Hub บน VPS)

# VPS hub : /etc/wireguard/wg-game.conf
[Interface]
PrivateKey = <VPS_PRIVATE_KEY>
Address = 10.70.0.1/24
ListenPort = 51821
# ให้ผู้เล่นคุยกันเองได้ (forward ภายใน wg-game)
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -A FORWARD -i %i -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o %i -j ACCEPT

[Peer]
# ผู้เล่น 1 (โฮสต์เกม)
PublicKey = <PLAYER1_PUBLIC_KEY>
AllowedIPs = 10.70.0.2/32

[Peer]
# ผู้เล่น 2
PublicKey = <PLAYER2_PUBLIC_KEY>
AllowedIPs = 10.70.0.3/32
# ผู้เล่น (split tunnel เฉพาะ 10.70.0.0/24 : เกมใช้ tunnel อินเทอร์เน็ตอื่นออกปกติ)
[Interface]
PrivateKey = <PLAYER2_PRIVATE_KEY>
Address = 10.70.0.3/32
MTU = 1380                                  # ลดเล็กน้อยกันปัญหา UDP ของเกม

[Peer]
PublicKey = <VPS_PUBLIC_KEY>
AllowedIPs = 10.70.0.0/24
Endpoint = 203.0.113.10:51821
PersistentKeepalive = 25

5.16.5 ข้อควรระวังและการทดสอบ

ping -c 20 10.70.0.2 | tail -n 3             # ดู min/avg/max/mdev
iperf3 -c 10.70.0.2 -u -b 10M -t 10          # วัด UDP jitter/loss (อีกฝั่งรัน iperf3 -s)

🧪 ตัวอย่าง 5.16: config hub/ผู้เล่น และสคริปต์ VXLAN ดูในไฟล์ตัวอย่าง


5.17 Mobile Roaming และ On-Demand VPN

5.17.1 สถานการณ์

ผู้ใช้เดินทางและสลับระหว่าง Wi-Fi บ้าน → 4G/5G → Wi-Fi ที่ทำงาน ต้องการให้ VPN ไม่หลุดและเปิดเฉพาะเมื่อจำเป็น

5.17.2 หลักการ Roaming

เมื่อ client เปลี่ยน IP ต้นทาง (เปลี่ยนเครือข่าย) แพ็กเก็ตถัดไปที่ส่งไปยัง server ยังคงถูกเข้ารหัสด้วย session key เดิม server ตรวจสอบสำเร็จ จึงอัปเดต endpoint ของ peer นั้นเป็น IP/port ใหม่ โดยไม่ต้อง handshake ใหม่หรือตั้งค่าใดเพิ่ม

sequenceDiagram
    participant M as มือถือ (Phone) Wi-Fi บ้าน
    participant S as Server (203.0.113.10)
    M->>S: ข้อมูลเข้ารหัส จาก 192.168.1.50 (NAT เป็น 61.x.x.x)
    S-->>M: ตอบกลับไป 61.x.x.x
    Note over M: ผู้ใช้ออกจากบ้าน สลับเป็น 4G (IP ใหม่ 171.y.y.y)
    M->>S: ข้อมูลเข้ารหัส จาก 171.y.y.y
    Note over S: ถอดรหัสสำเร็จ จึงอัปเดต endpoint เป็น 171.y.y.y
    S-->>M: ตอบกลับไป 171.y.y.y (ไม่ต้อง reconnect)

เงื่อนไขสำคัญ: ฝั่ง server ต้องไม่ล็อก endpoint ของ client (ไม่ตั้ง Endpoint ใน peer ของ client ฝั่ง server หรือแม้ตั้งไว้ก็จะถูกอัปเดตอัตโนมัติ) และฝั่ง client ควรตั้ง PersistentKeepalive = 25 เพื่อให้ NAT ใหม่เปิด mapping ได้เร็วและ server ส่งข้อมูลเข้ามาได้

5.17.3 On-Demand Rules ตาม SSID

iOS (แอป WireGuard): แก้ tunnel → On-Demand Activation เลือก:

ตัวเลือก ความหมาย
Cellular เปิด VPN อัตโนมัติเมื่อใช้เครือข่ายมือถือ
Wi-Fi เปิด VPN อัตโนมัติเมื่อใช้ Wi-Fi
Ethernet เปิดเมื่อใช้ Ethernet (iPad/ผ่านอะแดปเตอร์)
SSID ที่ ยกเว้น (Except) ไม่เปิดเมื่อเชื่อมกับ SSID ที่ระบุ (เช่น HomeWiFi)
SSID ที่ ระบุ (Only) เปิดเฉพาะบน SSID ที่ระบุ

ตัวอย่างนโยบาย: เปิด VPN เมื่อใช้ Cellular และ Wi-Fi ทุกวง ยกเว้น HomeWiFi และ OfficeWiFi (เพราะเครือข่ายเหล่านั้นเข้าถึงทรัพยากรได้โดยตรงอยู่แล้ว)

Android: ใช้ระบบ Always-on VPN (Settings → Network → VPN → ไอคอนเฟือง) ร่วมกับ Block connections without VPN สำหรับ kill-switch; บางรุ่นมี on-demand ตาม SSID ในแอป ถ้าไม่มีให้ใช้แอปอัตโนมัติ เช่น Tasker เปิด/ปิด tunnel ตาม SSID

# ตัวอย่าง Tasker (Android) แบบสรุปเงื่อนไข:
#  Profile: ไม่ได้เชื่อมต่อ SSID "HomeWiFi"  ->  Task: ส่ง intent ให้แอป WireGuard เปิด tunnel "home"
#  Profile: เชื่อมต่อ SSID "HomeWiFi"        ->  Task: ส่ง intent ให้แอป WireGuard ปิด tunnel "home"
#  (ต้องเปิด "Allow remote control apps" ในการตั้งค่าของแอป WireGuard ก่อน)

5.17.4 ข้อควรระวังและการทดสอบ

# บน server: ดู endpoint ของ client เปลี่ยนไปหลังสลับเครือข่าย
watch -n 2 'sudo wg show wg0 endpoints'
# สังเกตค่า endpoint ของ peer มือถือเปลี่ยนจาก 61.x.x.x:xxxxx เป็น 171.y.y.y:yyyyy โดยไม่มี handshake ใหม่
# (ดู handshake ล่าสุดได้ด้วย: sudo wg show wg0 latest-handshakes)

🧪 ตัวอย่าง 5.17: config มือถือแบบ full/split พร้อมสคริปต์ monitor endpoint ดูในไฟล์ตัวอย่าง


5.18 เชื่อม Cloud, Hybrid Cloud และ Multi-Cloud

5.18.1 สถานการณ์

เชื่อมเครือข่าย on-premise (192.168.0.0/16) เข้ากับ VPC บน AWS (10.100.0.0/16), GCP (10.110.0.0/16) และ Azure (10.120.0.0/16) ผ่าน WireGuard gateway VM ในแต่ละที่ แทนหรือเสริม managed VPN gateway

5.18.2 สถาปัตยกรรม

flowchart LR
    subgraph ONPREM["On-premise 192.168.0.0/16"]
        GO["Gateway On-prem
wg0 10.99.0.1"] end subgraph AWS["AWS VPC 10.100.0.0/16"] GA["WG VM (AWS)
wg0 10.99.0.2
Source/Dest check = off"] end subgraph GCP["GCP VPC 10.110.0.0/16"] GG["WG VM (GCP)
wg0 10.99.0.3
IP forwarding = on"] end subgraph AZ["Azure VNet 10.120.0.0/16"] GZ["WG VM (Azure)
wg0 10.99.0.4
IP forwarding on NIC = on"] end GO <--> GA GO <--> GG GO <--> GZ GA <--> GG

5.18.3 Config ตัวอย่าง (Gateway ใน AWS)

# /etc/wireguard/wg0.conf บน VM ใน AWS (Ubuntu)
[Interface]
PrivateKey = <AWS_GW_PRIVATE_KEY>
Address = 10.99.0.2/24
ListenPort = 51820
# เปิด forwarding ไม่ NAT เพื่อให้เห็น IP จริง (ต้องตั้ง route table ของ VPC ให้ชี้มาที่ VM นี้)
PostUp   = sysctl -w net.ipv4.ip_forward=1
PostUp   = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT

[Peer]
# Gateway on-prem (ใช้ dynamic IP จึงไม่ใส่ Endpoint)
PublicKey = <ONPREM_GW_PUBLIC_KEY>
AllowedIPs = 10.99.0.1/32, 192.168.0.0/16
PersistentKeepalive = 25

[Peer]
# Gateway GCP
PublicKey = <GCP_GW_PUBLIC_KEY>
AllowedIPs = 10.99.0.3/32, 10.110.0.0/16
Endpoint = 198.51.100.30:51820

การตั้งค่าฝั่ง Cloud ที่ต้องทำ:

Cloud สิ่งที่ต้องตั้ง หมายเหตุ
AWS Security Group: เปิด UDP 51820 inbound; EC2 Source/destination check = disabled; Route table ของ subnet: 192.168.0.0/16 → instance/ENI ของ WG VM ไม่ปิด source/dest check แล้วแพ็กเก็ตที่ไม่ใช่ของ VM จะถูกทิ้ง
GCP Firewall rule: allow UDP 51820; VM ตั้ง Can IP forward = on ตอนสร้าง; Custom route: ปลายทาง 192.168.0.0/16 next hop = instance ตั้ง canIpForward ตอนสร้าง VM เท่านั้น
Azure NSG: allow UDP 51820; NIC ตั้ง IP forwarding = enabled; User-defined route (UDR): 192.168.0.0/16 → Virtual appliance (IP ของ VM)
VPS ทั่วไป เปิดพอร์ตใน firewall ของผู้ให้บริการและบนเครื่อง อาจต้องตรวจนโยบาย MTU ของผู้ให้บริการ

5.18.4 เปรียบเทียบกับ Managed VPN Gateway

ประเด็น WireGuard บน VM Managed VPN Gateway
ต้นทุน ค่า VM + data transfer (ถูกกว่าในหลายกรณี) ค่า gateway รายชั่วโมง + ค่า tunnel + data transfer
ประสิทธิภาพ สูง (ขึ้นกับขนาด VM; หลาย Gbps บน instance ที่เหมาะสม) มักจำกัดต่อ tunnel (เช่น ~1.25 Gbps ต่อ tunnel สำหรับ IPsec บน AWS)
ความพร้อมใช้งาน ต้องออกแบบ HA เอง มี HA/redundancy ในตัว
การจัดการ ต้องดูแล OS, อัปเดต, monitoring เอง ผู้ให้บริการดูแล
การรองรับ BGP/dynamic routing ต้องเสริม (FRR/BIRD) มีในตัว
ความเข้ากันได้กับ compliance ขึ้นกับนโยบายองค์กร มักมีการรับรองมาตรฐาน

ตัวอย่างการคำนวณต้นทุนเชิงเปรียบเทียบ (ค่าสมมติเพื่อการเรียนรู้):

Cmonth = h × pvm + D × pdata

แทนค่า (สมมติ): VM เล็ก pvm = 0.02 USD/ชม., D = 500 GB, pdata = 0.09 USD/GB → C = 730 × 0.02 + 500 × 0.09 = 14.60 + 45.00 = 59.60 USD/เดือน ส่วน managed gateway จะมีค่า gateway/tunnel รายชั่วโมงเพิ่มเติมจาก data transfer ตัวเลขจริงขึ้นกับผู้ให้บริการและภูมิภาค ต้องตรวจราคาล่าสุดก่อนตัดสินใจ

5.18.5 การเชื่อมหลายผู้ให้บริการ Cloud

5.18.6 การทดสอบ

# จาก VM ใน AWS (10.100.1.10) ไปเครื่อง on-prem และ GCP
ping -c 3 192.168.1.10
ping -c 3 10.110.1.20
# ตรวจ route ใน VM: ต้องเห็นว่าปลายทาง on-prem ผ่าน wg0
ip route get 192.168.1.10
# วัดแบนด์วิดท์ข้าม cloud
iperf3 -c 10.110.1.20 -P 4 -t 15

🧪 ตัวอย่าง 5.18: config gateway ทั้ง 4 ที่ พร้อมสคริปต์ Terraform อย่างย่อ (ดูหัวข้อ 11.1) ดูในไฟล์ตัวอย่าง


ส่วนที่ 6 WireGuard บน Router, Cloud และ Container (WireGuard on Routers, Cloud and Containers)

6.1 Router และ Firewall Appliance

6.1.1 ภาพรวมแพลตฟอร์ม

แพลตฟอร์ม วิธีตั้งค่าหลัก หมายเหตุ
OpenWrt LuCI (Web UI) และ UCI (command line) ติดตั้งแพ็กเกจ wireguard-tools และ luci-proto-wireguard (ใช้ opkg หรือ apk ตามรุ่น)
pfSense Package ผ่าน Package Manager ตั้ง Tunnel, Peer, Interface assignment, กฎ firewall
OPNsense ส่วนประกอบ WireGuard (Instances และ Peers) ต้องสร้าง interface/กฎเองตามเอกสาร
MikroTik RouterOS WinBox/WebFig/CLI (RouterOS v7 ขึ้นไป) RouterOS 6 ไม่รองรับ WireGuard
Ubiquiti UniFi UniFi Network → VPN → WireGuard (gateway รุ่นใหม่) EdgeRouter ใช้แพ็กเกจชุมชน (ไม่เป็นทางการ)
ASUS / GL.iNet / อื่น ๆ เมนู VPN ใน firmware ASUS (firmware รุ่นใหม่ และ Asuswrt-Merlin), GL.iNet มี server/client ในตัว

6.1.2 OpenWrt (UCI)

# ติดตั้ง (บน OpenWrt รุ่นที่ใช้ opkg)
opkg update && opkg install wireguard-tools luci-proto-wireguard

# สร้างกุญแจ
umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub

# ตั้งค่า interface wg0 ด้วย UCI
uci set network.wg0=interface
uci set network.wg0.proto='wireguard'
uci set network.wg0.private_key="$(cat /etc/wireguard/server.key)"
uci set network.wg0.listen_port='51820'
uci add_list network.wg0.addresses='10.8.0.1/24'

# เพิ่ม peer (ชื่อ section รูปแบบ wireguard_<interface>)
uci add network wireguard_wg0
uci set network.@wireguard_wg0[-1].description='alice-phone'
uci set network.@wireguard_wg0[-1].public_key='<ALICE_PUBLIC_KEY>'
uci add_list network.@wireguard_wg0[-1].allowed_ips='10.8.0.2/32'
uci set network.@wireguard_wg0[-1].route_allowed_ips='1'     # ให้ OpenWrt เพิ่ม route ตาม AllowedIPs
uci commit network

# Firewall: เปิดพอร์ต WAN และสร้าง zone สำหรับ tunnel
uci add firewall rule
uci set firewall.@rule[-1].name='Allow-WireGuard'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].proto='udp'
uci set firewall.@rule[-1].dest_port='51820'
uci set firewall.@rule[-1].target='ACCEPT'

uci add firewall zone
uci set firewall.@zone[-1].name='wg'
uci set firewall.@zone[-1].input='ACCEPT'
uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].forward='REJECT'
uci add_list firewall.@zone[-1].network='wg0'

# อนุญาตให้ client ใน wg เข้าถึง lan
uci add firewall forwarding
uci set firewall.@forwarding[-1].src='wg'
uci set firewall.@forwarding[-1].dest='lan'
uci commit firewall

/etc/init.d/network restart && /etc/init.d/firewall restart

# ตรวจสอบ
wg show
# วิธีใช้บน LuCI: Network > Interfaces > Add new interface > Protocol: WireGuard VPN

6.1.3 MikroTik RouterOS v7

# สร้าง interface (RouterOS สร้าง private/public key ให้อัตโนมัติ)
/interface wireguard add name=wg0 listen-port=51820
/interface wireguard print                        # คัดลอก public-key ของ router ไปใส่ใน client

# กำหนด address และ peer
/ip address add address=10.8.0.1/24 interface=wg0
/interface wireguard peers add interface=wg0 name=alice public-key="<ALICE_PUBLIC_KEY>" \
    allowed-address=10.8.0.2/32 comment="Alice phone"

# firewall: อนุญาต UDP 51820 เข้า router และอนุญาต traffic จาก wg0 ไป LAN
/ip firewall filter add chain=input protocol=udp dst-port=51820 action=accept comment="Allow WireGuard" place-before=0
/ip firewall filter add chain=forward in-interface=wg0 action=accept comment="WG to LAN"

# NAT ให้ client ออกอินเทอร์เน็ต (ether1 = WAN)
/ip firewall nat add chain=srcnat src-address=10.8.0.0/24 out-interface=ether1 action=masquerade

6.1.4 pfSense / OPNsense (ขั้นตอนโดยสรุป)

  1. ติดตั้งและเปิดใช้ WireGuard (pfSense: Package Manager; OPNsense: ตามเอกสารของรุ่น)
  2. สร้าง Tunnel/Instance: ตั้ง Listen Port, สร้างคู่กุญแจ, ตั้ง Interface Address เช่น 10.8.0.1/24
  3. เพิ่ม Peer: ใส่ Public Key, Allowed IPs (10.8.0.2/32), (Endpoint ถ้าเป็นฝ่ายเริ่ม), Keepalive
  4. Assign interface (pfSense: Interfaces → Assignments เพิ่ม tun_wg0 แล้ว Enable)
  5. กฎ firewall: WAN อนุญาต UDP ตามพอร์ต; WireGuard interface อนุญาต traffic ที่ต้องการ
  6. ถ้าต้องการให้ client ออกอินเทอร์เน็ต ตั้ง Outbound NAT (Hybrid/Manual) ให้ subnet ของ tunnel

6.1.5 ข้อควรระวัง: Hardware Offload และ CPU

ตัวอย่างการประเมินแบบหยาบ: ถ้าซีพียูของ router เข้ารหัสได้ประมาณ 300 Mbit/s ต่อ core (วัดจริงด้วย iperf3 ข้าม tunnel) และ WireGuard ใช้ core ได้ไม่ถึง 100% เพราะภาระอื่น ๆ ความเร็วที่ใช้ได้จริงอาจประมาณ 200–250 Mbit/s จึงไม่ควรคาดหวัง gigabit เต็มสาย

🧪 ตัวอย่าง 6.1: สคริปต์ UCI ของ OpenWrt และคำสั่ง RouterOS ฉบับเต็ม ดูในไฟล์ตัวอย่าง


6.2 Docker และ Docker Compose

6.2.1 รัน WireGuard Server ใน Container

ดูตัวอย่าง docker-compose.yml ของ linuxserver/wireguard ในหัวข้อ 2.6.1 หลักการเดียวกันใช้ได้กับ image อื่น ๆ (เช่น wg-easy) สิ่งที่ต้องจำ: NET_ADMIN, UDP port mapping, sysctls, และ volume สำหรับ config/key

6.2.2 ให้ Container อื่นออกอินเทอร์เน็ตผ่าน VPN (Sidecar Pattern)

Sidecar pattern คือการให้ container แอปพลิเคชัน ใช้ network namespace ร่วมกับ container VPN (network_mode: "service:vpn") traffic ทั้งหมดของแอปจึงผ่าน VPN โดยแอปไม่ต้องรู้เรื่อง VPN

flowchart LR
    subgraph NS["Network namespace ร่วม (Shared namespace)"]
        VPN["Container: vpn (WireGuard)
wg0 + NAT"] APP["Container: app
network_mode: service:vpn"] end APP --> VPN VPN -- "UDP เข้ารหัส (Encrypted)" --> PROV["VPN server / ผู้ให้บริการ"] PROV --> NET["อินเทอร์เน็ต (Internet)"]
# docker-compose.yml : container app ออกอินเทอร์เน็ตผ่าน WireGuard เสมอ (kill-switch โดยธรรมชาติ)
services:
  vpn:
    image: lscr.io/linuxserver/wireguard:latest
    cap_add: [NET_ADMIN]
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
    volumes:
      # ไฟล์ config ของผู้ให้บริการ ตั้งชื่อ wg0.conf
      - ./wg-client/wg0.conf:/config/wg_confs/wg0.conf:ro
    restart: unless-stopped

  app:
    image: curlimages/curl:latest
    # ใช้ network ของ container "vpn" ร่วมกัน: ถ้า vpn ล่ม app จะไม่มีทางออกอินเทอร์เน็ตเลย
    network_mode: "service:vpn"
    depends_on: [vpn]
    command: ["sh", "-c", "while true; do curl -s https://ifconfig.me; echo; sleep 30; done"]

# วิธีใช้:
#   docker compose up -d
#   docker compose logs -f app          # ต้องแสดง IP ของ VPN ไม่ใช่ IP ของท่าน
#   docker compose stop vpn             # app จะ curl ไม่ได้ทันที (kill-switch)

6.2.3 ตัวอย่างการใช้ gluetun กับผู้ให้บริการ VPN

gluetun คือ container ที่รวม VPN client (รองรับ WireGuard และ OpenVPN) พร้อม firewall kill-switch และ preset ของผู้ให้บริการหลายราย

# docker-compose.yml : gluetun + ผู้ให้บริการแบบกำหนดค่าเอง (custom)
services:
  gluetun:
    image: qmcgaw/gluetun:latest
    cap_add: [NET_ADMIN]
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      - VPN_SERVICE_PROVIDER=custom
      - VPN_TYPE=wireguard
      # ค่าเหล่านี้ได้จากไฟล์ config ของผู้ให้บริการ
      - WIREGUARD_ENDPOINT_IP=198.51.100.7
      - WIREGUARD_ENDPOINT_PORT=51820
      - WIREGUARD_PUBLIC_KEY=<PROVIDER_SERVER_PUBLIC_KEY>
      - WIREGUARD_PRIVATE_KEY=<PROVIDER_ISSUED_PRIVATE_KEY>
      - WIREGUARD_ADDRESSES=10.66.12.34/32
      - TZ=Asia/Bangkok
    ports:
      - 127.0.0.1:8080:8080     # พอร์ตของแอปที่ใช้ network ร่วม (เปิดผ่าน gluetun)
    restart: unless-stopped

  web:
    image: nginx:alpine
    network_mode: "service:gluetun"
    depends_on: [gluetun]

# สำหรับผู้ให้บริการที่ gluetun รองรับ ใช้ VPN_SERVICE_PROVIDER=mullvad (หรืออื่น) และตั้ง
# WIREGUARD_PRIVATE_KEY, WIREGUARD_ADDRESSES, SERVER_COUNTRIES ตามเอกสารของ gluetun
docker compose up -d
docker compose logs gluetun | tail -n 20        # ต้องเห็นว่า VPN เชื่อมแล้วและแสดงข้อมูล public IP
docker compose exec gluetun wget -qO- https://ifconfig.me ; echo
curl -sS http://127.0.0.1:8080 | head -n 5      # เข้า nginx ผ่านพอร์ตที่ publish ที่ gluetun

6.2.4 ข้อควรระวัง

🧪 ตัวอย่าง 6.2: compose ทั้งสองแบบ (sidecar และ gluetun) ดูในไฟล์ตัวอย่าง


6.3 Kubernetes

6.3.1 WireGuard เป็น CNI Encryption (Cilium, Calico)

CNI (Container Network Interface) บางตัวสามารถเข้ารหัส traffic ระหว่าง node ด้วย WireGuard โดยอัตโนมัติ (transparent encryption) โดยแอปไม่ต้องแก้ไข

# Cilium: เปิด transparent encryption ด้วย WireGuard ผ่าน Helm
helm upgrade --install cilium cilium/cilium --namespace kube-system \
  --set encryption.enabled=true \
  --set encryption.type=wireguard

# ตรวจสอบสถานะการเข้ารหัส
kubectl -n kube-system exec ds/cilium -- cilium-dbg status | grep -i encryption
# ตัวอย่างผลลัพธ์: Encryption: Wireguard [cilium_wg0 (Pubkey: ..., Port: 51871, Peers: 2)]
# Calico: เปิด WireGuard ผ่าน FelixConfiguration (ต้องมี kernel รองรับบนทุก node)
kubectl patch felixconfiguration default --type='merge' -p '{"spec":{"wireguardEnabled":true}}'

# ตรวจสอบว่า node มี public key ของ WireGuard
kubectl get node <NODE> -o yaml | grep -i wireguard
# หรือบน node: sudo wg show

ข้อควรระวัง: WireGuard บน CNI ต้องการ UDP port (Cilium ค่าเริ่มต้น 51871, Calico 51820) เปิดระหว่าง node และลด MTU ของ pod network ลงตาม overhead (ตัว CNI ปรับให้อัตโนมัติ)

6.3.2 เชื่อมหลาย Cluster (Kilo และเครื่องมือคล้ายกัน)

Kilo (โดย squat) เป็น CNI add-on ที่ทำ multi-cluster/multi-location mesh ด้วย WireGuard จัดการ key และ peer ให้เอง โดยแบ่ง node เป็น location แล้วสร้าง tunnel ระหว่าง location

# แบ่ง node เป็น location (node ใน location เดียวกันเชื่อมกันผ่าน network ท้องถิ่น)
kubectl annotate node node-a1 kilo.squat.ai/location="bangkok"
kubectl annotate node node-b1 kilo.squat.ai/location="songkhla"

# node หลัง NAT หรือไม่มี public IP ให้กำหนด endpoint ที่ node อื่นใช้ติดต่อ
kubectl annotate node node-a1 kilo.squat.ai/force-endpoint="203.0.113.10:51820"

# ดู topology ด้วย kgctl (เครื่องมือของ Kilo)
kgctl graph | dot -Tsvg > topology.svg

เครื่องมืออื่นที่ใช้แนวคิดคล้ายกัน: Submariner (multi-cluster networking), Liqo, หรือใช้ Tailscale operator / Netbird กับ Kubernetes

6.3.3 เข้าถึง Cluster API ผ่าน WireGuard

ไม่เปิด API server (:6443) สู่อินเทอร์เน็ต แต่ให้ผู้ดูแลต่อ WireGuard เข้ามายัง control plane ผ่าน gateway

# ตัวอย่าง Pod gateway ที่รัน WireGuard ใน namespace ของมันเอง (ต้องใช้ privileged capability NET_ADMIN)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wg-gateway
  namespace: infra
spec:
  replicas: 1
  selector:
    matchLabels: { app: wg-gateway }
  template:
    metadata:
      labels: { app: wg-gateway }
    spec:
      containers:
        - name: wireguard
          image: lscr.io/linuxserver/wireguard:latest
          securityContext:
            capabilities:
              add: ["NET_ADMIN"]
          env:
            - { name: PUID, value: "1000" }
            - { name: PGID, value: "1000" }
          volumeMounts:
            - { name: wg-config, mountPath: /config/wg_confs }
          ports:
            - containerPort: 51820
              protocol: UDP
      volumes:
        - name: wg-config
          secret:
            secretName: wg-gateway-conf    # เก็บ wg0.conf ใน Secret (ไม่ใช่ ConfigMap)
---
apiVersion: v1
kind: Service
metadata:
  name: wg-gateway
  namespace: infra
spec:
  type: LoadBalancer           # หรือ NodePort ตามสภาพแวดล้อม
  selector: { app: wg-gateway }
  ports:
    - port: 51820
      protocol: UDP
      targetPort: 51820
# สร้าง Secret จากไฟล์ config และ apply
kubectl -n infra create secret generic wg-gateway-conf --from-file=wg0.conf=./wg0.conf
kubectl apply -f wg-gateway.yaml
kubectl -n infra get svc wg-gateway         # ดู EXTERNAL-IP
# ผู้ดูแลต่อ WireGuard แล้วใช้: kubectl --server=https://<API_INTERNAL_IP>:6443 get nodes

🧪 ตัวอย่าง 6.3: ไฟล์ wg-gateway.yaml และคำสั่งเปิด encryption ของ Cilium/Calico ดูในไฟล์ตัวอย่าง


6.4 Virtualization

6.4.1 Proxmox, KVM และ Hyper-V

6.4.2 ใช้ใน LXC (Proxmox)

LXC แชร์ kernel กับ host จึงใช้ โมดูล WireGuard ของ host ต้องให้ host โหลดโมดูลก่อน และให้ container มีสิทธิ์จัดการ network

# บน host Proxmox: ให้โมดูลโหลดตอนบูต
echo wireguard | sudo tee /etc/modules-load.d/wireguard.conf
sudo modprobe wireguard && lsmod | grep wireguard

# แก้ไฟล์ config ของ container: /etc/pve/lxc/<CTID>.conf
#   (ถ้าใช้ wireguard-go แบบ userspace ต้องส่ง /dev/net/tun เข้าไป)
cat <<'EOF' | sudo tee -a /etc/pve/lxc/101.conf
lxc.cgroup2.devices.allow: c 10:200 rwm
lxc.mount.entry: /dev/net dev/net none bind,create=dir
EOF

sudo pct stop 101 && sudo pct start 101
# ภายใน container: ติดตั้ง wireguard-tools แล้ว wg-quick up wg0
sudo pct exec 101 -- bash -c 'apt update && apt install -y wireguard-tools && ls -l /dev/net/tun'

ข้อควรระวัง: การใช้ kernel module ใน unprivileged container ขึ้นกับเวอร์ชัน host/kernel บนรุ่นใหม่มักใช้ได้ แต่ถ้าสร้าง interface ไม่ได้ ให้ใช้ privileged container หรือติดตั้ง wireguard-go (userspace) แทน และหลีกเลี่ยงการ modprobe จากภายใน container (ไม่ได้ผล)

🧪 ตัวอย่าง 6.4: ส่วนเพิ่มใน /etc/pve/lxc/<CTID>.conf และสคริปต์ตรวจสอบ ดูในไฟล์ตัวอย่าง


6.5 Cloud Infrastructure

6.5.1 ตั้ง WireGuard บน VM ของ AWS / GCP / Azure / VPS

ใช้ cloud-init (user-data) เพื่อให้ VM ติดตั้งและตั้งค่า WireGuard อัตโนมัติตอนสร้าง

#cloud-config
# user-data สำหรับ Ubuntu 22.04/24.04 : ติดตั้ง WireGuard server อัตโนมัติ
package_update: true
packages:
  - wireguard
  - qrencode

write_files:
  - path: /etc/sysctl.d/99-wireguard.conf
    content: |
      net.ipv4.ip_forward = 1
      net.ipv6.conf.all.forwarding = 1

  - path: /etc/wireguard/wg0.conf.tmpl
    permissions: '0600'
    content: |
      [Interface]
      PrivateKey = __PRIVATE_KEY__
      Address = 10.8.0.1/24
      ListenPort = 51820
      MTU = 1380
      PostUp   = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o __IFACE__ -j MASQUERADE
      PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o __IFACE__ -j MASQUERADE

runcmd:
  - sysctl --system
  - umask 077
  - wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
  # ใส่ private key และชื่อ interface ขาออกลงในเทมเพลต
  - IFACE=$(ip -o -4 route show to default | awk '{print $5}')
  - sed -e "s|__PRIVATE_KEY__|$(cat /etc/wireguard/server.key)|" -e "s|__IFACE__|$IFACE|g" /etc/wireguard/wg0.conf.tmpl > /etc/wireguard/wg0.conf
  - chmod 600 /etc/wireguard/wg0.conf
  - systemctl enable --now wg-quick@wg0
# วิธีใช้: วางข้อความนี้ในช่อง User data ตอนสร้าง VM แล้วเปิดพอร์ต UDP 51820 ที่ security group/firewall

6.5.2 Security Group / Firewall และ Source/Dest Check

เรื่อง AWS GCP Azure
เปิดพอร์ต Security Group: Inbound UDP 51820 Firewall rule: ingress UDP 51820 NSG: Inbound UDP 51820
ให้ VM forward แพ็กเก็ตของคนอื่น ปิด Source/dest check เปิด Can IP forward (ตั้งตอนสร้าง VM) เปิด IP forwarding บน NIC
ให้ subnet อื่นส่งมาที่ VM Route table: ปลายทาง → ENI/instance Custom static route → next hop instance UDR: next hop type = Virtual appliance
IP คงที่ Elastic IP Static external IP Static public IP

6.5.3 ความต่างของ Cloud Network ที่กระทบ Tunnel (MTU)

Cloud/ที่ตั้ง MTU เริ่มต้นของ interface (โดยทั่วไป) MTU wg0 ที่แนะนำ (IPv6 transport, ลบ 80)
Ethernet ทั่วไป / VPS ส่วนใหญ่ 1500 1420
Google Cloud VPC (ค่าเริ่มต้น) 1460 1380
AWS EC2 (ออกอินเทอร์เน็ต) 1500 (ภายใน VPC รองรับ jumbo 9001 แต่ออกนอก 1500) 1420
Azure 1500 1420

ตัวอย่างการคำนวณ: GCP: MTUwg = 1460 − 80 = 1380 ตั้ง MTU = 1380 ใน [Interface] ของ server และ client ที่เชื่อมกับ VM นั้น

อื่น ๆ ที่ควรระวัง:

🧪 ตัวอย่าง 6.5: ไฟล์ cloud-init.yaml ฉบับเต็ม ดูในไฟล์ตัวอย่าง


ส่วนที่ 7 เครื่องมือต่อยอดและระบบจัดการ (Management Tools and Ecosystem)

7.1 GUI และ Web UI สำหรับจัดการ

7.1.1 เครื่องมือที่นิยม

เครื่องมือ จุดเด่น หมายเหตุ
wg-easy Docker image เดียว, สร้าง client + QR code ผ่านเว็บ, แสดงสถานะ ตัวแปร environment เปลี่ยนตามรุ่น (ตรวจเอกสารของรุ่นที่ใช้)
WireGuard UI (ngoduykhanh/wireguard-ui) จัดการ client/server config, ส่ง config ทางอีเมลได้ ต้องตั้ง service ให้ apply config เอง
wg-access-server มี web UI พร้อมการ login (รองรับ OIDC) เหมาะกว่าเมื่อต้องการ user authentication

7.1.2 ตัวอย่าง wg-easy (Docker Compose)

# docker-compose.yml : wg-easy ให้ UI ฟังเฉพาะ localhost แล้วเข้าผ่าน VPN/SSH tunnel หรือ reverse proxy ที่มี auth
services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:latest
    container_name: wg-easy
    environment:
      - WG_HOST=vpn.example.com          # ชื่อ/IP สาธารณะของเซิร์ฟเวอร์ที่ client ใช้เชื่อม
      # ตั้งรหัสผ่านของ Web UI ตามที่รุ่นนั้นกำหนด (บางรุ่นใช้ PASSWORD_HASH ที่เป็น bcrypt hash)
      - PASSWORD_HASH=<BCRYPT_HASH>
      - WG_DEFAULT_ADDRESS=10.8.0.x
      - WG_DEFAULT_DNS=1.1.1.1
      - WG_MTU=1420
      - WG_PERSISTENT_KEEPALIVE=25
    volumes:
      - ./wg-easy-data:/etc/wireguard
    ports:
      - "51820:51820/udp"                # พอร์ต WireGuard เปิดสู่สาธารณะ
      - "127.0.0.1:51821:51821/tcp"      # Web UI เปิดเฉพาะ localhost เท่านั้น
    cap_add: [NET_ADMIN, SYS_MODULE]
    sysctls:
      - net.ipv4.conf.all.src_valid_mark=1
      - net.ipv4.ip_forward=1
    restart: unless-stopped

# วิธีใช้:
#   docker compose up -d
#   ssh -L 51821:127.0.0.1:51821 user@vpn.example.com     # ทำ SSH tunnel มาเปิดหน้าเว็บบนเครื่องท่าน
#   เปิดเบราว์เซอร์ที่ http://127.0.0.1:51821 แล้วกด "New client"

7.1.3 ข้อดี ข้อเสีย และความเสี่ยง

ประเด็น รายละเอียด
ข้อดี ใช้ง่าย สร้าง QR ได้ทันที เหมาะ home lab และทีมเล็ก
ข้อเสีย เก็บ private key ของ client ไว้ที่เซิร์ฟเวอร์ (ผิดหลักการ "key ไม่ออกจากเครื่อง"), ฟีเจอร์ ACL จำกัด
ความเสี่ยงหลัก Web UI ที่เปิดสู่ network เป็น พื้นที่โจมตีใหม่ ที่มักมี privilege สูง (แก้ config, คุม NAT) ถ้าถูกเจาะ ผู้โจมตีสร้าง peer ของตนเองได้ทันที
การลดความเสี่ยง ผูก UI กับ 127.0.0.1 เข้าผ่าน SSH/VPN เท่านั้น, ใช้รหัสผ่านแข็งแรง + reverse proxy ที่มี MFA, อัปเดตสม่ำเสมอ, ไม่เปิดพอร์ต UI สู่อินเทอร์เน็ต

🧪 ตัวอย่าง 7.1: docker-compose.yml ของ wg-easy และคำสั่งสร้าง bcrypt hash ดูในไฟล์ตัวอย่าง


7.2 Mesh VPN ที่สร้างบน WireGuard

7.2.1 แนวคิด Control Plane และ Data Plane

Mesh VPN คือระบบที่เชื่อมอุปกรณ์ทุกเครื่องให้คุยกันตรงแบบ mesh โดยอัตโนมัติ โดยแยกเป็นสองชั้น:

flowchart TD
    subgraph CP["Control plane (ชั้นควบคุม)"]
        COORD["Coordination server
เก็บ key, endpoint, ACL
ยืนยันตัวตน SSO"] end subgraph DP["Data plane (ชั้นข้อมูล)"] A["อุปกรณ์ A
(Device A)"] B["อุปกรณ์ B
(Device B)"] RELAY["Relay (DERP)
ใช้เมื่อเชื่อมตรงไม่ได้
(Fallback relay)"] end A -. "ลงทะเบียน/รับ peer list" .-> COORD B -. "ลงทะเบียน/รับ peer list" .-> COORD A <-- "WireGuard ตรง (Direct) ผ่าน NAT traversal" --> B A <-- "ถ้าตรงไม่ได้ (If direct fails)" --> RELAY RELAY <--> B

7.2.2 Tailscale

# ติดตั้ง (Linux) และเข้าร่วม tailnet
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up                             # เปิด URL ที่แสดงเพื่อ login

# ตรวจสถานะ และดูว่าเชื่อมตรงหรือผ่าน relay
tailscale status
tailscale ping nas                            # จะบอกว่า direct หรือ via DERP

# ทำ subnet router ให้เข้าถึง LAN 192.168.1.0/24 ผ่านเครื่องนี้ (ต้อง approve ใน admin console)
sudo sysctl -w net.ipv4.ip_forward=1
sudo tailscale up --advertise-routes=192.168.1.0/24

# ทำ exit node (ให้เครื่องอื่นออกอินเทอร์เน็ตผ่านเครื่องนี้)
sudo tailscale up --advertise-exit-node
// ตัวอย่างไฟล์ ACL policy (HuJSON) ของ Tailscale: ให้กลุ่ม dev เข้าถึงเฉพาะ tag:staging พอร์ต 22, 443
{
  "groups": { "group:dev": ["alice@example.com", "bob@example.com"] },
  "tagOwners": { "tag:staging": ["group:dev"] },
  "acls": [
    { "action": "accept", "src": ["group:dev"], "dst": ["tag:staging:22,443"] }
  ]
}

7.2.3 Headscale (Self-hosted Control Server)

Headscale คือ control server แบบโอเพนซอร์สที่เข้ากับ client ของ Tailscale ใช้โฮสต์เอง ไม่ต้องพึ่ง SaaS

# บนเซิร์ฟเวอร์ที่รัน Headscale (ตัวอย่างคำสั่งหลัก; ตรวจเอกสารของเวอร์ชันที่ใช้)
headscale users create team-a
headscale preauthkeys create --user team-a --reusable --expiration 24h   # สร้าง key สำหรับเข้าร่วม

# บน client ที่ต้องการเข้าร่วม: ชี้ไปยัง Headscale แทน Tailscale SaaS
sudo tailscale up --login-server https://hs.example.com --authkey <PREAUTH_KEY>

# ดูเครื่องที่ลงทะเบียน
headscale nodes list

7.2.4 Netbird, Netmaker, Firezone และตัวเลือกอื่น

เครื่องมือ แนวคิดหลัก จุดเด่น หมายเหตุ
Netbird Mesh VPN โอเพนซอร์ส + control plane (self-host ได้) SSO/OIDC, ACL, NAT traversal มี client ครบทุกแพลตฟอร์ม
Netmaker จัดการ WireGuard mesh/ไซต์ด้วย server ของตนเอง ควบคุมสูง, รองรับ gateway/egress ต้องดูแล server เอง
Firezone Gateway-centric (client ↔ gateway) + SSO/IdP เน้นแทน VPN ขององค์กร, policy ตามกลุ่ม โครงสร้างต่างจาก mesh แท้
ZeroTier/Nebula ไม่ใช่ WireGuard ทางเลือกอื่นในกลุ่ม overlay network ระบุเพื่อเปรียบเทียบ

7.2.5 เปรียบเทียบ: WireGuard ล้วน กับ Mesh Tool

ประเด็น WireGuard ล้วน Mesh tool (Tailscale/Netbird ฯลฯ)
ความซับซ้อนเมื่อ node เพิ่ม เพิ่มตามกำลังสอง n(n−1)/2 แทบไม่เพิ่ม (control plane จัดการ)
NAT traversal ไม่มี (ต้องทำเอง/ใช้ relay) มีในตัว พร้อม relay สำรอง
User auth/SSO ไม่มี มี (OIDC/SAML ตามผลิตภัณฑ์)
ACL ละเอียด ใช้ firewall เอง policy กลาง
การพึ่งพาบริการภายนอก ไม่ ขึ้นกับ SaaS (หรือโฮสต์เองได้)
ความโปร่งใส/ควบคุม เต็มที่ ขึ้นกับเครื่องมือ
เหมาะกับ โครงสร้างเล็ก/คงที่ ต้องการควบคุมเต็ม ทีม/อุปกรณ์จำนวนมาก เปลี่ยนบ่อย อยู่หลัง NAT
flowchart TD
    Q1{"จำนวนอุปกรณ์ไม่เกิน ~10
และมี public IP?
(<= ~10 devices with public IP?)"} Q1 -- "ใช่ (Yes)" --> R1["WireGuard ล้วน
(Plain WireGuard)"] Q1 -- "ไม่ (No)" --> Q2{"ต้องการ SSO และ ACL กลาง?
(Need SSO and central ACL?)"} Q2 -- "ใช่ (Yes)" --> Q3{"ยอมใช้ SaaS?
(OK with SaaS?)"} Q3 -- "ใช่ (Yes)" --> R2["Tailscale / Netbird Cloud"] Q3 -- "ไม่ (No)" --> R3["Headscale / Netbird self-host / Firezone"] Q2 -- "ไม่ (No)" --> R4["WireGuard + สคริปต์ + Hub
หรือ Netmaker"]

🧪 ตัวอย่าง 7.2: ชุดคำสั่ง Headscale + ตัวอย่างไฟล์ ACL ดูในไฟล์ตัวอย่าง


7.3 การแจกจ่าย Config และ Key

7.3.1 การสร้าง QR Code สำหรับมือถือ

# ติดตั้งเครื่องมือ
sudo apt install -y qrencode

# แสดง QR ในเทอร์มินัล (สแกนด้วยแอป WireGuard ได้ทันที)
qrencode -t ansiutf8 < alice-phone.conf

# บันทึกเป็นไฟล์ภาพ (ระวัง: ไฟล์ภาพมี private key ต้องลบหลังใช้)
qrencode -t png -o alice-phone.png < alice-phone.conf
shred -u alice-phone.png      # ลบอย่างปลอดภัยหลังสแกนเสร็จ

7.3.2 สคริปต์สร้าง Client Config อัตโนมัติ

#!/usr/bin/env bash
# make-client.sh : สร้างไฟล์ config ของ client พร้อม QR และพิมพ์บล็อก [Peer] สำหรับ server
# วิธีใช้:  ./make-client.sh alice-phone 10.8.0.2 [full|split]
#   ตัวแปรที่ต้องตั้งก่อน (หรือแก้ในสคริปต์): SERVER_PUB, ENDPOINT, DNS_IP
set -euo pipefail
NAME=${1:?ชื่อ client}; IP=${2:?IP ของ client}; MODE=${3:-split}
SERVER_PUB=${SERVER_PUB:?ตั้ง SERVER_PUB}      # public key ของ server
ENDPOINT=${ENDPOINT:-vpn.example.com:51820}    # endpoint ของ server
DNS_IP=${DNS_IP:-10.8.0.1}

umask 077
key=$(wg genkey); pub=$(printf '%s' "$key" | wg pubkey); psk=$(wg genpsk)

# กำหนดว่าจะส่ง traffic อะไรผ่าน VPN
if [ "$MODE" = full ]; then ALLOWED="0.0.0.0/0, ::/0"; else ALLOWED="10.8.0.0/24"; fi

cat > "$NAME.conf" <<EOF
[Interface]
PrivateKey = $key
Address = $IP/32
DNS = $DNS_IP

[Peer]
PublicKey = $SERVER_PUB
PresharedKey = $psk
AllowedIPs = $ALLOWED
Endpoint = $ENDPOINT
PersistentKeepalive = 25
EOF

echo "=== เพิ่มบล็อกนี้ใน server (หรือใช้ wg set) ==="
printf '[Peer]\n# %s\nPublicKey = %s\nPresharedKey = %s\nAllowedIPs = %s/32\n' "$NAME" "$pub" "$psk" "$IP"
echo "=== QR code ==="
qrencode -t ansiutf8 < "$NAME.conf"

7.3.3 แนวทางแจกจ่าย Key อย่างปลอดภัย

  1. ให้ผู้ใช้สร้าง private key เองบนเครื่องของตน แล้วส่งเฉพาะ public key มาที่ผู้ดูแล (ดีที่สุด: private key ไม่เคยออกจากอุปกรณ์)
  2. ถ้าผู้ดูแลต้องสร้างให้ ส่งผ่านช่องทางที่เข้ารหัสและหมดอายุ (ตัวจัดการรหัสผ่านแบบแชร์, ลิงก์ใช้ครั้งเดียว, ส่งต่อหน้า) ห้าม ส่งทางอีเมลธรรมดา/แชตสาธารณะ
  3. ใช้ PSK แยกต่ออุปกรณ์
  4. ตั้งอายุของ key (rotation) และบันทึกใน inventory (หัวข้อ 8.2)
  5. หลังส่งมอบ ลบไฟล์ config/QR ที่เซิร์ฟเวอร์ (shred -u)

🧪 ตัวอย่าง 7.3: make-client.sh พร้อมตัวอย่างการเรียกใช้ ดูในไฟล์ตัวอย่าง


7.4 การรวมกับระบบ Identity (Identity Integration)

7.4.1 ปัญหา: WireGuard ไม่มี User Authentication ในตัว

WireGuard ยืนยัน อุปกรณ์ ด้วยกุญแจ ไม่ได้ยืนยัน ผู้ใช้ ด้วยรหัสผ่านหรือ MFA ผลที่ตามมา:

7.4.2 แนวทางแก้: Control Plane ที่เชื่อม SSO/OIDC

sequenceDiagram
    participant U as ผู้ใช้ (User)
    participant C as Client app
    participant I as IdP (SSO/OIDC + MFA)
    participant P as Control plane
    participant G as Gateway (WireGuard)
    U->>C: เปิดแอป กด Connect
    C->>I: เปลี่ยนเส้นทางไป login (OIDC)
    I-->>C: ยืนยันตัวตน + MFA สำเร็จ ได้ token
    C->>P: ส่ง token + public key ของอุปกรณ์
    P->>P: ตรวจกลุ่มและนโยบาย (Policy check)
    P->>G: ติดตั้ง peer และ AllowedIPs ตามสิทธิ์ (Add peer)
    P-->>C: ส่ง config ของ gateway
    C->>G: WireGuard handshake
    Note over P,G: เมื่อ token หมดอายุหรือบัญชีถูกปิด control plane ลบ peer

ตัวอย่างระบบที่ทำได้: Tailscale (OIDC/SAML), Firezone (OIDC), Netbird (OIDC), wg-access-server (OIDC) และการเขียน control plane ของตนเอง (เช่น สคริปต์ที่ฟัง webhook จาก IdP แล้วเรียก wg set)

7.4.3 การ Revoke สิทธิ์ผู้ใช้

ขั้นตอน WireGuard ล้วน ผ่าน control plane
ปิดสิทธิ์ทันที wg set wg0 peer <PUBKEY> remove ปิดบัญชีที่ IdP / ลบอุปกรณ์ใน console
ลบถาวร ลบบล็อก [Peer] ใน wg0.conf และ DB ระบบลบ peer ออกจากทุก gateway ให้อัตโนมัติ
ป้องกันการกลับมา หมุน (rotate) PSK/Key ที่เกี่ยวข้อง session หมดอายุตามนโยบาย, บังคับ MFA ซ้ำ
ตรวจสอบ wg show wg0 peers ต้องไม่มี key นั้น ตรวจ audit log ของ control plane
#!/usr/bin/env bash
# revoke-peer.sh : ตัดสิทธิ์ peer ทันที และลบออกจากไฟล์ config ถาวร
# วิธีใช้:  sudo ./revoke-peer.sh wg0 <PUBLIC_KEY>
set -euo pipefail
IF=${1:?interface}; PUB=${2:?public key}
CONF=/etc/wireguard/$IF.conf

wg set "$IF" peer "$PUB" remove                      # 1) ตัดสิทธิ์ใน runtime ทันที
python3 - "$CONF" "$PUB" <<'PY'                      # 2) ลบบล็อก [Peer] ที่ตรงกันออกจากไฟล์
import re, sys
path, pub = sys.argv[1], sys.argv[2]
text = open(path).read()
blocks = re.split(r'(?m)^(?=\[(?:Interface|Peer)\])', text)
kept = [b for b in blocks if not (b.startswith('[Peer]') and pub in b)]
open(path, 'w').write(''.join(kept))
print("ลบบล็อกของ", pub[:8] + "... แล้ว")
PY
wg show "$IF" peers | grep -q "$PUB" && echo "ยังพบ key! ตรวจสอบ" || echo "revoke สำเร็จ"

🧪 ตัวอย่าง 7.4: revoke-peer.sh และกรณีทดสอบ ดูในไฟล์ตัวอย่าง


ส่วนที่ 8 ความปลอดภัยและการ Hardening (Security and Hardening)

8.1 Threat Model

8.1.1 WireGuard ปกป้องอะไร และไม่ปกป้องอะไร

WireGuard ปกป้อง WireGuard ไม่ปกป้อง
ความลับของข้อมูล (confidentiality) ระหว่างปลายทางของ tunnel ข้อมูลหลังออกจาก tunnel (เช่น ระหว่าง exit server กับเว็บปลายทาง ถ้าไม่ใช่ HTTPS)
ความถูกต้อง/ไม่ถูกแก้ไข (integrity) และการยืนยันตัว peer ด้วยกุญแจ เครื่องปลายทางที่ถูกยึด (malware, ผู้ใช้ภายในที่ประสงค์ร้าย)
การป้องกัน replay และ downgrade Metadata: ขนาดแพ็กเก็ต จังหวะเวลา IP ปลายทางของ tunnel
PFS: ย้อนถอดรหัส traffic เก่าไม่ได้แม้ key ระยะยาวรั่ว การระบุตัวผู้ใช้ (ไม่มี user auth), การควบคุมสิทธิ์ละเอียด (ต้องใช้ firewall)
ความเงียบต่อผู้ไม่รู้ public key (ไม่ตอบ scan) การถูกบล็อกโดย DPI/ไฟร์วอลล์ (ตรวจจับลายเซ็น protocol ได้)

8.1.2 ความเสี่ยงจากการที่ Peer ถูกยึดเครื่อง

ถ้าเครื่อง client ถูกยึด ผู้โจมตีได้ private key และสิทธิ์เท่ากับ client นั้น (ตาม AllowedIPs และ firewall) จึงควร:

8.1.3 Metadata และ Traffic Analysis

ผู้สังเกตบนเครือข่ายเห็น IP ต้นทาง–ปลายทาง พอร์ต UDP ขนาดและจังหวะของแพ็กเก็ต แม้ไม่เห็นเนื้อหา

🧪 ตัวอย่าง 8.1: ตารางประเมินความเสี่ยง (risk register) ตัวอย่างสำหรับองค์กรขนาดเล็ก ดูในไฟล์ตัวอย่าง


8.2 Key Management

8.2.1 การสร้าง จัดเก็บ และหมุนเวียน (Rotation)

ขั้นตอน แนวปฏิบัติ
สร้าง บนอุปกรณ์เจ้าของ หรือบนเครื่องผู้ดูแลที่เชื่อถือ ด้วย umask 077 + wg genkey
จัดเก็บ ไฟล์ permission 600 owner root; ไม่ commit เข้า Git; เข้ารหัสดิสก์
ใช้ ใช้ PSK แยกต่อคู่ peer
หมุนเวียน (rotate) ตามรอบ (เช่น 6–12 เดือน) และเมื่อมีเหตุ (คนลาออก, เครื่องหาย, สงสัยรั่ว)
ทำลาย shred -u ไฟล์ key เก่า ลบจาก backup ตามนโยบาย

ขั้นตอน rotate key ของ peer แบบไม่ตัดการใช้งานนาน:

#!/usr/bin/env bash
# rotate-peer-key.sh : หมุนเวียน key ของ client หนึ่งเครื่อง (รันบน server)
# วิธีใช้:  sudo ./rotate-peer-key.sh wg0 <OLD_PUBLIC_KEY> 10.8.0.2/32
set -euo pipefail
IF=${1:?}; OLD=${2:?}; IP=${3:?}
umask 077
new_key=$(wg genkey); new_pub=$(printf '%s' "$new_key" | wg pubkey); new_psk=$(wg genpsk)

# 1) เพิ่ม peer ใหม่ (คู่ขนานกับของเก่า) -- ช่วง AllowedIPs ซ้ำกันไม่ได้ จึงต้องลบเก่าก่อนในขั้น 3
# 2) ออกไฟล์ config ใหม่ให้ผู้ใช้ (ส่งผ่านช่องทางปลอดภัย)
cat > "client-new.conf" <<EOF
[Interface]
PrivateKey = $new_key
Address = ${IP%/*}/32
[Peer]
PublicKey = $(wg show "$IF" public-key)
PresharedKey = $new_psk
AllowedIPs = 10.8.0.0/24
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25
EOF
echo "ส่ง client-new.conf ให้ผู้ใช้ แล้วกด Enter เมื่อผู้ใช้เปลี่ยนเรียบร้อย"; read -r _

# 3) สลับ: ลบ key เก่า แล้วเพิ่ม key ใหม่ในที่อยู่เดิม
wg set "$IF" peer "$OLD" remove
wg set "$IF" peer "$new_pub" preshared-key <(printf '%s' "$new_psk") allowed-ips "$IP"

# 4) เขียนลง config ถาวร (ต้องลบบล็อก [Peer] ของ key เก่าใน wg0.conf ด้วยสคริปต์ revoke ในหัวข้อ 7.4.3)
printf '\n[Peer]\nPublicKey = %s\nPresharedKey = %s\nAllowedIPs = %s\n' "$new_pub" "$new_psk" "$IP" >> /etc/wireguard/$IF.conf
shred -u client-new.conf
echo "หมุนเวียนสำเร็จ: $new_pub"

8.2.2 ขั้นตอนเมื่ออุปกรณ์สูญหาย

flowchart TD
    L["อุปกรณ์สูญหาย/ถูกขโมย
(Device lost or stolen)"] --> R1["1. revoke peer ทันที
wg set wg0 peer KEY remove"] R1 --> R2["2. ลบบล็อก Peer ในไฟล์ถาวร
(Remove from config file)"] R2 --> R3["3. ตรวจ log: มี handshake/traffic หลังเวลาที่หาย?
(Check handshake after loss time)"] R3 --> R4["4. ถ้าใช้ PSK ร่วมกับ peer อื่น ให้หมุน PSK
(Rotate shared PSK if any)"] R4 --> R5["5. ออก key ใหม่ให้อุปกรณ์ใหม่
(Issue new key)"] R5 --> R6["6. บันทึกเหตุการณ์ใน inventory
(Document incident)"]

8.2.3 การเก็บ Secret ด้วย Secret Manager / pass / Vault

wg-quick ต้องอ่าน PrivateKey จากไฟล์ config จึงใช้แนวทาง เรนเดอร์ config จากเทมเพลตตอน deploy โดยดึง secret จากที่เก็บปลอดภัย แล้วเขียนไฟล์ปลายทางด้วย permission 600 (ไม่เก็บ config ที่มี key ใน Git)

# เทมเพลต (เก็บใน Git ได้ เพราะไม่มี secret): wg0.conf.tmpl
#   [Interface]
#   PrivateKey = ${WG_PRIVATE_KEY}
#   Address = 10.8.0.1/24
#   ListenPort = 51820

# 1) ใช้ pass (password-store) : เก็บ key ด้วย  pass insert wg/server-private-key
export WG_PRIVATE_KEY="$(pass show wg/server-private-key)"

# 2) หรือใช้ HashiCorp Vault (KV v2)
# export WG_PRIVATE_KEY="$(vault kv get -field=private_key secret/wireguard/server)"

# เรนเดอร์ไฟล์ปลายทางด้วย umask 077 (ใช้ envsubst จากแพ็กเกจ gettext-base)
umask 077
envsubst '${WG_PRIVATE_KEY}' < wg0.conf.tmpl | sudo tee /etc/wireguard/wg0.conf >/dev/null
unset WG_PRIVATE_KEY

sudo systemctl restart wg-quick@wg0
# ตรวจ: ls -l /etc/wireguard/wg0.conf ต้องเป็น -rw------- root root
เครื่องมือ เหมาะกับ
pass + GPG ผู้ดูแลคนเดียว/ทีมเล็ก
HashiCorp Vault / Secrets Manager ของ cloud องค์กร, audit log การเข้าถึง secret
SOPS + age (เข้ารหัสไฟล์ใน Git) GitOps (หัวข้อ 11.1)
systemd credentials (LoadCredential=) จ่าย secret ให้ service โดยไม่ผ่านไฟล์ถาวร

🧪 ตัวอย่าง 8.2: rotate-peer-key.sh และเทมเพลต wg0.conf.tmpl ดูในไฟล์ตัวอย่าง


8.3 Pre-shared Key และ Post-Quantum

8.3.1 ภัยคุกคาม "Harvest Now, Decrypt Later"

ผู้โจมตีเก็บ traffic ที่เข้ารหัสไว้ตั้งแต่วันนี้ แล้วรอจนมีคอมพิวเตอร์ควอนตัมที่แก้ปัญหา elliptic curve ได้ (อัลกอริทึมของ Shor) จึงถอดรหัสย้อนหลัง Curve25519 ที่ WireGuard ใช้ เสี่ยงต่อควอนตัม (แม้ ChaCha20 และ BLAKE2s ยังถือว่าทนทานกว่า)

ใช้ อสมการของ Mosca ประเมินว่าต้องกังวลหรือไม่:

X + Y > Z

ตัวอย่างการคำนวณ: ข้อมูลทางการแพทย์ต้องเก็บเป็นความลับ X = 20 ปี, ใช้เวลาย้ายระบบ Y = 3 ปี, สมมติ Z = 15 ปี → X + Y = 23 > 15 จึง เสี่ยง ควรใช้มาตรการเสริมตั้งแต่วันนี้ (เช่น PSK) ส่วนข้อมูลเว็บทั่วไปที่ต้องเป็นความลับเพียง X = 1 ปี, Y = 1 ปี → X + Y = 2 < 15 ความเสี่ยงต่ำ (ค่า Z เป็นเพียงสมมติเพื่อการสอน ไม่ใช่การคาดการณ์จริง)

8.3.2 PSK ช่วยได้อย่างไร

ใน handshake ของ WireGuard PresharedKey ถูกผสมเข้าใน key derivation (HKDF) ดังนั้นการถอดรหัส session key ต้องรู้ทั้ง ผลลัพธ์ของ Curve25519 และ PSK แม้ผู้โจมตีมีคอมพิวเตอร์ควอนตัมที่ทำลาย Curve25519 ได้ แต่ถ้าไม่มี PSK (ที่ไม่เคยส่งผ่าน handshake และมี entropy 256 bit) ก็ยังถอดรหัสไม่ได้

ข้อกำหนดเพื่อให้ได้ผล:

8.3.3 ข้อจำกัดและแนวทางของผู้ให้บริการ

# สร้างและใช้ PSK แยกต่อคู่ peer
umask 077
wg genpsk > /etc/wireguard/alice-laptop.psk

# ใส่ในไฟล์ config ของทั้งสองฝั่ง (ค่าต้องตรงกัน)
#   [Peer]
#   PublicKey = ...
#   PresharedKey = <ค่าในไฟล์ alice-laptop.psk>

# หรือเพิ่มแบบสดบน server
sudo wg set wg0 peer <ALICE_PUBLIC_KEY> preshared-key /etc/wireguard/alice-laptop.psk
sudo wg show wg0 | grep -A4 "peer: <ALICE_PREFIX>"      # ต้องเห็น preshared key: (hidden)

🧪 ตัวอย่าง 8.3: สคริปต์คำนวณ Mosca (mosca.py) พร้อมตัวอย่างค่า ดูในไฟล์ตัวอย่าง


8.4 Least Privilege

8.4.1 จำกัด AllowedIPs ให้แคบที่สุด

8.4.2 แยก Subnet/VLAN ตามบทบาท

บทบาท Subnet ใน tunnel VLAN/โซนปลายทาง
Admin 10.60.0.0/28 Management VLAN
Staff 10.60.1.0/24 Office/Services VLAN
Contractor 10.60.9.0/24 DMZ/โปรเจกต์เฉพาะ
IoT 10.60.20.0/24 IoT VLAN (ออกอินเทอร์เน็ตได้ เข้า LAN ไม่ได้)

8.4.3 ใช้ Firewall ภายใน Tunnel ควบคู่กัน

นอกจากกฎ forward ที่ gateway (หัวข้อ 4.2, 5.15) ควรจำกัดสิ่งที่ client เข้าถึง บนตัว gateway เอง (chain input) ด้วย

# /etc/nftables.d/gateway-input.nft : จำกัดบริการบน gateway สำหรับ traffic ที่มาจาก wg0
table inet gw_input {
    chain input {
        type filter hook input priority 0; policy drop;
        ct state established,related accept
        iif lo accept

        # พอร์ต WireGuard จากอินเทอร์เน็ต (จำกัดอัตราการ handshake เบื้องต้น)
        iifname "eth0" udp dport 51820 limit rate 50/second burst 100 packets accept

        # SSH เฉพาะจาก subnet ผู้ดูแลใน tunnel (ไม่เปิดจากอินเทอร์เน็ต)
        iifname "wg0" ip saddr 10.60.0.0/28 tcp dport 22 accept

        # DNS สำหรับ client ใน tunnel
        iifname "wg0" udp dport 53 accept
        iifname "wg0" tcp dport 53 accept

        # ping ภายใน tunnel
        iifname "wg0" icmp type echo-request accept
    }
}
# ใช้: sudo nft -f /etc/nftables.d/gateway-input.nft && sudo nft list ruleset | head -n 30
# หมายเหตุ: ระวังกฎ rate-limit ไม่ให้ตัด handshake ของผู้ใช้จริงเมื่อมีผู้ใช้จำนวนมาก (ปรับค่าตามขนาดองค์กร)

🧪 ตัวอย่าง 8.4: ชุดไฟล์ gateway-input.nft และตารางแผน VLAN ดูในไฟล์ตัวอย่าง


8.5 การ Hardening เครื่อง Server

8.5.1 จำกัด Service อื่นบนเครื่อง Gateway

# SSH ฟังเฉพาะ address ของ tunnel (ผู้ดูแลต้องต่อ VPN ก่อน)  /etc/ssh/sshd_config.d/10-wg.conf
#   ListenAddress 10.8.0.1
#   PasswordAuthentication no
#   PermitRootLogin no
# ระวัง: sshd ต้องเริ่มหลัง wg0 ขึ้น -> ตั้ง systemd drop-in
sudo mkdir -p /etc/systemd/system/ssh.service.d
cat <<'EOF' | sudo tee /etc/systemd/system/ssh.service.d/wg.conf
[Unit]
After=wg-quick@wg0.service
Requires=wg-quick@wg0.service
EOF
sudo systemctl daemon-reload && sudo systemctl restart ssh
ss -tlnp | grep ':22'          # ต้องเห็นเฉพาะ 10.8.0.1:22

8.5.2 การอัปเดต และมาตรการสำหรับ Service ข้างเคียง

sudo apt install -y unattended-upgrades fail2ban
sudo dpkg-reconfigure -plow unattended-upgrades

cat <<'EOF' | sudo tee /etc/sysctl.d/98-hardening.conf
# ลดการปลอมแปลง/โจมตีระดับเครือข่าย
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.tcp_syncookies = 1
EOF
sudo sysctl --system

8.5.3 ผลของพอร์ต WireGuard ที่ "เงียบ" ต่อการ Scan

เนื่องจากข้อความ handshake ต้องมี mac1 ที่คำนวณจาก public key ของ responder เซิร์ฟเวอร์จึง ไม่ตอบ แพ็กเก็ต UDP ที่ไม่ผ่านการตรวจนี้ ผู้สแกนด้วย nmap -sU จะเห็นพอร์ตเป็น open|filtered ซึ่งแยกไม่ออกจากพอร์ตที่ firewall ทิ้งแพ็กเก็ต

# ทดสอบจากเครื่องภายนอก: ไม่ควรได้คำตอบใด ๆ
sudo nmap -sU -p 51820 -Pn vpn.example.com
# ผลที่คาดหวัง:  51820/udp open|filtered unknown
# (ถ้าได้ "closed" แปลว่ามี ICMP port unreachable กลับมา: ตรวจว่า service/ firewall ทำงานถูกต้อง)

ข้อควรระวัง: "เงียบ" ไม่ใช่การป้องกันที่พอเพียง (security through obscurity) ผู้ที่รู้ public key ของ server ส่ง handshake ที่ถูกต้องได้ จึงต้องมี firewall ภายในและ least privilege ประกอบเสมอ

🧪 ตัวอย่าง 8.5: สคริปต์ตรวจ hardening (harden-check.sh) ดูในไฟล์ตัวอย่าง


8.6 Audit และ Compliance

8.6.1 การทบทวน Peer เป็นระยะ

ทบทวนทุกไตรมาส: เทียบรายชื่อ peer กับรายชื่อพนักงานปัจจุบัน และตรวจ peer ที่ไม่มี handshake เป็นเวลานาน (candidate สำหรับลบ)

#!/usr/bin/env bash
# audit-peers.sh : รายงาน peer ที่ไม่มี handshake เกิน N วัน (stale peers)
# วิธีใช้:  sudo ./audit-peers.sh wg0 30      # ค่าเริ่มต้น 30 วัน
# หมายเหตุ: ค่า handshake รีเซ็ตเมื่อ interface รีสตาร์ท ควรเก็บผลเป็นระยะ (cron) เพื่อดูแนวโน้ม
set -euo pipefail
IF=${1:-wg0}; DAYS=${2:-30}; now=$(date +%s); limit=$(( DAYS*86400 ))

printf '%-46s %-18s %s\n' "PUBLIC KEY" "ALLOWED IPS" "LAST HANDSHAKE"
wg show "$IF" dump | tail -n +2 | while IFS=$'\t' read -r pub psk ep aips hs rx tx ka; do
  if [ "$hs" -eq 0 ]; then age="never"; flag="STALE"
  else
    diff=$(( now - hs ))
    age="$(( diff/86400 ))d ago"
    flag=""; [ "$diff" -gt "$limit" ] && flag="STALE"
  fi
  printf '%-46s %-18s %s %s\n' "$pub" "$aips" "$age" "$flag"
done | sort -k4

8.6.2 การเก็บ Log และข้อจำกัด

# เก็บ snapshot สถานะ peer ทุก 5 นาที ลงไฟล์ (JSON lines) สำหรับ audit
#   /etc/cron.d/wg-snapshot
# */5 * * * * root /usr/local/bin/wg-snapshot.sh
cat <<'EOF' | sudo tee /usr/local/bin/wg-snapshot.sh
#!/bin/sh
# บันทึกเวลา, interface, public key, endpoint, handshake, ปริมาณข้อมูล ของทุก peer
ts=$(date -u +%FT%TZ)
wg show all dump | awk -F'\t' -v ts="$ts" 'NF==9 {
  printf "{\"ts\":\"%s\",\"if\":\"%s\",\"peer\":\"%s\",\"endpoint\":\"%s\",\"hs\":%s,\"rx\":%s,\"tx\":%s}\n",
         ts,$1,$2,$4,$6,$7,$8 }' >> /var/log/wg-snapshot.jsonl
EOF
sudo chmod +x /usr/local/bin/wg-snapshot.sh

8.6.3 ประเด็น Privacy และกฎหมาย (กล่าวสั้น ๆ)

🧪 ตัวอย่าง 8.6: audit-peers.sh, wg-snapshot.sh และแบบฟอร์มทบทวนสิทธิ์รายไตรมาส ดูในไฟล์ตัวอย่าง


ส่วนที่ 9 ประสิทธิภาพ การ Monitor และ Troubleshooting (Performance, Monitoring and Troubleshooting)

9.1 การปรับจูนประสิทธิภาพ (Performance Tuning)

9.1.1 ปัจจัยที่กระทบประสิทธิภาพ

flowchart TD
    P["ประสิทธิภาพ tunnel
(Tunnel performance)"] --> A["MTU / MSS
ลด fragmentation"] P --> B["CPU: การเข้ารหัส
(Crypto CPU cost)"] P --> C["UDP buffer ของ kernel
(Socket buffers)"] P --> D["NIC offload (GRO/GSO)"] P --> E["Multi-queue / CPU affinity"] P --> F["Kernel vs Userspace"] P --> G["เส้นทางเครือข่าย (RTT, loss)"]

9.1.2 MTU, UDP Buffer, rp_filter และ Offload ของ NIC

MTU: ตั้งให้พอดี (หัวข้อ 4.5) เพราะ fragmentation ทำให้ throughput ตกอย่างมาก

ประสิทธิภาพเชิงประสิทธิผลของการห่อหุ้ม (Encapsulation efficiency):

η = MTUwg−Htcpip MTUlink

แทนค่า: MTUwg = 1420, Htcpip = 40, MTUlink = 1500 → η = (1420 − 40) ÷ 1500 = 1380 ÷ 1500 = 0.92 (92%) เมื่อลิงก์ 1 Gbit/s จะได้ goodput สูงสุดเชิงทฤษฎี ≈ 920 Mbit/s (ยังไม่นับ overhead Ethernet 38 byte ต่อ frame ที่ทำให้ต่ำลงอีกเล็กน้อย)

TCP throughput ต่อ connection ถูกจำกัดโดย window และ RTT:

Rmax = W×8 RTT

แทนค่า: W = 64 KiB = 65,536 byte, RTT = 50 ms = 0.05 s → Rmax = (65,536 × 8) ÷ 0.05 = 10,485,760 bit/s ≈ 10.5 Mbit/s ต่อ connection ดังนั้นการวัดด้วย iperf3 -P 4 (หลาย stream) และปรับ window จึงสำคัญสำหรับลิงก์ที่ RTT สูง

UDP buffer และการปรับจูน kernel:

# เพิ่ม buffer ของ socket UDP เพื่อลดการสูญหายเมื่อ traffic เป็นช่วงสั้น ๆ
sudo tee /etc/sysctl.d/97-wg-perf.conf >/dev/null <<'EOF'
# ขนาด buffer สูงสุดของ socket (byte)
net.core.rmem_max = 26214400
net.core.wmem_max = 26214400
net.core.rmem_default = 1048576
net.core.wmem_default = 1048576
# คิวรับแพ็กเก็ตของ NIC ก่อนส่งให้ stack
net.core.netdev_max_backlog = 5000
# rp_filter แบบ loose (ค่า 2) ป้องกันการทิ้งแพ็กเก็ตเมื่อมี policy routing/asymmetric path
net.ipv4.conf.all.rp_filter = 2
EOF
sudo sysctl --system

# ตรวจ offload ของ NIC (GRO/GSO ช่วยลดภาระ CPU เมื่อ forward ผ่าน wg0)
ethtool -k eth0 | grep -E 'generic-receive-offload|generic-segmentation-offload|tx-udp-segmentation|rx-udp-gro-forwarding'
# เปิด GRO สำหรับ UDP ที่ forward (ช่วยประสิทธิภาพของ gateway บน kernel ใหม่; ตรวจว่าไดรเวอร์รองรับ)
sudo ethtool -K eth0 rx-udp-gro-forwarding on rx-gro-list off 2>/dev/null || true

9.1.3 Multi-queue, CPU Affinity และ Kernel เทียบกับ Userspace

ethtool -l eth0                       # ดูจำนวน queue ที่รองรับ/ใช้อยู่
nproc                                 # จำนวน core
cat /proc/interrupts | grep eth0      # ดูการกระจาย IRQ ของ NIC
# ตัวอย่าง: ตั้งจำนวน combined queue เป็น 4
sudo ethtool -L eth0 combined 4
# ดูภาระ CPU ระหว่างทดสอบ (ควรเห็นงาน kworker/wg-crypt กระจายหลาย core)
top -H -b -n 1 | head -n 20

9.1.4 การวัดผลด้วย iperf3

#!/usr/bin/env bash
# bench-wg.sh : วัด throughput และ latency ข้าม tunnel
# วิธีใช้:  ./bench-wg.sh 10.8.0.1 [output.csv]       (อีกฝั่งต้องรัน: iperf3 -s)
set -euo pipefail
SERVER=${1:?IP ปลายทางใน tunnel}; OUT=${2:-bench.csv}

echo "test,mtu,value,unit" > "$OUT"
MTU=$(ip -o link show wg0 | sed -n 's/.* mtu \([0-9]*\).*/\1/p')

# 1) TCP หนึ่ง stream, 4 stream, และทิศทางย้อนกลับ
for P in 1 4; do
  bps=$(iperf3 -c "$SERVER" -P "$P" -t 10 -J | python3 -c 'import sys,json; print(json.load(sys.stdin)["end"]["sum_received"]["bits_per_second"])')
  echo "tcp_P${P},${MTU},$(python3 -c "print(round($bps/1e6,1))"),Mbit/s" | tee -a "$OUT"
done
bps=$(iperf3 -c "$SERVER" -R -t 10 -J | python3 -c 'import sys,json; print(json.load(sys.stdin)["end"]["sum_received"]["bits_per_second"])')
echo "tcp_reverse,${MTU},$(python3 -c "print(round($bps/1e6,1))"),Mbit/s" | tee -a "$OUT"

# 2) UDP ที่ 200 Mbit/s: ดู loss และ jitter
iperf3 -c "$SERVER" -u -b 200M -t 10 -J > /tmp/iperf-udp.json
python3 - /tmp/iperf-udp.json <<'PY' | tee -a "$OUT"
import json, sys
r = json.load(open(sys.argv[1]))["end"]["sum"]
print("udp_loss,,%.2f,%%" % r["lost_percent"])
print("udp_jitter,,%.3f,ms" % r["jitter_ms"])
PY

# 3) latency
avg=$(ping -c 20 -q "$SERVER" | awk -F'/' '/rtt/ {print $5}')
echo "ping_avg,${MTU},${avg},ms" | tee -a "$OUT"
echo "บันทึกผลที่ $OUT"

ตัวอย่างตารางผลวัดก่อน–หลังปรับจูน (ตัวเลขด้านล่างเป็น ตัวอย่างสมมติเพื่อการสอน ไม่ใช่ผลวัดจริง ท่านควรวัดบนระบบของท่านเอง):

การทดสอบ ก่อนปรับจูน (MTU 1500 มี fragmentation) หลังปรับจูน (MTU 1420 + MSS clamp + buffer) เปลี่ยนแปลง
TCP 1 stream 310 Mbit/s 640 Mbit/s +106%
TCP 4 stream 520 Mbit/s 890 Mbit/s +71%
UDP 200 Mbit/s loss 4.2% 0.1% ลดลง 4.1 จุด
Ping เฉลี่ย 14.8 ms 14.1 ms ลดลง 0.7 ms

วิธีคำนวณการเปลี่ยนแปลง: เปอร์เซ็นต์ = (ค่าหลัง − ค่าก่อน) ÷ ค่าก่อน × 100 เช่น TCP 1 stream = (640 − 310) ÷ 310 × 100 ≈ 106.5%

🧪 ตัวอย่าง 9.1: bench-wg.sh และ sysctl ชุดปรับจูน ดูในไฟล์ตัวอย่าง


9.2 Monitoring

9.2.1 wg show และความหมายของแต่ละฟิลด์

interface: wg0
  public key: Xk3...=
  private key: (hidden)
  listening port: 51820

peer: bC9...=
  preshared key: (hidden)
  endpoint: 171.96.10.20:44321
  allowed ips: 10.8.0.2/32
  latest handshake: 37 seconds ago
  transfer: 15.20 MiB received, 48.77 MiB sent
  persistent keepalive: every 25 seconds
ฟิลด์ ความหมาย วิธีตีความ
public key (interface) public key ของเครื่องนี้ ใช้ส่งให้ peer ตั้งค่า
listening port พอร์ต UDP ที่รับ ตรวจให้ตรงกับ firewall
peer public key ของ peer ตัวระบุ peer
endpoint IP:port ล่าสุดที่เห็นจาก peer เปลี่ยนเมื่อ peer roam; ว่างคือยังไม่เคยเจอ
allowed ips Cryptokey routing ของ peer ตรวจให้ตรงที่ตั้งใจ
latest handshake เวลาตั้งแต่ handshake สำเร็จครั้งล่าสุด ปกติ < ~2–3 นาที เมื่อมี traffic; ไม่มีบรรทัดนี้ = ไม่เคย handshake
transfer ปริมาณข้อมูลสะสมนับจาก interface ขึ้น ถ้า sent เพิ่มแต่ received ไม่เพิ่ม = ฝั่งตรงข้ามไม่ตอบ
persistent keepalive ช่วง keepalive ตรวจให้ตรงที่ตั้งใจ

ข้อสังเกต: latest handshake เก่ามาก ไม่ได้แปลว่าเสียเสมอไป ถ้าไม่มี traffic และไม่มี keepalive ก็จะไม่มี handshake ใหม่ (WireGuard เจรจาเมื่อมีข้อมูลต้องส่ง)

9.2.2 Prometheus Exporter + Grafana

ใช้ exporter สำเร็จรูป (เช่น prometheus_wireguard_exporter) หรือเขียน textfile collector เองให้ node_exporter อ่านจาก wg show all dump

#!/usr/bin/env bash
# wg-textfile.sh : แปลงสถานะ WireGuard เป็น metric รูปแบบ Prometheus สำหรับ node_exporter textfile collector
# วิธีใช้:  ติดตั้งที่ /usr/local/bin แล้วตั้ง cron/systemd timer ทุก 15 วินาที
#   ตั้ง node_exporter ด้วย: --collector.textfile.directory=/var/lib/node_exporter/textfile
set -euo pipefail
DIR=/var/lib/node_exporter/textfile
TMP=$(mktemp "$DIR/.wg.XXXXXX")

{
  echo '# HELP wireguard_latest_handshake_seconds เวลา epoch ของ handshake ล่าสุด (0 = ไม่เคย)'
  echo '# TYPE wireguard_latest_handshake_seconds gauge'
  echo '# HELP wireguard_received_bytes_total จำนวน byte ที่รับจาก peer'
  echo '# TYPE wireguard_received_bytes_total counter'
  echo '# HELP wireguard_sent_bytes_total จำนวน byte ที่ส่งไป peer'
  echo '# TYPE wireguard_sent_bytes_total counter'
  # "wg show all dump": บรรทัด peer มี 9 ฟิลด์ (interface, pubkey, psk, endpoint, allowed-ips, handshake, rx, tx, keepalive)
  wg show all dump | awk -F'\t' '
    NF==9 {
      l = sprintf("interface=\"%s\",public_key=\"%s\",allowed_ips=\"%s\"", $1, $2, $5)
      printf "wireguard_latest_handshake_seconds{%s} %s\n", l, $6
      printf "wireguard_received_bytes_total{%s} %s\n", l, $7
      printf "wireguard_sent_bytes_total{%s} %s\n", l, $8
    }'
} > "$TMP"
chmod 644 "$TMP"; mv "$TMP" "$DIR/wireguard.prom"      # เขียนแบบ atomic
# prometheus.yml : ส่วน scrape ของ node_exporter
scrape_configs:
  - job_name: node
    scrape_interval: 15s
    static_configs:
      - targets: ["vpn.example.com:9100"]

Grafana panel ที่แนะนำ (ใช้ PromQL):

Panel PromQL
อายุ handshake ของแต่ละ peer (วินาที) time() - wireguard_latest_handshake_seconds
Throughput ขาเข้าต่อ peer (bit/s) rate(wireguard_received_bytes_total[5m]) * 8
Throughput ขาออกต่อ peer (bit/s) rate(wireguard_sent_bytes_total[5m]) * 8
จำนวน peer ที่ active (handshake < 3 นาที) count((time() - wireguard_latest_handshake_seconds) < 180)

ตัวอย่างการคำนวณ: counter รับเพิ่มจาก 1,000,000,000 เป็น 1,090,000,000 byte ใน 300 วินาที → rate = 90,000,000 ÷ 300 = 300,000 byte/s → × 8 = 2.4 Mbit/s

9.2.3 การแจ้งเตือนเมื่อ Handshake เก่าเกินกำหนด

# wireguard-alerts.yml : กฎแจ้งเตือนของ Prometheus
groups:
  - name: wireguard
    rules:
      - alert: WireGuardPeerStale
        # peer ที่เคย handshake แล้วแต่ไม่มี handshake ใหม่เกิน 5 นาที (ต้องมี PersistentKeepalive หรือ traffic ต่อเนื่อง)
        expr: (time() - wireguard_latest_handshake_seconds) > 300 and wireguard_latest_handshake_seconds > 0
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "Peer {{ $labels.public_key }} บน {{ $labels.interface }} ไม่มี handshake เกิน 5 นาที"
      - alert: WireGuardPeerNeverConnected
        expr: wireguard_latest_handshake_seconds == 0
        for: 1h
        labels: { severity: info }
        annotations:
          summary: "Peer {{ $labels.public_key }} ยังไม่เคยเชื่อมต่อ"

ข้อควรระวัง: ถ้า peer เป็นมือถือที่ไม่ได้ตั้ง keepalive จะ "stale" เป็นปกติเมื่อไม่ใช้งาน ให้ปรับเกณฑ์/กลุ่มการแจ้งเตือนตามประเภทอุปกรณ์

🧪 ตัวอย่าง 9.2: wg-textfile.sh, wireguard-alerts.yml และ JSON ของ Grafana dashboard อย่างย่อ ดูในไฟล์ตัวอย่าง


9.3 Debug เชิงลึก (Deep Debugging)

9.3.1 เปิด Dynamic Debug ของ Kernel Module

# เปิด debug log ของโมดูล wireguard (ผลลัพธ์อยู่ใน dmesg/journalctl -k)
echo 'module wireguard +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
sudo dmesg -w | grep -i wireguard      # ดูสด เช่น "Sending handshake initiation to peer 1"

# ปิดเมื่อเสร็จ (log จำนวนมาก)
echo 'module wireguard -p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# หมายเหตุ: ต้องมี debugfs mount (mount -t debugfs none /sys/kernel/debug) และ kernel เปิด CONFIG_DYNAMIC_DEBUG

ข้อความที่พบบ่อยและความหมาย:

ข้อความ (ตัวอย่าง) ความหมาย
Sending handshake initiation to peer N เริ่มเจรจา key (ฝั่งเรามีข้อมูลต้องส่ง)
Receiving handshake response from peer N ได้ตอบกลับ แปลว่า key ถูกและเส้นทาง UDP ถึงกัน
Handshake for peer N did not complete after 5 seconds, retrying ส่งไปแต่ไม่มีคำตอบ: firewall/endpoint/key ผิด
Invalid MAC of handshake, dropping packet from ... ส่งถึงเครื่อง แต่ public key ที่ระบุไม่ใช่ของ server
Packet has unallowed src IP ... from peer N แพ็กเก็ตถอดรหัสได้ แต่ source ไม่อยู่ใน AllowedIPs ของ peer นั้น

9.3.2 tcpdump และ Wireshark

# จับแพ็กเก็ต WireGuard บน interface ภายนอก (ชั้น UDP ที่เข้ารหัส)
sudo tcpdump -ni eth0 'udp port 51820' -vv

# จับ traffic ที่ถอดรหัสแล้ว บน wg0 (ชั้นใน)
sudo tcpdump -ni wg0 icmp or tcp port 22

# บันทึกไฟล์ .pcap เพื่อเปิดใน Wireshark
sudo tcpdump -ni eth0 'udp port 51820' -w wg-handshake.pcap -c 50

# จำแนกชนิด message จากไบต์แรกของ UDP payload: 1=initiation, 2=response, 3=cookie, 4=data
sudo tcpdump -ni eth0 'udp port 51820 and udp[8] == 1' -c 5      # handshake initiation
sudo tcpdump -ni eth0 'udp port 51820 and udp[8] == 2' -c 5      # handshake response
# ขนาด UDP payload: initiation = 148, response = 92, cookie = 64, keepalive = 32
sudo tcpdump -ni eth0 'udp port 51820 and udp[4:2] == 156' -c 5   # UDP length = 148 + 8 = 156

Wireshark: มี dissector ของ WireGuard ในตัว แสดงชนิด message, sender/receiver index, counter, ตรวจ mac1 ได้ การถอดรหัส payload ต้องมี key log (ตั้งที่ Preferences → Protocols → WireGuard → Key log filename) ซึ่งต้องมี ephemeral private key ที่ kernel ไม่บันทึกให้ในสภาวะปกติ (ใช้สคริปต์ debug ใน contrib ของ wireguard-tools เฉพาะเครื่องทดสอบ)

9.3.3 แยกชั้นปัญหา

flowchart TD
    S["ปัญหา: ใช้งาน tunnel ไม่ได้
(Tunnel not working)"] --> H{"wg show มี latest handshake?
(Handshake present?)"} H -- "ไม่มี (No)" --> H1["ชั้น Handshake:
ตรวจ key, Endpoint, พอร์ต UDP,
firewall, NAT, เวลา (clock)"] H -- "มี (Yes)" --> R{"ping IP ของ peer ใน tunnel ผ่าน?
(Ping peer tunnel IP?)"} R -- "ไม่ (No)" --> R1["ชั้น Routing/AllowedIPs:
ip route get, AllowedIPs ทั้งสองฝั่ง,
rp_filter, firewall บน wg0"] R -- "ผ่าน (Yes)" --> F{"เข้าถึง LAN/อินเทอร์เน็ตหลัง peer ได้?
(Reach beyond peer?)"} F -- "ไม่ (No)" --> F1["ชั้น Forwarding/NAT:
ip_forward, MASQUERADE,
route กลับ, FORWARD chain"] F -- "ผ่าน (Yes)" --> D{"เปิดเว็บด้วยชื่อโดเมนได้?
(Resolve names?)"} D -- "ไม่ (No)" --> D1["ชั้น DNS:
DNS= ใน config, resolvectl status,
firewall พอร์ต 53"] D -- "ผ่าน (Yes)" --> M{"บางหน้า/บางแอปช้าหรือค้าง?
(Some pages hang?)"} M -- "ใช่ (Yes)" --> M1["ชั้น MTU/MSS:
ping -M do -s, ลด MTU, MSS clamp"] M -- "ไม่ (No)" --> OK["ปกติ (OK)"]

🧪 ตัวอย่าง 9.3: diagnose.sh สคริปต์ตรวจตามขั้นตอนข้างต้นอัตโนมัติ ดูในไฟล์ตัวอย่าง


9.4 ปัญหาที่พบบ่อยและวิธีแก้ (Common Problems and Fixes)

อาการ (Symptom) สาเหตุที่พบบ่อย (Cause) วิธีตรวจ (Check) วิธีแก้ (Fix)
Handshake ไม่เกิดขึ้น (ไม่มี latest handshake) key ผิด/สลับ public–private; endpoint/port ผิด; firewall/security group บล็อก UDP; เวลาของเครื่องผิดมาก (timestamp) wg show; tcpdump -ni eth0 udp port 51820 ที่ server ว่าเห็น Initiation ไหม; dmesg (dynamic debug) แก้ key (public ของ อีกฝั่ง); เปิด UDP; ตรวจ Endpoint; ซิงก์เวลา (timedatectl, NTP)
Handshake สำเร็จแต่ ping ไม่ผ่าน AllowedIPs ไม่ครอบคลุม; ไม่มี route; ไม่เปิด forwarding; firewall บน wg0 ip route get <IP>; sysctl net.ipv4.ip_forward; nft list ruleset; tcpdump -ni wg0 เติม AllowedIPs ทั้งสองฝั่ง; เพิ่ม route; เปิด forwarding; แก้กฎ firewall
ได้ handshake, ping เครื่อง peer ผ่าน แต่เข้า LAN หลัง peer ไม่ได้ LAN ไม่มี route กลับ 10.8.0.0/24; ไม่มี NAT traceroute; ดู conntrack; tcpdump -ni eth1 ที่ gateway เพิ่ม MASQUERADE หรือ route กลับบน router ของ LAN
อินเทอร์เน็ตหายหลังเปิด full tunnel routing loop ของ endpoint; DNS ไม่ผ่าน; server ไม่ NAT/ไม่ forward ip route get <ENDPOINT_IP>; resolvectl status; ตรวจ server ใช้ wg-quick (fwmark) หรือเพิ่ม route เฉพาะไป endpoint; ตั้ง DNS=; เปิด NAT ที่ server
ช้า / เว็บบางหน้าค้าง MTU/MSS ไม่ตรง, ICMP ถูกบล็อก (PMTUD ล้มเหลว) ping -M do -s <size>; จับแพ็กเก็ตดู fragment ลด MTU (1420 → 1380 หรือ 1280); MSS clamping
เชื่อมได้แล้วหลุดเป็นระยะ NAT timeout; ไม่มี keepalive; Wi-Fi/มือถือ suspend wg show ดู handshake เก่า; NAT timeout ของ router PersistentKeepalive = 25 (หรือสั้นลง); ยกเว้น battery optimization
หลัง roam แล้วไม่กลับมา endpoint ฝั่งเรายังชี้ IP เก่า (DDNS เปลี่ยน); NAT mapping หมดอายุ wg show wg0 endpoints; dig +short <ชื่อ> ใช้ reresolve-dns/สคริปต์อัปเดต endpoint (หัวข้อ 9.5); keepalive
RTNETLINK answers: Operation not supported / ไม่มี interface wireguard kernel ไม่มีโมดูล (เก่า/ไม่ตรง header) modprobe wireguard; uname -r อัปเดต kernel หรือติดตั้ง DKMS / ใช้ wireguard-go
resolvconf: command not found (wg-quick ตั้ง DNS ไม่ได้) ไม่มี resolvconf/systemd-resolved which resolvconf resolvectl ติดตั้ง openresolv/resolvconf หรือเปิด systemd-resolved หรือลบ DNS= แล้วตั้ง DNS เอง
Warning: ... is world accessible permission ของ config กว้าง ls -l /etc/wireguard chmod 600
Peer สองเครื่อง "แย่ง" กัน (endpoint สลับไปมา) ใช้ key เดียวกันหลายอุปกรณ์ wg show ดู endpoint เปลี่ยนบ่อย ออก key แยกต่ออุปกรณ์
ซ้ำซ้อน AllowedIPs ทำให้ peer เดิมหลุดสิทธิ์ สอง peer มี AllowedIPs ทับกัน (ตัวหลังชนะ) wg show ดู allowed ips ของแต่ละ peer แก้ไม่ให้ซ้อนทับกัน

🧪 ตัวอย่าง 9.4: ชุด config ที่ใส่บั๊กไว้ 6 จุด (ใช้ใน Lab 8) ดูในไฟล์ตัวอย่าง


9.5 Dynamic DNS และ Endpoint ที่เปลี่ยนบ่อย

9.5.1 การใช้ DDNS กับ Endpoint

เมื่อฝั่งที่เป็น server มี IP แบบ dynamic ให้ใช้ชื่อโดเมน (DDNS) เป็น Endpoint = home.example.com:51820 แทน IP ตรง

ตัวอย่างการอัปเดต DDNS ด้วยสคริปต์ (DuckDNS):

#!/usr/bin/env bash
# ddns-update.sh : อัปเดต A record ของ DuckDNS ด้วย IP สาธารณะปัจจุบัน
# วิธีใช้: ตั้ง DOMAIN และ TOKEN แล้วรันด้วย cron ทุก 5 นาที   (*/5 * * * * /usr/local/bin/ddns-update.sh)
set -euo pipefail
DOMAIN="myhome"            # ชื่อ myhome.duckdns.org
TOKEN="<DUCKDNS_TOKEN>"    # token ของบัญชี (ห้ามเปิดเผย)
# ip= ว่าง ให้บริการตรวจ IP ต้นทางเอง
curl -fsS "https://www.duckdns.org/update?domains=${DOMAIN}&token=${TOKEN}&ip=" -o /tmp/duck.log
logger -t ddns "duckdns: $(cat /tmp/duck.log)"

9.5.2 ข้อจำกัดเรื่อง DNS Resolve เฉพาะตอนเริ่ม

wg และ wg-quick resolve ชื่อใน Endpoint เพียงครั้งเดียวตอนตั้งค่า (ตอน wg-quick up หรือ wg setconf) หลังจากนั้นถ้า IP ของชื่อนั้นเปลี่ยน WireGuard จะ ไม่รู้ และยังส่งไป IP เดิม ฝั่งที่เป็น server ไม่ได้รับผลกระทบ (เพราะเรียนรู้จากแพ็กเก็ตขาเข้า) แต่ ฝั่ง client ที่ใช้ชื่อ DDNS จะต่อไม่ติดจนกว่าจะ resolve ใหม่

ทางแก้: รันสคริปต์ resolve ซ้ำเป็นระยะ ตัวอย่างอย่างเป็นทางการอยู่ใน contrib/reresolve-dns/reresolve-dns.sh ของ wireguard-tools ซึ่งตรวจ peer ที่ handshake เก่าเกิน 135 วินาทีแล้ว resolve ชื่อใหม่ เขียนง่าย ๆ เองได้ดังนี้

#!/usr/bin/env bash
# wg-reresolve.sh : resolve ชื่อ Endpoint ใหม่ ถ้า handshake เก่าเกินกำหนด
# วิธีใช้:  sudo ./wg-reresolve.sh wg0 <PEER_PUBLIC_KEY> home.example.com 51820
set -euo pipefail
IF=${1:?}; PEER=${2:?}; NAME=${3:?}; PORT=${4:?}
MAX_AGE=135                                    # วินาที (อ้างอิงจากสคริปต์มาตรฐาน)

last=$(wg show "$IF" latest-handshakes | awk -v k="$PEER" '$1==k {print $2}')
age=$(( $(date +%s) - ${last:-0} ))

if [ "$age" -gt "$MAX_AGE" ]; then
  ip=$(getent ahostsv4 "$NAME" | awk 'NR==1 {print $1}')       # resolve IPv4 ใหม่
  [ -n "$ip" ] || { logger -t wg-reresolve "resolve $NAME ล้มเหลว"; exit 1; }
  wg set "$IF" peer "$PEER" endpoint "${ip}:${PORT}"            # อัปเดต endpoint สด ๆ
  logger -t wg-reresolve "อัปเดต endpoint ของ ${PEER:0:8}... เป็น ${ip}:${PORT} (age ${age}s)"
fi
# /etc/systemd/system/wg-reresolve.service
[Unit]
Description=Resolve endpoint ของ WireGuard ใหม่
[Service]
Type=oneshot
ExecStart=/usr/local/bin/wg-reresolve.sh wg0 <PEER_PUBLIC_KEY> home.example.com 51820

# /etc/systemd/system/wg-reresolve.timer
[Unit]
Description=รัน wg-reresolve ทุก 30 วินาที
[Timer]
OnBootSec=30
OnUnitActiveSec=30
[Install]
WantedBy=timers.target

# เปิดใช้:  sudo systemctl enable --now wg-reresolve.timer

ตัวอย่างการคำนวณ: ถ้าตั้ง timer ทุก 30 วินาทีและ MAX_AGE = 135 วินาที ระยะที่ต่อไม่ติดสูงสุดหลัง IP เปลี่ยน ≈ MAX_AGE + ช่วง timer ≈ 135 + 30 = 165 วินาที (~2.75 นาที)

🧪 ตัวอย่าง 9.5: ddns-update.sh, wg-reresolve.sh และ unit ของ systemd ดูในไฟล์ตัวอย่าง


ส่วนที่ 10 ข้อจำกัดและการหลบเลี่ยงการบล็อก (Limitations and Censorship Circumvention)

10.1 ข้อจำกัดของ WireGuard (WireGuard Limitations)

ข้อจำกัด อธิบาย ทางออก/ข้อควรทำ
ใช้ UDP เท่านั้น ไม่มีโหมด TCP ในตัว เครือข่ายที่บล็อก UDP (บางโรงแรม/องค์กร/ประเทศ) ใช้ไม่ได้ ลอง UDP พอร์ต 443/53; ครอบด้วย tunnel อื่น (หัวข้อ 10.2–10.3)
ไม่มี dynamic IP assignment ไม่มี DHCP-like ใน protocol ต้องกำหนด Address ของแต่ละ peer เอง ใช้สคริปต์จัดสรร IP/ฐานข้อมูล (หัวข้อ 5.15) หรือ control plane (หัวข้อ 7.2)
ไม่มี user authentication/authorization ยืนยันอุปกรณ์ด้วย key ไม่ใช่ผู้ใช้ ไม่มี MFA ใช้ control plane + SSO (หัวข้อ 7.4) และ firewall
Config อยู่ใน memory ของ kernel จัดการ peer จำนวนมากด้วยไฟล์เดียวยาก; การรีสตาร์ท interface ตัด session ทั้งหมด ใช้ wg syncconf, wg set, เครื่องมือสร้าง config อัตโนมัติ
ลายนิ้วมือชัดต่อ DPI ขนาดแพ็กเก็ต 148/92/32 byte, header 4 byte แรกคงที่, โครงสร้าง handshake เฉพาะตัว ใช้ obfuscation/wrapper (หัวข้อ 10.2)
ไม่มี cryptographic agility ถ้าพบจุดอ่อนต้องออก protocol ใหม่ วางแผนอัปเกรด; ใช้ PSK เป็นชั้นเสริม
ไม่มี log การเชื่อมต่อ ตรวจสอบย้อนหลังยาก เก็บจาก firewall/flow log (หัวข้อ 8.6)
Broadcast/multicast/L2 ไม่ผ่าน เป็น Layer 3 ซ้อน VXLAN/GRE หรือ broadcast relay (หัวข้อ 5.16)
ไม่มีการ discovery/NAT traversal ในตัว ต้องรู้ endpoint ล่วงหน้า ใช้ hub/relay หรือ mesh tool (หัวข้อ 5.7, 7.2)
Endpoint เดียวต่อ peer ไม่มี failover ระหว่างหลาย endpoint ในตัว สคริปต์ failover (หัวข้อ 5.12)

10.2 การ Obfuscation และ Tunneling ซ้อน (Obfuscation and Nested Tunneling)

10.2.1 แนวคิด

Obfuscation คือการทำให้ traffic ของ WireGuard ไม่มีลักษณะที่ระบบตรวจจับ (DPI: Deep Packet Inspection) จำแนกได้ หรือการ ห่อ (wrap) UDP ของ WireGuard ด้วยโปรโตคอลอื่นที่เครือข่ายอนุญาต เช่น fake TCP, WebSocket/HTTPS

flowchart LR
    APP["แอป (App)"] --> WG["wg0 (WireGuard)
UDP ที่เข้ารหัส"] WG --> WRAP["Wrapper ฝั่ง client
(udp2raw / wstunnel / Shadowsocks / obfs4)"] WRAP -- "ดูเหมือน TCP/HTTPS ทั่วไป
(Looks like normal TCP/HTTPS)" --> FW["Firewall / DPI"] FW --> UNWRAP["Wrapper ฝั่ง server"] UNWRAP --> WGS["wg0 ฝั่ง server
(WireGuard)"]

10.2.2 เครื่องมือที่ใช้บ่อย

เครื่องมือ วิธีทำงาน จุดเด่น ข้อเสีย
udp2raw ห่อ UDP เป็น fake TCP / ICMP / UDP พร้อมเข้ารหัสและกัน replay เร็วกว่า TCP จริง ไม่มี TCP-over-TCP ต้องใช้สิทธิ์ raw socket/iptables; ถูกตรวจจับได้ถ้า DPI ลึก
wstunnel ห่อ UDP/TCP ผ่าน WebSocket (ws/wss) บนพอร์ต 80/443 ผ่านพร็อกซี/CDN ได้; เหมือน HTTPS เป็น TCP จริง จึงเสี่ยง TCP meltdown; ภาระเพิ่ม
Shadowsocks (+ plugin) ทำ proxy เข้ารหัสสำหรับ TCP/UDP แพร่หลาย มี client มาก ต้องตั้งให้ส่ง UDP ของ WireGuard ผ่าน (UDP relay)
obfs4 / Lyrebird ทำ traffic ให้ดูเป็นสุ่ม ออกแบบมาต้านการตรวจจับ ทำงานระดับ TCP; ต้องต่อเชื่อมเอง
AmneziaWG ดัดแปลง WireGuard เอง: เติมแพ็กเก็ตขยะก่อน handshake, เปลี่ยนขนาด/header ของ message ไม่ต้องมี wrapper แยก; ยังเป็น UDP ไม่เข้ากันกับ WireGuard มาตรฐาน (ต้องใช้ทั้งสองฝั่ง)

10.2.3 ตัวอย่าง: udp2raw ครอบ WireGuard

# ----- ฝั่ง server (VPS 203.0.113.10) -----
# รับ fake-TCP ที่พอร์ต 4096 แล้วส่งต่อเป็น UDP ไปยัง WireGuard ที่ 127.0.0.1:51820
./udp2raw_amd64 -s -l0.0.0.0:4096 -r127.0.0.1:51820 -k "ChangeThisPassword" \
    --raw-mode faketcp -a

# ----- ฝั่ง client -----
# เปิด UDP ที่ 127.0.0.1:3333 รับจาก WireGuard แล้วห่อเป็น fake-TCP ส่งไป server
./udp2raw_amd64 -c -l127.0.0.1:3333 -r203.0.113.10:4096 -k "ChangeThisPassword" \
    --raw-mode faketcp -a

# -a  = ให้โปรแกรมเพิ่มกฎ iptables เองเพื่อกันไม่ให้ kernel ตอบ RST ต่อ fake-TCP
# wg0.conf ฝั่ง client: ให้ WireGuard ส่งไปที่ udp2raw ในเครื่อง แทนที่จะส่งไป server ตรง ๆ
[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.8.0.2/32
# ลด MTU เพราะมี overhead ของ wrapper เพิ่ม (ดูการคำนวณด้านล่าง)
MTU = 1280
# เพิ่ม route เฉพาะไปยัง server จริงให้ออกทาง gateway เดิม ไม่ให้ udp2raw วนเข้า tunnel (กรณี full tunnel)
PostUp   = ip route add 203.0.113.10/32 via $(ip route show default | awk '{print $3}') dev $(ip route show default | awk '{print $5}')
PostDown = ip route del 203.0.113.10/32

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0
Endpoint = 127.0.0.1:3333
PersistentKeepalive = 25

การคำนวณ MTU เมื่อมี wrapper:

MTUwg = MTUlink − Wwrap − 80

ตัวอย่าง: ถ้า Wwrap = 60 byte (ค่าสมมติ) → MTUwg = 1500 − 60 − 80 = 1360 แต่เพื่อความปลอดภัยเผื่อ overhead ที่ไม่แน่นอน จึงตั้ง 1280–1320 และวัดด้วย ping -M do -s (หัวข้อ 4.5.2)

10.2.4 ตัวอย่าง: wstunnel (WebSocket บนพอร์ต 443)

# ----- ฝั่ง server : รับ WSS ที่พอร์ต 443 (ใช้หลัง reverse proxy หรือให้ wstunnel จัดการ TLS) -----
# หมายเหตุ: รูปแบบคำสั่งของ wstunnel เปลี่ยนตามเวอร์ชัน ตรวจ  wstunnel server --help
wstunnel server wss://0.0.0.0:443

# ----- ฝั่ง client : ฟัง UDP ในเครื่อง 127.0.0.1:51821 แล้วส่งต่อผ่าน WSS ไปยัง WireGuard ที่ server (localhost:51820) -----
wstunnel client -L 'udp://127.0.0.1:51821:127.0.0.1:51820?timeout_sec=0' wss://vpn.example.com:443
# wg0.conf ฝั่ง client ให้ชี้ไปที่ตัวรับ UDP ท้องถิ่นของ wstunnel
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
AllowedIPs = 10.8.0.0/24
Endpoint = 127.0.0.1:51821
PersistentKeepalive = 25
# ต้องมี route ไปยัง vpn.example.com ผ่าน gateway เดิม (เหมือนตัวอย่าง udp2raw)

10.2.5 ตัวอย่าง: AmneziaWG

AmneziaWG เพิ่มพารามิเตอร์ใน [Interface] เพื่อเปลี่ยนลักษณะของ handshake (ใช้ได้เฉพาะเมื่อทั้งสองฝั่งใช้ AmneziaWG และค่าต้อง ตรงกัน บางตัว) ใช้เครื่องมือ awg / awg-quick แทน wg / wg-quick

# awg0.conf (ตัวอย่างพารามิเตอร์: ค่าจริงสุ่มเอง ตามเอกสารของโปรเจกต์)
[Interface]
PrivateKey = <PRIVATE_KEY>
Address = 10.8.0.2/32
Jc = 5          # จำนวนแพ็กเก็ตขยะที่ส่งก่อน handshake
Jmin = 50       # ขนาดต่ำสุดของแพ็กเก็ตขยะ (byte)
Jmax = 1000     # ขนาดสูงสุดของแพ็กเก็ตขยะ (byte)
S1 = 86         # ขนาดขยะที่เติมหน้า handshake initiation
S2 = 574        # ขนาดขยะที่เติมหน้า handshake response
H1 = 1084437620 # ค่า header ของ message type 1 (แทนที่ค่ามาตรฐาน)
H2 = 1480737130
H3 = 2108741032
H4 = 1623174710
# ค่า Jc/Jmin/Jmax ฝั่งไหนก็ได้ต่างกันได้; S1, S2, H1-H4 ต้องตรงกันทั้งสองฝั่ง

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0
Endpoint = vpn.example.com:51820
PersistentKeepalive = 25

10.2.6 ข้อดี ข้อเสีย และผลต่อประสิทธิภาพ

ประเด็น ผลกระทบ
Throughput ลดลงตามต้นทุนของ wrapper (CPU เพิ่ม, overhead ของ header) อาจลดลงสิบ–หลายสิบเปอร์เซ็นต์
Latency เพิ่มเล็กน้อยสำหรับ UDP-wrapper; เพิ่มมากกว่าสำหรับ TCP-based (WebSocket)
MTU ต้องลดลงอีก (ดูสูตรด้านบน)
ความซับซ้อน เพิ่มจุดที่อาจล้มเหลว (สองชั้น) ต้อง monitor ทั้งคู่
ความเสถียรต่อการตรวจจับ ขึ้นกับความสามารถ DPI ปลายทาง การตรวจจับพัฒนาตลอด ไม่มีวิธีใดรับประกัน

10.2.7 ข้อควรพิจารณาด้านกฎหมายและนโยบายขององค์กร

🧪 ตัวอย่าง 10.2: ไฟล์ config และคำสั่งของ udp2raw / wstunnel / AmneziaWG ดูในไฟล์ตัวอย่าง


10.3 WireGuard ผ่านพอร์ต TCP/443

10.3.1 เมื่อเครือข่ายอนุญาตเฉพาะ HTTPS

บางเครือข่ายอนุญาตเฉพาะ TCP 80/443 ทางเลือกคือ ห่อ UDP ของ WireGuard ใน TCP จริง (เช่น wstunnel, หรือ socat/proxy stream บน 443) ซึ่งผ่านได้แน่นอนกว่า fake-TCP แต่ต้องแลกกับปัญหาด้านประสิทธิภาพ

# ตัวอย่าง: ใช้ nginx stream มัลติเพล็กซ์พอร์ต 443 ระหว่าง HTTPS ปกติ กับ wstunnel ตาม SNI
# /etc/nginx/nginx.conf  (ต้องมีโมดูล stream และ ssl_preread)
stream {
    map $ssl_preread_server_name $backend {
        vpn.example.com   127.0.0.1:8443;     # ส่งให้ wstunnel
        default           127.0.0.1:9443;     # เว็บ HTTPS ปกติ
    }
    server {
        listen 443;
        ssl_preread on;                       # อ่าน SNI โดยไม่ถอดรหัส TLS
        proxy_pass $backend;
    }
}
# วิธีใช้: ให้ wstunnel ฟังที่ 127.0.0.1:8443 และเว็บฟังที่ 127.0.0.1:9443 แล้ว sudo nginx -t && sudo systemctl reload nginx

10.3.2 ข้อแลกเปลี่ยน: TCP-over-TCP Meltdown

เมื่อ TCP ซ้อนใน TCP (TCP ของแอป วิ่งใน WireGuard ซึ่งถูกห่อใน TCP ของ tunnel) ทั้งสองชั้นมีกลไก retransmission และ congestion control ของตัวเอง เมื่อเกิด packet loss:

  1. TCP ชั้นนอก (ของ tunnel) ส่งซ้ำและหน่วงข้อมูล (head-of-line blocking)
  2. TCP ชั้นใน (ของแอป) ไม่ได้รับ ACK ตามเวลา จึงเข้าใจว่าสูญหายและ ส่งซ้ำพร้อมลด window
  3. ข้อมูลที่ส่งซ้ำซ้อนทำให้ชั้นนอกยิ่งแออัด เกิดวงจรที่ทำให้ throughput ตกอย่างรวดเร็ว (meltdown) และ latency พุ่ง

ประมาณผลกระทบของ loss ต่อ TCP throughput ด้วยสูตรของ Mathis:

R ≈ MSS×8 RTT × C p

แทนค่า: MSS = 1,380 byte, RTT = 50 ms = 0.05 s, p = 1% = 0.01 → R ≈ (1,380 × 8 ÷ 0.05) × (1.22 ÷ √0.01) = 220,800 × (1.22 ÷ 0.1) = 220,800 × 12.2 ≈ 2.69 Mbit/s เมื่อใช้ TCP-over-TCP loss ที่ชั้นนอกถูกขยายและซ้ำซ้อนในชั้นใน ผลจริงมักต่ำกว่าค่านี้และมี latency สูงมาก

ข้อเสนอแนะ:

🧪 ตัวอย่าง 10.3: ไฟล์ตัวอย่าง nginx stream และสคริปต์ทดสอบ tcp-over-tcp-test.sh ดูในไฟล์ตัวอย่าง


ส่วนที่ 11 Automation และ Lab ปฏิบัติ (Automation and Hands-on Labs)

11.1 Automation

11.1.1 สคริปต์ Bash/Python สร้าง Peer

สคริปต์ที่เกี่ยวข้องในบทความนี้: add-client.sh (หัวข้อ 5.1), make-client.sh (7.3), user-lifecycle.sh (5.15), mesh-gen.py (5.6) และ provision-devices.sh (5.13) ตัวอย่างด้านล่างเป็นสคริปต์ Python ที่ จัดสรร IP อัตโนมัติ จากฐานข้อมูล JSON และสร้าง config ของ client

#!/usr/bin/env python3
"""wg-peers.py : จัดการ peer ของ WireGuard server ด้วย Python (จัดสรร IP อัตโนมัติ + สร้าง config)
วิธีใช้:
  sudo ./wg-peers.py add alice-laptop        # สร้าง peer ใหม่ พิมพ์ config ของ client
  sudo ./wg-peers.py list                    # แสดงรายการ peer
  sudo ./wg-peers.py remove alice-laptop     # ลบ peer
ตัวแปรที่ปรับได้อยู่ด้านบนของไฟล์
"""
import ipaddress, json, subprocess, sys
from pathlib import Path

IFACE = "wg0"                                   # ชื่อ interface ฝั่ง server
NETWORK = ipaddress.ip_network("10.8.0.0/24")  # tunnel network
SERVER_IP = ipaddress.ip_address("10.8.0.1")   # address ของ server (ข้ามไม่จัดสรร)
ENDPOINT = "vpn.example.com:51820"
DNS = "10.8.0.1"
DB = Path("/etc/wireguard/peers.json")          # ฐานข้อมูล: {ชื่อ: {ip, public_key}}

def run(cmd, inp=None):
    """รันคำสั่ง คืนค่า stdout (ใช้เรียก wg genkey/pubkey/genpsk/set)"""
    return subprocess.run(cmd, input=inp, text=True, capture_output=True, check=True).stdout.strip()

def load():
    return json.loads(DB.read_text()) if DB.exists() else {}

def save(db):
    DB.write_text(json.dumps(db, indent=2)); DB.chmod(0o600)

def next_ip(db):
    """หา IP ว่างตัวแรกใน NETWORK ที่ยังไม่ถูกใช้ (ไม่นับ network/broadcast/server)"""
    used = {ipaddress.ip_address(v["ip"]) for v in db.values()} | {SERVER_IP}
    for host in NETWORK.hosts():
        if host not in used:
            return host
    raise SystemExit("IP หมดแล้ว: ขยาย NETWORK")

def add(name):
    db = load()
    if name in db: raise SystemExit(f"มี {name} อยู่แล้ว")
    ip = next_ip(db)
    key = run(["wg", "genkey"]); pub = run(["wg", "pubkey"], key); psk = run(["wg", "genpsk"])
    # เพิ่ม peer แบบสด (ไม่ตัด session ของผู้อื่น) โดยส่ง PSK ผ่านไฟล์ชั่วคราวใน memory (/dev/stdin)
    subprocess.run(["wg", "set", IFACE, "peer", pub, "preshared-key", "/dev/stdin",
                    "allowed-ips", f"{ip}/32"], input=psk, text=True, check=True)
    db[name] = {"ip": str(ip), "public_key": pub}; save(db)
    server_pub = run(["wg", "show", IFACE, "public-key"])
    print(f"""# ---- config ของ {name} (ส่งผ่านช่องทางปลอดภัย) ----
[Interface]
PrivateKey = {key}
Address = {ip}/32
DNS = {DNS}

[Peer]
PublicKey = {server_pub}
PresharedKey = {psk}
AllowedIPs = {NETWORK}
Endpoint = {ENDPOINT}
PersistentKeepalive = 25
# ---- เพิ่มบล็อกนี้ใน /etc/wireguard/{IFACE}.conf ของ server เพื่อให้คงอยู่หลังรีสตาร์ท ----
# [Peer]
# # {name}
# PublicKey = {pub}
# PresharedKey = {psk}
# AllowedIPs = {ip}/32""")

def remove(name):
    db = load()
    if name not in db: raise SystemExit(f"ไม่พบ {name}")
    run(["wg", "set", IFACE, "peer", db[name]["public_key"], "remove"])
    del db[name]; save(db); print(f"ลบ {name} แล้ว (อย่าลืมลบจากไฟล์ {IFACE}.conf)")

if __name__ == "__main__":
    cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
    if cmd == "add": add(sys.argv[2])
    elif cmd == "remove": remove(sys.argv[2])
    else:
        for n, v in load().items(): print(f"{n:24} {v['ip']:16} {v['public_key'][:12]}...")

11.1.2 Ansible Role สำหรับ Server และ Client

โครงสร้างโฟลเดอร์ของ role:

ansible/
├── inventory.ini
├── site.yml
├── group_vars/all.yml                 # ตัวแปรร่วม (ไม่ใส่ secret)
└── roles/wireguard/
    ├── tasks/main.yml
    ├── handlers/main.yml
    └── templates/wg0.conf.j2
# inventory.ini
[wg_server]
vpn1 ansible_host=203.0.113.10 wg_address=10.8.0.1/24 wg_listen_port=51820

[wg_clients]
laptop1 ansible_host=198.51.100.50 wg_address=10.8.0.10/32
# roles/wireguard/tasks/main.yml : ติดตั้งและตั้งค่า WireGuard (ใช้ได้ทั้ง server และ client)
- name: ติดตั้งแพ็กเกจ WireGuard (Debian/Ubuntu)
  ansible.builtin.apt:
    name: [wireguard, wireguard-tools]
    update_cache: true
  when: ansible_os_family == "Debian"

- name: ติดตั้งแพ็กเกจ WireGuard (RedHat family)
  ansible.builtin.dnf:
    name: wireguard-tools
  when: ansible_os_family == "RedHat"

- name: ตรวจว่ามี private key แล้วหรือยัง
  ansible.builtin.stat:
    path: /etc/wireguard/private.key
  register: wg_key

- name: สร้าง private key ครั้งแรก (ไม่สร้างซ้ำ จึง idempotent)
  ansible.builtin.shell: umask 077 && wg genkey > /etc/wireguard/private.key && wg pubkey < /etc/wireguard/private.key > /etc/wireguard/public.key
  when: not wg_key.stat.exists

- name: อ่าน private key เข้าตัวแปร (no_log กันค่าหลุดใน log)
  ansible.builtin.slurp:
    src: /etc/wireguard/private.key
  register: wg_private
  no_log: true

- name: เปิด IP forwarding (เฉพาะ server)
  ansible.posix.sysctl:
    name: net.ipv4.ip_forward
    value: "1"
    sysctl_file: /etc/sysctl.d/99-wireguard.conf
    reload: true
  when: inventory_hostname in groups['wg_server']

- name: เรนเดอร์ไฟล์ config จากเทมเพลต
  ansible.builtin.template:
    src: wg0.conf.j2
    dest: /etc/wireguard/wg0.conf
    owner: root
    group: root
    mode: "0600"
  notify: reload wireguard

- name: เปิดใช้และเริ่ม wg-quick@wg0
  ansible.builtin.systemd:
    name: wg-quick@wg0
    enabled: true
    state: started
# roles/wireguard/handlers/main.yml
- name: reload wireguard
  # โหลด config ใหม่โดยไม่ตัด tunnel ที่ใช้งานอยู่
  ansible.builtin.shell: wg syncconf wg0 <(wg-quick strip wg0)
  args:
    executable: /bin/bash
{# roles/wireguard/templates/wg0.conf.j2 : เทมเพลต config #}
[Interface]
PrivateKey = {{ wg_private.content | b64decode | trim }}
Address = {{ wg_address }}
{% if wg_listen_port is defined %}ListenPort = {{ wg_listen_port }}{% endif %}

{% if inventory_hostname in groups['wg_server'] %}
{% for c in groups['wg_clients'] %}
[Peer]
# {{ c }}
PublicKey = {{ hostvars[c].wg_public_key }}
AllowedIPs = {{ hostvars[c].wg_address | regex_replace('/\d+$', '/32') }}
{% endfor %}
{% else %}
[Peer]
PublicKey = {{ hostvars[groups['wg_server'][0]].wg_public_key }}
AllowedIPs = 10.8.0.0/24
Endpoint = {{ hostvars[groups['wg_server'][0]].ansible_host }}:{{ hostvars[groups['wg_server'][0]].wg_listen_port }}
PersistentKeepalive = 25
{% endif %}
# วิธีใช้
# หมายเหตุ: ตัวแปร wg_public_key ของแต่ละเครื่องต้องรวบรวมก่อน (เช่น task แยกที่อ่าน /etc/wireguard/public.key
#           แล้ว set_fact) หรือเก็บใน group_vars ที่เข้ารหัสด้วย ansible-vault / SOPS
ansible-playbook -i inventory.ini site.yml --check --diff     # ทดลองแบบไม่เปลี่ยนจริง
ansible-playbook -i inventory.ini site.yml

11.1.3 Terraform: Provision VPS + Config

ตัวอย่างสร้าง VM บน AWS พร้อม security group และ cloud-init (ดูไฟล์ cloud-init.yaml ในหัวข้อ 6.5.1)

# main.tf : สร้าง WireGuard server บน AWS (ค่าทั้งหมดเป็นตัวอย่าง)
terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" {
  region = "ap-southeast-1"
}

variable "ssh_key_name" { type = string }                      # ชื่อ key pair ที่มีอยู่ใน AWS

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]                               # Canonical
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
  }
}

resource "aws_security_group" "wg" {
  name        = "wireguard"
  description = "WireGuard UDP 51820 + SSH"
  ingress {
    from_port   = 51820
    to_port     = 51820
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {                                                    # SSH: จำกัดเฉพาะ IP ผู้ดูแล
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["198.51.100.0/24"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "wg" {
  ami                    = data.aws_ami.ubuntu.id
  instance_type          = "t3.micro"
  key_name               = var.ssh_key_name
  vpc_security_group_ids = [aws_security_group.wg.id]
  source_dest_check      = false                               # จำเป็นเมื่อ VM ต้อง forward แพ็กเก็ตของเครื่องอื่น
  user_data              = file("${path.module}/cloud-init.yaml")
  tags                   = { Name = "wireguard" }
}

resource "aws_eip" "wg" {
  instance = aws_instance.wg.id                                # IP คงที่ ไม่เปลี่ยนเมื่อ stop/start
}

output "endpoint" {
  value = "${aws_eip.wg.public_ip}:51820"
}
# วิธีใช้
terraform init
terraform plan  -var ssh_key_name=my-key
terraform apply -var ssh_key_name=my-key
terraform output endpoint            # ใช้เป็น Endpoint ใน config ของ client
terraform destroy -var ssh_key_name=my-key   # ลบทั้งหมดเมื่อเลิกใช้ (หยุดค่าใช้จ่าย)

11.1.4 GitOps สำหรับ Config พร้อมวิธีเก็บ Secret

แนวคิด: เก็บ config/เทมเพลตใน Git เป็น source of truth แต่ ห้ามเก็บ private key แบบเปิด ใช้ SOPS + age เข้ารหัสไฟล์ secret แล้ว commit ได้อย่างปลอดภัย

# 1) สร้างคู่กุญแจ age (เก็บ private key ของ age ไว้ที่ปลอดภัย ไม่ใส่ใน Git)
age-keygen -o ~/.config/sops/age/keys.txt
# บรรทัด "public key: age1..." ใช้ในขั้นต่อไป

# 2) กำหนดกฎเข้ารหัสใน .sops.yaml (commit ได้)
cat > .sops.yaml <<'EOF'
creation_rules:
  - path_regex: secrets/.*\.yaml$
    age: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
EOF

# 3) สร้างไฟล์ secret แล้วเข้ารหัสในที่เดิม
mkdir -p secrets
cat > secrets/vpn1.yaml <<'EOF'
wg_private_key: <SERVER_PRIVATE_KEY>
peers:
  alice-laptop: { psk: <PSK_ALICE> }
EOF
sops --encrypt --in-place secrets/vpn1.yaml     # ตอนนี้ไฟล์ commit เข้า Git ได้ (ค่าเข้ารหัสแล้ว)

# 4) ตอน deploy: ถอดรหัสใน memory แล้วเรนเดอร์ (ไม่เขียน plaintext ลง repo)
export WG_PRIVATE_KEY="$(sops --decrypt --extract '["wg_private_key"]' secrets/vpn1.yaml)"
envsubst '${WG_PRIVATE_KEY}' < templates/wg0.conf.tmpl | sudo tee /etc/wireguard/wg0.conf >/dev/null
sudo chmod 600 /etc/wireguard/wg0.conf && unset WG_PRIVATE_KEY
flowchart LR
    DEV["ผู้ดูแล แก้ config ใน Git
(Admin edits config)"] --> PR["Pull request + review"] PR --> MAIN["merge เข้า main
(Merge)"] MAIN --> CI["CI/CD pipeline
ตรวจ syntax, wg-quick strip"] CI --> DEC["ถอดรหัส secret ด้วย SOPS/age
(Decrypt secrets in memory)"] DEC --> DEP["Ansible deploy + wg syncconf"] DEP --> MON["Monitoring ตรวจ handshake
(Verify)"]

🧪 ตัวอย่าง 11.1: wg-peers.py, role ของ Ansible ครบชุด, main.tf และ .sops.yaml ดูในไฟล์ตัวอย่าง


11.2 แล็บปฏิบัติ (Hands-on Labs)

Lab หัวข้อ ผลลัพธ์ที่คาดหวัง
Lab 1 Tunnel แบบ Point-to-Point ระหว่าง 2 เครื่อง ping ผ่าน tunnel ได้
Lab 2 Road Warrior + Full Tunnel + Kill switch traffic ออกผ่าน server, ไม่ leak
Lab 3 Site-to-Site เชื่อม 2 LAN host คนละ LAN คุยกันได้
Lab 4 Hub-and-Spoke พร้อม firewall policy จำกัดสิทธิ์ระหว่าง spoke ได้
Lab 5 เข้า Home Lab หลัง CGNAT ผ่าน VPS เข้าบริการที่บ้านจากภายนอกได้
Lab 6 Capture handshake ด้วย Wireshark วิเคราะห์ message ทั้ง 3 ชนิดได้
Lab 7 Monitoring ด้วย Prometheus + Grafana dashboard แสดงสถานะ peer
Lab 8 Troubleshooting: หาจุดผิดจาก config ที่ใส่บั๊กไว้ แก้ปัญหาได้ครบทุกข้อ

ข้อกำหนดเบื้องต้นของทุก Lab: เครื่อง Linux (VM ก็ได้) ที่ใช้ kernel 5.6 ขึ้นไป หรือมีโมดูล WireGuard, สิทธิ์ root, แพ็กเกจ wireguard-tools iproute2 iptables tcpdump สคริปต์ทั้งหมดอยู่ในไฟล์ wireguard-sample-data.md (ส่วนที่ 11) และใช้ network namespace จึงไม่ต้องมีเครื่องจริงหลายเครื่อง สคริปต์ลบแล็บ (down) ลบเฉพาะ namespace และกุญแจชั่วคราวของแล็บ ไม่แตะ config จริงของเครื่องท่าน

11.2.1 Lab 1: Point-to-Point

11.2.2 Lab 2: Road Warrior + Full Tunnel + Kill Switch

flowchart LR
    C["client
wg0 10.8.0.30
veth 198.51.100.2"] --- S["server (VPN + NAT)
veth 198.51.100.1
veth 203.0.113.1"] S --- I["inet (อินเทอร์เน็ตจำลอง)
203.0.113.2
เว็บ 192.0.2.80"]

11.2.3 Lab 3: Site-to-Site

11.2.4 Lab 4: Hub-and-Spoke พร้อม Firewall Policy

11.2.5 Lab 5: Home Lab หลัง CGNAT ผ่าน VPS

11.2.6 Lab 6: Capture Handshake ด้วย Wireshark

11.2.7 Lab 7: Monitoring ด้วย Prometheus + Grafana

flowchart LR
    WG["wg show all dump"] --> TXT["wg-textfile.sh
(textfile collector)"] TXT --> NE["node_exporter :9100"] NE --> PR["Prometheus :9090
+ กฎแจ้งเตือน (Alert rules)"] PR --> GF["Grafana :3000
Dashboard"]

11.2.8 Lab 8: Troubleshooting (บั๊ก 6 จุด)

บั๊ก อาการที่เห็น สาเหตุ วิธีแก้
1 ไม่มี handshake; dmesg มี Invalid MAC client ใช้ public key ผิด ใส่ public key ของ server ที่ถูกต้อง: wg set wg0 peer <ผิด> remove แล้วเพิ่มใหม่
2 ไม่มี handshake; tcpdump ที่ server ไม่เห็นแพ็กเก็ตเข้าพอร์ต 51820 Endpoint ใช้พอร์ต 51821 wg set wg0 peer <KEY> endpoint 198.51.100.1:51820
3 มี handshake แต่ ping 10.8.0.1 ไม่ผ่าน; server ทิ้งแพ็กเก็ตเพราะ src ไม่อยู่ใน AllowedIPs AllowedIPs ของ client ที่ server เป็น 10.8.0.3/32 ไม่ตรงกับ IP จริง 10.8.0.2 wg set wg0 peer <CLIENT> allowed-ips 10.8.0.2/32 (บน server)
4 ping 10.8.0.1 ผ่าน แต่ LAN 192.168.30.10 ไม่ผ่าน server ไม่เปิด ip_forward sysctl -w net.ipv4.ip_forward=1 บน server
5 ping 10.8.0.1 ผ่าน แต่แพ็กเก็ตไป LAN ไม่ถูกส่งเข้า tunnel (ไม่เห็น sent เพิ่ม) AllowedIPs ของ server บน client ไม่รวม 192.168.30.0/24 wg set wg0 peer <SERVER> allowed-ips 10.8.0.0/24,192.168.30.0/24
6 ping LAN ไม่ผ่าน; tcpdump -ni eth0 ที่ lan เห็นคำขอแต่ไม่มีคำตอบกลับไปทางถูก LAN ไม่มี route กลับ 10.8.0.0/24 และ server ไม่ทำ NAT เพิ่ม NAT: iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o lan-s -j MASQUERADE หรือ route กลับบน lan

แบบฝึกหัดเพิ่ม (MTU): รัน Lab 1 แล้วตั้ง MTU ของ veth ทั้งสองเป็น 1400 (ip -n a link set veth-a mtu 1400) แล้วทดสอบ ping -M do -s 1300 ผ่าน tunnel สังเกตผล และหา MTU ที่เหมาะสมด้วยสูตรหัวข้อ 4.5

🧪 ตัวอย่าง 11.2: สคริปต์ครบทั้ง 8 Lab และไฟล์ compose ของ Lab 7 ดูในไฟล์ตัวอย่าง


11.3 แนวทางจัดทำ Lab ด้วย Network Namespace

11.3.1 จำลองหลายเครื่องบนเครื่องเดียวด้วย ip netns

Network namespace คือการแยก network stack (interface, route, firewall, socket) ออกเป็นหลายชุดบน kernel เดียว เหมือนมีหลายเครื่องเสมือน แต่เบากว่า VM มาก WireGuard เหมาะกับการทำแล็บแบบนี้เพราะสร้าง interface ภายใน namespace ได้โดยตรง

flowchart LR
    subgraph HOST["เครื่องจริงเครื่องเดียว (Single host)"]
        subgraph NSA["namespace a (Node A)"]
            A1["veth-a 198.51.100.1"]
            A2["wg0 10.8.0.1"]
        end
        subgraph NSB["namespace b (Node B)"]
            B1["veth-b 198.51.100.2"]
            B2["wg0 10.8.0.2"]
        end
        A1 --- B1
    end

คำสั่งพื้นฐาน:

sudo ip netns add demo                       # สร้าง namespace
sudo ip netns list                           # แสดงรายการ
sudo ip netns exec demo ip addr              # รันคำสั่งใน namespace
sudo ip link add v0 type veth peer name v1   # สร้างคู่ veth (เหมือนสายแลนเสมือน)
sudo ip link set v1 netns demo               # ย้ายปลายหนึ่งเข้า namespace
sudo ip netns del demo                       # ลบ namespace (interface ภายในหายไปด้วย)

11.3.2 สคริปต์ Lab ฉบับเต็ม (Library และ Lab 1)

lab-common.sh : ฟังก์ชันช่วย (ใช้ร่วมกันทุก Lab)

#!/usr/bin/env bash
# lab-common.sh : ฟังก์ชันช่วยสำหรับแล็บ WireGuard ด้วย network namespace
# วิธีใช้: ไฟล์นี้ถูก source จากสคริปต์ lab อื่น  (source ./lab-common.sh)
# ต้องการ: root, iproute2, wireguard-tools, iptables ; kernel ที่มี WireGuard (5.6+)
set -euo pipefail

KEYDIR=${KEYDIR:-/tmp/wglab-keys}      # ที่เก็บกุญแจชั่วคราวของแล็บ (ลบเมื่อ down)

need_root() { [ "$(id -u)" -eq 0 ] || { echo "ต้องรันด้วย root (sudo)"; exit 1; }; }
need_tools() { for t in ip wg ping iptables; do command -v "$t" >/dev/null || { echo "ไม่พบคำสั่ง $t"; exit 1; }; done; }

# สร้าง namespace: ns_add a b c
ns_add() { for n in "$@"; do ip netns add "$n"; ip -n "$n" link set lo up; done; }
# ลบ namespace (ไม่ error ถ้าไม่มี): ns_del a b c
ns_del() { for n in "$@"; do ip netns del "$n" 2>/dev/null || true; done; }

# เชื่อมสอง namespace ด้วย veth:  veth_link ns1 if1 192.0.2.1/24 ns2 if2 192.0.2.2/24
veth_link() {
  local n1=$1 i1=$2 a1=$3 n2=$4 i2=$5 a2=$6
  ip link add "$i1" type veth peer name "$i2"
  ip link set "$i1" netns "$n1"; ip link set "$i2" netns "$n2"
  ip -n "$n1" addr add "$a1" dev "$i1"; ip -n "$n2" addr add "$a2" dev "$i2"
  ip -n "$n1" link set "$i1" up;        ip -n "$n2" link set "$i2" up
}

# สร้างคู่กุญแจ: gen_keys name  ->  $KEYDIR/name.key และ name.pub
gen_keys() {
  umask 077; mkdir -p "$KEYDIR"
  for n in "$@"; do wg genkey | tee "$KEYDIR/$n.key" | wg pubkey > "$KEYDIR/$n.pub"; done
}
pub() { cat "$KEYDIR/$1.pub"; }

# สร้าง interface WireGuard ใน namespace:  wg_if ns wg0 10.8.0.1/24 keyname [listen-port] [mtu]
wg_if() {
  local ns=$1 ifn=$2 addr=$3 key=$4 port=${5:-} mtu=${6:-}
  ip -n "$ns" link add "$ifn" type wireguard
  if [ -n "$port" ]; then
    ip netns exec "$ns" wg set "$ifn" private-key "$KEYDIR/$key.key" listen-port "$port"
  else
    ip netns exec "$ns" wg set "$ifn" private-key "$KEYDIR/$key.key"
  fi
  ip -n "$ns" addr add "$addr" dev "$ifn"
  [ -n "$mtu" ] && ip -n "$ns" link set "$ifn" mtu "$mtu"
  ip -n "$ns" link set "$ifn" up
}

# เพิ่ม peer:  wg_peer ns wg0 peerkeyname allowed-ips [endpoint] [keepalive]
wg_peer() {
  local ns=$1 ifn=$2 peer=$3 allowed=$4 ep=${5:-} ka=${6:-}
  local args=(peer "$(pub "$peer")" allowed-ips "$allowed")
  [ -n "$ep" ] && args+=(endpoint "$ep")
  [ -n "$ka" ] && args+=(persistent-keepalive "$ka")
  ip netns exec "$ns" wg set "$ifn" "${args[@]}"
}

# รันคำสั่งใน namespace:  nsx ns cmd...
nsx() { local ns=$1; shift; ip netns exec "$ns" "$@"; }

# แสดงผลทดสอบแบบ PASS/FAIL:  check "คำอธิบาย" คำสั่ง...
check() {
  local desc=$1; shift
  if "$@" >/dev/null 2>&1; then echo "  [PASS] $desc"; else echo "  [FAIL] $desc"; fi
}

lab1-p2p.sh : Lab 1

#!/usr/bin/env bash
# lab1-p2p.sh : Lab 1 -- Tunnel แบบ Point-to-Point ระหว่าง 2 เครื่อง (จำลองด้วย namespace a และ b)
# วิธีใช้:  sudo ./lab1-p2p.sh up     # สร้างแล็บและทดสอบ
#           sudo ./lab1-p2p.sh down   # ลบแล็บทั้งหมด
# ผลที่คาดหวัง: ping 10.8.0.2 จาก a ผ่านได้ และ wg show มี latest handshake
source "$(dirname "$0")/lab-common.sh"
need_root; need_tools

case "${1:-up}" in
  up)
    ns_del a b; ns_add a b
    # เครือข่ายด้านล่าง (underlay) 198.51.100.0/24 แทนอินเทอร์เน็ตระหว่างสองเครื่อง
    veth_link a veth-a 198.51.100.1/24 b veth-b 198.51.100.2/24
    gen_keys a b
    # Node A = ฝั่ง server (listen 51820)  Node B = ฝั่ง client (ตั้ง endpoint ไปหา A)
    wg_if a wg0 10.8.0.1/24 a 51820
    wg_if b wg0 10.8.0.2/24 b
    wg_peer a wg0 b 10.8.0.2/32
    wg_peer b wg0 a 10.8.0.1/32 198.51.100.1:51820 25
    echo "== ทดสอบ =="
    nsx b ping -c 3 -W 2 10.8.0.1 || true
    check "B ping A ผ่าน tunnel"  nsx b ping -c 1 -W 2 10.8.0.1
    check "A ping B ผ่าน tunnel"  nsx a ping -c 1 -W 2 10.8.0.2
    echo "== wg show (ฝั่ง B) =="; nsx b wg show
    ;;
  down)
    ns_del a b; rm -rf "$KEYDIR"; echo "ลบแล็บแล้ว" ;;
  *) echo "ใช้: $0 up|down"; exit 1 ;;
esac

วิธีใช้:

chmod +x lab-common.sh lab1-p2p.sh
sudo ./lab1-p2p.sh up
# ผลลัพธ์ที่คาดหวัง:
#   == ทดสอบ ==
#     [PASS] B ping A ผ่าน tunnel
#     [PASS] A ping B ผ่าน tunnel
#   == wg show (ฝั่ง B) ==
#   interface: wg0 ... peer: ... latest handshake: N seconds ago
sudo ./lab1-p2p.sh down      # ลบแล็บเมื่อเสร็จ

11.3.3 ใช้ร่วมกับ Docker, Containerlab และ Vagrant

เครื่องมือ เหมาะกับ หมายเหตุ
ip netns แล็บเร็ว เบา ใช้ใน CI ต้อง root และ kernel รองรับ WireGuard
Docker Compose แล็บที่ต้องมีบริการหลายตัว (DNS, web, Prometheus) ต้องใช้ cap_add: NET_ADMIN และ network ที่แยกกัน
Containerlab จำลอง topology เครือข่ายด้วย container และกำหนดลิงก์ด้วยไฟล์ YAML เหมาะกับแล็บเชิงเครือข่ายที่ซับซ้อน
Vagrant แล็บที่ต้องการ kernel/OS จริงหลายเครื่อง (VM) ใช้ทรัพยากรมากกว่า แต่ใกล้เคียงของจริงที่สุด
# Vagrantfile : VM สองเครื่อง (server, client) พร้อมติดตั้ง WireGuard ตอน provision
Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/jammy64"
  config.vm.provision "shell", inline: "apt-get update && apt-get install -y wireguard tcpdump iperf3"

  config.vm.define "server" do |s|
    s.vm.hostname = "wg-server"
    s.vm.network "private_network", ip: "192.168.56.10"     # underlay ระหว่าง VM
  end
  config.vm.define "client" do |c|
    c.vm.hostname = "wg-client"
    c.vm.network "private_network", ip: "192.168.56.11"
  end
end
# วิธีใช้:  vagrant up  &&  vagrant ssh server   แล้วทำตามหัวข้อ 3.3 โดยใช้ Endpoint = 192.168.56.10:51820
#           vagrant destroy -f   เมื่อเสร็จ
# containerlab topology (เชิงโครงสร้าง) : สองโหนด Linux ต่อกันด้วยลิงก์เดียว
# ตรวจรูปแบบล่าสุดจากเอกสารของ containerlab ก่อนใช้
name: wg-lab
topology:
  nodes:
    server:
      kind: linux
      image: lscr.io/linuxserver/wireguard:latest
      cap-add: [NET_ADMIN]
    client:
      kind: linux
      image: lscr.io/linuxserver/wireguard:latest
      cap-add: [NET_ADMIN]
  links:
    - endpoints: ["server:eth1", "client:eth1"]
# วิธีใช้:  sudo containerlab deploy -t wg-lab.clab.yml ; sudo containerlab destroy -t wg-lab.clab.yml

🧪 ตัวอย่าง 11.3: lab-common.sh, Vagrantfile และ topology ของ containerlab ดูในไฟล์ตัวอย่าง


ภาคผนวก (Appendices)

ก. Cheat Sheet คำสั่งที่ใช้บ่อย (Command Cheat Sheet)

ก.1 คำสั่ง wg

งาน คำสั่ง
สร้าง private key / public key / PSK wg genkey / wg pubkey / wg genpsk
ดูสถานะทั้งหมด sudo wg show
ดูเฉพาะ interface sudo wg show wg0
ดู handshake ล่าสุด (epoch) sudo wg show wg0 latest-handshakes
ดู endpoint ของแต่ละ peer sudo wg show wg0 endpoints
ดู transfer sudo wg show wg0 transfer
รายการ peer ที่ parse ได้ง่าย sudo wg show wg0 dump
เพิ่ม/แก้ peer สด sudo wg set wg0 peer <PUB> allowed-ips 10.8.0.5/32 endpoint 1.2.3.4:51820
ลบ peer sudo wg set wg0 peer <PUB> remove
ตั้ง PSK sudo wg set wg0 peer <PUB> preshared-key /path/psk
โหลด config ใหม่ไม่ตัด session sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
แสดง config ปัจจุบัน sudo wg showconf wg0

ก.2 คำสั่ง wg-quick และ systemd

งาน คำสั่ง
เปิด / ปิด tunnel sudo wg-quick up wg0 / sudo wg-quick down wg0
ตัดส่วนขยายของ wg-quick ออก wg-quick strip wg0
เริ่มอัตโนมัติ sudo systemctl enable --now wg-quick@wg0
ดู log journalctl -u wg-quick@wg0 -e

ก.3 คำสั่ง ip

งาน คำสั่ง
สร้าง interface sudo ip link add dev wg0 type wireguard
กำหนด address sudo ip address add 10.8.0.2/24 dev wg0
เปิด / ปิด interface sudo ip link set up dev wg0 / sudo ip link set down dev wg0
ตั้ง MTU sudo ip link set mtu 1420 dev wg0
เพิ่ม route sudo ip route add 192.168.20.0/24 dev wg0
ดูเส้นทางที่จะใช้จริง ip route get 192.168.20.5
ดู policy rule / ตาราง ip rule show / ip route show table 51820
ลบ interface sudo ip link del dev wg0

ก.4 คำสั่ง nft / iptables

งาน nftables iptables
แสดงกฎทั้งหมด sudo nft list ruleset sudo iptables -S ; sudo iptables -t nat -S
เปิดพอร์ต UDP 51820 nft add rule inet filter input udp dport 51820 accept iptables -A INPUT -p udp --dport 51820 -j ACCEPT
Masquerade ip saddr 10.8.0.0/24 oifname "eth0" masquerade (ตาราง nat) iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
MSS clamp tcp flags syn tcp option maxseg size set rt mtu iptables -t mangle -A FORWARD -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

ก.5 คำสั่ง tcpdump

งาน คำสั่ง
จับ UDP ของ WireGuard sudo tcpdump -ni eth0 'udp port 51820'
จับ traffic ใน tunnel sudo tcpdump -ni wg0
บันทึกไฟล์ sudo tcpdump -ni eth0 'udp port 51820' -w wg.pcap
เฉพาะ handshake initiation sudo tcpdump -ni eth0 'udp port 51820 and udp[8] == 1'

ข. Config Template สำเร็จรูป (Ready-made Config Templates)

แทนค่าใน <...> และปรับ IP ให้ตรงกับเครือข่ายของท่าน ไฟล์พร้อมใช้ฉบับเต็มอยู่ใน wireguard-sample-data.md (ภาคผนวก ข)

ข.1 Remote Access (Server)

[Interface]
PrivateKey = <SERVER_PRIVATE_KEY>
Address = 10.8.0.1/24
ListenPort = 51820
PostUp   = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE; iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE; iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT

[Peer]
# <CLIENT_NAME>
PublicKey = <CLIENT_PUBLIC_KEY>
PresharedKey = <CLIENT_PSK>
AllowedIPs = 10.8.0.2/32

ข.2 Site-to-Site (Gateway)

[Interface]
PrivateKey = <GATEWAY_PRIVATE_KEY>
Address = 10.200.0.1/30
ListenPort = 51820
PostUp   = sysctl -w net.ipv4.ip_forward=1
[Peer]
PublicKey = <REMOTE_GATEWAY_PUBLIC_KEY>
PresharedKey = <SITE_PSK>
AllowedIPs = 10.200.0.2/32, <REMOTE_LAN_CIDR>
Endpoint = <REMOTE_PUBLIC_IP_OR_DDNS>:51820
PersistentKeepalive = 25

ข.3 Hub-and-Spoke (Hub)

[Interface]
PrivateKey = <HUB_PRIVATE_KEY>
Address = 10.9.0.1/24
ListenPort = 51820
PostUp   = sysctl -w net.ipv4.ip_forward=1; iptables -A FORWARD -i %i -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o %i -j ACCEPT
[Peer]
# spoke-1
PublicKey = <SPOKE1_PUBLIC_KEY>
AllowedIPs = 10.9.0.2/32, <SPOKE1_LAN_CIDR>

ข.4 Full Tunnel + Kill Switch (Client)

[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY>
Address = 10.8.0.30/32, fd42:8:0::30/128
DNS = 10.8.0.1
PostUp   = iptables  -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PostUp   = ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown  = iptables  -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown  = ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
PresharedKey = <CLIENT_PSK>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = <SERVER_HOST>:51820
PersistentKeepalive = 25

ค. ตารางวางแผน Addressing (Addressing Plan)

ค.1 ขนาด Subnet สำหรับ Tunnel Network

จำนวน address ที่ใช้งานได้ H = 2(32−p) − 2 (สูตรในหัวข้อ 3.3.2)

Prefix Netmask Host ที่ใช้ได้ ใช้กับ
/30 255.255.255.252 2 Site-to-site สองจุด
/29 255.255.255.248 6 ทีมเล็ก / lab
/28 255.255.255.240 14 ผู้ดูแลระบบ
/27 255.255.255.224 30 ทีมย่อย
/26 255.255.255.192 62 แผนก
/25 255.255.255.128 126 องค์กรเล็ก
/24 255.255.255.0 254 มาตรฐาน
/23 255.255.254.0 510 องค์กรกลาง
/22 255.255.252.0 1,022 อุปกรณ์ IoT จำนวนมาก
/16 255.255.0.0 65,534 เครือข่ายขนาดใหญ่

ค.2 การแบ่ง Subnet ย่อยจากบล็อกใหญ่

จำนวน subnet ขนาด /s ที่แบ่งได้จากบล็อก /p (เมื่อ s ≥ p):

Nsubnets = 2s−p

ตัวอย่างการคำนวณ: แบ่ง 10.60.0.0/16 เป็น /24 → N = 224−16 = 28 = 256 subnet (แต่ละ subnet ใช้ได้ 254 host); แบ่งเป็น /28 → N = 228−16 = 4,096 subnet (14 host ต่อ subnet)

ค.3 ตัวอย่างแผนการแบ่ง 10.60.0.0/16

Subnet ใช้งาน จำนวน host ใช้ได้
10.60.0.0/28 Admin 14
10.60.1.0/24 Engineering 254
10.60.2.0/24 Finance 254
10.60.3.0/24 HR 254
10.60.9.0/24 Contractors 254
10.60.20.0/24 IoT 254
10.60.100.0/22 สำรองสำหรับขยาย 1,022

ค.4 ช่วง Address ที่แนะนำและที่ควรเลี่ยง

ช่วง ข้อแนะนำ
192.168.0.0/24, 192.168.1.0/24, 10.0.0.0/24, 10.0.1.0/24 เลี่ยง: ชนกับ router บ้านและ Wi-Fi สาธารณะบ่อย
172.17.0.0/16 เลี่ยง: Docker bridge ค่าเริ่มต้น
100.64.0.0/10 เลี่ยง: CGNAT (และ Tailscale ใช้ช่วงนี้)
10.8.0.0/24, 10.77.0.0/24, 10.200.0.0/16, 172.30.0.0/16 เหมาะ: ไม่ค่อยถูกใช้
fd00::/8 (ULA) เหมาะ สำหรับ IPv6 ภายใน: สร้าง prefix /48 แบบสุ่มตาม RFC 4193

ง. เปรียบเทียบเครื่องมือจัดการ (Management Tools Comparison)

ประเด็น WireGuard ล้วน wg-easy Tailscale Headscale Netbird Netmaker Firezone
ลักษณะ Protocol + CLI Web UI สำหรับ server Mesh VPN (SaaS) Control server โอเพนซอร์ส (self-host) สำหรับ client Tailscale Mesh VPN (cloud/self-host) Mesh/site manager (self-host) Gateway + policy (SSO)
โทโพโลยี ตามที่ออกแบบ Hub-and-spoke Mesh Mesh Mesh Mesh / hub Client ↔ Gateway
NAT traversal ไม่มี ไม่มี มี + DERP relay มี (ใช้ DERP) มี + relay มี ผ่าน gateway
User auth / SSO ไม่มี รหัสผ่าน UI มี (OIDC/SAML) OIDC (ตามการตั้งค่า) มี (OIDC) มี มี (OIDC)
ACL กลาง ไม่มี (ใช้ firewall) จำกัด มี มี มี มี มี
ความเรียบง่ายในการติดตั้ง ปานกลาง ง่ายมาก ง่ายมาก ปานกลาง ง่าย–ปานกลาง ปานกลาง–ยาก ปานกลาง
การพึ่งพาบริการภายนอก ไม่ ไม่ ใช่ (SaaS) ไม่ เลือกได้ ไม่ เลือกได้
เหมาะกับ ควบคุมเต็ม โครงสร้างเล็ก Home lab / ทีมเล็ก ทีม/บุคคลที่ต้องการง่ายและเร็ว ต้องการ Tailscale แบบ self-host ทีมที่ต้องการโอเพนซอร์ส + SSO ผู้ดูแลที่ต้องการควบคุมระดับ network แทน VPN ขององค์กร

หมายเหตุ: ฟีเจอร์ของผลิตภัณฑ์เปลี่ยนเร็ว ตรวจเอกสารล่าสุดของแต่ละเครื่องมือก่อนตัดสินใจ


จ. อภิธานศัพท์ (Glossary)

ศัพท์ ความหมาย
AEAD (Authenticated Encryption with Associated Data) การเข้ารหัสที่ตรวจความถูกต้องของข้อมูลพร้อมกัน (ChaCha20-Poly1305)
AllowedIPs รายการ IP ของ peer ทำหน้าที่เป็น routing (ขาออก) และ ACL (ขาเข้า)
CGNAT (Carrier-Grade NAT) NAT ระดับผู้ให้บริการ ทำให้ผู้ใช้ไม่มี public IP ของตนเอง
Cookie กลไกใน handshake ป้องกัน DoS โดยผูกกับ IP ต้นทาง
Cryptokey Routing การผูก public key ของ peer กับ AllowedIPs
DERP (Designated Encrypted Relay for Packets) เซิร์ฟเวอร์ relay ของ Tailscale ใช้เมื่อเชื่อมตรงไม่ได้
DPI (Deep Packet Inspection) การตรวจเนื้อหา/รูปแบบแพ็กเก็ตเพื่อจำแนกหรือบล็อก protocol
Endpoint IP:port ที่ใช้ติดต่อ peer
fwmark ค่า mark ที่ติดกับแพ็กเก็ตใน kernel ใช้กับ policy routing
Handshake การเจรจา session key ระหว่าง peer (Initiation/Response)
HKDF ฟังก์ชันสร้างกุญแจย่อยจากความลับร่วม
Interface network device เสมือน (เช่น wg0)
MSS (Maximum Segment Size) ขนาด payload TCP สูงสุดต่อ segment
MTU (Maximum Transmission Unit) ขนาดแพ็กเก็ตสูงสุดที่ส่งได้ไม่ต้อง fragment
NAT traversal เทคนิคให้สองฝั่งที่อยู่หลัง NAT เชื่อมถึงกัน (เช่น hole punching)
Noise Protocol Framework กรอบการออกแบบ handshake ที่ WireGuard ใช้ (Noise_IKpsk2)
Peer ปลายทางที่ interface สื่อสารด้วย ระบุด้วย public key
PersistentKeepalive การส่ง keepalive เป็นระยะเพื่อรักษา NAT mapping
PFS (Perfect Forward Secrecy) key ระยะยาวรั่วไม่ทำให้ถอดรหัส traffic เก่าได้
Policy routing การเลือกเส้นทางตามเงื่อนไขอื่นนอกจาก IP ปลายทาง (ip rule)
PSK (Pre-shared Key) กุญแจลับร่วมเสริมความปลอดภัยใน handshake
Rekey การเจรจา session key ใหม่ (ทุก ~2 นาทีเมื่อมี traffic)
Road warrior ผู้ใช้เคลื่อนที่ที่เชื่อมต่อจากภายนอกด้วย IP ที่เปลี่ยนไป
Split tunnel ส่งเฉพาะบาง traffic ผ่าน VPN
Full tunnel ส่ง traffic ทั้งหมดผ่าน VPN (0.0.0.0/0)
Transport data แพ็กเก็ตข้อมูลที่เข้ารหัส (message type 4)
TUN device เสมือน Layer 3 ที่ userspace อ่าน/เขียนแพ็กเก็ต IP
ULA (Unique Local Address) IPv6 ภายในช่วง fd00::/8

ฉ. แหล่งอ้างอิง (References)


สิ้นสุดบทความ WireGuard ฉบับสมบูรณ์ทุกกรณีการใช้งาน — ไฟล์ข้อมูลตัวอย่างสำหรับทดลองอยู่ใน wireguard-sample-data.md