Last active
October 21, 2025 03:52
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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