diff options
| author | Your Name <you@example.com> | 2026-02-08 20:23:31 +0800 |
|---|---|---|
| committer | Your Name <you@example.com> | 2026-02-08 20:23:31 +0800 |
| commit | c0e80820010cc5b0fdea1eb69bafef7f575e3201 (patch) | |
| tree | 94b8be5284bec668e1911bbe65e1b215ccd7657a | |
| -rwxr-xr-x | client.sh | 44 | ||||
| -rw-r--r-- | docs | 37 | ||||
| -rwxr-xr-x | server.sh | 151 |
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 | ||
| 6 | set -e | ||
| 7 | |||
| 8 | die() { echo "Error: $1" >&2; exit 1; } | ||
| 9 | info() { echo " $1"; } | ||
| 10 | |||
| 11 | [[ $EUID -eq 0 ]] || die "Must run as root" | ||
| 12 | |||
| 13 | SERVER_IP="${1:?Usage: $0 <server_ip> <auth_key>}" | ||
| 14 | AUTH_KEY="${2:?Usage: $0 <server_ip> <auth_key>}" | ||
| 15 | LOGIN_SERVER="http://${SERVER_IP}:8080" | ||
| 16 | |||
| 17 | echo "=== Installing Tailscale Client ===" | ||
| 18 | echo "Server: $LOGIN_SERVER" | ||
| 19 | echo "" | ||
| 20 | |||
| 21 | # 1. Install tailscale | ||
| 22 | if command -v tailscale &>/dev/null; then | ||
| 23 | info "Tailscale already installed: $(tailscale version | head -1)" | ||
| 24 | else | ||
| 25 | info "Installing tailscale..." | ||
| 26 | dnf install -y tailscale | ||
| 27 | fi | ||
| 28 | |||
| 29 | # 2. Start tailscaled | ||
| 30 | info "Starting tailscaled..." | ||
| 31 | systemctl enable --now tailscaled | ||
| 32 | |||
| 33 | # 3. Register with headscale | ||
| 34 | info "Registering with headscale at $LOGIN_SERVER..." | ||
| 35 | tailscale up --login-server "$LOGIN_SERVER" --authkey "$AUTH_KEY" | ||
| 36 | |||
| 37 | # 4. Show status | ||
| 38 | echo "" | ||
| 39 | echo "=== Connected ===" | ||
| 40 | tailscale status | ||
| 41 | echo "" | ||
| 42 | echo "To disconnect: tailscale down" | ||
| 43 | echo "To switch server: tailscale down && tailscale up --login-server http://<new_ip>:8080 --authkey <key> --force-reauth" | ||
| 44 | echo "To remove: tailscale down && systemctl disable --now tailscaled && dnf remove -y tailscale" | ||
| @@ -0,0 +1,37 @@ | |||
| 1 | # Headscale | ||
| 2 | # exercise to assert that it works | ||
| 3 | |||
| 4 | ## 1. Create VMs | ||
| 5 | python3 vm.py c mk testvm2-1 --image fedora42 --vcpu 6 --ram 4 --auto-download | ||
| 6 | python3 vm.py c mk testvm2-2 --image fedora42 --vcpu 6 --ram 4 --auto-download | ||
| 7 | |||
| 8 | ## 2. Check available public IPs | ||
| 9 | python3 vm.py n ipv4 --pool | ||
| 10 | |||
| 11 | ## 3. Attach public IP to server VM | ||
| 12 | python3 vm.py n ipv4 --attach 37.27.166.243 testvm2-1 | ||
| 13 | |||
| 14 | ## 4. SCP install.sh to server VM and run it | ||
| 15 | scp -i ~/k/k1 -o StrictHostKeyChecking=no /root/hypervisor/vm-claude/headscale/install.sh user@<testvm2-1-private-ip>:/tmp/install.sh | ||
| 16 | sshi 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 | ||
| 21 | scp -i ~/k/k1 -o StrictHostKeyChecking=no /root/hypervisor/vm-claude/headscale/client.sh user@<testvm2-2-private-ip>:/tmp/client.sh | ||
| 22 | sshi 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 | ||
| 25 | sshi testvm2-2.i "ping -c 3 100.64.0.1" | ||
| 26 | |||
| 27 | ## Cleanup | ||
| 28 | python3 vm.py n ipv4 --detach 37.27.166.243 testvm2-1 | ||
| 29 | echo "y" | python3 vm.py c rm testvm2-1 | ||
| 30 | echo "y" | python3 vm.py c rm testvm2-2 | ||
| 31 | |||
| 32 | ## Useful commands on the headscale server VM | ||
| 33 | headscale node list # list all registered nodes | ||
| 34 | headscale users list # list users | ||
| 35 | headscale preauthkeys create --user <USER_ID> --expiration 2160h --reusable # new auth key | ||
| 36 | headscale preauthkeys list --user <USER_ID> # list auth keys | ||
| 37 | curl 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 | ||
| 9 | set -e | ||
| 10 | |||
| 11 | die() { echo "Error: $1" >&2; exit 1; } | ||
| 12 | info() { echo " $1"; } | ||
| 13 | |||
| 14 | [[ $EUID -eq 0 ]] || die "Must run as root" | ||
| 15 | |||
| 16 | SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" | ||
| 17 | PUBLIC_IP="${1:?Usage: $0 <public_ip>}" | ||
| 18 | |||
| 19 | HEADSCALE_VERSION="0.28.0" | ||
| 20 | ARCH="amd64" | ||
| 21 | |||
| 22 | echo "=== Installing Headscale ===" | ||
| 23 | echo "Public IP: $PUBLIC_IP" | ||
| 24 | echo "" | ||
| 25 | |||
| 26 | # 1. Download and install binary from GitHub releases (no RPM exists, only deb and bare binary) | ||
| 27 | info "Downloading headscale v${HEADSCALE_VERSION}..." | ||
| 28 | curl -fsSL -o /var/tmp/headscale "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${ARCH}" | ||
| 29 | install -m 755 /var/tmp/headscale /usr/local/bin/headscale | ||
| 30 | rm -f /var/tmp/headscale | ||
| 31 | info "Installed: $(headscale version 2>/dev/null | head -1)" | ||
| 32 | |||
| 33 | # 2. Create headscale user/group (same as what the deb postinst does) | ||
| 34 | info "Creating headscale user/group..." | ||
| 35 | groupadd --force --system headscale | ||
| 36 | useradd --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) | ||
| 39 | info "Creating directories..." | ||
| 40 | mkdir -p /etc/headscale /var/lib/headscale /var/run/headscale | ||
| 41 | chown 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) | ||
| 45 | info "Installing systemd service..." | ||
| 46 | cat > /etc/systemd/system/headscale.service <<'EOF' | ||
| 47 | [Unit] | ||
| 48 | After=network.target | ||
| 49 | Description=headscale coordination server for Tailscale | ||
| 50 | X-Restart-Triggers=/etc/headscale/config.yaml | ||
| 51 | |||
| 52 | [Service] | ||
| 53 | Type=simple | ||
| 54 | User=headscale | ||
| 55 | Group=headscale | ||
| 56 | ExecStart=/usr/local/bin/headscale serve | ||
| 57 | ExecReload=/usr/bin/kill -HUP $MAINPID | ||
| 58 | Restart=always | ||
| 59 | RestartSec=5 | ||
| 60 | |||
| 61 | WorkingDirectory=/var/lib/headscale | ||
| 62 | ReadWritePaths=/var/lib/headscale | ||
| 63 | |||
| 64 | AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_CHOWN | ||
| 65 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_CHOWN | ||
| 66 | LockPersonality=true | ||
| 67 | NoNewPrivileges=true | ||
| 68 | PrivateDevices=true | ||
| 69 | PrivateMounts=true | ||
| 70 | PrivateTmp=true | ||
| 71 | ProcSubset=pid | ||
| 72 | ProtectClock=true | ||
| 73 | ProtectControlGroups=true | ||
| 74 | ProtectHome=true | ||
| 75 | ProtectHostname=true | ||
| 76 | ProtectKernelLogs=true | ||
| 77 | ProtectKernelModules=true | ||
| 78 | ProtectKernelTunables=true | ||
| 79 | ProtectProc=invisible | ||
| 80 | ProtectSystem=strict | ||
| 81 | RemoveIPC=true | ||
| 82 | RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX | ||
| 83 | RestrictNamespaces=true | ||
| 84 | RestrictRealtime=true | ||
| 85 | RestrictSUIDSGID=true | ||
| 86 | RuntimeDirectory=headscale | ||
| 87 | RuntimeDirectoryMode=0750 | ||
| 88 | StateDirectory=headscale | ||
| 89 | StateDirectoryMode=0750 | ||
| 90 | SystemCallArchitectures=native | ||
| 91 | SystemCallFilter=@chown | ||
| 92 | SystemCallFilter=@system-service | ||
| 93 | SystemCallFilter=~@privileged | ||
| 94 | UMask=0077 | ||
| 95 | |||
| 96 | [Install] | ||
| 97 | WantedBy=multi-user.target | ||
| 98 | EOF | ||
| 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) | ||
| 103 | info "Downloading example config..." | ||
| 104 | curl -fsSL -o /var/tmp/config-example.yaml "https://raw.githubusercontent.com/juanfont/headscale/v${HEADSCALE_VERSION}/config-example.yaml" | ||
| 105 | cp /var/tmp/config-example.yaml /etc/headscale/config.yaml | ||
| 106 | rm -f /var/tmp/config-example.yaml | ||
| 107 | sed -i "s|server_url: http://127.0.0.1:8080|server_url: http://${PUBLIC_IP}:8080|" /etc/headscale/config.yaml | ||
| 108 | sed -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 | ||
| 110 | sed -i 's| - https://controlplane.tailscale.com/derpmap/default| # - https://controlplane.tailscale.com/derpmap/default|' /etc/headscale/config.yaml | ||
| 111 | sed -i 's| auto_update_enabled: true| auto_update_enabled: false|' /etc/headscale/config.yaml | ||
| 112 | chown -R headscale:headscale /etc/headscale | ||
| 113 | |||
| 114 | # 6. Start headscale | ||
| 115 | info "Enabling and starting headscale..." | ||
| 116 | systemctl daemon-reload | ||
| 117 | systemctl enable --now headscale | ||
| 118 | |||
| 119 | # 7. Create default user and reusable auth key | ||
| 120 | info "Creating default user..." | ||
| 121 | headscale users create default 2>/dev/null || true | ||
| 122 | USER_ID=$(headscale users list -o json 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)[0]['id'])") | ||
| 123 | info "Creating auth key (reusable, 90 days)..." | ||
| 124 | AUTH_KEY=$(headscale preauthkeys create --user "$USER_ID" --expiration 2160h --reusable) | ||
| 125 | |||
| 126 | # 8. Register this machine as a tailscale client | ||
| 127 | info "Installing tailscale client on this machine..." | ||
| 128 | dnf install -y tailscale | ||
| 129 | systemctl enable --now tailscaled | ||
| 130 | tailscale up --login-server "http://${PUBLIC_IP}:8080" --authkey "$AUTH_KEY" | ||
| 131 | |||
| 132 | echo "" | ||
| 133 | echo "=== Headscale installed ===" | ||
| 134 | echo "" | ||
| 135 | echo "Health check:" | ||
| 136 | echo " curl http://${PUBLIC_IP}:8080/health" | ||
| 137 | echo "" | ||
| 138 | echo "Auth key (reusable, 90 days):" | ||
| 139 | echo " $AUTH_KEY" | ||
| 140 | echo "" | ||
| 141 | echo "Connect a client:" | ||
| 142 | echo " sudo ./client.sh ${PUBLIC_IP} ${AUTH_KEY}" | ||
| 143 | echo "" | ||
| 144 | echo "Create a new user:" | ||
| 145 | echo " headscale users create <username>" | ||
| 146 | echo "" | ||
| 147 | echo "Create an auth key:" | ||
| 148 | echo " headscale preauthkeys create --user <USER_ID> --expiration 2160h --reusable" | ||
| 149 | echo "" | ||
| 150 | echo "List nodes:" | ||
| 151 | echo " headscale node list" | ||
