# คู่มือทำ Site-to-Site VPN ด้วย Headscale แบบ step-by-step
Table of Contents
ภาพรวมก่อนเริ่ม
เป้าหมายของคู่มือนี้คือเชื่อม subnet ของหลาย site เข้า mesh เดียวกัน โดย traffic ระหว่าง site วิ่งเป็น WireGuard P2P ตรงถึงกัน ไม่ผ่าน gateway กลาง
ในคู่มือนี้ผมจะใช้ค่าตัวอย่างดังนี้ (เปลี่ยนเป็นของคุณได้เลย):
| ส่วนประกอบ | ค่าตัวอย่าง |
|---|---|
| Headscale control server | headscale.example.com |
| Site A (สำนักงาน) subnet | 192.168.10.0/24 |
| Site B (cloud VPC) subnet | 10.0.0.0/16 |
| OS ของ subnet router | Linux (Debian/Ubuntu) |
ขั้นที่ 1 — Deploy Headscale Control Server
วาง Headscale ด้วย Docker บน server ที่มี public IP แล้วเอา nginx มา terminate TLS ข้างหน้า
services: headscale: image: headscale/headscale:latest command: serve volumes: - ./config:/etc/headscale - ./data:/var/lib/headscale ports: - '127.0.0.1:8080:8080' restart: unless-stoppedในไฟล์ config หลัก ตั้ง server_url ให้ตรงกับ domain ที่จะใช้:
server_url: https://headscale.example.comlisten_addr: 0.0.0.0:8080
prefixes: v4: 100.64.0.0/10 # IP range ของ Tailnet
dns: base_domain: example-net.internalฝั่ง nginx reverse proxy เข้า 127.0.0.1:8080 — สิ่งสำคัญคืออย่าตั้ง timeout สั้นเกินไป เพราะ Headscale ใช้ long-lived connection สำหรับ control channel:
server { listen 443 ssl; server_name headscale.example.com;
location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $http_connection; proxy_set_header Host $host; proxy_read_timeout 86400s; # กัน control channel หลุด }}ยิง compose ขึ้นแล้วเช็คว่า server ตอบ:
docker compose up -dcurl -fsSL https://headscale.example.com/health # ควรได้ 200ขั้นที่ 2 — สร้าง User และ Pre-auth Key
Headscale จัดกลุ่ม node ด้วย user (เดิมเรียก namespace) สร้าง user แล้วออก pre-auth key สำหรับ enroll subnet router แต่ละตัว
headscale users create corpheadscale preauthkeys create --user corp --reusable --expiration 1hเก็บค่า key ที่ได้ (hskey-...) ไว้ใช้ในขั้นถัดไป
ขั้นที่ 3 — ตั้ง Subnet Router ที่ Site A
ไปที่เครื่อง subnet router ของ Site A ติดตั้ง Tailscale client ตามปกติ:
curl -fsSL https://tailscale.com/install.sh | shจากนั้น เปิด IP forwarding ในระดับ kernel — ขั้นนี้ห้ามลืม ไม่งั้น tunnel ติดแต่เครื่องหลัง router เข้าไม่ถึงกัน:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale.confecho 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.confsudo sysctl -p /etc/sysctl.d/99-tailscale.confแล้ว up โดยชี้ไปที่ Headscale ของเรา พร้อม advertise subnet ของ Site A:
sudo tailscale up \ --login-server https://headscale.example.com \ --authkey hskey-xxxxxxxxxxxx \ --advertise-routes=192.168.10.0/24 \ --accept-routesขั้นที่ 4 — ตั้ง Subnet Router ที่ Site B
ทำแบบเดียวกันที่เครื่อง subnet router ของ Site B (cloud VPC) — ติดตั้ง client, เปิด IP forwarding เหมือนขั้นที่ 3 แล้ว up โดย advertise subnet ของ Site B แทน:
sudo tailscale up \ --login-server https://headscale.example.com \ --authkey hskey-xxxxxxxxxxxx \ --advertise-routes=10.0.0.0/16 \ --accept-routesขั้นที่ 5 — Approve Routes บน Headscale
ตอนนี้ subnet router ทั้งสอง advertise route เข้ามาแล้ว แต่ Headscale ยังไม่เปิดใช้จนกว่าเราจะ approve — ดู route ที่ค้างอยู่:
headscale nodes listheadscale routes listจะเห็น route ของแต่ละ site พร้อม id แล้ว enable ทีละอัน:
headscale routes enable -r 1 # 192.168.10.0/24 (Site A)headscale routes enable -r 2 # 10.0.0.0/16 (Site B)ขั้นที่ 6 — เขียน ACL as-code
มาถึงจุดที่ผมชอบที่สุด — กำหนดว่า site ไหนคุยกับ site ไหนได้ ผ่านไฟล์ ACL (HuJSON) ที่เก็บใน git ได้
สมมติเราต้องการให้ สอง site คุยกันได้สองทาง ก็เขียนแบบนี้:
{ "tagOwners": { "tag:site-a": ["corp"], "tag:site-b": ["corp"], }, "acls": [ // Site A <-> Site B คุยกันได้ทั้งสองทาง { "action": "accept", "src": ["tag:site-a", "tag:site-b"], "dst": ["192.168.10.0/24:*", "10.0.0.0/16:*"], }, ],}ถ้าอยากได้ one-way (เช่นให้ Site A เข้า Site B ได้ แต่ไม่ให้ย้อนกลับ) ก็แยก src/dst ให้ชัด:
"acls": [ { "action": "accept", "src": ["tag:site-a"], "dst": ["10.0.0.0/16:*"], },]โหลด policy เข้า Headscale แล้ว tag node ให้ตรง:
headscale policy set -f acl.hujsonheadscale nodes tag -i 1 -t tag:site-aheadscale nodes tag -i 2 -t tag:site-bขั้นที่ 7 — Verify ให้ครบทุกเส้น
อย่าเพิ่งวางใจว่าเสร็จ — ทดสอบจริงทีละเส้น จากเครื่อง ที่อยู่ใน LAN หลัง subnet router (ไม่ใช่ตัว router เอง) เพื่อพิสูจน์ว่า routing ทะลุถึงเครื่องปลายทางจริง
ping 10.0.0.5curl http://10.0.0.5:8080/healthtraceroute 10.0.0.5 # ดูว่า traffic วิ่งผ่าน tunnel จริงแล้วเช็คฝั่ง tunnel ว่าต่อแบบ P2P หรือ relay:
tailscale statustailscale ping site-b # บอกว่า direct หรือ via relaychecklist สรุป
ก่อนปิดงาน ไล่เช็คให้ครบ:
- Headscale ขึ้น +
/healthตอบ 200, nginx timeout ยาวพอ - สร้าง user + pre-auth key แล้ว
- subnet router ทุก site เปิด IP forwarding ถาวร
- ทุก site
upด้วย--advertise-routes+--accept-routes - approve route ทุกอันบน Headscale แล้ว
- ACL apply + tag node ตรง
- ทดสอบจาก host ใน LAN (ไม่ใช่ตัว router) ผ่านทุกเส้น
-
tailscale pingขึ้นdirect(ถ้าได้)
สรุป
หัวใจของการทำ site-to-site VPN ด้วย Headscale มีแค่ไม่กี่จุด — advertise + approve + accept routes ให้ครบทั้งสองฝั่ง, เปิด IP forwarding บน subnet router, แล้วคุม access ด้วย ACL as-code จุดที่คนพลาดบ่อยที่สุดคือลืม approve route บน Headscale กับลืมเปิด IP forwarding — ถ้าจำสองอย่างนี้ได้ ที่เหลือก็ตรงไปตรงมาครับ