Skip to content

Instantly share code, notes, and snippets.

@vothanhkiet
Last active October 21, 2025 03:52
Show Gist options
  • Save vothanhkiet/7cea4b90df2efa99552d29cb282ece9b to your computer and use it in GitHub Desktop.
Save vothanhkiet/7cea4b90df2efa99552d29cb282ece9b to your computer and use it in GitHub Desktop.
Install Tailscale for Batocera with Subnet routing, advert exit node, and accept ssh
#!/bin/bash
# --- Configuration & Argument Check ---
TS_VERSION="$1"
INSTALL_DIR="/userdata/tailscale"
TEMP_DIR="/userdata/temp"
SERVICE_FILE="/userdata/system/services/tailscale"
if [ -z "$TS_VERSION" ]; then
echo "🚨 Error: No Tailscale version provided."
echo "Usage: $0 <version>"
echo "Example: $0 1.88.5"
exit 1
fi
echo "πŸš€ Starting Tailscale installation for version $TS_VERSION on Batocera..."
# --- 1. Architecture Detection ---
MACHINE_ARCH="$(uname -m)"
TS_ARCH=""
# Map system architecture names to Tailscale tarball architecture suffixes
case "$MACHINE_ARCH" in
x86_64 | amd64)
TS_ARCH="amd64"
;;
aarch64)
TS_ARCH="arm64"
;;
aarch32 | armv7l)
TS_ARCH="arm"
;;
riscv64)
TS_ARCH="riscv64"
;;
# Assuming any other 32-bit x86 is 386
x86)
TS_ARCH="386"
;;
*)
echo "❌ Error: Unsupported architecture '$MACHINE_ARCH'."
exit 1
;;
esac
TAR_FILENAME="tailscale_${TS_VERSION}_${TS_ARCH}.tgz"
EXTRACTED_DIR="tailscale_${TS_VERSION}_${TS_ARCH}"
WGET_URL="https://pkgs.tailscale.com/stable/$TAR_FILENAME"
echo "βœ… Detected architecture: $MACHINE_ARCH -> Tailscale arch: $TS_ARCH"
# --- 2. Service Stop and Disable ---
echo "πŸ›‘ Stopping and disabling existing tailscale service..."
batocera-services stop tailscale
batocera-services disable tailscale
# --- 3. Download and Extract ---
echo "🧹 Preparing temporary directory $TEMP_DIR..."
rm -rf "$TEMP_DIR"
mkdir -p "$TEMP_DIR"
cd "$TEMP_DIR" || exit 1
echo "⬇️ Downloading $TAR_FILENAME..."
# Using -q for quiet download
if ! wget -q "$WGET_URL"; then
echo "❌ Error: Failed to download $WGET_URL"
cd /userdata || exit 1
rm -rf "$TEMP_DIR"
exit 1
fi
echo "πŸ“¦ Extracting files..."
if ! tar -xf "$TAR_FILENAME"; then
echo "❌ Error: Failed to extract $TAR_FILENAME"
cd /userdata || exit 1
rm -rf "$TEMP_DIR"
exit 1
fi
# --- 4. Installation and Cleanup ---
echo "πŸ“₯ Installing files to $INSTALL_DIR..."
# 4a. Clean up old installation and create the new one
rm -rf "$INSTALL_DIR"
mkdir -p "$INSTALL_DIR"
# 4b. Move necessary executables and systemd folder
if ! mv "$EXTRACTED_DIR/systemd" "$INSTALL_DIR/systemd"; then
echo "❌ Warning: Could not move systemd folder."
fi
if ! mv "$EXTRACTED_DIR/tailscale" "$INSTALL_DIR/tailscale"; then
echo "❌ Error: Failed to move tailscale executable."
cd /userdata || exit 1
rm -rf "$TEMP_DIR"
exit 1
fi
if ! mv "$EXTRACTED_DIR/tailscaled" "$INSTALL_DIR/tailscaled"; then
echo "❌ Error: Failed to move tailscaled executable."
cd /userdata || exit 1
rm -rf "$TEMP_DIR"
exit 1
fi
cd /userdata || exit 1
echo "🧹 Cleaning up temporary directory..."
rm -rf "$TEMP_DIR"
# --- 5. Generate Dynamic Service Script ---
echo "πŸ› οΈ Generating dynamic Tailscale service script at $SERVICE_FILE..."
mkdir -p /userdata/system/services
rm -rf "$SERVICE_FILE"
# The HERE document generates the tailscale service script.
cat << 'EOF' > "$SERVICE_FILE"
#!/bin/bash
sleep 60
# --- Interface and CIDR Detection ---
INTERFACE=$(ip -o -4 route show to default | awk '{print $5}')
if ! ip link show "$INTERFACE" > /dev/null 2>&1; then
echo "Error: Interface $INTERFACE does not exist." >&2
exit 1
fi
CIDDR=$(ip -o -f inet addr show "$INTERFACE" | awk '{print $4}')
if [ -z "$CIDDR" ]; then
echo "Error: No IP address found for interface $INTERFACE." >&2
exit 1
fi
IP=$(echo "$CIDDR" | cut -d'/' -f1)
PREFIX=$(echo "$CIDDR" | cut -d'/' -f2)
# Calculate Network CIDR (Used for --advertise-routes)
MASK=$(( 0xFFFFFFFF << (32 - PREFIX) & 0xFFFFFFFF ))
MASK_OCTETS=$(printf "%d.%d.%d.%d" $(( (MASK >> 24) & 0xFF )) \
$(( (MASK >> 16) & 0xFF )) \
$(( (MASK >> 8) & 0xFF )) \
$(( MASK & 0xFF )))
IFS=. read -r o1 o2 o3 o4 <<< "$IP"
IFS=. read -r m1 m2 m3 m4 <<< "$MASK_OCTETS"
NETWORK=$(printf "%d.%d.%d.%d" $(( o1 & m1 )) \
$(( o2 & m2 )) \
$(( o3 & m3 )) \
$(( o4 & m4 )))
CIDR=$(printf "%s/%s" "$NETWORK" "$PREFIX")
# -------------------------------------
# --- System Setup (TUN, Forwarding, Firewall) ---
rm -rf /dev/net
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 600 /dev/net/tun
cp /etc/sysctl.conf /etc/sysctl.conf.bak
cat <<EOL > "/etc/sysctl.conf"
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOL
sysctl -p /etc/sysctl.conf
# Interface-specific settings for better routing performance
ethtool -K "$INTERFACE" rx-udp-gro-forwarding on rx-gro-list off
ethtool -K "$INTERFACE" gro off
# NAT MASQUERADE rules for exit node functionality
iptables -t nat -A POSTROUTING -o "$INTERFACE" -j MASQUERADE
ip6tables -t nat -A POSTROUTING -o "$INTERFACE" -j MASQUERADE
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT
# Save changes to Batocera overlay
batocera-save-overlay
if [[ "$1" != "start" ]]; then
exit 0
fi
# --- Start Tailscale Daemon and Client ---
/userdata/tailscale/tailscaled -state /userdata/tailscale/state > /userdata/tailscale/tailscaled.log 2>&1 &
/userdata/tailscale/tailscale up --advertise-routes="$CIDR" --snat-subnet-routes=false --accept-routes --advertise-exit-node --accept-dns=true --ssh
EOF
chmod +x "$SERVICE_FILE"
# --- 6. Apply System Configuration and Start ---
echo "πŸ”§ Configuring TUN device and IP forwarding permanently..."
rm -rf /dev/net
mkdir -p /dev/net
mknod /dev/net/tun c 10 200
chmod 600 /dev/net/tun
cp /etc/sysctl.conf /etc/sysctl.conf.bak
cat <<EOL > "/etc/sysctl.conf"
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOL
sysctl -p /etc/sysctl.conf
batocera-save-overlay
echo "🟒 Starting Tailscale for initial login (SSH enabled)..."
/userdata/tailscale/tailscaled -state /userdata/tailscale/state > /userdata/tailscale/tailscaled.log 2>&1 &
/userdata/tailscale/tailscale up --ssh
echo "βœ… Tailscale is running. Please log in and approve the machine."
# --- 7. Final Enablement and Info ---
batocera-services enable tailscale
echo "βœ… Batocera tailscale service enabled for next reboot."
# Final CIDR calculation and daemon start (redundant but ensures immediate full startup)
INTERFACE=$(ip -o -4 route show to default | awk '{print $5}')
CIDDR=$(ip -o -f inet addr show "$INTERFACE" | awk '{print $4}')
IP=$(echo "$CIDDR" | cut -d'/' -f1)
PREFIX=$(echo "$CIDDR" | cut -d'/' -f2)
MASK=$(( 0xFFFFFFFF << (32 - PREFIX) & 0xFFFFFFFF ))
MASK_OCTETS=$(printf "%d.%d.%d.%d" $(( (MASK >> 24) & 0xFF )) $(( (MASK >> 16) & 0xFF )) $(( (MASK >> 8) & 0xFF )) $(( MASK & 0xFF )))
IFS=. read -r o1 o2 o3 o4 <<< "$IP"
IFS=. read -r m1 m2 m3 m4 <<< "$MASK_OCTETS"
NETWORK=$(printf "%d.%d.%d.%d" $(( o1 & m1 )) $(( o2 & m2 )) $(( o3 & m3 )) $(( o4 & m4 )))
CIDR=$(printf "%s/%s" "$NETWORK" "$PREFIX")
echo "Starting Tailscale with Subnet Route ($CIDR), Exit Node, and SSH enabled..."
ethtool -K "$INTERFACE" rx-udp-gro-forwarding on rx-gro-list off
ethtool -K "$INTERFACE" gro off
/userdata/tailscale/tailscale up --advertise-routes="$CIDR" --snat-subnet-routes=false --accept-routes --advertise-exit-node --accept-dns=true --ssh
# Final firewall rules
iptables -t nat -A POSTROUTING -o "$INTERFACE" -j MASQUERADE
ip6tables -t nat -A POSTROUTING -o "$INTERFACE" -j MASQUERADE
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT
echo "---"
echo "🌐 Your Interface is: **$INTERFACE**"
echo "πŸ”— Advertised CIDR is: **$CIDR**"
echo "---"
echo "You should now see the Tailscale interface (`ip a`)."
echo "Go to the Tailscale admin console, find this machine, and **approve the Subnets and Exit Node**."
echo "βœ… Installation and configuration complete."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment