Skip to content

Instantly share code, notes, and snippets.

@fltd
Last active November 23, 2025 21:23
Show Gist options
  • Select an option

  • Save fltd/ba99e9ad3e700468de70df8aa5612145 to your computer and use it in GitHub Desktop.

Select an option

Save fltd/ba99e9ad3e700468de70df8aa5612145 to your computer and use it in GitHub Desktop.
Tailsman: Tailscale Manager for AsusWRT-Merlin

Tailsman: Tailscale Manager for AsusWRT-Merlin

Tailsman is a comprehensive, single-file shell script designed to simplify the installation, management, and maintenance of Tailscale on routers running AsusWRT-Merlin firmware. It provides simple commands to handle everything from initial installation and version management to starting, stopping, and automating the Tailscale service.

Features

  • Single-File Script: No dependencies besides standard router utilities (curl, tar, cru).
  • Simple Installation: A single install latest command downloads, installs, and starts the correct tailscaled daemon.
  • Automatic Firewall Management: Automatically applies necessary iptables rules to allow traffic on the Tailscale interface. crucially, it integrates with firewall-start to ensure these rules persist even when the router's firewall reloads.
  • Bootstrapped Time Sync: Includes robust NTP checks to ensure the system clock is accurate before starting Tailscale, fixing common connection issues on routers without battery-backed clocks.
  • Version Management: Install any specific version or latest. The upgrade command is a simple alias for install latest.
  • Smart CLI Matching: Automatically downloads a tailscale CLI version that matches the running tailscaled daemon, preventing client/daemon version mismatch warnings.
  • Simple Service Management: Easy commands to start, stop, and restart the service.
  • Flexible Automation: An enable command integrates the script with your router's startup process and cron for auto-restarts, with an optional flag (--with-auto-upgrade) to enable daily auto-updates.
  • Resilient Networking: Uses a bootstrap DNS for robust installation and upgrades, but fast system DNS for normal CLI commands. Includes a --manual-resolve override for troubleshooting.
  • Clean Disabling: A disable command cleanly removes all system hooks, firewall rules, and cron jobs.
  • Built-in Help: A detailed help command explains all available functions.

Prerequisites

  1. An Asus router running a recent version of AsusWRT-Merlin firmware.
  2. The JFFS partition must be enabled and formatted on your router.
  3. SSH access to your router must be enabled.
  4. An active internet connection on the router for downloading Tailscale.

Installation

Follow these steps to install Tailsman on your router.

  1. SSH into your router:

    ssh your_router_username@your_router_ip
  2. Create the directory for the script:

    mkdir -p /jffs/tailscale
  3. Create the script file: Use a text editor like vi or nano to create the script file.

    vi /jffs/tailscale/tailsman.sh

    Press i to enter insert mode, then paste the entire content of the tailsman.sh script. After pasting, press Esc, then type :wq and press Enter to save and quit.

  4. Make the script executable:

    chmod +x /jffs/tailscale/tailsman.sh
  5. Install Tailscale: This will download the latest tailscaled binary and start the service for the first time.

    /jffs/tailscale/tailsman.sh install latest
  6. Enable system automation: This is the key step to integrate Tailsman with your router. It will set up the necessary startup scripts, firewall hooks, and cron jobs. Choose one of the following options:

    Option A: Enable with Auto-Upgrade (Recommended) This will also check for a new Tailscale version every day at 4:06 AM.

    /jffs/tailscale/tailsman.sh enable --with-auto-upgrade

    Option B: Enable without Auto-Upgrade This will ensure Tailscale starts on boot and stays running, but will not automatically upgrade it.

    /jffs/tailscale/tailsman.sh enable
  7. Authenticate and configure Tailscale: Run the tailscale up command to log in and connect your router to your Tailnet.

    # Replace 192.168.50.0/24 with your actual LAN subnet
    /jffs/tailscale/tailsman.sh tailscale up --accept-routes --advertise-routes=192.168.50.0/24

    Follow the authentication URL that appears in your terminal. After authenticating, remember to approve the advertised routes in the Tailscale Admin Console.

