Skip to content

Commit

Permalink
add local subnets to ip rules
Browse files Browse the repository at this point in the history
  • Loading branch information
lmagyar authored and frenck committed Oct 15, 2023
1 parent 4318158 commit 8a11c91
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 38 deletions.
6 changes: 6 additions & 0 deletions tailscale/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 to lose network
conection. This also means that using the same subnet on multiple nodes for load
balancing and failover is not possible with the current add-on behavior.

### Option: `proxy`

When not set, this option is enabled by default.
Expand Down
1 change: 1 addition & 0 deletions tailscale/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
1 change: 1 addition & 0 deletions tailscale/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ arch:
init: false
hassio_api: true
host_network: true
host_dbus: true
privileged:
- NET_ADMIN
- NET_RAW
Expand Down
Original file line number Diff line number Diff line 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
Empty file.
61 changes: 23 additions & 38 deletions tailscale/rootfs/etc/s6-overlay/s6-rc.d/post-tailscaled/run
Original file line number Diff line number Diff line change
Expand Up @@ -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)")

Expand Down Expand Up @@ -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
[[ $(</proc/sys/net/ipv6/conf/all/forwarding) -eq 0 ]] && continue
else
[[ $(</proc/sys/net/ipv4/ip_forward) -eq 0 ]] && continue
fi

ipinfo="$(/usr/bin/ipcalc --json "${address}")"
routes+=("$(bashio::jq "${ipinfo}" '.NETWORK + "/" + .PREFIX')")
fi
done

# Remove duplicate entries
readarray -t routes < <(printf "%s\n" "${routes[@]}" | sort -u)

# Find interfaces and matching addresses and extract routes to be advertised
readarray -t routes < <(subnet-routes)
IFS=","
options+=(--advertise-routes="${routes[*]}")
unset IFS
Expand Down Expand Up @@ -123,6 +89,25 @@ fi

bashio::log.info "Tailscale is running"

# Notify about colliding subnet routes if non-userspace-networking is enabled
if bashio::config.false "userspace_networking"; then
readarray -t colliding_routes < <( \
comm -1 -2 \
<(printf "%s" "${routes[@]/%/$'\n'}") \
<(/opt/tailscale status --json --peers=true --self=false \
| jq -rc '.Peer[] | select(has("PrimaryRoutes")) | .PrimaryRoutes[]' \
| sort -u))
if (( 0 < ${#colliding_routes[@]} )); then
bashio::log.warning "Currently the following subnets are both present as local subnets"
bashio::log.warning "and are also routed within your tailnet to other nodes!"
bashio::log.warning "Please reconfigure your subnet routing within your tailnet"
bashio::log.warning "to prevent current or future collisions."
fi
for route in "${colliding_routes[@]}"; do
bashio::log.warning " ${route}"
done
fi

# Warn about userspace networking
if ! bashio::config.has_value "userspace_networking" || \
bashio::config.true "userspace_networking";
Expand Down
Empty file.
29 changes: 29 additions & 0 deletions tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/finish
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Home Assistant Community Add-on: Tailscale
# Take down the S6 supervision tree when protect-subnets fails
# ==============================================================================
declare exit_code
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
readonly exit_code_service="${1}"
readonly exit_code_signal="${2}"
readonly service="protect-subnets"

unprotect-subnet-routes

bashio::log.info \
"Service ${service} exited with code ${exit_code_service}" \
"(by signal ${exit_code_signal})"

if [[ "${exit_code_service}" -eq 256 ]]; then
if [[ "${exit_code_container}" -eq 0 ]]; then
echo $((128 + $exit_code_signal)) > /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
12 changes: 12 additions & 0 deletions tailscale/rootfs/etc/s6-overlay/s6-rc.d/protect-subnets/run
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
longrun
Empty file.
8 changes: 8 additions & 0 deletions tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions tailscale/rootfs/usr/bin/protect-subnet-routes
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions tailscale/rootfs/usr/bin/subnet-routes
Original file line number Diff line number Diff line change
@@ -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
[[ $(</proc/sys/net/ipv6/conf/all/forwarding) -eq 0 ]] && continue
else
[[ $(</proc/sys/net/ipv4/ip_forward) -eq 0 ]] && continue
fi

ipinfo="$(/usr/bin/ipcalc --json "${address}")"
routes+=("$(bashio::jq "${ipinfo}" '.NETWORK + "/" + .PREFIX')")
fi
done

# Remove duplicate entries
readarray -t routes < <(printf "%s" "${routes[@]/%/$'\n'}" | sort -u)

printf -v response "%s" "${routes[@]/%/$'\n'}"
bashio::cache.set 'subnet-routes' "${response}"
fi

printf "%s" "${response}"
25 changes: 25 additions & 0 deletions tailscale/rootfs/usr/bin/unprotect-subnet-routes
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# In case of non userspace networking,
# remove local subnets from ip rules
# ==============================================================================

declare -a routes=()
declare route family

if bashio::config.false "userspace_networking"; then
readarray -t routes < <( \
{ ip -4 rule list; ip -6 rule list; } \
| { grep -E '^5000:' || true ;} \
| sed -nr 's/^\d+:\s+from all to ([^\s]+) lookup main$/\1/p')
for route in "${routes[@]}"; do
bashio::log.info "Removing route ${route} from ip rules"
if [[ "${route}" =~ .*:.* ]]; then
family="-6"
else
family="-4"
fi
ip "${family}" rule del to "${route}"
done
fi

0 comments on commit 8a11c91

Please sign in to comment.