# จาก NetBird สู่ Headscale — ทำไมผมย้าย mesh VPN อีกครั้ง
Table of Contents
ทบทวนก่อน — ทำไมตอนแรกเลือก NetBird
ในโพสต์ก่อนผมเล่าว่า NetBird ตอบโจทย์เรื่อง open-source ทั้ง client และ server ซึ่งยังจริงอยู่ และ UI สำหรับจัดการ access control ก็ใช้ง่ายมาก เหมาะกับทีมที่อยากให้คนที่ไม่ใช่ dev จัดการ policy ได้
แต่พอใช้ไปสักพักในงานจริง ผมเริ่มเจอความต้องการที่ต่างออกไป:
- ผมอยากให้ network policy อยู่ใน git เป็น source of truth เดียว ไม่ใช่คลิกผ่าน UI แล้วไม่มี history
- ผมอยากให้ automation / AI มาช่วย generate และ review policy ได้ — ซึ่งต้องเป็น declarative file
- ผมอยากใช้ Tailscale client ตัวจริง เพราะ ecosystem มันโตกว่า ทั้งแอป iOS/Android, exit node, MagicDNS, SSH
นี่ไม่ได้แปลว่า NetBird แย่นะครับ — มันเป็นเรื่องของ trade-off ที่ตรงกับ workflow ของผม มากกว่า
Headscale คืออะไร
Headscale เป็น open-source implementation ของ Tailscale control server เขียนด้วย Go เป็น single binary ตัวเดียว หน้าที่ของมันคือทำ coordination — เก็บ registry ของ node, แจก key, กระจาย ACL policy และช่วย peer หากันเพื่อสร้าง WireGuard tunnel แบบ P2P
จุดสำคัญคือ Headscale ใช้ client ของ Tailscale ตัวจริง — คุณติดตั้ง tailscale ตามปกติ แค่ชี้ --login-server มาที่ Headscale ของคุณเองแทนที่จะเป็น cloud ของ Tailscale
tailscale up --login-server https://headscale.example.com| Headscale | NetBird | Tailscale (SaaS) | |
|---|---|---|---|
| Client | Tailscale ตัวจริง | NetBird client | Tailscale ตัวจริง |
| Control plane | Self-host (Go binary) | Self-host | Proprietary cloud |
| Open-source server | Yes (BSD-3) | Yes (AGPLv3) | No |
| ACL | HuJSON as-code | UI Dashboard | HuJSON as-code |
| UI ในตัว | ไม่มี (CLI) | มี Dashboard | มี Dashboard |
| เหมาะกับ | GitOps / automation | ทีมที่ชอบ UI | คนที่ไม่อยาก self-host |
Deploy Headscale บน Cloud
ผม deploy Headscale ด้วย Docker แล้ววางไว้หลัง nginx (TLS) บน domain ของตัวเอง — สมมติว่าเป็น headscale.example.com
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ตัว nginx ทำหน้าที่ terminate TLS แล้ว reverse proxy เข้า 127.0.0.1:8080 — ข้อควรระวังคือ Headscale ใช้ทั้ง HTTP ปกติและ long-lived connection สำหรับ control channel เพราะฉะนั้นอย่าลืม config nginx ให้ไม่ตัด connection ทิ้งเร็วเกินไป
จากนั้นสร้าง user (namespace) และ pre-auth key สำหรับ enroll เครื่อง:
headscale users create homelabheadscale preauthkeys create --user homelab --reusable --expiration 24hSubnet Router — เชื่อมทั้ง LAN เข้า mesh
หัวใจของ setup ผมคือ subnet router — แทนที่จะลง client บนทุกเครื่องใน LAN ผมให้เครื่องตัวหนึ่งทำหน้าที่ advertise subnet ของ LAN ทั้งวงเข้าไปใน mesh เครื่องอื่นใน Tailnet ก็จะ route หา subnet นั้นผ่าน router ตัวนั้นได้
tailscale up \ --login-server https://headscale.example.com \ --advertise-routes=192.168.0.0/24 \ --accept-routesฝั่ง Headscale ต้อง approve route ที่ถูก advertise มาก่อน ถึงจะใช้งานได้จริง:
headscale routes listheadscale routes enable -r <route-id>ด้วยวิธีนี้เราสามารถมี subnet router หลายตัวเพื่อเชื่อมหลาย site เข้าด้วยกัน เช่น site สำนักงาน, สาขาอื่น, หรือ VPC บน cloud — แต่ละ site วาง router หนึ่งตัว advertise subnet ของตัวเองเข้า mesh เดียวกัน
ACL เป็น code — จุดที่ผมตามหา
นี่คือเหตุผลหลักที่ย้ายมา Headscale ครับ — ACL policy เป็นไฟล์ HuJSON (JSON ที่ใส่ comment ได้) ที่ผมเอาไปอยู่ใน git ได้เลย
สมมติผมต้องการ one-way access — ให้ฝั่ง site A เข้าถึง subnet ของ site B ได้ แต่ไม่ให้ย้อนกลับ ตัวอย่าง policy แบบง่ายๆ:
{ "tagOwners": { "tag:site-a": ["admin"], "tag:site-b": ["admin"], }, "acls": [ // site A -> site B (ทางเดียว) { "action": "accept", "src": ["tag:site-a"], "dst": ["10.0.20.0/24:*"], }, ],}เพราะไม่มี rule ให้ tag:site-b เป็น src ที่วิ่งกลับมาหา site A การเข้าถึงจึงเป็น one-way ตามที่ออกแบบ — และทั้งหมดนี้อยู่ในไฟล์เดียวที่ review ผ่าน merge request ได้
ใช้งานหลักของผม — Site-to-Site VPN
จริงๆ แล้ว use case หลักที่ผมใช้ Headscale ไม่ใช่การให้ user แต่ละคนลง client เข้า network — แต่เป็น site-to-site VPN เชื่อม network ของหลาย site (สำนักงานใหญ่, สาขา, VPC บน cloud) ให้คุยกันได้เหมือนอยู่วงเดียวกัน
วิธีคิดง่ายๆ คือ แต่ละ site วาง subnet router หนึ่งตัว ที่ advertise subnet ของ site นั้นเข้า mesh จากนั้น subnet router ของแต่ละ site จะสร้าง WireGuard tunnel แบบ P2P ตรงถึงกัน — traffic ระหว่าง site วิ่งตรง ไม่ต้องวนผ่าน VPN gateway กลางเหมือน hub-and-spoke แบบเดิม
จากภาพจะเห็น 2 ระนาบที่แยกกันชัดเจน:
- Control plane (เส้นประ) — subnet router ทุกตัวคุยกับ Headscale เพื่อรับ config, key และ ACL policy เท่านั้น
- Data plane (เส้นเขียว) — traffic จริงวิ่งเป็น WireGuard tunnel ตรงระหว่าง site ที่เข้ารหัส ไม่ผ่าน Headscale และไม่ผ่าน gateway กลาง
ข้อดีของการทำ site-to-site แบบนี้:
- ไม่มีคอขวดตรงกลาง — traffic ระหว่าง site วิ่งตรง latency ต่ำ ไม่ต้องวนผ่าน server กลางเหมือน OpenVPN hub-and-spoke
- เพิ่ม site ใหม่ง่าย — แค่ deploy subnet router อีกตัว advertise subnet เข้ามา แล้ว approve route ก็เชื่อมเข้า mesh ได้เลย
- คุม access ได้ละเอียด — จะให้ site ไหนเข้าถึง site ไหนได้บ้างก็เขียนใน ACL (as-code) อย่างเดียว เช่นที่ทำ one-way access ไว้ด้านบน
สรุป — เลือกตัวไหนดี
ถ้าให้สรุปตรงๆ จากที่ใช้มาทั้งสองตัว:
- เลือก NetBird ถ้าอยากได้ open-source เต็มตัวทั้ง client+server, อยากได้ UI จัดการ access control และทีมไม่ได้อยากทำทุกอย่างเป็น code
- เลือก Headscale ถ้าอยากใช้ Tailscale client ตัวจริง + ecosystem ของมัน และอยากได้ ACL as-code ที่อยู่ใน git ให้ automation จัดการ — แลกกับการไม่มี UI ในตัว
สำหรับผมในจังหวะนี้ Headscale ตอบโจทย์กว่า เพราะ workflow ผมเป็น GitOps + automation หมด แต่ถ้าวันหนึ่งทีมโตขึ้นและต้องการ UI ให้คนอื่นจัดการ policy NetBird ก็ยังเป็นตัวเลือกที่ดีอยู่เสมอ