
คู่มือฉบับสมบูรณ์เกี่ยวกับ WireGuard VPN ตั้งแต่หลักการเข้ารหัส (Noise Protocol) การติดตั้งทุกแพลตฟอร์ม การตั้งค่า Network/Firewall/DNS ไปจนถึง 18 กรณีการใช้งานจริง การ Hardening การ Monitoring การหลบเลี่ยงการบล็อก และแล็บปฏิบัติ 8 ชุด พร้อมไฟล์ข้อมูลตัวอย่าง (
wireguard-sample-data.md) สำหรับทดลองได้จริง
วิธีใช้เอกสารนี้ (How to use this document)
wireguard-sample-data.md ที่หัวข้อเลขเดียวกัน203.0.113.0/24, 198.51.100.0/24) ให้ปรับตามเครือข่ายจริงของท่านwg genkey เสมอ (ดูหัวข้อ 3.1)sudo หรือขึ้นต้นด้วย # ในตัวอย่างWireGuard คือ VPN (Virtual Private Network) แบบ Layer 3 tunnel ที่ออกแบบให้เรียบง่าย รวดเร็ว และปลอดภัยสมัยใหม่ ทำงานโดยห่อหุ้ม (encapsulate) แพ็กเก็ต IP ด้วยการเข้ารหัส แล้วส่งผ่าน UDP ไปยังปลายทาง ผู้ใช้สร้าง network interface เสมือน (เช่น wg0) ขึ้นมา แล้วตั้งค่า IP address และ route ให้ traffic ที่ต้องการวิ่งผ่าน interface นั้น
ลักษณะเด่นที่ทำให้ WireGuard ต่างจาก 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 |
ผู้ออกแบบและพัฒนาหลักคือ 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: สคริปต์ตรวจความพร้อมของเครื่อง (check-wg-ready.sh) ดูในไฟล์ตัวอย่าง
| หัวข้อ (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 | ง่าย | ยาก | ยากมาก | ยาก/ทำไม่ได้ (ปิด) |
ตารางต่อไปนี้เป็น ค่าเชิงแนวคิดแบบสัมพัทธ์ (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% | ▊ สูง | พอใช้ |
ควรเลือก 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 ดูในไฟล์ตัวอย่าง
wg0 มี private key, listen port (ถ้าต้องการรับ connection) และ IP addressCryptokey Routing คือหัวใจของ WireGuard: การผูก public key ของ peer เข้ากับ รายการ IP ที่อนุญาต (AllowedIPs) ตารางนี้ทำหน้าที่สองอย่างพร้อมกัน
ตัวอย่างตาราง 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 |
up แล้ว จะเงียบ จนกว่าจะมีแพ็กเก็ตที่ต้องส่งไปยัง peer จึงเริ่ม handshakewg show คือเวลาล่าสุดที่เจรจา key สำเร็จ ไม่ใช่สถานะ "online"PersistentKeepalive)
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 ในรูปตาราง ดูในไฟล์ตัวอย่าง
WireGuard ใช้ Noise Protocol Framework (ออกแบบโดย Trevor Perrin) รูปแบบ handshake ที่ใช้คือ Noise_IKpsk2 โดยชื่อเต็มของชุดที่ใช้ในเอกสารต้นฉบับคือ Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s
PublicKey ของ peer ไว้)| หน้าที่ (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 ยาวขึ้น) |
Cipher negotiation คือการที่สองฝั่งเจรจาเลือก algorithm ร่วมกัน ซึ่งเป็นต้นเหตุของช่องโหว่ในหลาย protocol (downgrade attack, config ผิดพลาด, ซับซ้อน)
WireGuard เลือก ไม่มี negotiation:
Perfect Forward Secrecy (PFS) หมายถึง การที่กุญแจเซสชันถูกสร้างจาก ephemeral key ซึ่งถูกทิ้งหลังใช้ ดังนั้นแม้ภายหลัง private key ระยะยาวรั่วไหล ผู้โจมตีก็ถอดรหัส traffic ในอดีตที่บันทึกไว้ไม่ได้
WireGuard จะ rekey (เจรจา session key ใหม่) อัตโนมัติ เมื่อ:
REKEY_AFTER_TIME) และมีการส่งข้อมูลอยู่REKEY_AFTER_MESSAGES (ประมาณ 260)REJECT_AFTER_TIME)ตัวอย่างการคำนวณ: จำนวน handshake ต่อวันเมื่อ tunnel ใช้งานตลอดเวลา
แทนค่า: 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 จำนวนแพ็กเก็ตแทบไม่เป็นปัญหา
แทนค่า: ถ้า R = 1,000,000 แพ็กเก็ต/วินาที จะได้ t ≈ 1.15×1018 ÷ 3.16×107 ≈ 36,500 ปี ดังนั้นในทางปฏิบัติ rekey จะถูกกระตุ้นด้วยเวลา (120 วินาที) ก่อนเสมอ
PSK คือกุญแจลับสมมาตรขนาด 32 byte ที่ทั้งสองฝั่งมีร่วมกัน ถูกผสมเข้าใน handshake เป็นชั้นป้องกันเพิ่ม ประโยชน์คือ:
wg genpsk และกำหนดเป็น PresharedKey ใน [Peer] ทั้งสองฝั่ง ค่าต้องเหมือนกัน🧪 ตัวอย่าง 1.4: สคริปต์สร้างชุดกุญแจและ PSK พร้อมแสดงขนาดและรูปแบบ (demo-keys.sh) ดูในไฟล์ตัวอย่าง
| 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) |
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 ใหม่
ขั้นตอนโดยย่อ:
เมื่อ responder ตรวจพบว่าถูกโหลดหนัก (มี handshake เข้ามามากผิดปกติ) จะไม่ประมวลผลการคำนวณ Curve25519 ที่หนัก แต่ส่ง Cookie Reply กลับไปแทน:
Roaming คือความสามารถในการเปลี่ยน IP/port ของ peer ได้โดยไม่ต้องตั้งค่าใหม่:
Endpoint ไว้ แต่ peer ย้ายที่ ก็ยังรับแพ็กเก็ตจากที่ใหม่ได้ และตอบกลับไปยังที่ใหม่อุปกรณ์ NAT/stateful firewall จะลบ mapping ของ UDP ที่เงียบเกินเวลาหนึ่ง (มักราว 30-120 วินาที) ทำให้ฝั่งนอก NAT ส่งหาฝั่งใน NAT ไม่ได้ PersistentKeepalive = N ทำให้ WireGuard ส่ง keepalive ทุก N วินาทีเพื่อรักษา mapping
ตัวอย่างการคำนวณ: ต้นทุนของ keepalive 25 วินาที
แทนค่า: 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 จากขนาด ดูในไฟล์ตัวอย่าง
| 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 |
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
if_wg ใน kernel (ในเวอร์ชันใหม่) และมีแพ็กเกจ wireguard-tools/wireguard-gowg(4) ใน base system ตั้งค่าผ่าน ifconfig และ hostname.wg0wireguard-go) หรือตามการรองรับของเวอร์ชันตัวอย่างการคำนวณ: ประเมินความเร็วสูงสุดเชิงทฤษฎีของ tunnel เมื่อ CPU เป็นคอขวด
แทนค่า: ให้ 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 ดูในไฟล์ตัวอย่าง
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"]
# ดูเวอร์ชัน 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)
| 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 ใหม่มีในตัว |
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
# โหลดโมดูล (ถ้า 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 และตรวจสอบผลลัพธ์ ดูในไฟล์ตัวอย่าง
wireguard.com (ตรวจ digital signature ของไฟล์ก่อนรัน).confWireGuardTunnel$ชื่อ ทำงานแม้ไม่มีผู้ใช้ login ถ้าตั้งให้เริ่มอัตโนมัติAllowedIPs มี 0.0.0.0/0 แอปเปิด Block untunneled traffic (kill-switch) ให้โดยค่าเริ่มต้น# ติดตั้ง 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
# ดูหมวดหมู่เครือข่ายของ 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 ติดตั้ง/ถอน ดูในไฟล์ตัวอย่าง
| วิธี | ข้อดี | ข้อจำกัด |
|---|---|---|
| แอป 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 |
# ติดตั้งเครื่องมือ (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 สำหรับเปิด/ปิด/ตรวจสถานะ ดูในไฟล์ตัวอย่าง
.conf หรือสร้าง tunnel เปล่าAndroid แอปรองรับการเลือกแอปที่ใช้/ไม่ใช้ tunnel ในหน้าแก้ไข tunnel:
[Interface] ใช้คีย์เฉพาะของแอป Android คือ ExcludedApplications / IncludedApplications (ใช้ package name คั่นด้วย comma)# ตัวอย่าง 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: สคริปต์สร้าง QR code ของ config มือถือ (ใช้ qrencode) ดูในไฟล์ตัวอย่าง
# ติดตั้ง 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
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
ใช้ผ่าน pkgsrc: net/wireguard-go และ net/wireguard-tools (userspace implementation) ตั้งค่า TUN device แล้วรัน wireguard-go wg0
🧪 ตัวอย่าง 2.5: ไฟล์ config ตัวอย่างสำหรับ FreeBSD และ OpenBSD ดูในไฟล์ตัวอย่าง
| 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
SYS_MODULE ออก เพื่อลดสิทธิ์ (least privilege)# รัน 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 ดูในไฟล์ตัวอย่าง
| คำสั่ง | หน้าที่ | ผลลัพธ์ |
|---|---|---|
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)"
600 (เจ้าของอ่าน/เขียนเท่านั้น) และเจ้าของเป็น root สำหรับไฟล์ใน /etc/wireguard/umask 077 ก่อนสร้างกุญแจ เพื่อไม่ให้ไฟล์เคยมี permission กว้างแม้เพียงชั่วขณะwg-quick พบ config ที่ group/other อ่านได้ จะแจ้งเตือน (Warning: ... is world accessible)# ตั้ง 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
alice-laptop.key🧪 ตัวอย่าง 3.1: สคริปต์ gen-keys.sh สร้างกุญแจของทุก peer ในครั้งเดียวและตั้ง permission ดูในไฟล์ตัวอย่าง
ไฟล์ 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"]
[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 |
ระวัง: จะเขียนทับไฟล์ที่แก้ด้วยมือ |
[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 |
%i จะถูกแทนด้วยชื่อ interface (เช่น wg0)PostUp และลบกฎด้วยคำสั่งตรงข้ามใน PostDown (ต้องจับคู่กัน)[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
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 จะถูกทิ้ง |
กฎสำคัญ:
0.0.0.0/0 หมายถึง "ส่งทุกอย่างผ่าน peer นี้" (default route) ควรมี peer เดียว เท่านั้นที่มีค่านี้AllowedIPs ของ server คือ "สิ่งที่ client จะส่งผ่าน VPN"; ใน config ของ server AllowedIPs ของ client คือ "IP ที่ client คนนั้นมีสิทธิ์ใช้เป็น source" (ปกติคือ /32 ของ client)🧪 ตัวอย่าง 3.2: ไฟล์ config อ้างอิงที่ใส่ comment ทุกตัวเลือก (reference.conf) ดูในไฟล์ตัวอย่าง
เชื่อมสองเครื่อง คือ 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
ควรเลือกช่วง 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:
| 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 ตามรูปแบบการใช้งาน
ขั้นที่ 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
latest handshake ไม่ปรากฏ แสดงว่าไม่เกิด handshake ให้ตรวจ key, endpoint, firewall (ดูหัวข้อ 9.4)ping ทดสอบหลังมี handshake เท่านั้น การ ping ครั้งแรกอาจช้าเพราะต้องเจรจา key ก่อนAddress ของ tunnel ชนกับ subnet LAN ที่เครื่องนั้นต่ออยู่🧪 ตัวอย่าง 3.3: สคริปต์ทดลองบนเครื่องเดียวด้วย network namespace (lab-p2p-netns.sh) พร้อมผลลัพธ์ที่คาดหวัง ดูในไฟล์ตัวอย่าง
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)
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 ให้)
# เปิด/ปิดแบบชั่วคราว
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
/etc/wireguard/wg0.conf (permission 600)systemctl enable wg-quick@wg0After=network-online.target อยู่แล้ว แต่หากใช้ Endpoint เป็นชื่อโดเมน ต้องแน่ใจว่า DNS ใช้ได้ตอนบูตwg show🧪 ตัวอย่าง 3.4: ไฟล์ wg0.conf + สคริปต์ตั้งค่า manual (manual-setup.sh) ดูในไฟล์ตัวอย่าง
# นำเข้าไฟล์ 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 แยก
สร้างสองไฟล์: .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
# /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
# /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
| วิธี | ข้อดี | ข้อเสีย | เหมาะกับ |
|---|---|---|---|
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 ดูในไฟล์ตัวอย่าง
เครื่องที่ทำหน้าที่เป็น 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
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
| สถานการณ์ | ควรทำ | เหตุผล |
|---|---|---|
| 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 |
เปิดเฉพาะ 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
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)"]
อย่าพึ่ง 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) ดูในไฟล์ตัวอย่าง
| ประเด็น | 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 |
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
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
อาการ: เปิด 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 ดูในไฟล์ตัวอย่าง
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
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)
DNS leak คือการที่คำถาม DNS ถูกส่งออกนอก tunnel (ไปยัง DNS ของ ISP/Wi-Fi) ทั้งที่ผู้ใช้คิดว่าใช้ full tunnel อยู่ ทำให้เปิดเผยโดเมนที่เข้าถึง
สาเหตุที่พบบ่อย:
DNS = ใน full tunnel::/0แนวทางป้องกัน:
DNS = ให้ชี้ DNS ภายใน tunnelAllowedIPs = 0.0.0.0/0, ::/0 เพื่อให้ IPv6 ผ่าน tunnel ด้วยwg0 (ดูตัวอย่าง kill switch หัวข้อ 5.2)ให้บริการ 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) ดูในไฟล์ตัวอย่าง
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 |
wg0 (byte)ตัวอย่างการคำนวณ:
| กรณี | 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 นั้นไม่ได้
ping แพ็กเก็ตเล็กผ่าน แต่แพ็กเก็ตใหญ่ไม่ผ่าน# ทดสอบหา 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)
MSS (Maximum Segment Size) คือขนาด payload TCP สูงสุดต่อ segment ในการจับมือ TCP แต่ละฝั่งประกาศ MSS ของตน ถ้า MSS ใหญ่เกิน MTU ของ tunnel จะเกิดปัญหาเมื่อ ICMP "Fragmentation needed" ถูกบล็อก MSS clamping คือการให้ router แก้ค่า MSS ใน SYN ให้พอดีกับ MTU ของ tunnel
wg0แทนค่า: 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 ที่เหมาะสมแบบอัตโนมัติ ดูในไฟล์ตัวอย่าง
ใช้ 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
Endpoint ของ IPv6 เขียนใน [ ] เช่น Endpoint = [2001:db8::10]:51820ถ้าเครือข่ายเป็น 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 ดูในไฟล์ตัวอย่าง
ส่วนนี้คือหัวใจของบทความ แต่ละกรณีมีโครงสร้างเดียวกัน: สถานการณ์ → สถาปัตยกรรม → 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 | ★★★ |
พนักงานหรือผู้ดูแลระบบอยู่นอกสถานที่ ต้องการเข้าถึงเครือข่ายองค์กร/บ้าน (192.168.10.0/24) อย่างปลอดภัยจากโน้ตบุ๊กและมือถือ Road Warrior คือผู้ใช้ที่เคลื่อนที่และมี IP เปลี่ยนไปเรื่อย ๆ
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
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
Address ของ client ตั้งเป็น /32 เมื่อ AllowedIPs ฝั่ง client ระบุ subnet ชัดเจน เพื่อไม่ให้ระบบเพิ่ม route ซ้ำซ้อน10.8.0.0/24 ต้อง NAT (ตัวอย่างข้างบน) หรือเพิ่ม route กลับบน router ของ LAN10.8.0.0/24 เสมือนเครือข่ายภายนอกที่เชื่อถือบางส่วน# บน 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 แบบสด ดูในไฟล์ตัวอย่าง
ผู้ใช้อยู่บน Wi-Fi สาธารณะ (ร้านกาแฟ สนามบิน โรงแรม) ต้องการเข้ารหัส traffic ทั้งหมดและซ่อน IP ต้นทางจริงจากเครือข่ายนั้น
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)"]
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
0.0.0.0/0; บน Android/iOS ใช้ "Always-on VPN + Block connections without VPN"Endpoint (ตัวอย่างใช้ fwmark เพื่อยกเว้น)::/0 ออกและ ปิด IPv6 บน client หรือบล็อกด้วย firewall แทนที่จะปล่อยให้รั่ว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 ดูในไฟล์ตัวอย่าง
ต้องการให้เฉพาะ traffic ภายใน (เซิร์ฟเวอร์องค์กร, subnet ที่บ้าน) ผ่าน VPN ส่วน traffic อินเทอร์เน็ตทั่วไป (YouTube, อัปเดตระบบ) ออกตามปกติเพื่อประหยัดแบนด์วิดท์และลด latency
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)"]
บางครั้งต้องการ "ส่งทุกอย่างผ่าน VPN ยกเว้นเครือข่าย LAN ท้องถิ่น (192.168.0.0/16)" แต่ AllowedIPs ไม่มีตัวดำเนินการ "ยกเว้น" จึงต้องแตก 0.0.0.0/0 ออกเป็นหลายบล็อก CIDR ที่ ไม่ครอบคลุม ช่วงที่ยกเว้น
หลักการ: ถ้ายกเว้นบล็อกเดียวที่ prefix /p ออกจาก 0.0.0.0/0 จะได้บล็อกที่เหลือ p บล็อกพอดี (หนึ่งบล็อกต่อหนึ่งบิตของ prefix ที่ต่างจากบล็อกที่ยกเว้น)
192.168.0.0/16)ตัวอย่างการคำนวณ: ยกเว้น 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)
รายแอป (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 ของเบราว์เซอร์)
# ดูว่า 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 ดูในไฟล์ตัวอย่าง
เชื่อมเครือข่ายสองสาขาเข้าด้วยกัน: 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 ทั้งสองคุยกันได้โดยตรง
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 (เช่นเดียวกัน) |
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)
192.168.1.0/24) ต้องเปลี่ยน subnet ของไซต์ใดไซต์หนึ่ง หรือใช้ NAT 1:1 แปลง subnet# บน 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 สามเครื่อง ดูในไฟล์ตัวอย่าง
มีหลายสาขาหรือหลายเครื่อง (spoke) ที่ต้องคุยกันเองได้ โดยให้ทุกเครื่องเชื่อมเฉพาะกับ hub กลาง (เช่น VPS หนึ่งเครื่อง) เหมาะกับกรณีที่ spoke หลายตัวอยู่หลัง NAT และไม่สามารถติดต่อกันโดยตรง
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
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
| ประเด็น | ข้อดี | ข้อเสีย |
|---|---|---|
| ความซับซ้อน | n − 1 tunnel สำหรับ n จุด; config น้อย | Hub มี config ใหญ่ที่สุด |
| การผ่าน NAT | spoke หลัง NAT ได้ทั้งหมด (ทุกตัวต่อออกไปหา hub) | - |
| การควบคุม | ตั้งนโยบาย firewall ที่จุดเดียว | - |
| ความพร้อมใช้งาน | - | Hub เป็นจุดล้มเหลวเดียว (single point of failure) |
| ประสิทธิภาพ | - | Latency รวมสูงกว่าเส้นตรง, แบนด์วิดท์ของ Hub เป็นคอขวด |
ตัวอย่าง: 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) และต้องเข้ารหัส/ถอดรหัสเท่ากัน
# จาก 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 ดูในไฟล์ตัวอย่าง
ต้องการให้ทุกเครื่องเชื่อมถึงกันโดยตรง (latency ต่ำสุด ไม่มี single point of failure) เหมาะกับคลัสเตอร์เซิร์ฟเวอร์ที่มี public IP หรือกลุ่มเล็ก
flowchart LR
A["Node A"] --- B["Node B"]
A --- C["Node C"]
A --- D["Node D"]
B --- C
B --- D
C --- D
[Peer] n − 1 ตัวต่อ node)| จำนวน 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) นี่คือเหตุผลที่ต้องใช้เครื่องมือช่วยจัดการ
| แนวทาง | หมายเหตุ |
|---|---|
| สคริปต์สร้าง 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])
เมื่อบาง 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
AllowedIPs ให้ครอบคลุม subnet ทั้งหมดผ่าน Rpython3 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 ดูในไฟล์ตัวอย่าง
ISP ให้ IP แบบ CGNAT (Carrier-Grade NAT) หรือไม่เปิด public IP ให้ ทำให้เปิดพอร์ตเข้าบ้านไม่ได้ ตรวจสอบได้โดยเปรียบเทียบ IP ที่ router แสดงบนหน้า WAN กับ IP ที่เห็นจากอินเทอร์เน็ต ถ้าต่างกัน หรืออยู่ในช่วง 100.64.0.0/10 แสดงว่าอยู่หลัง CGNAT
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 ที่เปิดไว้
UDP hole punching คือเทคนิคให้สองฝั่งที่อยู่หลัง NAT ส่ง UDP หากันพร้อมกัน เพื่อให้ NAT ทั้งสองเปิด mapping และคุยตรงกันได้โดยไม่ผ่านตัวกลาง โดยมี server กลางช่วยแลก endpoint ที่สังเกตได้ (เช่น STUN)
wg set ... endpoint พร้อมกันPersistentKeepalive ให้ถี่พอที่จะรักษา mapping ไว้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
ค่าที่เหมาะสมต้อง น้อยกว่า NAT timeout ของ UDP ที่สั้นที่สุดในเส้นทาง
PersistentKeepalive (วินาที)ตัวอย่าง: NAT บางตัว timeout 30 วินาที → K ≤ 15 วินาที; NAT ทั่วไปที่ 60–120 วินาที → K = 25 ก็เพียงพอ ค่าที่ถี่เกินจำเป็นสิ้นเปลืองแบตเตอรี่และข้อมูล
wg0 ก็เพียงพอEndpoint ทุกเครื่อง# บน 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) ดูในไฟล์ตัวอย่าง
มี NAS, Home Assistant, Git server (Gitea/Forgejo), media server (Jellyfin) ในบ้าน ต้องการเข้าถึงจากภายนอกอย่างปลอดภัย โดยไม่ต้องเปิดพอร์ตของแต่ละบริการสู่อินเทอร์เน็ต
| ประเด็น | เปิดพอร์ตตรง ๆ (Port forward) | เข้าผ่าน WireGuard |
|---|---|---|
| พื้นที่โจมตี (Attack surface) | ทุกบริการ ถูกสแกน/โจมตีจากทั่วโลก | พอร์ต UDP เดียว และเงียบต่อผู้ไม่มี key |
| การยืนยันตัวตน | ขึ้นกับแต่ละบริการ (หลายตัวอ่อนแอ) | ผ่าน key ก่อนเห็นบริการใด ๆ |
| การเข้ารหัส | ต้องตั้ง TLS/certificate เอง | เข้ารหัสทั้ง tunnel อยู่แล้ว |
| ความสะดวกของผู้ใช้ภายนอก | เข้าได้จาก browser ทันที | ต้องมี client WireGuard |
| ใช้ได้เมื่ออยู่หลัง CGNAT | ไม่ได้ | ได้ (ผ่าน VPS) |
| การแชร์ให้คนทั่วไป | ทำได้ง่าย | ไม่เหมาะ (ใช้ reverse tunnel หัวข้อ 5.9) |
ตั้ง 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
192.168.1.0/24 ซึ่งตรงกับ Wi-Fi สาธารณะจำนวนมาก จะเกิด subnet ชนกัน ให้เปลี่ยน LAN ที่บ้านเป็นช่วงที่ไม่ธรรมดา (เช่น 192.168.77.0/24)# จากนอกบ้าน (ปิด 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 และสคริปต์เช็กบริการ ดูในไฟล์ตัวอย่าง
ต้องการเปิดบริการที่รันที่บ้าน (เช่น เว็บ blog, Git server สาธารณะ) ให้คนทั่วไปเข้าถึงได้ผ่านโดเมน โดยไม่เปิดเผย IP บ้านและแม้อยู่หลัง CGNAT ใช้ VPS ที่มี public IP เป็น "ประตูหน้า" ส่งต่อ traffic ผ่าน WireGuard มายังเครื่องที่บ้าน
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 ให้ถูกต้อง |
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
}
| วิธี | การทำงาน |
|---|---|
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 ก่อนส่งต่อ
# จากอินเทอร์เน็ตภายนอก
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 ดูในไฟล์ตัวอย่าง
ผู้ให้บริการอย่าง Mullvad, ProtonVPN, IVPN ฯลฯ เปิดให้ดาวน์โหลดไฟล์ config WireGuard เพื่อใช้กับอุปกรณ์ใดก็ได้ (router, Raspberry Pi, Linux) โดยไม่ต้องใช้แอปของผู้ให้บริการ
[Interface]: PrivateKey, Address (ผู้ให้บริการกำหนดให้), DNS[Peer]: PublicKey ของ server, Endpoint, AllowedIPs = 0.0.0.0/0, ::/0/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
ติดตั้งบน 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
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
ข้อแลกเปลี่ยน:
wg-exit ให้ไม่เกิน MTU ของ wg-entry ลบ 80sudo 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 ดูในไฟล์ตัวอย่าง
ต้องการให้เครื่องหนึ่ง (เช่น VPS ในต่างประเทศ) เป็น ทางออกอินเทอร์เน็ต ของเครื่องอื่น และเลือก "ตำแหน่ง" ทางออกได้ (เช่น ออกที่ญี่ปุ่นสำหรับบริการหนึ่ง ออกที่สิงคโปร์สำหรับอีกบริการ) รวมถึงแยกตามอุปกรณ์หรือ subnet
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"]
# /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 ตรง |
# ตรวจ 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 ดูในไฟล์ตัวอย่าง
ต้องการความต่อเนื่องของ VPN: ถ้า server หลักล่ม ให้ย้ายไป server สำรองอัตโนมัติ หรือกระจายโหลดไปหลาย tunnel
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
ใช้ไฟล์ 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
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
ip route add default nexthop dev wg-a weight 1 nexthop dev wg-b weight 1 โดย kernel กระจายตาม hash ของ flow (ไม่ใช่ต่อแพ็กเก็ต) จึงไม่เกิดการสลับลำดับ# ตัวอย่าง 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 อิสระ
แทนค่า: 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)
# จำลอง 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 ดูในไฟล์ตัวอย่าง
มีอุปกรณ์ภาคสนามจำนวนมาก (Raspberry Pi เก็บข้อมูลเซนเซอร์, gateway ในฟาร์ม, ตู้คอนโทรล) ที่อยู่หลัง SIM/4G แบบ NAT ต้องการเข้าไป SSH/ดึงข้อมูล/อัปเดตระยะไกล ESP-class (ESP32) บางรุ่นมีไลบรารี WireGuard ให้ใช้ แต่ทรัพยากรจำกัด จึงนิยมใช้ Raspberry Pi หรือ gateway ที่รัน Linux เป็นตัวกลาง ให้ MCU เชื่อมผ่าน LAN ท้องถิ่น
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
การออกแบบ 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"
PersistentKeepalive ยาวขึ้น (เช่น 60–120 วินาที) หรือไม่ใช้ หากอุปกรณ์เป็นฝ่ายเริ่มส่งข้อมูลเป็นรอบwg-quick@wg0 ถ้า handshake เก่าเกินกำหนดssh, rsync, ansible (หัวข้อ 11.1)# /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'
# บน 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 เครื่อง ดูในไฟล์ตัวอย่าง
นักพัฒนาต้องเข้าถึง staging API, ฐานข้อมูล, Kubernetes API และเครื่องภายใน โดยไม่เปิดสู่อินเทอร์เน็ต เดิมใช้ bastion/jump host (SSH) ที่ต้องจัดการ key ของผู้ใช้บนเครื่องกลาง WireGuard ช่วยทดแทนได้ด้วยการให้เครื่องนักพัฒนาอยู่ใน private network โดยตรง
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"]
นโยบายแบบละเอียดด้วย 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
172.17.0.0/16, 10.244.0.0/16 กับ tunnel ของท่าน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 ดูในไฟล์ตัวอย่าง
บริษัทมีทีมหลายแผนก (Engineering, Finance, HR, ผู้รับเหมา) ต้องการให้ทำงานจากที่บ้านโดยเข้าถึงเฉพาะทรัพยากรที่เกี่ยวข้อง ตามหลัก least privilege และมีขั้นตอน onboard/offboard ที่ชัดเจน
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
}
}
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
# จากเครื่อง 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 ตัวอย่าง ดูในไฟล์ตัวอย่าง
เล่นเกมเก่าหรือเกมที่ออกแบบสำหรับ LAN (ค้นหาผู้เล่นด้วย broadcast) ร่วมกับเพื่อนผ่านอินเทอร์เน็ต เกมจำนวนมากต้องการให้ผู้เล่นอยู่ในเครือข่ายเดียวกันและใช้ IP ตรง ๆ
| โทโพโลยี | เหมาะกับ | 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 เกมแอ็กชันจะรู้สึกหน่วง
WireGuard เป็น Layer 3 จึงไม่ส่ง broadcast/multicast ข้ามอุโมงค์ ทางแก้:
10.70.0.2) ในเมนู "Direct connect"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
# 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
ping และเครื่องมือ latency ของเกมวัดผลจริง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 ดูในไฟล์ตัวอย่าง
ผู้ใช้เดินทางและสลับระหว่าง Wi-Fi บ้าน → 4G/5G → Wi-Fi ที่ทำงาน ต้องการให้ VPN ไม่หลุดและเปิดเฉพาะเมื่อจำเป็น
เมื่อ 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 ส่งข้อมูลเข้ามาได้
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 ก่อน)
ListenPort = 443 หรือ 53 (UDP) บน server# บน 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 ดูในไฟล์ตัวอย่าง
เชื่อมเครือข่าย 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
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
# /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 ของผู้ให้บริการ |
| ประเด็น | 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 | ขึ้นกับนโยบายองค์กร | มักมีการรับรองมาตรฐาน |
ตัวอย่างการคำนวณต้นทุนเชิงเปรียบเทียบ (ค่าสมมติเพื่อการเรียนรู้):
แทนค่า (สมมติ): 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 ตัวเลขจริงขึ้นกับผู้ให้บริการและภูมิภาค ต้องตรวจราคาล่าสุดก่อนตัดสินใจ
# จาก 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) ดูในไฟล์ตัวอย่าง
| แพลตฟอร์ม | วิธีตั้งค่าหลัก | หมายเหตุ |
|---|---|---|
| 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 ในตัว |
# ติดตั้ง (บน 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
# สร้าง 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
10.8.0.1/2410.8.0.2/32), (Endpoint ถ้าเป็นฝ่ายเริ่ม), Keepalivetun_wg0 แล้ว Enable)ตัวอย่างการประเมินแบบหยาบ: ถ้าซีพียูของ router เข้ารหัสได้ประมาณ 300 Mbit/s ต่อ core (วัดจริงด้วย iperf3 ข้าม tunnel) และ WireGuard ใช้ core ได้ไม่ถึง 100% เพราะภาระอื่น ๆ ความเร็วที่ใช้ได้จริงอาจประมาณ 200–250 Mbit/s จึงไม่ควรคาดหวัง gigabit เต็มสาย
🧪 ตัวอย่าง 6.1: สคริปต์ UCI ของ OpenWrt และคำสั่ง RouterOS ฉบับเต็ม ดูในไฟล์ตัวอย่าง
ดูตัวอย่าง docker-compose.yml ของ linuxserver/wireguard ในหัวข้อ 2.6.1 หลักการเดียวกันใช้ได้กับ image อื่น ๆ (เช่น wg-easy) สิ่งที่ต้องจำ: NET_ADMIN, UDP port mapping, sysctls, และ volume สำหรับ config/key
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)
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
FIREWALL_OUTBOUND_SUBNETS ใน gluetun)🧪 ตัวอย่าง 6.2: compose ทั้งสองแบบ (sidecar และ gluetun) ดูในไฟล์ตัวอย่าง
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 ปรับให้อัตโนมัติ)
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
ไม่เปิด 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 ดูในไฟล์ตัวอย่าง
ethtool -K eth0 tx off rx off gso off gro off) เพื่อวินิจฉัยwg0 ตาม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 และสคริปต์ตรวจสอบ ดูในไฟล์ตัวอย่าง
ใช้ 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
| เรื่อง | 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 |
| 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 ฉบับเต็ม ดูในไฟล์ตัวอย่าง
| เครื่องมือ | จุดเด่น | หมายเหตุ |
|---|---|---|
| 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 |
# 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"
| ประเด็น | รายละเอียด |
|---|---|
| ข้อดี | ใช้ง่าย สร้าง 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 ดูในไฟล์ตัวอย่าง
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
nas, laptop) แทน IP# ติดตั้ง (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"] }
]
}
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
| เครื่องมือ | แนวคิดหลัก | จุดเด่น | หมายเหตุ |
|---|---|---|---|
| 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 | ระบุเพื่อเปรียบเทียบ |
| ประเด็น | 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 ดูในไฟล์ตัวอย่าง
# ติดตั้งเครื่องมือ
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 # ลบอย่างปลอดภัยหลังสแกนเสร็จ
#!/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"
shred -u)🧪 ตัวอย่าง 7.3: make-client.sh พร้อมตัวอย่างการเรียกใช้ ดูในไฟล์ตัวอย่าง
WireGuard ยืนยัน อุปกรณ์ ด้วยกุญแจ ไม่ได้ยืนยัน ผู้ใช้ ด้วยรหัสผ่านหรือ MFA ผลที่ตามมา:
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)
| ขั้นตอน | 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 และกรณีทดสอบ ดูในไฟล์ตัวอย่าง
| 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 ได้) |
ถ้าเครื่อง client ถูกยึด ผู้โจมตีได้ private key และสิทธิ์เท่ากับ client นั้น (ตาม AllowedIPs และ firewall) จึงควร:
ผู้สังเกตบนเครือข่ายเห็น IP ต้นทาง–ปลายทาง พอร์ต UDP ขนาดและจังหวะของแพ็กเก็ต แม้ไม่เห็นเนื้อหา
🧪 ตัวอย่าง 8.1: ตารางประเมินความเสี่ยง (risk register) ตัวอย่างสำหรับองค์กรขนาดเล็ก ดูในไฟล์ตัวอย่าง
| ขั้นตอน | แนวปฏิบัติ |
|---|---|
| สร้าง | บนอุปกรณ์เจ้าของ หรือบนเครื่องผู้ดูแลที่เชื่อถือ ด้วย 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"
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)"]
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 ดูในไฟล์ตัวอย่าง
ผู้โจมตีเก็บ traffic ที่เข้ารหัสไว้ตั้งแต่วันนี้ แล้วรอจนมีคอมพิวเตอร์ควอนตัมที่แก้ปัญหา elliptic curve ได้ (อัลกอริทึมของ Shor) จึงถอดรหัสย้อนหลัง Curve25519 ที่ WireGuard ใช้ เสี่ยงต่อควอนตัม (แม้ ChaCha20 และ BLAKE2s ยังถือว่าทนทานกว่า)
ใช้ อสมการของ Mosca ประเมินว่าต้องกังวลหรือไม่:
ตัวอย่างการคำนวณ: ข้อมูลทางการแพทย์ต้องเก็บเป็นความลับ X = 20 ปี, ใช้เวลาย้ายระบบ Y = 3 ปี, สมมติ Z = 15 ปี → X + Y = 23 > 15 จึง เสี่ยง ควรใช้มาตรการเสริมตั้งแต่วันนี้ (เช่น PSK) ส่วนข้อมูลเว็บทั่วไปที่ต้องเป็นความลับเพียง X = 1 ปี, Y = 1 ปี → X + Y = 2 < 15 ความเสี่ยงต่ำ (ค่า Z เป็นเพียงสมมติเพื่อการสอน ไม่ใช่การคาดการณ์จริง)
ใน handshake ของ WireGuard PresharedKey ถูกผสมเข้าใน key derivation (HKDF) ดังนั้นการถอดรหัส session key ต้องรู้ทั้ง ผลลัพธ์ของ Curve25519 และ PSK แม้ผู้โจมตีมีคอมพิวเตอร์ควอนตัมที่ทำลาย Curve25519 ได้ แต่ถ้าไม่มี PSK (ที่ไม่เคยส่งผ่าน handshake และมี entropy 256 bit) ก็ยังถอดรหัสไม่ได้
ข้อกำหนดเพื่อให้ได้ผล:
wg genpsk (ห้ามตั้งเป็นรหัสผ่านที่คนจำได้)# สร้างและใช้ 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) พร้อมตัวอย่างค่า ดูในไฟล์ตัวอย่าง
AllowedIPs = <IP ของมัน>/32 เท่านั้น (ป้องกันการปลอม source IP ของเครื่องอื่นใน tunnel)0.0.0.0/0 เมื่อไม่จำเป็น| บทบาท | 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 ไม่ได้) |
นอกจากกฎ 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 ดูในไฟล์ตัวอย่าง
systemctl list-units --type=service --state=running)# 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
dnf-automatic สำหรับแพตช์ความปลอดภัย แล้วตรวจสอบหลังอัปเดต kernel (อาจต้องรีบูต)wireguard-tools และ kernel สม่ำเสมอ (ช่องโหว่ใน WireGuard พบน้อยแต่ไม่ใช่ศูนย์)crowdsec) สำหรับ service ข้างเคียงที่เปิดอยู่ เช่น SSH, reverse proxy; ไม่ต้อง ใช้กับ WireGuard เองเพราะไม่ตอบแพ็กเก็ตที่ไม่ผ่านการยืนยันrp_filter และปิด source routing เพื่อลดการปลอมแปลง IP: net.ipv4.conf.all.rp_filter=1 (ระวังกรณี policy routing ที่ asymmetric ให้ใช้ค่า 2 loose)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
เนื่องจากข้อความ 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) ดูในไฟล์ตัวอย่าง
ทบทวนทุกไตรมาส: เทียบรายชื่อ 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
wg show แสดงเพียงสถานะปัจจุบัน (endpoint ล่าสุด, handshake ล่าสุด, ปริมาณข้อมูล)nft log), conntrack, log ของบริการปลายทาง, flow log (NetFlow/ sFlow/ cloud VPC flow logs), และ snapshot wg show ตามรอบ# เก็บ 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: audit-peers.sh, wg-snapshot.sh และแบบฟอร์มทบทวนสิทธิ์รายไตรมาส ดูในไฟล์ตัวอย่าง
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)"]
MTU: ตั้งให้พอดี (หัวข้อ 4.5) เพราะ fragmentation ทำให้ throughput ตกอย่างมาก
ประสิทธิภาพเชิงประสิทธิผลของการห่อหุ้ม (Encapsulation efficiency):
wg0แทนค่า: 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:
แทนค่า: 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
ethtool -l eth0 ให้จำนวน queue สอดคล้องกับจำนวน core และกระจาย IRQ ด้วย irqbalanceiperf3) คนละ core เพื่อลดการแย่งกัน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
#!/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 ชุดปรับจูน ดูในไฟล์ตัวอย่าง
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 เจรจาเมื่อมีข้อมูลต้องส่ง)
ใช้ 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
# 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 อย่างย่อ ดูในไฟล์ตัวอย่าง
# เปิด 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 นั้น |
# จับแพ็กเก็ต 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 เฉพาะเครื่องทดสอบ)
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 สคริปต์ตรวจตามขั้นตอนข้างต้นอัตโนมัติ ดูในไฟล์ตัวอย่าง
| อาการ (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) ดูในไฟล์ตัวอย่าง
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)"
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 ดูในไฟล์ตัวอย่าง
| ข้อจำกัด | อธิบาย | ทางออก/ข้อควรทำ |
|---|---|---|
| ใช้ 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) |
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)"]
| เครื่องมือ | วิธีทำงาน | จุดเด่น | ข้อเสีย |
|---|---|---|---|
| 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 มาตรฐาน (ต้องใช้ทั้งสองฝั่ง) |
# ----- ฝั่ง 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:
wg0ตัวอย่าง: ถ้า Wwrap = 60 byte (ค่าสมมติ) → MTUwg = 1500 − 60 − 80 = 1360 แต่เพื่อความปลอดภัยเผื่อ overhead ที่ไม่แน่นอน จึงตั้ง 1280–1320 และวัดด้วย ping -M do -s (หัวข้อ 4.5.2)
# ----- ฝั่ง 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)
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
| ประเด็น | ผลกระทบ |
|---|---|
| Throughput | ลดลงตามต้นทุนของ wrapper (CPU เพิ่ม, overhead ของ header) อาจลดลงสิบ–หลายสิบเปอร์เซ็นต์ |
| Latency | เพิ่มเล็กน้อยสำหรับ UDP-wrapper; เพิ่มมากกว่าสำหรับ TCP-based (WebSocket) |
| MTU | ต้องลดลงอีก (ดูสูตรด้านบน) |
| ความซับซ้อน | เพิ่มจุดที่อาจล้มเหลว (สองชั้น) ต้อง monitor ทั้งคู่ |
| ความเสถียรต่อการตรวจจับ | ขึ้นกับความสามารถ DPI ปลายทาง การตรวจจับพัฒนาตลอด ไม่มีวิธีใดรับประกัน |
🧪 ตัวอย่าง 10.2: ไฟล์ config และคำสั่งของ udp2raw / wstunnel / AmneziaWG ดูในไฟล์ตัวอย่าง
บางเครือข่ายอนุญาตเฉพาะ 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
เมื่อ TCP ซ้อนใน TCP (TCP ของแอป วิ่งใน WireGuard ซึ่งถูกห่อใน TCP ของ tunnel) ทั้งสองชั้นมีกลไก retransmission และ congestion control ของตัวเอง เมื่อเกิด packet loss:
ประมาณผลกระทบของ loss ต่อ TCP throughput ด้วยสูตรของ Mathis:
แทนค่า: 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 สูงมาก
ข้อเสนอแนะ:
sysctl net.ipv4.tcp_congestion_control=bbr) ที่ชั้นนอกช่วยได้บ้าง🧪 ตัวอย่าง 10.3: ไฟล์ตัวอย่าง nginx stream และสคริปต์ทดสอบ tcp-over-tcp-test.sh ดูในไฟล์ตัวอย่าง
สคริปต์ที่เกี่ยวข้องในบทความนี้: 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]}...")
โครงสร้างโฟลเดอร์ของ 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
ตัวอย่างสร้าง 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 # ลบทั้งหมดเมื่อเลิกใช้ (หยุดค่าใช้จ่าย)
แนวคิด: เก็บ 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 ดูในไฟล์ตัวอย่าง
| 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 จริงของเครื่องท่าน
a (server) และ b (client) แล้วยืนยันด้วย ping และ wg showlab1-p2p.sh (ฉบับเต็มอยู่ในหัวข้อ 11.3.2)[PASS] B ping A ผ่าน tunnel, latest handshake ปรากฏใน wg showPersistentKeepalive เป็น 0 และสังเกตว่า handshake แรกเกิดเมื่อใด
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"]
sudo ./lab2-roadwarrior.sh up → ตรวจว่า client เข้า 192.0.2.80 ผ่าน VPN → sudo ./lab2-roadwarrior.sh leaktestwg0 แล้ว ping ยัง "ผ่าน" (รั่วทางตรง) (2) เปิด kill switch: ปิด wg0 แล้ว ping "ไม่ผ่าน" (ไม่รั่ว)sudo ./lab3-site2site.sh uphostA (192.168.10.50) ping hostB (192.168.20.60) ได้ และ ip route get แสดงว่าผ่าน gatewayAllowedIPs ของ LAN ฝั่งตรงข้ามออกจาก gwA แล้วสังเกตอาการ (ping จาก gateway ได้ แต่จาก host ไม่ได้)sudo ./lab4-hubspoke.sh uplog ให้ทุกแพ็กเก็ตที่ถูก dropsudo ./lab5-cgnat.sh up192.168.1.50 ไม่ได้ (NAT ปิด), แต่ laptop ping 10.10.0.2 (home) ผ่าน VPS ได้PersistentKeepalive ออกจาก home แล้วรอ (หรือล้าง conntrack ที่ router ด้วย conntrack -F) สังเกตว่า laptop ติดต่อ home ไม่ได้จนกว่า home จะส่งข้อมูลออกไปใหม่sudo ./lab6-capture.sh จะสร้างไฟล์ /tmp/wg-lab6.pcap และสรุปชนิด messagewg → ดูฟิลด์ Type, Sender index, Receiver index, Counter
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"]
up) → ติดตั้ง wg-textfile.sh (หัวข้อ 9.2.2) → docker compose up -d จากไฟล์ lab7-monitoring/docker-compose.yml ในไฟล์ตัวอย่าง → เปิด Grafana ที่ http://localhost:3000 (admin/admin เปลี่ยนรหัสทันที)ping แล้วรอเกิน 5 นาทีเห็นการแจ้งเตือน WireGuardPeerStalewg-textfile.sh ผ่าน ip netns exec a ... หรือใช้ WireGuard บน host จริงsudo ./lab8-bugs.sh up 1 → ใช้ ip netns exec client wg show, ping, tcpdump วินิจฉัย → แก้ → sudo ./lab8-bugs.sh check → down แล้วลองบั๊ก 2–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 ดูในไฟล์ตัวอย่าง
ip netnsNetwork 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 ภายในหายไปด้วย)
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 # ลบแล็บเมื่อเสร็จ
| เครื่องมือ | เหมาะกับ | หมายเหตุ |
|---|---|---|
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 ดูในไฟล์ตัวอย่าง
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 |
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 |
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 |
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 |
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' |
แทนค่าใน
<...>และปรับ IP ให้ตรงกับเครือข่ายของท่าน ไฟล์พร้อมใช้ฉบับเต็มอยู่ในwireguard-sample-data.md(ภาคผนวก ข)
[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
[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
[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>
[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
จำนวน 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 | เครือข่ายขนาดใหญ่ |
จำนวน subnet ขนาด /s ที่แบ่งได้จากบล็อก /p (เมื่อ s ≥ 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)
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 |
| ช่วง | ข้อแนะนำ |
|---|---|
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 |
| ประเด็น | 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 ขององค์กร |
หมายเหตุ: ฟีเจอร์ของผลิตภัณฑ์เปลี่ยนเร็ว ตรวจเอกสารล่าสุดของแต่ละเครื่องมือก่อนตัดสินใจ
| ศัพท์ | ความหมาย |
|---|---|
| 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 |
https://www.wireguard.com/ (Quick Start, Install, Cross-platform, Netns, Performance)https://www.wireguard.com/papers/wireguard.pdf)man 8 wg, man 8 wg-quick (และ man 5 wg-quick ตามเวอร์ชันที่ติดตั้ง)wireguard-tools (มีตัวอย่าง reresolve-dns, extract-keys ฯลฯ ใน contrib/)https://noiseprotocol.org/noise.htmlสิ้นสุดบทความ WireGuard ฉบับสมบูรณ์ทุกกรณีการใช้งาน — ไฟล์ข้อมูลตัวอย่างสำหรับทดลองอยู่ใน
wireguard-sample-data.md