#!/bin/sh # Copyright 2024-2025, Lime Technology # Copyright 2024-2025, Christoph Hummer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License version 2, # as published by the Free Software Foundation. # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. exec_entrypoint() { echo "Starting container..." echo echo "=======================" echo eval "exec ${ORG_ENTRYPOINT} ${ORG_CMD} ${ORG_POSTARGS}" } error_handler() { if [ "${DISABLE_ERROR_HANDLER}" != "true" ]; then echo "ERROR: Unraid Docker Hook script throw an error!" echo " Starting container without Tailscale!" echo exec_entrypoint fi } echo "=======================" echo echo "Executing Unraid Docker Hook for Tailscale" echo if [ "$(id -u)" != "0" ]; then echo "ERROR: No root privileges!" error_handler fi if [ "${OFFICIAL_TAILSCALE_SIDECAR}" = "true" ]; then echo "Official Tailscale Sidecar container routine enabled!" echo OFFICIAL_TS_SIDECAR="true" apk update >/dev/null 2>&1 apk add jq >/dev/null 2>&1 elif [ ! -f /usr/bin/tailscale ] || [ ! -f /usr/bin/tailscaled ]; then OFFICIAL_TS_SIDECAR="false" if [ ! -z "${TAILSCALE_EXIT_NODE_IP}" ]; then if [ ! -c /dev/net/tun ]; then echo "ERROR: Device /dev/net/tun not found!" echo " Make sure to pass through /dev/net/tun to the container." error_handler fi INSTALL_IPTABLES="iptables " fi echo "Detecting Package Manager..." if which apt-get >/dev/null 2>&1; then echo "Detected Advanced Package Tool!" PACKAGES_UPDATE="apt-get update" PACKAGES_INSTALL="apt-get -y install --no-install-recommends" elif which apk >/dev/null 2>&1; then echo "Detected Alpine Package Keeper!" PACKAGES_UPDATE="apk update" PACKAGES_INSTALL="apk add" elif which pacman >/dev/null 2>&1; then echo "Detected pacman Package Manager!" PACKAGES_INSTALL="pacman -Syu --noconfirm" else echo "ERROR: Detection from Package Manager failed!" error_handler fi if [ "${TAILSCALE_TROUBLESHOOTING}" = "true" ]; then if which apt-get >/dev/null 2>&1; then PACKAGES_TROUBLESHOOTING="curl dnsutils iputils-ping speedtest-cli " elif which apk >/dev/null 2>&1; then PACKAGES_TROUBLESHOOTING="curl bind-tools iputils-ping speedtest-cli " elif which pacman >/dev/null 2>&1; then PACKAGES_TROUBLESHOOTING="curl dnsutils iputils speedtest-cli " fi echo "Tailscale Troubleshooting enabled!" echo "Installing additional packages: $(echo "${PACKAGES_TROUBLESHOOTING}" | sed 's/[[:blank:]]*$//' | sed 's/ /, /g')" fi echo "Installing packages..." echo "Please wait..." if [ ! -z "${PACKAGES_UPDATE}" ]; then UPDATE_LOG=$(${PACKAGES_UPDATE} 2>&1) fi INSTALL_LOG=$(${PACKAGES_INSTALL} jq wget ca-certificates ${INSTALL_IPTABLES}${PACKAGES_TROUBLESHOOTING} 2>&1) INSTALL_RESULT=$? if [ "${INSTALL_RESULT}" -eq 0 ]; then echo "Packages installed!" unset INSTALL_LOG else echo "ERROR: Installing packages!" echo "${UPDATE_LOG}" echo "${INSTALL_LOG}" error_handler fi if [ "${INSTALL_IPTABLES}" = "iptables " ]; then if ! iptables -L >/dev/null 2>&1; then echo "ERROR: Cap: NET_ADMIN not available!" echo " Make sure to add --cap-add=NET_ADMIN to the Extra Parameters" error_handler fi fi echo "Tailscale not found, downloading..." echo "Please wait..." TAILSCALE_VERSION=$(wget -qO- 'https://pkgs.tailscale.com/stable/?mode=json' | jq -r '.TarballsVersion') if [ -z "${TAILSCALE_VERSION}" ]; then echo "ERROR: Can't get Tailscale JSON" error_handler fi if [ ! -d /tmp/tailscale ]; then mkdir -p /tmp/tailscale fi if wget -q -nc --show-progress --progress=bar:force:noscroll -O /tmp/tailscale/tailscale.tgz "https://pkgs.tailscale.com/stable/tailscale_${TAILSCALE_VERSION}_amd64.tgz" ; then echo "Download from Tailscale version ${TAILSCALE_VERSION} successful!" else echo "ERROR: Download from Tailscale version ${TAILSCALE_VERSION} failed!" rm -rf /tmp/tailscale error_handler fi tar -C /tmp/tailscale -xf /tmp/tailscale/tailscale.tgz cp /tmp/tailscale/tailscale_${TAILSCALE_VERSION}_amd64/tailscale /usr/bin/tailscale cp /tmp/tailscale/tailscale_${TAILSCALE_VERSION}_amd64/tailscaled /usr/bin/tailscaled rm -rf /tmp/tailscale echo "Installation Done!" else OFFICIAL_TS_SIDECAR="false" echo "Tailscale found, continuing..." fi unset TSD_PARAMS unset TS_PARAMS if [ "${OFFICIAL_TS_SIDECAR}" = "true" ]; then if [ -z "${TS_STATE_DIR}" ]; then echo "No Tailscale State Directory specified, falling back to: /var/lib/tailscale" export TS_STATE_DIR="/var/lib/tailscale" else export TS_STATE_DIR="${TS_STATE_DIR}" fi TSD_STATE_DIR="${TS_STATE_DIR}" elif [ ! -z "${TAILSCALE_STATE_DIR}" ]; then TSD_STATE_DIR="${TAILSCALE_STATE_DIR}" elif [ ! -z "${SERVER_DIR}" ]; then TSD_STATE_DIR="${SERVER_DIR}/.tailscale_state" elif [ ! -z "${DATA_DIR}" ]; then TSD_STATE_DIR="${DATA_DIR}/.tailscale_state" elif [ ! -z "${USER_HOME}" ]; then TSD_STATE_DIR="${USER_HOME}/.tailscale_state" elif [ -d "/config" ]; then TSD_STATE_DIR="/config/.tailscale_state" elif [ -d "/data" ]; then TSD_STATE_DIR="/data/.tailscale_state" elif [ ! -z "${CA_TS_FALLBACK_DIR}" ]; then TSD_STATE_DIR="${CA_TS_FALLBACK_DIR}/.tailscale_state" else echo "ERROR: Couldn't detect persistent Docker directory for .tailscale_state!" echo " Please enable Tailscale Advanced Settings in the Docker template and set the Tailscale State Directory manually!" sleep infinity fi echo "Settings Tailscale state dir to: ${TSD_STATE_DIR}" if [ ! -z "${TSD_STATE_DIR}" ] && [ ! -d "${TSD_STATE_DIR}" ]; then mkdir -p ${TSD_STATE_DIR} fi if [ ! -z "${TAILSCALE_EXIT_NODE_IP}" ]; then echo "Disabling userspace networking! Tailscale DNS available" echo "Using ${TAILSCALE_EXIT_NODE_IP} as Exit Node! See https://tailscale.com/kb/1103/exit-nodes" TS_PARAMS=" --exit-node=${TAILSCALE_EXIT_NODE_IP}" if [ "${TAILSCALE_ALLOW_LAN_ACCESS}" = "true" ]; then echo "Enabling local LAN Access to the container!" TS_PARAMS="${TS_PARAMS} --exit-node-allow-lan-access" fi else if [ -z "${TAILSCALE_USERSPACE_NETWORKING}" ] || [ "${TAILSCALE_USERSPACE_NETWORKING}" = "true" ]; then echo "Enabling userspace networking! Tailscale DNS not available" TSD_PARAMS="-tun=userspace-networking " else if [ ! -c /dev/net/tun ]; then echo "ERROR: Device /dev/net/tun not found!" echo " Make sure to pass through /dev/net/tun to the container and add the" echo " parameter --cap-add=NET_ADMIN to the Extra Parameters!" error_handler fi fi fi if [ ! -z "${TAILSCALE_ADVERTISE_ROUTES}" ]; then TAILSCALE_ADVERTISE_ROUTES="$(echo ${TAILSCALE_ADVERTISE_ROUTES} | sed 's/ //g')" echo "Advertising custom routes! See https://tailscale.com/kb/1019/subnets#advertise-subnet-routes" TS_PARAMS="${TS_PARAMS} --advertise-routes=${TAILSCALE_ADVERTISE_ROUTES}" fi if [ "${TAILSCALE_ACCEPT_ROUTES}" = "true" ]; then echo "Accepting subnet routes! See https://tailscale.com/kb/1019/subnets#use-your-subnet-routes-from-other-devices" TS_PARAMS="${TS_PARAMS} --accept-routes" fi if [ "${TAILSCALE_USE_SSH}" = "true" ]; then echo "Enabling SSH! See https://tailscale.com/kb/1193/tailscale-ssh" TS_PARAMS="${TS_PARAMS} --ssh" fi if [ "${OFFICIAL_TS_SIDECAR}" != "true" ]; then if [ "${TAILSCALE_LOG}" != "false" ]; then TSD_PARAMS="${TSD_PARAMS} >>/var/log/tailscaled 2>&1 " TSD_MSG=" with log file location: /var/log/tailscaled" else TSD_PARAMS="${TSD_PARAMS} >/dev/null 2>&1 " TSD_MSG=" with logging disabled" fi fi if [ ! -z "${TAILSCALE_HOSTNAME}" ]; then echo "Setting host name to \"${TAILSCALE_HOSTNAME}\"" TAILSCALE_HOSTNAME="$(echo "$TAILSCALE_HOSTNAME" | tr -d ' ')" TS_PARAMS="${TS_PARAMS} --hostname=${TAILSCALE_HOSTNAME}" fi if [ "${OFFICIAL_TS_SIDECAR}" = "true" ]; then if [ ! -z "${TAILSCALE_HOSTNAME}" ]; then export TS_STATE_DIR="${TS_STATE_DIR}/${TAILSCALE_HOSTNAME}" TSD_STATE_DIR="${TS_STATE_DIR}/${TAILSCALE_HOSTNAME}" else export TS_STATE_DIR="${TS_STATE_DIR}/$(hostname)" TSD_STATE_DIR="${TS_STATE_DIR}/$(hostname)" fi fi if [ "${TAILSCALE_EXIT_NODE}" = "true" ]; then echo "Configuring container as Exit Node! See https://tailscale.com/kb/1103/exit-nodes" TS_PARAMS="${TS_PARAMS} --advertise-exit-node" fi if [ "${OFFICIAL_TS_SIDECAR}" = "true" ]; then if [ ! -z "${TAILSCALED_PARAMS}" ]; then export TS_TAILSCALED_EXTRA_ARGS="${TAILSCALED_PARAMS} ${TSD_PARAMS}" else export TS_TAILSCALED_EXTRA_ARGS="${TSD_PARAMS}" fi if [ ! -z "${TAILSCALE_PARAMS}" ]; then export TS_EXTRA_ARGS="${TAILSCALE_PARAMS}${TS_PARAMS}" else export TS_EXTRA_ARGS="${TS_PARAMS}" fi exec_entrypoint & TAILSCALE_PID=$! else if [ ! -z "${TAILSCALED_PARAMS}" ]; then TSD_PARAMS="${TAILSCALED_PARAMS} ${TSD_PARAMS}" fi if [ ! -z "${TAILSCALE_PARAMS}" ]; then TS_PARAMS="${TAILSCALE_PARAMS}${TS_PARAMS}" fi fi if [ "${OFFICIAL_TS_SIDECAR}" != "true" ]; then echo "Starting tailscaled${TSD_MSG}" eval tailscaled -statedir=${TSD_STATE_DIR} ${TSD_PARAMS}& echo "Starting tailscale" eval tailscale up ${TS_PARAMS} --reset EXIT_STATUS="$?" if [ "${EXIT_STATUS}" != "0" ]; then echo "ERROR: Connecting to Tailscale not successful!" if [ -f /var/log/tailscaled ]; then echo "Please check the logs:" tail -20 /var/log/tailscaled fi error_handler fi unset EXIT_STATUS else DISABLE_ERROR_HANDLER="true" sleep 2 fi while true; do TAILSCALE_ONLINE=$(tailscale status --json | jq '.Self.Online') if [ "${TAILSCALE_ONLINE}" = "true" ]; then break fi sleep 2 done if [ ! -z "${TAILSCALE_SERVE_PORT}" ] && [ "$(tailscale status --json | jq -r '.CurrentTailnet.MagicDNSEnabled')" != "false" ] && [ -z "$(tailscale status --json | jq -r '.Self.Capabilities[] | select(. == "https")')" ]; then echo "ERROR: Enable MagicDNS and HTTPS on your Tailscale account to use Tailscale Serve/Funnel." echo "See: https://tailscale.com/kb/1153/enabling-https" error_handler fi if [ "${TAILSCALE_EXIT_NODE}" = "true" ]; then if [ "$(tailscale status --json | jq -r '.Self.ExitNodeOption')" = "false" ]; then TSIP=$(tailscale status --json | jq -r '.Self.TailscaleIPs[0]') echo "WARNING: Exit Node not yet approved." echo " Navigate to https://login.tailscale.com/admin/machines/${TSIP} and approve it." fi fi KEY_EXPIRY=$(tailscale status --json | jq -r '.Self.KeyExpiry') if [ "${KEY_EXPIRY}" != "null" ]; then EXPIRY_EPOCH=$(date -d "${KEY_EXPIRY}" +"%s" 2>/dev/null) if [ -z "${EXPIRY_EPOCH}" ]; then EXPIRY_EPOCH=$(date -d "$(echo "${KEY_EXPIRY}" | sed 's/T/ /; s/Z//')" +"%s") fi CUR_EPOCH=$(date -u +%s) DIFF_EPOCH=$((EXPIRY_EPOCH - CUR_EPOCH)) DIFF_DAYS=$((DIFF_EPOCH / 86400)) HOST=$(tailscale status --json | jq -r '.Self.HostName') if [ -z "${EXPIRY_EPOCH}" ]; then echo "WARNING: An error occurred while retrieving the Tailscale key expiry!" elif [ -n "${DIFF_DAYS}" ] && echo "${DIFF_DAYS}" | grep -Eq '^[0-9]+$'; then echo "WARNING: Tailscale Key will expire in ${DIFF_DAYS} days." echo " Navigate to https://login.tailscale.com/admin/machines and 'Disable Key Expiry' for ${HOST}" else echo "ERROR: Tailscale Key expired!" echo " Navigate to https://login.tailscale.com/admin/machines and Renew/Disable Key Expiry for ${HOST}" fi echo "See: https://tailscale.com/kb/1028/key-expiry" fi if [ ! -z "${TAILSCALE_ADVERTISE_ROUTES}" ]; then APPROVED_ROUTES="$(tailscale status --json | jq -r '.Self.PrimaryRoutes')" IFS=',' set -- ${TAILSCALE_ADVERTISE_ROUTES} ROUTES="$@" for route in ${ROUTES}; do if ! echo "${APPROVED_ROUTES}" | grep -q "${route}"; then NOT_APPROVED="$NOT_APPROVED ${route}" fi done if [ ! -z "${NOT_APPROVED}" ]; then TSIP="$(tailscale status --json | jq -r '.Self.TailscaleIPs[0]')" echo "WARNING: The following route(s) are not approved:${NOT_APPROVED}" echo " Navigate to https://login.tailscale.com/admin/machines/${TSIP} and approve it." fi fi if [ ! -z "${TAILSCALE_SERVE_PORT}" ]; then if [ ! -z "${TAILSCALE_SERVE_PATH}" ]; then TAILSCALE_SERVE_PATH="=${TAILSCALE_SERVE_PATH}" fi if [ -z "${TAILSCALE_SERVE_PROTOCOL}" ]; then TAILSCALE_SERVE_PROTOCOL="https" fi if [ -z "${TAILSCALE_SERVE_PROTOCOL_PORT}" ]; then TAILSCALE_SERVE_PROTOCOL_PORT="=443" fi if [ -z "${TAILSCALE_SERVE_TARGET}" ]; then TAILSCALE_SERVE_TARGET="http://localhost" fi if [ "${TAILSCALE_FUNNEL}" = "true" ]; then echo "Enabling Funnel! See https://tailscale.com/kb/1223/funnel" eval tailscale funnel --bg --"${TAILSCALE_SERVE_PROTOCOL}"${TAILSCALE_SERVE_PROTOCOL_PORT}${TAILSCALE_SERVE_PATH} ${TAILSCALE_SERVE_TARGET}:"${TAILSCALE_SERVE_PORT}${TAILSCALE_SERVE_LOCALPATH}" | grep -v "To disable the proxy" else echo "Enabling Serve! See https://tailscale.com/kb/1312/serve" eval tailscale serve --bg --"${TAILSCALE_SERVE_PROTOCOL}"${TAILSCALE_SERVE_PROTOCOL_PORT}${TAILSCALE_SERVE_PATH} ${TAILSCALE_SERVE_TARGET}:"${TAILSCALE_SERVE_PORT}${TAILSCALE_SERVE_LOCALPATH}" | grep -v "To disable the proxy" fi if [ "${TAILSCALE_SERVE_PROTOCOL}" = "https" ]; then TS_DNSNAME="$(tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//')" if [ ! -f "${TSD_STATE_DIR}/certs/${TS_DNSNAME}.crt" ] || [ ! -f "${TSD_STATE_DIR}/certs/${TS_DNSNAME}.key" ]; then if [ ! -d "${TSD_STATE_DIR}/certs" ]; then mkdir -p "${TSD_STATE_DIR}/certs" fi echo "Generating Tailscale certs! This can take some time, please wait..." timeout 30 tailscale cert --cert-file="${TSD_STATE_DIR}/certs/${TS_DNSNAME}.crt" --key-file="${TSD_STATE_DIR}/certs/${TS_DNSNAME}.key" "${TS_DNSNAME}" >/dev/null 2>&1 EXIT_STATUS="$?" if [ "${EXIT_STATUS}" != "0" ] && [ "${OFFICIAL_TS_SIDECAR}" != "true" ]; then echo "ERROR: Can't generate certificates!" echo "Please check the logs:" tail -10 /var/log/tailscaled else echo "Done!" fi unset EXIT_STATUS fi fi fi if [ "${OFFICIAL_TS_SIDECAR}" != "true" ]; then exec_entrypoint else trap "kill -SIGTERM ${TAILSCALE_PID}; exit 0" SIGTERM wait "${TAILSCALE_PID}" fi