diff --git a/tailscale/DOCS.md b/tailscale/DOCS.md index 317e45c7..4ccfce4c 100644 --- a/tailscale/DOCS.md +++ b/tailscale/DOCS.md @@ -203,6 +203,12 @@ If you want to access other clients on your tailnet even from your local subnet, execute steps 2 and 3 as described on [Site-to-site networking][tailscale_info_site_to_site]. +In case your local subnets collide with subnet routes within your tailnet, your +local network access has priority, and these addresses won't be routed toward +your tailnet. This will prevent your Home Assistant instance from losing network +connection. This also means that using the same subnet on multiple nodes for load +balancing and failover is impossible with the current add-on behavior. + ### Option: `proxy` When not set, this option is enabled by default. diff --git a/tailscale/Dockerfile b/tailscale/Dockerfile index 038e5641..196a6a56 100755 --- a/tailscale/Dockerfile +++ b/tailscale/Dockerfile @@ -16,6 +16,7 @@ RUN \ iptables=1.8.9-r2 \ nginx=1.24.0-r6 \ coreutils=9.3-r1 \ + networkmanager-common=1.42.8-r0 \ \ && ln -sf /sbin/xtables-nft-multi /sbin/ip6tables \ && ln -sf /sbin/xtables-nft-multi /sbin/iptables \ diff --git a/tailscale/config.yaml b/tailscale/config.yaml index e07d9d6a..c9f84b2c 100644 --- a/tailscale/config.yaml +++ b/tailscale/config.yaml @@ -19,6 +19,7 @@ arch: init: false hassio_api: true host_network: true +host_dbus: true privileged: - NET_ADMIN - NET_RAW diff --git a/tailscale/rootfs/etc/NetworkManager/dispatcher.d/connectivity-change b/tailscale/rootfs/etc/NetworkManager/dispatcher.d/connectivity-change new file mode 100755 index 00000000..8d2bb0f0 --- /dev/null +++ b/tailscale/rootfs/etc/NetworkManager/dispatcher.d/connectivity-change @@ -0,0 +1,14 @@ +#!/bin/bash +# DO NOT use #!/command/with-contenv bashio, because that won't set the environment variables + +if [[ "$NM_DISPATCHER_ACTION" == "connectivity-change" ]]; then + if [[ "$CONNECTIVITY_STATE" == "FULL" ]]; then + if ! protect-subnet-routes; then + # Better stop add-on than risking losing all network connections + echo -n 1 > /run/s6-linux-init-container-results/exitcode + exec /run/s6/basedir/bin/halt + fi + else # UNKNOWN, NONE, PORTAL, LIMITED + unprotect-subnet-routes + fi +fi diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/dependencies.d/protect-subnets b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/dependencies.d/protect-subnets new file mode 100644 index 00000000..e69de29b diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run index 85da8e64..f61c5acf 100755 --- a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run @@ -4,19 +4,14 @@ # Home Assistant Community Add-on: Tailscale # Runs after the machine has been logged in into the Tailscale network # ============================================================================== -declare -a addresses=() declare -a options declare -a routes=() -declare ipinfo +declare route +declare -a colliding_routes=() declare login_server declare tags declare keyexpiry -function appendarray() { - local -n array=${1} - readarray -t -O "${#array[@]}" array -} - # Default options options+=(--hostname "$(bashio::info.hostname)") @@ -58,37 +53,8 @@ fi tags=$(bashio::config "tags//[] | join(\",\")" "") options+=(--advertise-tags="${tags}") -# Find interfaces and matching addresses from which we can extract routes to be advertised -for interface in $(bashio::network.interfaces); do - appendarray addresses < <(bashio::network.ipv4_address "${interface}") - appendarray addresses < <(bashio::network.ipv6_address "${interface}") -done - -# Extract routes to be advertised -for address in "${addresses[@]}"; do - if bashio::var.has_value "${address}"; then - # Skip local link addresses - if [[ "${address:0:6}" == "fe80::" ]] || [[ "${address:0:8}" == "169.254." ]]; - then - continue - fi - - # Skip if forwarding for the address family is disabled - if [[ "${address}" =~ .*:.* ]]; - then - [[ $( /run/s6-linux-init-container-results/exitcode + fi + [[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt +elif [[ "${exit_code_service}" -ne 0 ]]; then + if [[ "${exit_code_container}" -eq 0 ]]; then + echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode + fi + exec /run/s6/basedir/bin/halt +fi diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/run new file mode 100755 index 00000000..32dc5dc2 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/run @@ -0,0 +1,12 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Prevent local subnets to be routed toward the tailnet +# ============================================================================== + +protect-subnet-routes + +# runs scripts in /etc/NetworkManager/dispatcher.d +# --debug is used to prevent logging to syslog (HA cli) +exec /usr/libexec/nm-dispatcher --persist --debug > /dev/null diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/type b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/type @@ -0,0 +1 @@ +longrun diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/protect-subnets b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/protect-subnets new file mode 100644 index 00000000..e69de29b diff --git a/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh b/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh index 3139ff53..07040394 100755 --- a/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh +++ b/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh @@ -5,6 +5,14 @@ # S6 Overlay stage2 hook to customize services # ============================================================================== +# Disable protect-subnets service when userspace-networking is enabled +if ! bashio::config.has_value "userspace_networking" || \ + bashio::config.true "userspace_networking"; +then + rm /etc/s6-overlay/s6-rc.d/user/contents.d/protect-subnets + rm /etc/s6-overlay/s6-rc.d/post-tailscaled/dependencies.d/protect-subnets +fi + # Disable taildrop service when it is has been explicitly disabled if bashio::config.false 'taildrop'; then rm /etc/s6-overlay/s6-rc.d/user/contents.d/taildrop diff --git a/tailscale/rootfs/usr/bin/protect-subnet-routes b/tailscale/rootfs/usr/bin/protect-subnet-routes new file mode 100755 index 00000000..859bdf1f --- /dev/null +++ b/tailscale/rootfs/usr/bin/protect-subnet-routes @@ -0,0 +1,70 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# In case of non userspace networking, +# add local subnets to ip rules with higher priority than Tailscale's routing +# ============================================================================== + +declare -a routes=() +declare route family +declare ipv4_multiple_tables_enabled +declare ipv6_multiple_tables_enabled +declare protected_routes=0 +declare response +declare wait_counter=0 + +if bashio::config.false "userspace_networking"; then + ipv4_multiple_tables_enabled=$(zcat /proc/config.gz | { grep -Ec '^CONFIG_IP_MULTIPLE_TABLES=y$' || true ;}) + ipv6_multiple_tables_enabled=$(zcat /proc/config.gz | { grep -Ec '^CONFIG_IPV6_MULTIPLE_TABLES=y$' || true ;}) + + # If it is called after network configuration is changed, we need to drop cached network info + bashio::cache.flush_all + # It is possible to get "ERROR: Got unexpected response from the API: System is not ready with state: setup" + # So we wait a little + while ! bashio::api.supervisor GET "/addons/self/options/config" false &> /dev/null; do + if (( wait_counter++ == 15 )); then + bashio::log.error "Supervisor is unreachable" + bashio::exit.nok + fi + bashio::log.info "Waiting for the supervisor to be ready..." + sleep 2 + done + if (( wait_counter != 0 )); then + bashio::log.info "Supervisor is ready" + fi + + readarray -t routes < <(subnet-routes) + if (( 0 < ${#routes[@]} )); then + bashio::log.info "Adding advertised local subnets to ip rules with higher priority than Tailscale's routing," + bashio::log.info "to prevent routing advertised local subnets if the same subnet is routed within your tailnet." + fi + for route in "${routes[@]}"; do + if [[ "${route}" =~ .*:.* ]]; then + if (( 0 == ${ipv6_multiple_tables_enabled} )); then + bashio::log.warning " IPv6 multiple routing tables are not enabled, skip adding route ${route} to ip rules" + continue + fi + family="-6" + else + if (( 0 == ${ipv4_multiple_tables_enabled} )); then + bashio::log.warning " IPv4 multiple routing tables are not enabled, skip adding route ${route} to ip rules" + continue + fi + family="-4" + fi + bashio::log.info " Adding route ${route} to ip rules" + if ! response=$(ip "${family}" rule add to "${route}" priority 5000 table main 2>&1); then + if [[ "${response}" != "RTNETLINK answers: File exists" ]]; then + echo "${response}" + bashio::exit.nok + else + bashio::log.notice " Route ${route} is already added to ip rules" + fi + fi + (( protected_routes+=1 )) + done + if (( 0 < ${#routes[@]} && 0 == ${protected_routes} )); then + bashio::log.error "Can't protect any subnets" + bashio::exit.nok + fi +fi diff --git a/tailscale/rootfs/usr/bin/subnet-routes b/tailscale/rootfs/usr/bin/subnet-routes new file mode 100755 index 00000000..27191d6f --- /dev/null +++ b/tailscale/rootfs/usr/bin/subnet-routes @@ -0,0 +1,56 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Print possible subnet routes to stdout +# ============================================================================== +declare interface +declare -a addresses=() +declare -a routes=() +declare ipinfo +declare response + +function appendarray() { + local -n array=${1} + readarray -t -O "${#array[@]}" array +} + +if bashio::cache.exists 'subnet-routes'; then + readarray -t routes < <(bashio::cache.get 'subnet-routes') + printf -v response "%s" "${routes[@]/%/$'\n'}" +else + # Find interfaces and matching addresses from which we can extract routes to be advertised + for interface in $(bashio::network.interfaces); do + appendarray addresses < <(bashio::network.ipv4_address "${interface}") + appendarray addresses < <(bashio::network.ipv6_address "${interface}") + done + + # Extract routes to be advertised + for address in "${addresses[@]}"; do + if bashio::var.has_value "${address}"; then + # Skip local link addresses + if [[ "${address:0:6}" == "fe80::" ]] || [[ "${address:0:8}" == "169.254." ]]; + then + continue + fi + + # Skip if forwarding for the address family is disabled + if [[ "${address}" =~ .*:.* ]]; + then + [[ $(