diff --git a/tailscale/DOCS.md b/tailscale/DOCS.md index ffd62c465..22efd07fb 100644 --- a/tailscale/DOCS.md +++ b/tailscale/DOCS.md @@ -86,15 +86,14 @@ userspace_networking: true ### Option: `accept_dns` -If you are experiencing trouble with MagicDNS on this device and wish to -disable, you can do so using this option. +This option allows you to accept the DNS settings of your tailnet that are +configured on the [DNS page][tailscale_dns] of the admin console. When disabled, +Tailscale's DNS resolves only tailnet addresses, no global nameservers from the +admin console are applied. -When not set, this option is enabled by default. +For more information, see the "DNS" section of this documentation. -MagicDNS may cause issues if you run things like Pi-hole or AdGuard Home -on the same machine as this add-on. In such cases disabling `accept_dns` -will help. You can still leverage MagicDNS on other devices on your network, -by adding `100.100.100.100` as a DNS server in your Pi-hole or AdGuard Home. +When not set, this option is enabled by default. ### Option: `accept_routes` @@ -339,9 +338,10 @@ When not set, this option is enabled by default. If you need to access other clients on your tailnet from your Home Assistant instance, disable userspace networking mode, which will create a `tailscale0` -network interface on your host. To be able to address those clients not only -with their tailnet IP, but with their tailnet name, you have to configure Home -Assistant's DNS options also. +network interface on your host. + +To be able to address other clients on your tailnet not only by their tailnet IP +but also by their tailnet name, see the "DNS" section of this documentation. If you want to access other clients on your tailnet even from your local subnet, follow steps in the [Site-to-site networking][tailscale_info_site_to_site] guide @@ -377,6 +377,46 @@ CGNAT networks). You can test connections with `tailscale ping When not set, an automatically selected port is used by default. +## DNS + +When the `userspace_networking` option is disabled, Tailscale provides a DNS (at +100.100.100.100 and fd7a:115c:a1e0::53) to be able to address other clients on +your tailnet not only by their tailnet IP but also by their tailnet name. + +More information: [What is 100.100.100.100][tailscale_info_quad100], +[DNS in Tailscale][tailscale_info_dns], [MagicDNS][tailscale_info_magicdns], +[Access a Pi-hole from anywhere][tailscale_info_pi_hole]. + +1. Check that the `userspace_networking` option is disabled. + +1. Check that under **Settings** -> **System** -> **Network** Tailscale's DNS is + **_not_** configured as a DNS server. + +1. In the command line, execute `ha dns options --servers dns://100.100.100.100`. + + **Note:** _This command replaces the existing DNS server list in Home + Assistant and restarts the internal DNS server. To specify an empty DNS list + (i.e. to remove `dns://100.100.100.100` from the list), you must use + `ha dns reset` and `ha dns restart` commands both. This server list is + additional and queried before the DNS servers specified in Network settings + above._ + +**Note:** The only difference compared to the general Tailscale experience, is +that you always have to use the fully qualified domain name instead of only the +device name, i.e. `ping some-tailnet-device.tail1234.ts.net` works, but `ping +some-tailnet-device` does not work. + +**Note:** If you are running your own DNS (like AdGuard) on this Home Assistant +device also, and this device is configured as global nameserver on the [DNS +page][tailscale_dns] of the admin console, then: + +1. Disable the `accept_dns` option to prevent the Tailscale DNS from redirecting + queries from your device back to itself, which would cause a loop. + +1. Configure your DNS for Home Assistant, and in your DNS configure Tailscale + DNS for your tailnet domain as upstream DNS server (e.g. in case of AdGuard + `[/tail1234.ts.net/]100.100.100.100`). + ## Changelog & Releases This repository keeps a change log using [GitHub's releases][releases] @@ -451,12 +491,16 @@ SOFTWARE. [semver]: https://semver.org/spec/v2.0.0.html [tailscale_acls]: https://login.tailscale.com/admin/acls [tailscale_dns]: https://login.tailscale.com/admin/dns +[tailscale_info_dns]: https://tailscale.com/kb/1054/dns [tailscale_info_exit_nodes]: https://tailscale.com/kb/1103/exit-nodes [tailscale_info_app_connectors]: https://tailscale.com/kb/1281/app-connectors [tailscale_info_funnel]: https://tailscale.com/kb/1223/funnel [tailscale_info_funnel_policy_requirement]: https://tailscale.com/kb/1223/funnel#requirements-and-limitations [tailscale_info_https]: https://tailscale.com/kb/1153/enabling-https [tailscale_info_key_expiry]: https://tailscale.com/kb/1028/key-expiry +[tailscale_info_magicdns]: https://tailscale.com/kb/1081/magicdns +[tailscale_info_pi_hole]: https://tailscale.com/kb/1114/pi-hole +[tailscale_info_quad100]: https://tailscale.com/kb/1381/what-is-quad100 [tailscale_info_serve]: https://tailscale.com/kb/1312/serve [tailscale_info_site_to_site]: https://tailscale.com/kb/1214/site-to-site [tailscale_info_subnets]: https://tailscale.com/kb/1019/subnets diff --git a/tailscale/Dockerfile b/tailscale/Dockerfile index 498e2bedb..769c3597f 100755 --- a/tailscale/Dockerfile +++ b/tailscale/Dockerfile @@ -10,6 +10,8 @@ ARG BUILD_ARCH=amd64 ARG TAILSCALE_VERSION="v1.90.2" RUN \ apk add --no-cache \ + bind-tools=9.20.15-r0 \ + dnsmasq=2.91-r0 \ ethtool=6.14.1-r0 \ ipcalc=1.0.3-r0 \ iproute2=6.15.0-r0 \ diff --git a/tailscale/apparmor.txt b/tailscale/apparmor.txt new file mode 100644 index 000000000..73e6fedd6 --- /dev/null +++ b/tailscale/apparmor.txt @@ -0,0 +1,47 @@ +#include + +profile tailscale flags=(attach_disconnected,mediate_deleted) { + #include + + # Capabilities + file, + signal (send) set=(kill,term,int,hup,cont), + + # S6-Overlay + /init ix, + /bin/** ix, + /usr/bin/** ix, + /run/{s6,s6-rc*,service}/** ix, + /package/** ix, + /command/** ix, + /etc/services.d/** rwix, + /etc/cont-init.d/** rwix, + /etc/cont-finish.d/** rwix, + /run/{,**} rwk, + /dev/tty rw, + + # Bashio + /usr/lib/bashio/** ix, + /tmp/** rwk, + + # Access to options.json and other files within your addon + /data/** rw, + + # General - based on complain + capability net_bind_service, + capability dac_override, + capability fsetid, + capability setgid, + capability setuid, + capability chown, + capability kill, + + # General - based on Config.yaml + capability net_admin, + capability net_raw, + + # Mount for MagicDNS fix + capability sys_admin, + mount options=(rw, rprivate) -> /, # unshare -m + mount options=(rw, bind) /etc/resolv.for-tailscaled.conf -> /etc/resolv.conf, # mount --bind +} diff --git a/tailscale/config.yaml b/tailscale/config.yaml index 64b21f82e..ae53a6b3a 100644 --- a/tailscale/config.yaml +++ b/tailscale/config.yaml @@ -21,6 +21,7 @@ host_dbus: true privileged: - NET_ADMIN - NET_RAW + - SYS_ADMIN devices: - /dev/net/tun map: diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/forwarding/dependencies.d/magicdns-ingress-proxy b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/forwarding/dependencies.d/magicdns-ingress-proxy new file mode 100644 index 000000000..e69de29bb diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/dependencies.d/base b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/dependencies.d/base new file mode 100644 index 000000000..e69de29bb diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/down b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/down new file mode 100644 index 000000000..8e0d981bc --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/down @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/finish diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/finish b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/finish new file mode 100755 index 000000000..723cda9ab --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/finish @@ -0,0 +1,8 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Remove forwarding +# ============================================================================== + +magicdns-ingress-proxy-forwarding remove drop diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/run new file mode 100755 index 000000000..54a967129 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/run @@ -0,0 +1,74 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Runs the dnsmasq proxies initialization +# ============================================================================== + +readonly MAGIC_DNS_IPV4="100.100.100.100" +readonly MAGIC_DNS_IPV6="fd7a:115c:a1e0::53" + +readonly DEFAULT_LOGIN_SERVER="controlplane.tailscale.com" +readonly LOG_SERVER="log.tailscale.com" +readonly LETSENCRYPT_API="acme-v02.api.letsencrypt.org" +readonly DNSMASQ_BLACK_WHITE_LIST_LOCATION="/etc/dnsmasq-black-white-list" + +declare dns +declare invalid_dns_config + +declare login_server="${DEFAULT_LOGIN_SERVER}" +declare -a black_white_list=() + +# Check DNS configuration +invalid_dns_config="true" +for dns in $(bashio::dns.servers); do + if bashio::var.equals "${dns}" "dns://${MAGIC_DNS_IPV4}" || \ + bashio::var.equals "${dns}" "dns://${MAGIC_DNS_IPV6}" + then + invalid_dns_config="false" + break + fi +done +if bashio::var.true "${invalid_dns_config}"; then + bashio::log.notice \ + "To use MagicDNS in Home Assistant, configure MagicDNS's IP address as DNS server with cli," \ + "eg. 'ha dns options --servers dns://${MAGIC_DNS_IPV4}'" + bashio::log.notice \ + "Please check your configuration based on the add-on's documentation under the \"DNS\" section" +fi +invalid_dns_config="false" +for dns in $(bashio::dns.locals); do + if bashio::var.equals "${dns}" "dns://${MAGIC_DNS_IPV4}" || \ + bashio::var.equals "${dns}" "dns://${MAGIC_DNS_IPV6}" + then + bashio::log.fatal "Do not configure MagicDNS's IP address (${dns:6}) as DNS server under Settings -> System -> Network" + invalid_dns_config="true" + fi +done +if bashio::var.true "${invalid_dns_config}"; then + bashio::exit.nok +fi + +# We have to be able to determine login_server from this address +if ! login_server=$(awk -F[/:] '{print $4}' <<<$(bashio::config "login_server")) || \ + ! bashio::var.has_value "${login_server}" +then + bashio::exit.nok "Determining host name from '$(bashio::config "login_server")' has failed" +fi +black_white_list+=(${login_server}) + +# When log upload is enabled, resolve log server also +if bashio::debug; then + black_white_list+=(${LOG_SERVER}) +fi + +# If serve or funnel is used, resolve letsencrypt's api also +if ! bashio::config.equals 'share_homeassistant' 'disabled'; then + black_white_list+=(${LETSENCRYPT_API}) +fi + +printf "%s" "${black_white_list[@]/%/$'\n'}" > "${DNSMASQ_BLACK_WHITE_LIST_LOCATION}" + +# This is necessary to prevent accessing MagicDNS before the ingress proxy starts up +# The ingress proxy will remove these entries on startup +magicdns-ingress-proxy-forwarding setup drop diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/type b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/type new file mode 100644 index 000000000..bdd22a185 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/type @@ -0,0 +1 @@ +oneshot diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/up b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/up new file mode 100644 index 000000000..f81376c7f --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/up @@ -0,0 +1 @@ +/etc/s6-overlay/s6-rc.d/init-magicdns-proxies/run diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/dependencies.d/init-magicdns-proxies b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/dependencies.d/init-magicdns-proxies new file mode 100644 index 000000000..e69de29bb diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/finish b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/finish new file mode 100755 index 000000000..ee5fdb6db --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/finish @@ -0,0 +1,25 @@ +#!/command/with-contenv bashio +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Take down the S6 supervision tree when MagicDNS egress proxy fails +# ============================================================================== +readonly exit_code_container=$( /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/magicdns-egress-proxy/notification-fd b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/notification-fd new file mode 100644 index 000000000..00750edc0 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/notification-fd @@ -0,0 +1 @@ +3 diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/run new file mode 100755 index 000000000..09e042665 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/run @@ -0,0 +1,74 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Runs the MagicDNS egress proxy +# ============================================================================== + +# Note: This script assumes that internal hassio network is available in IPv4. +# Sets up IPv4 upstream DNS for tailscaled, and redirects the white_list to IPv4 hassio DNS. +# In case of an IPv6-only hassio network this IPv4 solution won't work. +# But running an egress DNS proxy on ::1 IPv6 localhost is impossible, because DNS add-ons can bind to this address also, +# but we can specify only port 53 DNS in resolv.conf for tailscaled. +# So in case of IPv6-only hassio, we can't use [::1]:53 for the egress DNS proxy, but we can't use anything else in resolv.conf for tailscaled. +# For the status of IPv6 support see https://github.com/home-assistant/supervisor/issues/2133 + +source /usr/lib/trace.sh + +readonly DNSMASQ_EGRESS_ADDRESS_IPV4="127.100.100.100" +readonly DNSMASQ_EGRESS_PORT=53 +readonly DNSMASQ_BLACK_WHITE_LIST_LOCATION="/etc/dnsmasq-black-white-list" + +declare hassio_dns_ipv4 +declare -a white_list +declare domain +declare -a options + +bashio::log.info "Starting MagicDNS egress proxy..." + +function dig_hassio_dns() { + local type="${1}" + dig dns.local.hass.io "${type}" +short \ + | { grep -Ev '^;|\.$|^$' || true ;} \ + | head -n 1 +} + +options+=(--no-hosts) +options+=(--no-resolv) +options+=(--conf-file=/dev/null) +options+=(--keep-in-foreground) +options+=(--log-facility='-') +options+=(--cache-size=0) + +options+=(--listen-address=${DNSMASQ_EGRESS_ADDRESS_IPV4}) +options+=(--bind-dynamic) +options+=(--port=${DNSMASQ_EGRESS_PORT}) + +# Hassio DNS's IP addresses +if ! hassio_dns_ipv4=$(dig_hassio_dns A) || \ + bashio::var.is_empty "${hassio_dns_ipv4}" +then + bashio::exit.nok "Failed to resolve Home Assistant's IPv4 DNS address" +fi + +# White-list +readarray -t white_list < "${DNSMASQ_BLACK_WHITE_LIST_LOCATION}" + +# Return NXDOMAIN for everything, except the white_list +options+=(--address=/#/) +for domain in "${white_list[@]}"; do + options+=(--server=/${domain}/${hassio_dns_ipv4}) +done + +if bashio_custom::trace; then + options+=(--log-queries) + options+=(--log-debug) +fi + +# We need to delay the starting of the dependent services until the conf file is written +echo "nameserver ${DNSMASQ_EGRESS_ADDRESS_IPV4}" > /etc/resolv.dnsmasq.conf +echo "" >&3 + +# This DNS forwards the white_list to HA's DNS, otherwise replies NXDOMAIN for everything +# It must run on port 53 to be able to specify it in a resolv.conf +exec dnsmasq "${options[@]}" diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/type b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-egress-proxy/type @@ -0,0 +1 @@ +longrun diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/dependencies.d/init-magicdns-proxies b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/dependencies.d/init-magicdns-proxies new file mode 100644 index 000000000..e69de29bb diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/dependencies.d/post-tailscaled b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/dependencies.d/post-tailscaled new file mode 100644 index 000000000..e69de29bb diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/finish b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/finish new file mode 100755 index 000000000..22f85311e --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/finish @@ -0,0 +1,28 @@ +#!/command/with-contenv bashio +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Take down the S6 supervision tree when MagicDNS ingress proxy fails +# ============================================================================== +readonly exit_code_container=$( /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/magicdns-ingress-proxy/notification-fd b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/notification-fd new file mode 100644 index 000000000..00750edc0 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/notification-fd @@ -0,0 +1 @@ +3 diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/run new file mode 100755 index 000000000..94aa00133 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/run @@ -0,0 +1,70 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Runs the MagicDNS ingress proxy +# ============================================================================== + +source /usr/lib/trace.sh + +readonly MAGIC_DNS_IPV4="100.100.100.100" + +readonly DNSMASQ_INGRESS_PORT=53 +readonly DNSMASQ_BLACK_WHITE_LIST_LOCATION="/etc/dnsmasq-black-white-list" + +declare tailscale_address_ipv4 +declare tailscale_address_ipv6 +declare -a black_list +declare domain +declare -a options + +bashio::log.info "Starting MagicDNS ingress proxy..." + +options+=(--no-hosts) +options+=(--no-resolv) +options+=(--conf-file=/dev/null) +options+=(--keep-in-foreground) +options+=(--log-facility='-') +options+=(--cache-size=0) + +# Tailscale's local IP addresses +if ! tailscale_address_ipv4=$(/opt/tailscale ip -4) || \ + ! tailscale_address_ipv6=$(/opt/tailscale ip -6) || \ + bashio::var.is_empty "${tailscale_address_ipv4-}" && bashio::var.is_empty "${tailscale_address_ipv6-}" +then + bashio::exit.nok "Failed to retrieve Tailscale's local address" +fi + +# Listen addresses +if bashio::var.has_value "${tailscale_address_ipv4-}"; then + options+=(--listen-address=${tailscale_address_ipv4}) +fi +if bashio::var.has_value "${tailscale_address_ipv6-}"; then + options+=(--listen-address=${tailscale_address_ipv6}) +fi + +options+=(--bind-dynamic) +options+=(--port=${DNSMASQ_INGRESS_PORT}) + +# Black-list +readarray -t black_list < "${DNSMASQ_BLACK_WHITE_LIST_LOCATION}" + +# Forward everything to MagicDNS, except the black_list +options+=(--server=${MAGIC_DNS_IPV4}) +for domain in "${black_list[@]}"; do + options+=(--server=/${domain}/) +done + +if bashio_custom::trace; then + options+=(--log-queries) + options+=(--log-debug) +fi + +magicdns-ingress-proxy-forwarding setup forwarding +magicdns-ingress-proxy-forwarding remove drop + +# We need to delay the starting of the dependent services until iptables are configured +echo "" >&3 + +# This DNS replies NXDOMAIN for the black_list, otherwise forwards everything to MagicDNS +exec dnsmasq "${options[@]}" diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/type b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/type new file mode 100644 index 000000000..5883cff0c --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/magicdns-ingress-proxy/type @@ -0,0 +1 @@ +longrun diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/dependencies.d/magicdns-egress-proxy b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/dependencies.d/magicdns-egress-proxy new file mode 100644 index 000000000..e69de29bb diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/run index 8ea71ed7c..4c6a34b25 100755 --- a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/run +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/run @@ -4,6 +4,10 @@ # Home Assistant Community Add-on: Tailscale # Runs tailscale # ============================================================================== + +readonly TAILSCALED_LOGLEVEL_NOTICE="Tailscale logs will be suppressed after 200 lines, set add-on's configuration option 'log_level' to 'debug' to see further logs" +readonly TAILSCALED_LOGLEVEL_MESSAGE="[further tailscaled logs suppressed, set add-on's configuration option 'log_level' to 'debug' to see further tailscaled logs]" + declare -a options declare udp_port @@ -31,15 +35,30 @@ then fi # Run Tailscale -if bashio::debug ; then - exec /opt/tailscaled "${options[@]}" +# If exists, resolv.dnsmasq.conf pointing to the dummy dnsmasq will be mounted in place of the real resolv.conf only for tailscaled +# This will prevent the DNS at 100.100.100.100 to call back to hassio_dns causing a loop +if ! bashio::fs.file_exists "/etc/resolv.dnsmasq.conf"; then + # Running the regular way, no resolv.conf replacement + if bashio::debug ; then + exec /opt/tailscaled "${options[@]}" + else + bashio::log.notice "${TAILSCALED_LOGLEVEL_NOTICE}" + /opt/tailscaled "${options[@]}" 2>&1 \ + | stdbuf -i0 -oL -eL \ + sed -n -e '1,200p' \ + -e "201c${TAILSCALED_LOGLEVEL_MESSAGE}" + fi else - bashio::log.notice \ - "Tailscale logs will be suppressed after 200 lines, set add-on's" \ - "configuration option 'log_level' to 'debug' to see further logs" - - /opt/tailscaled "${options[@]}" 2>&1 \ - | stdbuf -i0 -oL -eL \ - sed -n -e '1,200p' \ - -e "201c[further tailscaled logs suppressed, set add-on's configuration option 'log_level' to 'debug' to see further tailscaled logs]" + # Using fake resolv.conf + bashio::log.info "Using dnsmasq as upstream DNS server for tailscaled" + mv /etc/resolv.dnsmasq.conf /etc/resolv.for-tailscaled.conf + if bashio::debug ; then + exec unshare -m bash -c "mount --bind /etc/resolv.for-tailscaled.conf /etc/resolv.conf; exec /opt/tailscaled $(printf "\"%s\" " "${options[@]}")" + else + bashio::log.notice "${TAILSCALED_LOGLEVEL_NOTICE}" + unshare -m bash -c "mount --bind /etc/resolv.for-tailscaled.conf /etc/resolv.conf; exec /opt/tailscaled $(printf "\"%s\" " "${options[@]}") 2>&1" \ + | stdbuf -i0 -oL -eL \ + sed -n -e '1,200p' \ + -e "201c${TAILSCALED_LOGLEVEL_MESSAGE}" + fi fi diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/magicdns-ingress-proxy b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/magicdns-ingress-proxy new file mode 100644 index 000000000..e69de29bb diff --git a/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh b/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh index 30a1687c5..29e2bf87c 100755 --- a/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh +++ b/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh @@ -58,6 +58,15 @@ if bashio::var.has_value "${proxy_and_funnel_port}"; then bashio::addon.option 'proxy_and_funnel_port' fi +# Disable MagicDNS proxy services when userspace-networking is enabled or accepting dns is disabled +if bashio::config.true "userspace_networking" || \ + bashio::config.false "accept_dns"; +then + rm /etc/s6-overlay/s6-rc.d/tailscaled/dependencies.d/magicdns-egress-proxy + rm /etc/s6-overlay/s6-rc.d/forwarding/dependencies.d/magicdns-ingress-proxy + rm /etc/s6-overlay/s6-rc.d/user/contents.d/magicdns-ingress-proxy +fi + # Disable protect-subnets service when userspace-networking is enabled or accepting routes is disabled if ! bashio::config.has_value "userspace_networking" || \ bashio::config.true "userspace_networking" || \ diff --git a/tailscale/rootfs/usr/bin/magicdns-ingress-proxy-forwarding b/tailscale/rootfs/usr/bin/magicdns-ingress-proxy-forwarding new file mode 100755 index 000000000..74ea51db9 --- /dev/null +++ b/tailscale/rootfs/usr/bin/magicdns-ingress-proxy-forwarding @@ -0,0 +1,155 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Setup DNAT for incoming MagicDNS queries toward ingress dnsmasq proxy +# ============================================================================== + +readonly MAGIC_DNS_IPV4="100.100.100.100" +readonly MAGIC_DNS_IPV6="fd7a:115c:a1e0::53" + +readonly DNSMASQ_INGRESS_PORT=53 + +declare hassio_dns_ipv4 +declare hassio_dns_ipv6 + +declare tailscale_address_ipv4 +declare tailscale_address_ipv6 + +function setup_forwarding() { + local cmd="${1}" + local sub_cmd="${2}" + local proto="${3}" + local ip_version="${4}" + local address_bits="${5}" + local source_address="${6}" + local from_address="${7}" + local to_address="${8}" + local to_port="${9}" + local name="${10}" + + bashio::log.info "Setting up ${name} for MagicDNS ingress proxy (${proto}, ${ip_version})" + if ${cmd} -t nat -S PREROUTING \ + | grep -Eq "^-A PREROUTING -s ${source_address//[\[\]]/}/${address_bits} -d ${from_address//[\[\]]/}/${address_bits} -i hassio -p ${proto} -m ${proto} --dport 53 -j DNAT --to-destination $(sed -r 's/(\[|\])/\\\1/g' <<< "${to_address}"):${to_port}$" + then + bashio::log.notice "${name^} is already set for MagicDNS ingress proxy (${proto}, ${ip_version})" + else + if ! ${cmd} -t nat "-${sub_cmd}" PREROUTING -s "${source_address//[\[\]]/}" -d "${from_address//[\[\]]/}" -i hassio -p "${proto}" --dport 53 -j DNAT --to-destination "${to_address}:${to_port}"; then + bashio::exit.nok "Setting up ${name} for MagicDNS ingress proxy is unsuccessful (${proto}, ${ip_version})" + fi + fi +} + +function remove_forwarding() { + local cmd="${1}" + local ip_version="${2}" + local address_bits="${3}" + local source_address="${4}" + local from_address="${5}" + local to_address="${6}" + local to_port="${7}" + local name="${8}" + + local proto + + for proto in $( \ + ${cmd} -t nat -S PREROUTING \ + | { grep -E "^-A PREROUTING -s ${source_address//[\[\]]/}/${address_bits} -d ${from_address//[\[\]]/}/${address_bits} -i hassio -p \S+ -m \S+ --dport 53 -j DNAT --to-destination $(sed -r 's/(\[|\])/\\\1/g' <<< "${to_address}"):${to_port}$" || true ;} \ + | sed -nr 's/^.*?-p\s(\S+).*$/\1/p') + do + bashio::log.info "Removing ${name} for MagicDNS ingress proxy (${proto}, ${ip_version})" + if ! ${cmd} -t nat -D PREROUTING -s "${source_address//[\[\]]/}" -d "${from_address//[\[\]]/}" -i hassio -p "${proto}" --dport 53 -j DNAT --to-destination "${to_address}:${to_port}"; then + bashio::log.warning "Removing ${name} for MagicDNS ingress proxy is unsuccessful (${proto}, ${ip_version})" + fi + done +} + +function dig_hassio_dns() { + local type="${1}" + dig dns.local.hass.io "${type}" +short | { grep -Ev '^;|\.$|^$' || true ;} | head -n 1 +} + +# This is useful when stopping services and dig fails but we already added this value to iptables +function get_hassio_dns_from_iptables() { + local cmd="${1}" + local address_bits="${2}" + local from_address="${3}" + + ${cmd} -t nat -S PREROUTING \ + | { grep -E "^-A PREROUTING -s \S+ -d ${from_address}/${address_bits} -i hassio -p udp -m udp --dport 53 -j DNAT --to-destination \S+$" || true ;} \ + | sed -nr 's/^.*?-s\s([^\/]+)\/\d+\s-d.*$/\1/p' \ + | head -n 1 +} + +# Hassio DNS's IP addresses +# For the status of IPv6 support see https://github.com/home-assistant/supervisor/issues/2133 +if ! hassio_dns_ipv4=$(dig_hassio_dns A) || \ + ! hassio_dns_ipv6=$(dig_hassio_dns AAAA) || \ + bashio::var.is_empty "${hassio_dns_ipv4-}" && bashio::var.is_empty "${hassio_dns_ipv6-}" +then + if ! hassio_dns_ipv4=$(get_hassio_dns_from_iptables "iptables" "32" "${MAGIC_DNS_IPV4}") || \ + ! hassio_dns_ipv6=$(get_hassio_dns_from_iptables "ip6tables" "128" "${MAGIC_DNS_IPV6}") || \ + bashio::var.is_empty "${hassio_dns_ipv4-}" && bashio::var.is_empty "${hassio_dns_ipv6-}" + then + bashio::exit.nok "Failed to resolve Home Assistant's DNS address" + fi +fi +# Tailscale's local IP addresses +if bashio::var.equals "${2-}" "forwarding"; then + if ! tailscale_address_ipv4=$(/opt/tailscale ip -4) || \ + ! tailscale_address_ipv6=$(/opt/tailscale ip -6) || \ + bashio::var.is_empty "${tailscale_address_ipv4-}" && bashio::var.is_empty "${tailscale_address_ipv6-}" + then + bashio::exit.nok "Failed to retrieve Tailscale's local address" + fi + if (bashio::var.has_value "${hassio_dns_ipv4-}" && bashio::var.is_empty "${tailscale_address_ipv4-}") || \ + (bashio::var.has_value "${hassio_dns_ipv6-}" && bashio::var.is_empty "${tailscale_address_ipv6-}") + then + bashio::exit.nok "Failed to retrieve Tailscale's local address with matching IP version (v4 or v6) to Home Assistant's DNS address" + fi +fi + +case "${1-}-${2-}" in + setup-drop) + # Due to forwarding to localhost is not enabled in HA OS, this is de facto a DROP (see martian packets). + # This temporary trick is needed, because when TS starts, it adds a general ACCEPT FORWARD line in front of existing settings, + # so we have to drop it during PREROUTING. + if bashio::var.has_value "${hassio_dns_ipv4-}"; then + setup_forwarding "iptables" "I" "udp" "IPv4" "32" "${hassio_dns_ipv4}" "${MAGIC_DNS_IPV4}" "127.0.0.1" "0" "${2}" + setup_forwarding "iptables" "I" "tcp" "IPv4" "32" "${hassio_dns_ipv4}" "${MAGIC_DNS_IPV4}" "127.0.0.1" "0" "${2}" + fi + if bashio::var.has_value "${hassio_dns_ipv6-}"; then + setup_forwarding "ip6tables" "I" "udp" "IPv6" "128" "${hassio_dns_ipv6}" "${MAGIC_DNS_IPV6}" "[::1]" "0" "${2}" + setup_forwarding "ip6tables" "I" "tcp" "IPv6" "128" "${hassio_dns_ipv6}" "${MAGIC_DNS_IPV6}" "[::1]" "0" "${2}" + fi + ;; + setup-forwarding) + if bashio::var.has_value "${hassio_dns_ipv4-}"; then + setup_forwarding "iptables" "A" "udp" "IPv4" "32" "${hassio_dns_ipv4}" "${MAGIC_DNS_IPV4}" "${tailscale_address_ipv4}" "${DNSMASQ_INGRESS_PORT}" "${2}" + setup_forwarding "iptables" "A" "tcp" "IPv4" "32" "${hassio_dns_ipv4}" "${MAGIC_DNS_IPV4}" "${tailscale_address_ipv4}" "${DNSMASQ_INGRESS_PORT}" "${2}" + fi + if bashio::var.has_value "${hassio_dns_ipv6-}"; then + setup_forwarding "ip6tables" "A" "udp" "IPv6" "128" "${hassio_dns_ipv6}" "${MAGIC_DNS_IPV6}" "[${tailscale_address_ipv6}]" "${DNSMASQ_INGRESS_PORT}" "${2}" + setup_forwarding "ip6tables" "A" "tcp" "IPv6" "128" "${hassio_dns_ipv6}" "${MAGIC_DNS_IPV6}" "[${tailscale_address_ipv6}]" "${DNSMASQ_INGRESS_PORT}" "${2}" + fi + ;; + remove-drop) + if bashio::var.has_value "${hassio_dns_ipv4-}"; then + remove_forwarding "iptables" "IPv4" "32" "${hassio_dns_ipv4}" "${MAGIC_DNS_IPV4}" "127.0.0.1" "0" "${2}" + fi + if bashio::var.has_value "${hassio_dns_ipv6-}"; then + remove_forwarding "ip6tables" "IPv6" "128" "${hassio_dns_ipv6}" "${MAGIC_DNS_IPV6}" "[::1]" "0" "${2}" + fi + ;; + remove-forwarding) + if bashio::var.has_value "${hassio_dns_ipv4-}"; then + remove_forwarding "iptables" "IPv4" "32" "${hassio_dns_ipv4}" "${MAGIC_DNS_IPV4}" "${tailscale_address_ipv4}" "${DNSMASQ_INGRESS_PORT}" "${2}" + fi + if bashio::var.has_value "${hassio_dns_ipv6-}"; then + remove_forwarding "ip6tables" "IPv6" "128" "${hassio_dns_ipv6}" "${MAGIC_DNS_IPV6}" "[${tailscale_address_ipv6}]" "${DNSMASQ_INGRESS_PORT}" "${2}" + fi + ;; + *) + echo "Usage: $(basename "$0") setup|remove drop|forwarding" 1>&2 + exit 1 + ;; +esac diff --git a/tailscale/rootfs/usr/lib/trace.sh b/tailscale/rootfs/usr/lib/trace.sh new file mode 100755 index 000000000..f3e4e1889 --- /dev/null +++ b/tailscale/rootfs/usr/lib/trace.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# ------------------------------------------------------------------------------ +# Checks if we are currently running in trace mode, based on the bashio log module. +# ------------------------------------------------------------------------------ +function bashio_custom::trace() { + if [[ "${__BASHIO_LOG_LEVEL}" -lt "${__BASHIO_LOG_LEVEL_TRACE}" ]]; then + return "${__BASHIO_EXIT_NOK}" + fi + + return "${__BASHIO_EXIT_OK}" +} diff --git a/tailscale/translations/en.yaml b/tailscale/translations/en.yaml index c963994c3..f1811e623 100644 --- a/tailscale/translations/en.yaml +++ b/tailscale/translations/en.yaml @@ -3,9 +3,9 @@ configuration: accept_dns: name: Accept DNS description: >- - If you are experiencing trouble with MagicDNS on this device and wish to - disable, you can do so using this option. - When not set, this option is enabled by default. + This option allows you to accept DNS settings of your tailnet that are + configured on the DNS page of the admin console. + This option is enabled by default. accept_routes: name: Accept routes description: >-