Your router is now running Tailscale and is configured to automatically start it on boot.

Usage

You can manage the entire lifecycle of Tailscale using the tailsman.sh script.

Usage: /jffs/tailscale/tailsman.sh <command> [options]

Service Commands

  • start Starts the tailscaled daemon and applies firewall rules. It will fail with a helpful error if tailscaled is not installed via the install command first.

  • stop Stops the running tailscaled daemon.

  • restart Stops, then starts the tailscaled daemon.

  • install <ver|latest> Installs a specific version or the latest version of tailscaled. This is the only command that installs or modifies the daemon. It will stop the service, download the binary, and restart the service.

  • upgrade Alias for install latest. Checks for and installs the latest version if not already installed.

  • tailscale [args...] Executes a command with the tailscale CLI tool. The script ensures the CLI tool's version matches the running tailscaled daemon to prevent version mismatch errors.

    • Example: /jffs/tailscale/tailsman.sh tailscale status
    • Example: /jffs/tailscale/tailsman.sh tailscale set --hostname=my-merlin-router
    • --manual-resolve: If passed as the first argument (e.g., ... tailscale --manual-resolve status), it forces the script to use its bootstrap DNS resolver instead of the system's. Useful if system DNS is failing.

Automation Control

  • enable [--with-auto-upgrade] Installs the startup scripts, firewall persistence hooks, and cron jobs for full automation.

    • If --with-auto-upgrade is provided, a daily cron job is added to automatically run upgrade.
    • If run without the flag, any existing auto-upgrade cron job is removed.
  • disable Removes all system startup scripts, firewall hooks, and cron jobs created by enable. This stops all automation. The script and binaries are kept for manual use.

  • sentinel This command is used internally by the cron job. It checks if tailscaled is running and starts it if it has crashed or stopped. It also performs a daily time-sync check.

Other Commands

  • help Shows a detailed help message explaining all commands.

How Automation Works

When you run the enable command, Tailsman intelligently integrates with your router's OS:

  • Service Startup: An entry is added to /jffs/scripts/services-start, which is a script Merlin runs after all other system services have been started at boot. This ensures tailsman.sh start is automatically executed.
  • Firewall Persistence: Entries are added to /jffs/scripts/firewall-start. In AsusWRT-Merlin, the firewall is frequently reloaded (e.g., when VPNs connect or WAN state changes). Tailsman ensures that the iptables rules allowing traffic on the tailscale0 interface are re-applied immediately after every reload.
  • Sentinel Cron Job: A cron job is added that runs every minute. This job checks if the tailscaled process is alive and restarts it if it's not, ensuring high availability.
  • Auto-Upgrade Cron Job: Another cron job is added only if you used the --with-auto-upgrade flag. It runs once daily (at 4:06 AM local time) to check for new Tailscale versions and automatically upgrade the daemon.

Disabling Automation

To disable all automation and stop Tailsman from running automatically on your router, simply run:

