本文Ivon討論如何架設Headscale控制伺服器 + Headplane網頁控制面板。
之前分享過Tailscale,這是一款讓你實現內網穿透功能的VPN服務,可以讓個裝置組成虛擬區網,互相連線。
雖然Tailscale客戶端是開源的方案,不過它有一個關鍵點在於,伺服端是閉源產品。連線需要經過Tailscale公司的伺服器,需要註冊帳號才能使用服務。而那些伺服端的設施,是Tailscale公司的商業機密。
如果我們想要完全掌控連線過程呢,不想要在Tailscale官網註冊帳號呢?其實呀,Tailscale客戶端是允許你更換連線伺服器的喔!
來自架一個「Headscale」伺服器吧!它能夠取代Tailscale公司的控制伺服器與中繼伺服器,讓你能自主掌控Tailscale網路。
即使你已經註冊了Tailscale帳號,也能夠快速地切換到Headscale的體系。MagicDNS設定的當的話能夠直接沿用。
這個伺服器軟體的名字很有趣,用Head映襯Tail,可以說是對應「妳是主人我是僕」的關係吧!
1. 預期的連線架構#
Tailscale原本的連線是這樣,所有Tailscale的客戶端,登入Tailscale的帳號,加入名為Tailnet的虛擬區網,需要依賴Tailscale公司的控制伺服器才能找到彼此。當P2P失敗的時候,relay伺服器(DERP)還要承擔傳輸流量的功能。
在我們自架Headscale後,所有Tailnet的連線過程都掌握在我們手裡。只需要更換控制伺服器的部分,Tailscale客戶端可以繼續沿用。它充當控制伺服器與relay伺服器(DERP)的角色。你就從此與Tailscale公司的伺服器無關了。
2. 你應該自架Headscale嗎?#
跟Tailscale免費方案限制100台裝置比起來,Headscale能夠加入的裝置理論上是無上限,就看你的伺服器撐不撐得住。
最好準備一台獨立的伺服器跑Headscale。考慮到跑Headscale伺服器要一直在線上負責處理連線。租個VPS來架會比較妥當,並且要有自訂網域與SSL。Headscale很輕量,不到1GB RAM就可以部署。流量部份,如果Tailscale成功讓裝置P2P,不依賴relay伺服器,則大部分網路傳輸流量是不會經過Headscale所在的伺服器的,因此這個不用太在意。另外要注意延遲問題,最好選離台灣近一點的國家的機房。
再者,要想想維護容易度。Headscale是第三方開發者Juan Font維護的專案,不是Tailscale公司開發的。根據官網FAQ,作者說這個專案不適合用於大型組織,而是適合個人用戶存取自家服務使用。
Headscale功能不保證能像Tailscale的公司提供的產品一樣完整,使用起來沒有那麼無腦了。
Headscale只提供基本的指令控制面板,做到最小相容Tailscale客戶端所使用的協定。Headscale本身就是第三方專案了,還得靠第三方開發的方案才有網頁管理界面。
總體來說,自架Headscale並不如Tailscale公司提供的控制面板好上手,你要自己處理一切麻煩事。如果日後要進一步擴張你的私人Tailscale虛擬區域網路,甚至邀請不太懂技術的用戶加入網路的話,要處理的問題就更多了。
但若你要自主可控的Tailscale連線環境,就學著適應吧。
3. 用Docker部署Headscale#
建立存放資料的目錄
cd ~
mkdir -p ./headscale/{config,lib}
cd ./headscale- 建立docker-compose
vim docker-compose.yaml- 範本如下,大部分維持預設。ports冒號左邊的通訊埠可以任意更改。這裡加入一個STUN的3478通訊埠協助P2P hole punching。
services:
headscale:
image: docker.io/headscale/headscale:latest
restart: unless-stopped
container_name: headscale
read_only: true
tmpfs:
- /var/run/headscale
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:9090:9090"
- "3478:3478/udp"
volumes:
- ./config:/etc/headscale:ro
- ./lib:/var/lib/headscale
command: serve
healthcheck:
test: ["CMD", "headscale", "health"]- 從Github下載Headscale範本設定檔
wget -O ./config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml - 編輯
config.yaml
vim ./config/config.yaml- 在config.yaml填入你的網域。我這裡是分配一個子網域的方式來處理Headscale的連線。例如
https://headscale.example.com
server_url: https://headscale.example.com- 若上面的docker-compose有修改預設的通訊埠,設定監聽的網址為
0.0.0.0
listen_addr: 0.0.0.0:8080- 在dns這一段,設定建立MagicDNS的規則,例如在你的網域前面加上一串子網域,裝置加入後網址就會變成
linux-machine.tailscale.example.com。你可以打ssh use@linux-machine連線。
dns:
magic_dns: true
base_domain: tailscale.example.com- (選擇性)在derp這一段,啟用DERP伺服器,也就是relay伺服器。Tailscale客戶端在P2P失敗之後會嘗試去連Tailscale公司的relay伺服器,這可以保留以備不時之需。若將url填入空陣列,Tailscale就只能使用我們自架的DERP了。DERP預設使用443進行HTTPS連線,但後面Nginx已經佔用了。讓DERP監聽另一個通訊埠,再由Nginx轉發。
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
stun_listen_addr: "0.0.0.0:3478"
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 24h- 編輯完退出。啟動容器
docker compose up -d- 檢查伺服器是否上線,應該會回傳success。
curl http://127.0.0.1:8080/health- 用
docker logs headscale指令檢視伺服器輸出日誌。
4. 建立Nginx反向代理#
我這裡是分配一個子網域的方式來處理Headscale的連線。例如
https://headscale.example.com,透過Nginx代理到伺服器上Headscale的http://127.0.0.1:8080通訊埠。編輯
/etc/nginx/nginx.conf,加入這段
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}- 再編輯
/etc/nginx/sites-available/headscale,加入以下內容
server {
listen 80;
server_name <子網域>;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name <子網域>;
ssl_certificate /etc/letsencrypt/live/<子網域>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<子網域>/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080; # Docker容器的通訊埠
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
proxy_buffering off;
}
} - 重新載入nginx設定檔
sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx- 若有啟用防火牆,允許STUN通過
sudo ufw allow 3478/udp5. 初始化Headscale管理員帳號#
Headscale註冊帳號的機制很基本,就是建立一個管理員帳號,然後加入節點。沒有外部帳號註冊機制。
- 進入容器
docker exec -it headscale \
headscale help- 新增任意使用者名稱
docker exec -it headscale \
headscale users create <使用者名稱>6. 給Headscale安裝Headplane網頁管理界面#
(選擇性步驟)
不幸的是,Headscale沒有網頁管理界面,這個也得靠第三方開發的方案。
這裡我採用Aarnav Tale開發的「Headplane」方案,它是各方方案中相對完備的管理面板。
- 在Headscale建立一個API金鑰,用於給Headplane存取
docker exec headscale \
headscale apikeys create- 增存放資料的目錄,我將它與Headscale放在一起
cd ~/headscale
mkdir -p ./{headplane-config,headplane-data}- 編輯剛剛新增的
docker-compose,額外加入一個新的容器服務。Headplane同時要讀取Headscale的設定檔
services:
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
ports:
- '127.0.0.1:3000:3000'
volumes:
- './headplane-config/config.yaml:/etc/headplane/config.yaml'
- './headplane-data:/var/lib/headplane'- 下載Headplane的範本設定檔
wget -O ./headplane-config/config.yaml https://raw.githubusercontent.com/tale/headplane/refs/heads/main/config.example.yaml編輯
headplane-config/config.yaml填入Headscale伺服器的公網網址,子網域
headscale:
url: "https://headscale.example.com"- 填入Headscale 的 API Key
headscale_api_key: ""- 用
openssl rand -base64 32 | head -c 32 ; echo指令產生一個長隨機字串作為cookie secret
server:
cookie_secret: ""- 重新啟動容器
docker compose down
docker compose up -d新增一個子網域,例如
https://headplane.exmaple.com用
docker logs headplane指令檢視伺服器輸出日誌。設定Nginix服務
/etc/nginx/sites-available/headplane,填入以下內容。透過子網域連向Headplane的http://127.0.0.1:3000
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name <子網域>;
ssl_certificate /etc/letsencrypt/live/<子網域>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<子網域>/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://localhost:8080/; # Headscale的通訊埠
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_buffering off;
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;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
location /admin/ {
proxy_pass http://localhost:3000; # Headplane通訊埠
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_redirect http:// https://;
proxy_buffering off;
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;
}
}- 重新載入nginx設定檔
sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx- 開啟
https://headplane.exmaple.com/admin登入,輸入API金鑰。
註:目前版本似乎有bug無法連線,參見https://github.com/tale/headplane/issues/82
7. 將Tailscale客戶端連線到Headscale伺服器#
目前還沒有所謂第三方開發的Tailscale客戶端,就沿用官方專案的就好,它允許你切換連線伺服器。要是哪天Tailscale公司吃相變難看,禁止使用第三方伺服器,到時候就會有fork出來了吧。
請先將Tailscale客戶端的帳號登出,斷開與Tailscale公司伺服器的連結。Headscale的資料與Tailscale公司無關,登出Tailscale之後,你在Tailscale公司伺服器的資料還是會在,日後要用再加回去就好。
Linux#
執行以下指令,連線到Headscale(註:若你是透過SSH操作,建議不要用Tailscale的IP登入,改用主機的真實IP,免得設定到一半斷線)
tailscale up --force-reauth --login-server https://網域點選畫面顯示的網址

按照網頁顯示的指令,在Headscale伺服器輸入金鑰
docker exec -it headscale \
headscale nodes register --key <金鑰> --user <使用者名稱>這樣就加入節點成功了。
在Headscale使用以下指令確認節點,以及虛擬區域IP。
docker exec -it headscale \
headscale nodes list- MagicDNS的網址依照節點的名稱決定,重新命名節點為你喜歡的名字
docker exec -it headscale \
headscale nodes rename <id> <新名稱>9 . 在Tailscale客戶端使用指令確認目前的連線
tailscale status
tailscale netcheckAndroid#
Android手機點右上角 Accounts → use an alternative server,填入Headscale伺服器網域

然後依照網頁指示在Headscale伺服器執行指令,加入節點。


