summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-02-08 20:23:31 +0800
committerYour Name <you@example.com>2026-02-08 20:23:31 +0800
commitc0e80820010cc5b0fdea1eb69bafef7f575e3201 (patch)
tree94b8be5284bec668e1911bbe65e1b215ccd7657a
-rwxr-xr-xclient.sh44
-rw-r--r--docs37
-rwxr-xr-xserver.sh151
3 files changed, 232 insertions, 0 deletions
diff --git a/client.sh b/client.sh
new file mode 100755
index 0000000..2118a73
--- /dev/null
+++ b/client.sh
@@ -0,0 +1,44 @@
1#!/bin/bash
2# Install tailscale and register with a headscale server
3# Copy to a VM and run: sudo ./client.sh <server_ip> <auth_key>
4# Example: sudo ./client.sh 37.27.166.243 hskey-auth-xxxx
5# macOS CLI: echo 'alias tailscale="/Applications/Tailscale.app/Contents/MacOS/Tailscale"' >> ~/.zshrc
6set -e
7
8die() { echo "Error: $1" >&2; exit 1; }
9info() { echo " $1"; }
10
11[[ $EUID -eq 0 ]] || die "Must run as root"
12
13SERVER_IP="${1:?Usage: $0 <server_ip> <auth_key>}"
14AUTH_KEY="${2:?Usage: $0 <server_ip> <auth_key>}"
15LOGIN_SERVER="http://${SERVER_IP}:8080"
16
17echo "=== Installing Tailscale Client ==="
18echo "Server: $LOGIN_SERVER"
19echo ""
20
21# 1. Install tailscale
22if command -v tailscale &>/dev/null; then
23 info "Tailscale already installed: $(tailscale version | head -1)"
24else
25 info "Installing tailscale..."
26 dnf install -y tailscale
27fi
28
29# 2. Start tailscaled
30info "Starting tailscaled..."
31systemctl enable --now tailscaled
32
33# 3. Register with headscale
34info "Registering with headscale at $LOGIN_SERVER..."
35tailscale up --login-server "$LOGIN_SERVER" --authkey "$AUTH_KEY"
36
37# 4. Show status
38echo ""
39echo "=== Connected ==="
40tailscale status
41echo ""
42echo "To disconnect: tailscale down"
43echo "To switch server: tailscale down && tailscale up --login-server http://<new_ip>:8080 --authkey <key> --force-reauth"
44echo "To remove: tailscale down && systemctl disable --now tailscaled && dnf remove -y tailscale"
diff --git a/docs b/docs
new file mode 100644
index 0000000..7384a99
--- /dev/null
+++ b/docs
@@ -0,0 +1,37 @@
1# Headscale
2# exercise to assert that it works
3
4## 1. Create VMs
5python3 vm.py c mk testvm2-1 --image fedora42 --vcpu 6 --ram 4 --auto-download
6python3 vm.py c mk testvm2-2 --image fedora42 --vcpu 6 --ram 4 --auto-download
7
8## 2. Check available public IPs
9python3 vm.py n ipv4 --pool
10
11## 3. Attach public IP to server VM
12python3 vm.py n ipv4 --attach 37.27.166.243 testvm2-1
13
14## 4. SCP install.sh to server VM and run it
15scp -i ~/k/k1 -o StrictHostKeyChecking=no /root/hypervisor/vm-claude/headscale/install.sh user@<testvm2-1-private-ip>:/tmp/install.sh
16sshi testvm2-1.i "sudo bash /tmp/install.sh 37.27.166.243"
17# Output includes the auth key, e.g.:
18# hskey-auth-JK4Q793swFSJ-owovbUSFU1T71UyRiywcrIgERltcWq14h6vXT2LIFFA1naYkKLLGfL8E46cgUTOQ
19
20## 5. SCP client.sh to client VM and run it
21scp -i ~/k/k1 -o StrictHostKeyChecking=no /root/hypervisor/vm-claude/headscale/client.sh user@<testvm2-2-private-ip>:/tmp/client.sh
22sshi testvm2-2.i "sudo bash /tmp/client.sh 37.27.166.243 <AUTH_KEY>"
23
24## 6. Verify - ping server's tailscale IP from client
25sshi testvm2-2.i "ping -c 3 100.64.0.1"
26
27## Cleanup
28python3 vm.py n ipv4 --detach 37.27.166.243 testvm2-1
29echo "y" | python3 vm.py c rm testvm2-1
30echo "y" | python3 vm.py c rm testvm2-2
31
32## Useful commands on the headscale server VM
33headscale node list # list all registered nodes
34headscale users list # list users
35headscale preauthkeys create --user <USER_ID> --expiration 2160h --reusable # new auth key
36headscale preauthkeys list --user <USER_ID> # list auth keys
37curl http://<PUBLIC_IP>:8080/health # health check
diff --git a/server.sh b/server.sh
new file mode 100755
index 0000000..41b88ce
--- /dev/null
+++ b/server.sh
@@ -0,0 +1,151 @@
1#!/bin/bash
2# Install and configure headscale locally
3# Copy this directory to a VM and run: sudo ./install.sh <public_ip>
4# Example: sudo ./install.sh 37.27.166.244
5# docs:
6# this script assumes the ip addresses is pointed to the current machine, and this script runs on 0.0.0.0
7# configuration is kinda manual cuz the official packaging is for .deb and i want rhel based system
8# fallback (Designated Encrypted Relay for Packets) is disabled. if NAT traversal fails, there will be no connection
9set -e
10
11die() { echo "Error: $1" >&2; exit 1; }
12info() { echo " $1"; }
13
14[[ $EUID -eq 0 ]] || die "Must run as root"
15
16SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
17PUBLIC_IP="${1:?Usage: $0 <public_ip>}"
18
19HEADSCALE_VERSION="0.28.0"
20ARCH="amd64"
21
22echo "=== Installing Headscale ==="
23echo "Public IP: $PUBLIC_IP"
24echo ""
25
26# 1. Download and install binary from GitHub releases (no RPM exists, only deb and bare binary)
27info "Downloading headscale v${HEADSCALE_VERSION}..."
28curl -fsSL -o /var/tmp/headscale "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${ARCH}"
29install -m 755 /var/tmp/headscale /usr/local/bin/headscale
30rm -f /var/tmp/headscale
31info "Installed: $(headscale version 2>/dev/null | head -1)"
32
33# 2. Create headscale user/group (same as what the deb postinst does)
34info "Creating headscale user/group..."
35groupadd --force --system headscale
36useradd --system --shell /usr/sbin/nologin --gid headscale --home-dir /var/lib/headscale --comment headscale headscale 2>/dev/null || true
37
38# 3. Create directories: /etc/headscale (config), /var/lib/headscale (data/db), /var/run/headscale (socket)
39info "Creating directories..."
40mkdir -p /etc/headscale /var/lib/headscale /var/run/headscale
41chown headscale:headscale /var/lib/headscale /var/run/headscale
42
43# 4. Install systemd service (from official packaging/systemd/headscale.service,
44# only change: ExecStart points to /usr/local/bin/headscale instead of /usr/bin/headscale)
45info "Installing systemd service..."
46cat > /etc/systemd/system/headscale.service <<'EOF'
47[Unit]
48After=network.target
49Description=headscale coordination server for Tailscale
50X-Restart-Triggers=/etc/headscale/config.yaml
51
52[Service]
53Type=simple
54User=headscale
55Group=headscale
56ExecStart=/usr/local/bin/headscale serve
57ExecReload=/usr/bin/kill -HUP $MAINPID
58Restart=always
59RestartSec=5
60
61WorkingDirectory=/var/lib/headscale
62ReadWritePaths=/var/lib/headscale
63
64AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN
65CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN
66LockPersonality=true
67NoNewPrivileges=true
68PrivateDevices=true
69PrivateMounts=true
70PrivateTmp=true
71ProcSubset=pid
72ProtectClock=true
73ProtectControlGroups=true
74ProtectHome=true
75ProtectHostname=true
76ProtectKernelLogs=true
77ProtectKernelModules=true
78ProtectKernelTunables=true
79ProtectProc=invisible
80ProtectSystem=strict
81RemoveIPC=true
82RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
83RestrictNamespaces=true
84RestrictRealtime=true
85RestrictSUIDSGID=true
86RuntimeDirectory=headscale
87RuntimeDirectoryMode=0750
88StateDirectory=headscale
89StateDirectoryMode=0750
90SystemCallArchitectures=native
91SystemCallFilter=@chown
92SystemCallFilter=@system-service
93SystemCallFilter=~@privileged
94UMask=0077
95
96[Install]
97WantedBy=multi-user.target
98EOF
99
100# 5. Install config — example config with two changes:
101# - server_url: http://<public_ip>:8080 (so clients know the public address)
102# - listen_addr: 0.0.0.0:8080 (listen on all interfaces, not just localhost)
103info "Downloading example config..."
104curl -fsSL -o /var/tmp/config-example.yaml "https://raw.githubusercontent.com/juanfont/headscale/v${HEADSCALE_VERSION}/config-example.yaml"
105cp /var/tmp/config-example.yaml /etc/headscale/config.yaml
106rm -f /var/tmp/config-example.yaml
107sed -i "s|server_url: http://127.0.0.1:8080|server_url: http://${PUBLIC_IP}:8080|" /etc/headscale/config.yaml
108sed -i "s|listen_addr: 127.0.0.1:8080|listen_addr: 0.0.0.0:8080|" /etc/headscale/config.yaml
109# Disable DERP relays — all nodes have public IPs, force direct WireGuard connections only
110sed -i 's| - https://controlplane.tailscale.com/derpmap/default| # - https://controlplane.tailscale.com/derpmap/default|' /etc/headscale/config.yaml
111sed -i 's| auto_update_enabled: true| auto_update_enabled: false|' /etc/headscale/config.yaml
112chown -R headscale:headscale /etc/headscale
113
114# 6. Start headscale
115info "Enabling and starting headscale..."
116systemctl daemon-reload
117systemctl enable --now headscale
118
119# 7. Create default user and reusable auth key
120info "Creating default user..."
121headscale users create default 2>/dev/null || true
122USER_ID=$(headscale users list -o json 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['id'])")
123info "Creating auth key (reusable, 90 days)..."
124AUTH_KEY=$(headscale preauthkeys create --user "$USER_ID" --expiration 2160h --reusable)
125
126# 8. Register this machine as a tailscale client
127info "Installing tailscale client on this machine..."
128dnf install -y tailscale
129systemctl enable --now tailscaled
130tailscale up --login-server "http://${PUBLIC_IP}:8080" --authkey "$AUTH_KEY"
131
132echo ""
133echo "=== Headscale installed ==="
134echo ""
135echo "Health check:"
136echo " curl http://${PUBLIC_IP}:8080/health"
137echo ""
138echo "Auth key (reusable, 90 days):"
139echo " $AUTH_KEY"
140echo ""
141echo "Connect a client:"
142echo " sudo ./client.sh ${PUBLIC_IP} ${AUTH_KEY}"
143echo ""
144echo "Create a new user:"
145echo " headscale users create <username>"
146echo ""
147echo "Create an auth key:"
148echo " headscale preauthkeys create --user <USER_ID> --expiration 2160h --reusable"
149echo ""
150echo "List nodes:"
151echo " headscale node list"