/jffs/tailscale/tailsman.sh disable
#!/bin/sh
# Tailsman: Tailscale Manager for AsusWRT-Merlin
# A single script to install, manage, and maintain Tailscale.
TAILSCALE_DIR="/jffs/tailscale"
TAILSCALED_BIN="${TAILSCALE_DIR}/tailscaled"
STATE_FILE="${TAILSCALE_DIR}/tailscaled.state"
TAILSCALE_TMP_BIN="/tmp/home/root/tailscale"
ARCH="arm"
SCRIPT_PATH="/jffs/tailscale/tailsman.sh"
TAILSCALE_IF="tailscale0"
# --- Bootstrap Network Configuration ---
BOOTSTRAP_DNS_SERVER="1.1.1.1"
TAILSCALE_PKGS_HOST="pkgs.tailscale.com"
BOOTSTRAP_NTP_SERVER="time.cloudflare.com" # NTP server for initial time sync
# Global variable to cache the resolved IP address within a single script run
TAILSCALE_PKGS_IP=""
# Global flag to control DNS behavior
# 0 = Use system DNS (fast, for 'tailscale' command)
# 1 = Use bootstrap DNS (resilient, for 'install', 'upgrade', or --manual-resolve)
TAILSMAN_USE_BOOTSTRAP_DNS=0
log() {
logger -t "tailsman" "$1" -p user.notice
echo "tailsman: $1" >&2
}
is_tailscaled_running() {
if [ -n "$(pidof tailscaled)" ]; then
return 0
else
return 1
fi
}
apply_tailscale_firewall_rules() {
# Idempotent: only insert if not already present
if iptables -C INPUT -i "${TAILSCALE_IF}" -j ACCEPT 2>/dev/null; then
log "Firewall rules for ${TAILSCALE_IF} already present; skipping."
return
fi
log "Applying firewall rules for ${TAILSCALE_IF}..."
iptables -I INPUT 1 -i "${TAILSCALE_IF}" -j ACCEPT
iptables -I FORWARD 1 -i "${TAILSCALE_IF}" -j ACCEPT
iptables -I FORWARD 1 -o "${TAILSCALE_IF}" -j ACCEPT
}
bootstrap_time_sync() {
# This function *always* uses bootstrap DNS, as it's meant to fix a broken state.
if [ "$(date +%Y)" -lt 2025 ]; then
log "System time is incorrect. Attempting bootstrap NTP sync using bootstrap DNS ${BOOTSTRAP_DNS_SERVER}..."
local ntp_server_ip
ntp_server_ip=$(nslookup "$BOOTSTRAP_NTP_SERVER" "$BOOTSTRAP_DNS_SERVER" | grep Address | head -n 2 | tail -n 1 | cut -d: -f2 | awk '{print $1}')
if [ -n "$ntp_server_ip" ]; then
log "Resolved ${BOOTSTRAP_NTP_SERVER} to ${ntp_server_ip}. Forcing time sync..."
ntpd -q -n -p "$ntp_server_ip"
sleep 2 # Give a moment for the clock to settle
if [ "$(date +%Y)" -lt 2025 ]; then
log "FATAL: Bootstrap NTP sync FAILED. System time is still incorrect."
return 1
else
log "System time successfully synchronized: $(date)"
nvram set ntp_ready=1
nvram commit
return 2 # Signifies a successful sync occurred
fi
else
log "FATAL: Could not resolve bootstrap NTP server ${BOOTSTRAP_NTP_SERVER}."
return 1
fi
fi
# Time is already correct
return 0
}
# Internal helper function to resolve the package host IP address *if* in bootstrap mode.
resolve_pkgs_host() {
# If we are not using bootstrap DNS, we do nothing.
# We let curl/nslookup use the system resolver.
if [ "$TAILSMAN_USE_BOOTSTRAP_DNS" -eq 0 ]; then
# Quick check to see if system DNS is likely working
if ! nslookup -timeout=2 "$TAILSCALE_PKGS_HOST" >/dev/null 2>&1; then
log "Warning: System DNS may be unable to resolve ${TAILSCALE_PKGS_HOST}."
log "If downloads fail, try '$0 tailscale --manual-resolve ...'"
fi
return 0 # Success (we're letting the system handle it)
fi
# --- Using Bootstrap DNS ---
# If the IP is already resolved and cached, do nothing.
if [ -n "$TAILSCALE_PKGS_IP" ]; then
return 0
fi
log "Resolving ${TAILSCALE_PKGS_HOST} using bootstrap DNS ${BOOTSTRAP_DNS_SERVER}..."
TAILSCALE_PKGS_IP=$(nslookup "$TAILSCALE_PKGS_HOST" "$BOOTSTRAP_DNS_SERVER" | grep Address | head -n 2 | tail -n 1 | cut -d: -f2 | awk '{print $1}')
if [ -z "$TAILSCALE_PKGS_IP" ]; then
log "Error: Could not resolve \"${TAILSCALE_PKGS_HOST}\" using bootstrap DNS."
return 1
else
log "Resolved ${TAILSCALE_PKGS_HOST} to ${TAILSCALE_PKGS_IP}."
return 0
fi
}
get_latest_version() {
if ! resolve_pkgs_host; then
# Fallback to prevent a failed "upgrade" to an empty version string.
get_installed_version "$TAILSCALED_BIN"
return
fi
local resolve_args=""
if [ "$TAILSMAN_USE_BOOTSTRAP_DNS" -eq 1 ] && [ -n "$TAILSCALE_PKGS_IP" ]; then
resolve_args="--resolve ${TAILSCALE_PKGS_HOST}:443:${TAILSCALE_PKGS_IP}"
fi
curl --silent --show-error \
$resolve_args \
"https://pkgs.tailscale.com/stable/" | \
grep "tailscale_" | grep "_${ARCH}.tgz" | head -1 | cut -d'_' -f 2
}
get_installed_version() {
local binary_path="$1"
if [ ! -f "$binary_path" ]; then
echo "0.0.0"
return
fi
"$binary_path" --version | head -n1 | cut -d' ' -f1
}
download_and_install() {
local component="$1"
local version="$2"
local install_dir="$3"
local binary_path="${install_dir}/${component}"
if [ -z "$version" ]; then
log "Error: Version for download is empty. Aborting installation."
return 1
fi
log "Downloading Tailscale ${component} version ${version} for ${ARCH}..."
local version_arch="${version}_${ARCH}"
local tarball_name="tailscale_${version_arch}.tgz"
local tarball_path="/tmp/${tarball_name}"
local extract_list_path="/tmp/extract_list.txt"
local extracted_dir_path="${install_dir}/tailscale_${version_arch}"
local extracted_binary_path="${extracted_dir_path}/${component}" # Full path to the new binary
# --- Pre-cleanup of old temp files from a previous *failed* run ---
rm -f "$tarball_path"
rm -f "$extract_list_path"
rm -rf "$extracted_dir_path"
# ------------------------------------------------------------------
if ! resolve_pkgs_host; then
return 1
fi
local resolve_args=""
if [ "$TAILSMAN_USE_BOOTSTRAP_DNS" -eq 1 ] && [ -n "$TAILSCALE_PKGS_IP" ]; then
resolve_args="--resolve ${TAILSCALE_PKGS_HOST}:443:${TAILSCALE_PKGS_IP}"
fi
curl --silent --show-error --fail -o "$tarball_path" \
$resolve_args \
"https://pkgs.tailscale.com/stable/${tarball_name}"
if [ $? -ne 0 ]; then
log "Error: Failed to download ${tarball_name}. It might not exist."
rm -f "$tarball_path"
return 1
fi
echo "tailscale_${version_arch}/${component}" > "$extract_list_path"
log "Extracting ${component} from ${tarball_name}..."
tar x -zvf "$tarball_path" -C "$install_dir" -T "$extract_list_path"
# --- ADDED ERROR CHECK 1 ---
# Check if tar failed OR if the file it was supposed to create doesn't exist
if [ $? -ne 0 ] || [ ! -f "$extracted_binary_path" ]; then
log "Error: Failed to extract ${component}."
log "This could be due to a corrupt download or lack of space."
# Clean up all temporary files
rm -f "$tarball_path"
rm -f "$extract_list_path"
rm -rf "$extracted_dir_path"
return 1
fi
log "Moving new binary into place at ${binary_path}..."
mv "$extracted_binary_path" "$binary_path"
# --- ADDED ERROR CHECK 2 ---
# Check if the move command failed
if [ $? -ne 0 ]; then
log "Error: Failed to move ${extracted_binary_path} to ${binary_path}."
log "Check permissions and free space on /jffs."
# Clean up all temporary files
rm -f "$tarball_path"
rm -f "$extract_list_path"
rm -rf "$extracted_dir_path"
return 1
fi
# Clean up successfully
rm -rf "$extracted_dir_path"
rm -f "$tarball_path"
rm -f "$extract_list_path"
chmod +x "$binary_path"
log "${component} successfully installed at ${binary_path}"
return 0
}
do_start() {
# First, ensure system time is correct. Abort if it fails.
bootstrap_time_sync
local sync_result=$?
if [ "$sync_result" -eq 1 ]; then # Abort only on hard failure (1)
log "Aborting start due to time sync failure."
return 1
fi
if is_tailscaled_running; then
echo "tailscaled is already running (PID: $(pidof tailscaled))." >&2
return
fi
if [ ! -f "$TAILSCALED_BIN" ]; then
log "Error: tailscaled binary not found at ${TAILSCALED_BIN}."
log "Please run '$0 install latest' to install it."
return 1
fi
log "Starting tailscaled..."
modprobe tun
nohup env GODEBUG=asyncpreemptoff=1 GOMAXPROCS=1 \
"$TAILSCALED_BIN" --no-logs-no-support --state="$STATE_FILE" --statedir="$TAILSCALE_DIR" >/dev/null 2>&1 &
sleep 2
apply_tailscale_firewall_rules
log "tailscaled started."
service restart_dnsmasq >/dev/null 2>&1
}
do_stop() {
if ! is_tailscaled_running; then
echo "tailscaled is not running." >&2
return
fi
log "Stopping tailscaled..."
kill "$(pidof tailscaled)"
log "tailscaled stopped. Firewall rules will be cleaned up automatically."
}
do_tailscale_cmd() {
TAILSMAN_USE_BOOTSTRAP_DNS=0 # Default to system DNS for speed
if [ "$1" = "--manual-resolve" ]; then
log "Manual resolve requested. Using bootstrap DNS."
TAILSMAN_USE_BOOTSTRAP_DNS=1
TAILSCALE_PKGS_IP="" # Clear the cache
shift # Remove the flag from the argument list
fi
local daemon_version
local client_version
local target_version
daemon_version=$(get_installed_version "$TAILSCALED_BIN")
client_version=$(get_installed_version "$TAILSCALE_TMP_BIN")
if [ "$daemon_version" != "0.0.0" ]; then
target_version="$daemon_version"
else
log "tailscaled not found. Will fetch *latest* client as fallback."
target_version=$(get_latest_version) # This will call resolve_pkgs_host
if [ -z "$target_version" ] || [ "$target_version" = "0.0.0" ]; then
log "Error: Could not determine latest version. Aborting."
return 1
fi
log "Latest client version is ${target_version}."
fi
if [ "$client_version" != "$target_version" ] || [ ! -f "$TAILSCALE_TMP_BIN" ]; then
log "Client version ($client_version) does not match target ($target_version). Downloading matching client..."
if ! download_and_install "tailscale" "$target_version" "/tmp/home/root"; then # This will also call resolve_pkgs_host
log "Error: Failed to download client. Aborting command."
return 1
fi
fi
"$TAILSCALE_TMP_BIN" "$@"
}
do_install() {
TAILSMAN_USE_BOOTSTRAP_DNS=1 # Use resilient bootstrap DNS for installs
local version_to_install="$1"
if [ -z "$version_to_install" ]; then
log "Error: You must specify a version or 'latest'."
echo "Usage: $0 install <version|latest>" >&2
return 1
fi
if [ "$version_to_install" = "latest" ]; then
log "Checking for latest version..."
version_to_install=$(get_latest_version)
if [ -z "$version_to_install" ]; then
log "Error: Could not determine latest version."
return 1
fi
log "Latest version is ${version_to_install}."
fi
local installed_version
installed_version=$(get_installed_version "$TAILSCALED_BIN")
if [ "$installed_version" = "$version_to_install" ]; then
log "Already at version ${version_to_install}. No change made."
# Ensure it's running
if ! is_tailscaled_running; then
do_start
fi
return 0
fi
log "Installing tailscaled version ${version_to_install} (from ${installed_version})..."
do_stop
if ! download_and_install "tailscaled" "$version_to_install" "$TAILSCALE_DIR"; then
log "Error: Failed to install version ${version_to_install}. Service remains stopped."
return 1
fi
log "Installation of ${version_to_install} complete. Starting service..."
do_start
}
do_upgrade() {
TAILSMAN_USE_BOOTSTRAP_DNS=1 # Use resilient bootstrap DNS for upgrades
log "Checking for upgrades..."
do_install "latest"
}
do_enable() {
local auto_upgrade_flag="$1"
log "Enabling automation: setting up system hooks and cron jobs..."
ensure_script_entry() {
local script_path="$1"
local entry_to_add="$2"
local script_dir
script_dir=$(dirname "$script_path")
mkdir -p "$script_dir"
if [ ! -f "$script_path" ]; then
printf '#!/bin/sh\n\n' > "$script_path"
chmod +x "$script_path"
fi
if ! grep -qF "$entry_to_add" "$script_path"; then
printf '%s\n' "$entry_to_add" >> "$script_path"
log "Added entry \"${entry_to_add}\" to $script_path"
else
log "Entry \"${entry_to_add}\" already exists in $script_path"
fi
}
local services_start_script="/jffs/scripts/services-start"
# First, remove any existing 'enable' entry to avoid duplicates
if [ -f "$services_start_script" ]; then
sed -i "\#${SCRIPT_PATH} enable#d" "$services_start_script"
log "Cleaned old 'enable' entries from $services_start_script"
fi
# Now add the correct entries
ensure_script_entry "$services_start_script" "/bin/sh ${SCRIPT_PATH} start"
ensure_script_entry "$services_start_script" "/bin/sh ${SCRIPT_PATH} enable $auto_upgrade_flag"
if ! cru l | grep -q "#tailsman-sentinel#"; then
cru a tailsman-sentinel "* * * * *" "/bin/sh ${SCRIPT_PATH} sentinel"
log "Added sentinel cron job."
else
log "Sentinel cron job already exists."
fi
if [ "$auto_upgrade_flag" = "--with-auto-upgrade" ]; then
if ! cru l | grep -q "#tailsman-auto-upgrade#"; then
cru a tailsman-auto-upgrade "6 4 * * *" "/bin/sh ${SCRIPT_PATH} upgrade"
log "Added auto-upgrade cron job (runs daily at 4:06 AM)."
else
log "Auto-upgrade cron job already exists."
fi
else
log "Auto-upgrade not requested. Removing any existing auto-upgrade cron job."
if cru l | grep -q "#tailsman-auto-upgrade#"; then
cru d tailsman-auto-upgrade
log "Removed existing auto-upgrade cron job."
fi
fi
# --- Ensure firewall rules for tailscale0 survive Merlin firewall reloads ---
local firewall_start_script="/jffs/scripts/firewall-start"
# These rules treat the Tailscale interface as trusted, so traffic doesn't get
# dropped by Merlin's final DROP rules when the firewall is rebuilt.
ensure_script_entry "$firewall_start_script" "iptables -I INPUT 1 -i ${TAILSCALE_IF} -j ACCEPT"
ensure_script_entry "$firewall_start_script" "iptables -I FORWARD 1 -i ${TAILSCALE_IF} -j ACCEPT"
ensure_script_entry "$firewall_start_script" "iptables -I FORWARD 1 -o ${TAILSCALE_IF} -j ACCEPT"
log "Automation has been enabled (including persistent firewall rules for ${TAILSCALE_IF})."
}
do_disable() {
log "Disabling automation: removing system hooks, cron jobs, and firewall rules..."
if is_tailscaled_running; then
do_stop
fi
# Remove cron jobs
if cru l | grep -q "#tailsman-sentinel#"; then
cru d tailsman-sentinel
log "Removed sentinel cron job."
fi
if cru l | grep -q "#tailsman-auto-upgrade#"; then
cru d tailsman-auto-upgrade
log "Removed auto-upgrade cron job."
fi
# Remove services-start entries
local services_start_script="/jffs/scripts/services-start"
if [ -f "$services_start_script" ]; then
log "Removing entries from ${services_start_script}..."
sed -i "\#${SCRIPT_PATH} start#d" "$services_start_script"
sed -i "\#${SCRIPT_PATH} enable#d" "$services_start_script"
fi
# NEW: remove persistent firewall rules for tailscale0 from firewall-start
local firewall_start_script="/jffs/scripts/firewall-start"
if [ -f "$firewall_start_script" ]; then
log "Removing firewall rules for ${TAILSCALE_IF} from ${firewall_start_script}..."
sed -i "\#iptables -I INPUT 1 -i ${TAILSCALE_IF} -j ACCEPT#d" "$firewall_start_script"
sed -i "\#iptables -I FORWARD 1 -i ${TAILSCALE_IF} -j ACCEPT#d" "$firewall_start_script"
sed -i "\#iptables -I FORWARD 1 -o ${TAILSCALE_IF} -j ACCEPT#d" "$firewall_start_script"
fi
# OPTIONAL: remove the currently loaded iptables rules (best effort)
iptables -D INPUT -i "${TAILSCALE_IF}" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -i "${TAILSCALE_IF}" -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -o "${TAILSCALE_IF}" -j ACCEPT 2>/dev/null || true
log "Automation and firewall hooks have been disabled."
}
do_help() {
echo "Tailsman: Tailscale Manager for AsusWRT-Merlin"
echo ""
echo "Usage: $0 <command> [options]"
echo ""
echo "Service Commands:"
echo " start Starts the tailscaled daemon. Fails if not installed."
echo " stop Stops the running tailscaled daemon."
echo " restart Restarts the tailscaled daemon."
echo " install <ver|latest> Installs a specific version or the latest version of tailscaled."
echo " upgrade Alias for 'install latest'. Checks for and installs the latest version."
echo " tailscale [...] Passes arguments to the tailscale CLI (e.g., '$0 tailscale status')."
echo " --manual-resolve (As first argument) Forces use of bootstrap DNS for this command."
echo ""
echo "Automation Control:"
echo " enable [--with-auto-upgrade] Installs startup scripts and (optionally) a cron job for auto-upgrades."
echo " disable Removes all startup scripts and cron jobs."
echo " sentinel Used by cron; ensures time is correct and tailscaled is running."
echo ""
echo "Other Commands:"
echo " help Shows this help message."
}
COMMAND="$1"
if [ -z "$COMMAND" ]; then
do_help
exit 1
fi
shift # Remove the command from the argument list, "$@" now holds the rest
case "$COMMAND" in
start) do_start "$@" ;;
stop) do_stop ;;
restart)
do_stop
sleep 2
do_start "$@"
;;
tailscale)
do_tailscale_cmd "$@"
;;
sentinel)
TAILSMAN_USE_BOOTSTRAP_DNS=1 # Sentinel must be resilient
# Proactively ensure system time is correct on every run.
bootstrap_time_sync
sync_result=$? # Capture the return code immediately
if [ "$sync_result" -eq 2 ]; then
# A successful time sync just occurred. Restart tailscaled to apply.
log "Sentinel: Time was corrected. Restarting tailscaled to ensure connection health."
do_stop
sleep 2
do_start
elif [ "$sync_result" -eq 0 ]; then
# Time was already correct. Check if the service is running.
if ! is_tailscaled_running; then
log "Sentinel: tailscaled not running. Attempting to restart..."
do_start
fi
fi
# If sync_result is 1 (failure), we do nothing further. The failure is already logged.
;;
install) do_install "$@" ;;
upgrade) do_upgrade ;;
enable) do_enable "$@" ;;
disable) do_disable ;;
help) do_help ;;
*)
echo "Error: Unknown command '$COMMAND'" >&2
echo "" >&2
do_help
exit 1
;;
esac
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment