diff --git a/tailscale/CHANGELOG.md b/tailscale/CHANGELOG.md index 49da9029..754a1640 100644 --- a/tailscale/CHANGELOG.md +++ b/tailscale/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 0.12.0.1 (forked) + +***BREAKING CHANGES:*** +- Proxy and Funnel is disabled by default, because this got to be the default in the original add-on. + **If you previously used the default settings, enable them explicitly before installing this update:** + ``` + funnel: true + proxy: true + ``` + +Nonbreaking changes: +- New: Make Tailscale Proxy and Funnel port configurable +- New: Make auth-key configurable (inspired by [@laenbdarceq](https://github.com/laenbdarceq)) +- New: Optionally copy Tailscale Proxy's certificate files to /ssl folder +- Bugfix: Really disable Tailscale Proxy and Funnel when they are disabled +- Bugfix: Always protect the _local_ subnets (not the configurable _advertised_ subnets) from collision +- Merge changes from original add-on + - Sync all details of the merged and unmerged PRs + - Update Add-on base image to v14.2.2 + ## 0.11.1.26 (forked) - Warn when userspace networking is used to turn it off to access other clients on the tailnet diff --git a/tailscale/DOCS.md b/tailscale/DOCS.md index d4cf5a27..8e984498 100644 --- a/tailscale/DOCS.md +++ b/tailscale/DOCS.md @@ -5,26 +5,13 @@ > This is a **fork** of the [community add-on][community_addon]! > > Changes: -> - Release unreleased changes from community add-on: -> - Update tailscale/tailscale to v1.50.1 -> - Enable Tailscale's builtin inbound HTTPS proxy -> - Fix login-server option -> - Drop userspace networking -> - Make accepting magicDNS optional -> - Make exit node advertisement configurable -> - Make Taildrop configurable > - Release unmerged changes from community add-on: -> - Enable Tailscale's Funnel feature -> - Test Home Assistant's HTTP reverse proxy configuration -> - Warn about key expiration on add-on startup -> - Make userspace networking configurable -> - Make advertised subnet routes configurable -> - Make accepting subnet routes configurable -> - Protect advertised local subnets from being routed toward Tailscale subnets if they collide -> - Clamp the MSS to the MTU for all advertised subnet's interface (to support site-to-site networking better) -> - Make subnet source NAT configurable (to support advanced site-to-site networking) -> - Create fallback page for iOS browsers failing to open Tailscale login page -> - Do not opt out of client log upload in debug log level +> - Make Tailscale Proxy and Funnel port configurable +> - Make auth-key configurable +> - Optionally copy Tailscale Proxy's certificate files to /ssl folder +> - Really disable Tailscale Proxy and Funnel when they are disabled +> - Always protect the _local_ subnets (not the configurable _advertised_ subnets) from collision +> - Test Home Assistant's HTTP reverse proxy configuration on add-on start ![Warning][warning_stripe] @@ -61,8 +48,6 @@ however, it is nice to know where you need to go later on. ## How to use -1. **See the "Option: `proxy`" section of this documentation for the necessary - configuration changes in Home Assistant!** 1. Start the "Tailscale with features" add-on. 1. Check the logs of the "Tailscale with features" add-on to see if everything went well. @@ -113,10 +98,13 @@ advertise_exit_node: true advertise_routes: - 192.168.1.0/24 - fd12:3456:abcd::/64 -funnel: true +auth_key: "tskey-auth-xxx" +funnel: false +lets_encrypt_certfile: fullchain.pem +lets_encrypt_keyfile: privkey.pem log_level: info login_server: "https://controlplane.tailscale.com" -proxy: true +proxy: false snat_subnet_routes: true tags: - tag:example @@ -173,6 +161,14 @@ More information: [Subnet routers][tailscale_info_subnets] When not set, the add-on by default will advertise routes to your subnets on all supported interfaces. +### Option: `auth_key` + +This options allows to couple your Home Assistant instance with your Tailscale +account using an Auth key instead of the regular authentication flow using the +Web UI. + +More information: [Auth keys][tailscale_info_auth_keys] + ### Option: `funnel` This requires Tailscale Proxy to be enabled. @@ -180,53 +176,92 @@ This requires Tailscale Proxy to be enabled. **Important:** See also the "Option: `proxy`" section of this documentation for the necessary configuration changes in Home Assistant! -When not set, this option is enabled by default. +When not set, this option is disabled by default. -With the Tailscale Funnel feature you can access your Home Assistant instance +With the Tailscale Funnel feature, you can access your Home Assistant instance from the wider internet using your Tailscale domain (like `https://homeassistant.tail1234.ts.net`) even from devices **without installed -Tailscale VPN client** (eg. general phones, tablets, laptops). +Tailscale VPN client** (for example, on general phones, tablets, and laptops). **Client** ⇒ _Internet_ ⇒ **Tailscale Funnel** (TCP proxy) ⇒ _VPN_ ⇒ **Tailscale Proxy** (HTTPS proxy) → **HA** (HTTP web-server) Without the Tailscale Funnel feature, you will be able to access your Home -Assistant instance only when your devices (eg. phones, tablets, laptops) are -connected to your Tailscale VPN, there will be no Internet ⇒ VPN TCP +Assistant instance only when your devices (for example, phones, tablets, and laptops) +are connected to your Tailscale VPN, there will be no Internet ⇒ VPN TCP proxying for HTTPS communication. More information: [Tailscale Funnel][tailscale_info_funnel] -1. Navigate to the [Access controls page][tailscale_acls] of the admin console, - and add the below policy entries to the policy file. See [Server role - accounts using ACL tags][tailscale_info_acls] for more information. - - ```json - { - "nodeAttrs": [ - { - "target": ["autogroup:members"], - "attr": ["funnel"] - } - ] - } - ``` +1. Navigate to the [Access controls page][tailscale_acls] of the admin console: -1. Restart the add-on. + - Add the required `funnel` node attribute to the tailnet policy file. See + [Tailnet policy file requirement][tailscale_info_funnel_policy_requirement] + for more information. -**Note**: _After initial set up it can take up to 10 minutes for the domain to -be publicly available. You can use the `dig` command (Linux/MacOS) to regularly -check if an A-record is already present for your domain (`dig -..ts.net +short` should return an IP address once -the record is published)._ +1. Restart the add-on. -**Note:** _You should not use any port number in the url that you used -previously to access Home Assistant. Tailscale Funnel works on the default HTTPS -port 443._ +**Note**: _After initial setup, it can take up to 10 minutes for the domain to +be publicly available._ **Note:** _If you encounter strange browser behaviour or strange error messages, try to clear all site related cookies, clear all browser cache, restart browser._ +### _Note on the `lets_encrypt` options below_ + +_Until a bug in the Supervisor/UI is not fixed (see +[#4606](https://github.com/home-assistant/supervisor/issues/4606) and +[#2640](https://github.com/home-assistant/supervisor/issues/2640)), we can't use +the normal configuration schema (see below) as optional values. If the issues +get fixed in the future, configuration will be changed back to something better, +like:_ + +``` +lets_encrypt: + certfile: fullchain.pem + keyfile: privkey.pem +``` + +### Option: `lets_encrypt_certfile` + +This requires Tailscale Proxy to be enabled. + +**Important:** See also the "Option: `proxy`" section of this documentation for +the necessary configuration changes in Home Assistant! + +The name of the certificate file generated by Tailscale Proxy using Let's +Encrypt. Use "." to save the file with the original name containing the domain +(like "homeassistant.tail1234.ts.net.crt"), or use the regular +"fullchain.pem" or any file or folder name you prefer. + +Both `lets_encrypt` options (`lets_encrypt_certfile` and `lets_encrypt_keyfile`) +has to be specified or omitted together. + +**Note:** _The file is stored in the /ssl/ folder, which is the default for Home +Assistant._ + +When not set, this option is disabled by default. + +### Option: `lets_encrypt_keyfile` + +This requires Tailscale Proxy to be enabled. + +**Important:** See also the "Option: `proxy`" section of this documentation for +the necessary configuration changes in Home Assistant! + +The name of the private key file generated by Tailscale Proxy using Let's +Encrypt. Use "." to save the file with the original name containing the domain +(like "homeassistant.tail1234.ts.net.key"), or use the regular +"privkey.pem" or any file or folder name you prefer. + +Both `lets_encrypt` options (`lets_encrypt_certfile` and `lets_encrypt_keyfile`) +has to be specified or omitted together. + +**Note:** _The file is stored in the /ssl/ folder, which is the default for Home +Assistant._ + +When not set, this option is disabled by default. + ### Option: `log_level` Optionally enable tailscaled debug messages in the add-on's log. Turn it on only @@ -253,14 +288,13 @@ you are troubleshooting. ### Option: `login_server` -This option lets you specify you to specify a custom control server instead of -the default (`https://controlplane.tailscale.com`). This is useful if you -are running your own Tailscale control server, for example, a self-hosted -[Headscale] instance. +This option lets you to specify a custom control server instead of the default +(`https://controlplane.tailscale.com`). This is useful if you are running your +own Tailscale control server, for example, a self-hosted [Headscale] instance. ### Option: `proxy` -When not set, this option is enabled by default. +When not set, this option is disabled by default. Tailscale can provide a TLS certificate for your Home Assistant instance within your tailnet domain. @@ -289,7 +323,7 @@ More information: [Enabling HTTPS][tailscale_info_https] 1. Navigate to the [DNS page][tailscale_dns] of the admin console: - - Choose a Tailnet name. + - Choose a tailnet name. - Enable MagicDNS if not already enabled. @@ -297,14 +331,10 @@ More information: [Enabling HTTPS][tailscale_info_https] 1. Restart the add-on. -**Note:** _You should not use any port number in the URL that you used -previously to access Home Assistant. Tailscale Proxy works on the default HTTPS -port 443._ - ### Option: `snat_subnet_routes` This option allows subnet devices to see the traffic originating from the subnet -router, and this simplifyies routing configuration. +router, and this simplifies routing configuration. When not set, this option is enabled by default. @@ -338,18 +368,18 @@ accessible within your tailnet. 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, that will create a `tailscale0` +instance, disable userspace networking mode, which will create a `tailscale0` network interface on your host. If you want to access other clients on your tailnet even from your local subnet, -execute Step 2 and 3 as described on [Site-to-site +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. +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. ## Support @@ -375,8 +405,10 @@ You could also [open an issue here][issue] on GitHub. [tailscale_acls]: https://login.tailscale.com/admin/acls [tailscale_dns]: https://login.tailscale.com/admin/dns [tailscale_info_acls]: https://tailscale.com/kb/1068/acl-tags/ +[tailscale_info_auth_keys]: https://tailscale.com/kb/1085/auth-keys [tailscale_info_exit_nodes]: https://tailscale.com/kb/1103/exit-nodes/ [tailscale_info_funnel]: https://tailscale.com/kb/1223/tailscale-funnel/ +[tailscale_info_funnel_policy_requirement]: https://tailscale.com/kb/1223/tailscale-funnel/#tailnet-policy-file-requirement [tailscale_info_https]: https://tailscale.com/kb/1153/enabling-https/ [tailscale_info_key_expiry]: https://tailscale.com/kb/1028/key-expiry/ [tailscale_info_site_to_site]: https://tailscale.com/kb/1214/site-to-site/ diff --git a/tailscale/Dockerfile b/tailscale/Dockerfile index 2680c29a..72cf4863 100755 --- a/tailscale/Dockerfile +++ b/tailscale/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=ghcr.io/hassio-addons/base/amd64:14.2.0 +ARG BUILD_FROM=ghcr.io/hassio-addons/base/amd64:14.2.2 # hadolint ignore=DL3006 FROM ${BUILD_FROM} @@ -14,9 +14,10 @@ RUN \ ipcalc=1.0.2-r0 \ iproute2=6.3.0-r0 \ iptables=1.8.9-r2 \ - nginx=1.24.0-r6 \ + nginx=1.24.0-r7 \ coreutils=9.3-r1 \ networkmanager-common=1.42.8-r0 \ + inotify-tools=3.22.6.0-r2 \ \ && ln -sf /sbin/xtables-nft-multi /sbin/ip6tables \ && ln -sf /sbin/xtables-nft-multi /sbin/iptables \ diff --git a/tailscale/README.md b/tailscale/README.md index 3832e8c7..a1fc9e81 100644 --- a/tailscale/README.md +++ b/tailscale/README.md @@ -7,26 +7,13 @@ Zero config VPN for building secure networks. > This is a **fork** of the [community add-on][community_addon]! > > Changes: -> - Release unreleased changes from community add-on: -> - Update tailscale/tailscale to v1.50.1 -> - Enable Tailscale's builtin inbound HTTPS proxy -> - Fix login-server option -> - Drop userspace networking -> - Make accepting magicDNS optional -> - Make exit node advertisement configurable -> - Make Taildrop configurable > - Release unmerged changes from community add-on: -> - Enable Tailscale's Funnel feature -> - Test Home Assistant's HTTP reverse proxy configuration -> - Warn about key expiration on add-on startup -> - Make userspace networking configurable -> - Make advertised subnet routes configurable -> - Make accepting subnet routes configurable -> - Protect advertised local subnets from being routed toward Tailscale subnets if they collide -> - Clamp the MSS to the MTU for all advertised subnet's interface (to support site-to-site networking better) -> - Make subnet source NAT configurable (to support advanced site-to-site networking) -> - Create fallback page for iOS browsers failing to open Tailscale login page -> - Do not opt out of client log upload in debug log level +> - Make Tailscale Proxy and Funnel port configurable +> - Make auth-key configurable +> - Optionally copy Tailscale Proxy's certificate files to /ssl folder +> - Really disable Tailscale Proxy and Funnel when they are disabled +> - Always protect the _local_ subnets (not the configurable _advertised_ subnets) from collision +> - Test Home Assistant's HTTP reverse proxy configuration on add-on start ![Warning][warning_stripe] diff --git a/tailscale/build.yaml b/tailscale/build.yaml index 9b2b2db6..0cde8ad2 100644 --- a/tailscale/build.yaml +++ b/tailscale/build.yaml @@ -1,7 +1,7 @@ --- build_from: - aarch64: ghcr.io/hassio-addons/base/aarch64:14.2.0 - amd64: ghcr.io/hassio-addons/base/amd64:14.2.0 - armhf: ghcr.io/hassio-addons/base/armhf:14.2.0 - armv7: ghcr.io/hassio-addons/base/armv7:14.2.0 - i386: ghcr.io/hassio-addons/base/i386:14.2.0 + aarch64: ghcr.io/hassio-addons/base/aarch64:14.2.2 + amd64: ghcr.io/hassio-addons/base/amd64:14.2.2 + armhf: ghcr.io/hassio-addons/base/armhf:14.2.2 + armv7: ghcr.io/hassio-addons/base/armv7:14.2.2 + i386: ghcr.io/hassio-addons/base/i386:14.2.2 diff --git a/tailscale/config.yaml b/tailscale/config.yaml index af06a4ff..76a425cd 100644 --- a/tailscale/config.yaml +++ b/tailscale/config.yaml @@ -1,6 +1,6 @@ --- name: Tailscale with features -version: 0.11.1.26 +version: 0.12.0.1 slug: tailscale description: Zero config VPN for building secure networks url: https://github.com/lmagyar/homeassistant-addon-tailscale @@ -27,19 +27,27 @@ devices: - /dev/net/tun map: - share:rw + - ssl:rw schema: accept_dns: bool? accept_routes: bool? advertise_exit_node: bool? advertise_routes: - "match(^(((25[0-5]|(2[0-4]|1\\d|[1-9]?)\\d)\\.){3}(25[0-5]|(2[0-4]|1\\d|[1-9]?)\\d)\\/(3[0-2]|[12]?\\d)|[a-fA-F\\d.:]+:[a-fA-F\\d.:]+\\/(12[0-8]|(1[01]|[1-9]?)\\d))$)?" + auth_key: str? + funnel: bool? + lets_encrypt_certfile: str? + lets_encrypt_keyfile: str? log_level: list(trace|debug|info|notice|warning|error|fatal)? login_server: url? + proxy: bool? snat_subnet_routes: bool? tags: - "match(^tag:[a-zA-Z0-9]-?[a-zA-Z0-9]+$)?" taildrop: bool? userspace_networking: bool? - proxy: bool? - funnel: bool? +ports: + 443/tcp: 443 +ports_description: + 443/tcp: Tailscale Proxy and Funnel port image: ghcr.io/lmagyar/{arch}-addon-tailscale diff --git a/tailscale/rootfs/etc/nginx/templates/ingress.gtpl b/tailscale/rootfs/etc/nginx/templates/ingress.gtpl index 13632655..61361974 100644 --- a/tailscale/rootfs/etc/nginx/templates/ingress.gtpl +++ b/tailscale/rootfs/etc/nginx/templates/ingress.gtpl @@ -11,7 +11,7 @@ server { proxy_pass http://backend; sub_filter_once off; - sub_filter 'document.location.href = url' 'var result = window.open(url, "_blank"); if (result!== null) {result.focus()} else {document.write(\'
Unable to open Tailscale in new window. Please copy this URL; open in a separate browser; and re-load the addon Web UI here when complete. \'+url+\'
\')}'; + sub_filter 'document.location.href = url' 'var result = window.open(url, "_blank"); if (result!== null) {result.focus()} else {document.write(\'
Unable to open Tailscale in new window. Please copy this URL, open it in a separate browser, and re-load the addon Web UI here when complete. \'+url+\'
\')}'; } } diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/dependencies.d/proxy b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/dependencies.d/proxy new file mode 100644 index 00000000..e69de29b diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/finish b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/finish new file mode 100755 index 00000000..86055726 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/finish @@ -0,0 +1,27 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Take down the S6 supervision tree when certificate fails +# ============================================================================== +declare exit_code +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/certificate/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/run new file mode 100755 index 00000000..0f0e4497 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/run @@ -0,0 +1,68 @@ +#!/command/with-contenv bashio +# shellcheck shell=bash +# ============================================================================== +# Home Assistant Community Add-on: Tailscale +# Copies Tailscale Proxy certificates to /ssl folder +# ============================================================================== + +readonly CERTS_FOLDER=/data/state/certs + +declare domain +declare certfile keyfile +declare cp_log + +# Check if Tailscale HTTPS is enabled +if ! /opt/tailscale status --self=true --peers=false --json \ + | jq -rce '.Self.CapMap | has("https")' > /dev/null; +then + bashio::log.error "Tailscale's HTTPS support is disabled" + bashio::exit.nok +fi + +domain=$(/opt/tailscale status --self=true --peers=false --json | jq -rc ".CertDomains[0]") +certfile=$(bashio::config "lets_encrypt_certfile") +keyfile=$(bashio::config "lets_encrypt_keyfile") + +# After tailscale serv is up, it will automatically update the files, we can catch the events +# But if the update is already happened, inotifywait can't catch it +# That's why inotifywait is running in non-quiet mode, and the event "Watches established." +# printed on stderr triggers an initial update of the files, +# and it updates the files on the first usage of the service also +inotifywait --monitor "${CERTS_FOLDER}" --include='^.*?\.(crt|key)$' \ + --event='close_write,moved_to' --format '%f' \ + |& while IFS= read -r changed +do + # Currently if the machine name is changed, tailscale changes the json status, + # but the proxy isn't restarted and do not change to the new domain, and won't create new certs for the new domain, + # restart of the add-on is required, and a restart will "update" the $domain variable + # # Check if domain has changed + # domain=$(/opt/tailscale status --self=true --peers=false --json | jq -rc ".CertDomains[0]") + + if [[ "${changed}" == "Watches established." ]]; then + changed="[WATCHES_ESTABLISHED]" + fi + + # Check if filename is the domain name (do not update on other certificate changes) + if [[ "${changed}" == "[WATCHES_ESTABLISHED]" || "${changed%.*}" == "${domain}" ]]; then + if [[ "${changed}" == "[WATCHES_ESTABLISHED]" || "${changed##*.}" == "crt" ]]; then + if ! cp_log=$(cp --force --update=older --preserve=mode,ownership,timestamps --verbose \ + "${CERTS_FOLDER}/${domain}.crt" "/ssl/${certfile}"); + then + bashio::log.error "Unable to copy certificate file '${CERTS_FOLDER}/${domain}.crt' to '/ssl/${certfile}'" + bashio::log.error "Check configuration and restart add-on" + elif [[ "${cp_log:0:7}" != "skipped" ]]; then + bashio::log.info "Updated certificate file '${domain}.crt' to '/ssl/${certfile}'" + fi + fi + if [[ "${changed}" == "[WATCHES_ESTABLISHED]" || "${changed##*.}" == "key" ]]; then + if ! cp_log=$(cp --force --update=older --preserve=mode,ownership,timestamps --verbose \ + "${CERTS_FOLDER}/${domain}.key" "/ssl/${keyfile}"); + then + bashio::log.error "Unable to copy key file '${CERTS_FOLDER}/${domain}.key' to '/ssl/${keyfile}'" + bashio::log.error "Check configuration and restart add-on" + elif [[ "${cp_log:0:7}" != "skipped" ]]; then + bashio::log.info "Updated key file '${domain}.key' to '/ssl/${keyfile}'" + fi + fi + fi +done diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/type b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/type new file mode 100644 index 00000000..5883cff0 --- /dev/null +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/certificate/type @@ -0,0 +1 @@ +longrun diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/funnel/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/funnel/run index 572dce0c..d22bfba2 100755 --- a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/funnel/run +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/funnel/run @@ -5,31 +5,60 @@ # Enables Tailscale Funnel feature # ============================================================================== +declare already_configured_funnel_port +declare https_port declare domain -# Check if Tailscale HTTPS is enabled -if ! /opt/tailscale status --self=true --peers=false --json \ - | jq -rce '.Self.CapMap | has("https")' > /dev/null; -then - bashio::log.notice "Tailscale's HTTPS support is disabled, therefore add-on's Tailscale Funnel functionality is disabled" - bashio::exit.ok -fi +# Tailscale serve and funnel configuration is persistent between restarts +# Disable previously configured tailscale funnel functionality with deleting the tailscale funnel configuration +# There is no tailscale funnel reset command currently, we loop through previous config +for already_configured_funnel_port in $( \ + /opt/tailscale funnel status --json \ + | jq -rc '.AllowFunnel // [] | keys[]' \ + | sed -nr 's/^[^:]*:(\d*)$/\1/p'); +do + if ! /opt/tailscale funnel "${already_configured_funnel_port}" off; then + bashio::log.error "Unable to reset Tailscale Funnel settings" + bashio::exit.nok + fi +done -domain=$(/opt/tailscale status --self=true --peers=false --json | jq -rc ".CertDomains[0]") +# Enable funnel service only when it has been explicitly enabled +if bashio::config.true 'proxy' && bashio::config.true 'funnel'; then -# Check if Funnel is available -if ! /opt/tailscale status --self=true --peers=false --json \ - | jq -rce '.Self.CapMap | has("funnel")' > /dev/null; -then - bashio::log.notice "Tailscale's Funnel support is disabled, therefore add-on's Tailscale Funnel functionality is disabled" - bashio::exit.ok -fi + https_port=$(bashio::addon.port 443) + + # Check if HTTPS port is configured properly + if ! bashio::var.has_value "${https_port}"; then + bashio::log.error "No HTTPS port is configured" + bashio::exit.nok + fi + + # Check if Tailscale HTTPS is enabled + if ! /opt/tailscale status --self=true --peers=false --json \ + | jq -rce '.Self.CapMap | has("https")' > /dev/null; + then + bashio::log.error "Tailscale's HTTPS support is disabled" + bashio::exit.nok + fi + + domain=$(/opt/tailscale status --self=true --peers=false --json | jq -rc ".CertDomains[0]") + + # Check if Funnel is available + if ! /opt/tailscale status --self=true --peers=false --json \ + | jq -rce '.Self.CapMap | has("funnel")' > /dev/null; + then + bashio::log.error "Tailscale's Funnel support is disabled" + bashio::exit.nok + fi + + # Set up funnel + if ! /opt/tailscale funnel "${https_port}" on; then + bashio::log.error "Unable to configure Tailscale Funnel" + bashio::exit.nok + fi + bashio::log.info "Tailscale Funnel is enabled:" + bashio::log.info " Your Home Assistant instance is publicly available on the internet at" + bashio::log.info " https://${domain}:${https_port}" -# Set up funnel -if ! /opt/tailscale funnel 443 on; then - bashio::log.error "Unable to configure Tailscale Funnel" - bashio::exit.nok fi -bashio::log.info "Tailscale Funnel is enabled:" -bashio::log.info " Your Home Assistant instance is publicly available on the internet at" -bashio::log.info " https://${domain}" diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/mss-clamping/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/mss-clamping/run index 5cf464d0..360c9c06 100755 --- a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/mss-clamping/run +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/mss-clamping/run @@ -9,7 +9,7 @@ declare -a routes=() declare -a interfaces=() declare route family interface -readarray -t routes < <(subnet-routes) +readarray -t routes < <(subnet-routes advertised) # In case of non userspace networking, clamp the MSS to the MTU for all advertised subnet's interface # If user later enables subnet routing for site-to-site networking, these settings are already there 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 6f138376..14989c2c 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 @@ -8,6 +8,7 @@ declare -a options declare -a routes=() declare route declare -a colliding_routes=() +declare auth_key declare login_server declare tags declare keyexpiry @@ -42,6 +43,13 @@ else options+=(--advertise-exit-node=false) fi +# Use configured auth-key +if bashio::config.has_value "auth_key"; +then + auth_key=$(bashio::config "auth_key") + options+=(--auth-key="${auth_key}") +fi + # Get configured control server if bashio::config.has_value "login_server"; then @@ -63,7 +71,7 @@ tags=$(bashio::config "tags//[] | join(\",\")" "") options+=(--advertise-tags="${tags}") # Advertise subnet routes -readarray -t routes < <(subnet-routes) +readarray -t routes < <(subnet-routes advertised) IFS="," options+=(--advertise-routes="${routes[*]}") unset IFS @@ -91,14 +99,21 @@ done bashio::log.info "Tailscale is running" -# Notify about colliding subnet routes if non-userspace networking and accepting routes are enabled +# Warn about key expiration +if keyexpiry=$(/opt/tailscale status --self=true --peers=false --json | jq -rce '.Self.KeyExpiry'); then + bashio::log.warning "The connection's key will expire on: ${keyexpiry}" + bashio::log.warning "Consider disabling key expiry to avoid losing connection to your Home Assistant device." + bashio::log.warning "Please check your configuration based on the add-on's documentation under \"Configuration\"" +fi + +# Warn about colliding subnet routes if non-userspace networking and accepting routes are enabled if bashio::config.false "userspace_networking" && \ (! bashio::config.has_value "accept_routes" || \ bashio::config.true "accept_routes"); then readarray -t colliding_routes < <( \ comm -1 -2 \ - <(printf "%s" "${routes[@]/%/$'\n'}") \ + <(subnet-routes local) \ <(/opt/tailscale status --json --peers=true --self=false \ | jq -rc '.Peer[] | select(has("PrimaryRoutes")) | .PrimaryRoutes[]' \ | sort -u)) @@ -113,19 +128,21 @@ then done fi -# Warn about key expiration -if keyexpiry=$(/opt/tailscale status --self=true --peers=false --json | jq -rce '.Self.KeyExpiry'); then - bashio::log.warning "The connection's key will expire on: ${keyexpiry}" - bashio::log.warning "Consider disabling key expiry to avoid losing connection to your Home Assistant device." - bashio::log.warning "Please check your configuration based on the add-on's Documentation under \"Configuration\"" +# Warn about invalid certificate service configuration (can't be checked by the UI) +if (bashio::config.has_value "lets_encrypt_certfile" && ! bashio::config.has_value "lets_encrypt_keyfile") || + (! bashio::config.has_value "lets_encrypt_certfile" && bashio::config.has_value "lets_encrypt_keyfile"); +then + bashio::log.warning \ + "Both 'lets_encrypt' options ('lets_encrypt_certfile' and 'lets_encrypt_keyfile')" \ + "has to be specified or omitted together." fi -# Warn about userspace networking +# Notify about userspace networking if ! bashio::config.has_value "userspace_networking" || \ bashio::config.true "userspace_networking"; then - bashio::log.warning "The add-on uses userspace networking mode." - bashio::log.warning "If you need to access other clients on your tailnet from your Home Assistant instance," - bashio::log.warning "disable userspace networking mode, that will create a \"tailscale0\" network interface on your host." - bashio::log.warning "Please check your configuration based on the add-on's Documentation under \"Option: userspace_networking\"" + bashio::log.notice "The add-on uses userspace networking mode." + bashio::log.notice "If you need to access other clients on your tailnet from your Home Assistant instance," + bashio::log.notice "disable userspace networking mode, that will create a \"tailscale0\" network interface on your host." + bashio::log.notice "Please check your configuration based on the add-on's documentation under \"Option: userspace_networking\"" fi diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/proxy/run b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/proxy/run index e815bffa..61b3b6f1 100755 --- a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/proxy/run +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/proxy/run @@ -5,42 +5,57 @@ # Enables Tailscale Proxy feature # ============================================================================== +declare https_port declare domain -# Check if Tailscale HTTPS is enabled -if ! /opt/tailscale status --self=true --peers=false --json \ - | jq -rce '.Self.CapMap | has("https")' > /dev/null; -then - bashio::log.notice "Tailscale's HTTPS support is disabled, therefore add-on's Tailscale Proxy functionality is disabled" - bashio::exit.ok +# Tailscale serve and funnel configuration is persistent between restarts +# Disable previously configured tailscale serve functionality with deleting the tailscale serve configuration +if ! /opt/tailscale serve reset; then + bashio::log.error "Unable to reset Tailscale Proxy settings" + bashio::exit.nok fi -domain=$(/opt/tailscale status --self=true --peers=false --json | jq -rc ".CertDomains[0]") +# Enable proxy service only when it has been explicitly enabled +if bashio::config.true 'proxy'; then -# Checking if SSL is used -if bashio::var.true "$(bashio::core.ssl)"; then - bashio::log.error "Tailscale's HTTPS support is enabled, but Home Assistant is not accessible through plain HTTP connection" - bashio::exit.nok -fi + https_port=$(bashio::addon.port 443) -# Test Home Assistant's HTTP reverse proxy configuration -if (( 200 != $(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$(bashio::core.port)" -H "X-Forwarded-For: 127.0.0.1") )); then - bashio::log.error "Unable to connect to Home Assistant on http://127.0.0.1:$(bashio::core.port)" - bashio::log.error "Please check your configuration based on the add-on's Documentation under \"Option: proxy\"" - bashio::exit.nok -fi + # Check if HTTPS port is configured properly + if ! bashio::var.has_value "${https_port}"; then + bashio::log.error "No HTTPS port is configured" + bashio::exit.nok + fi + + # Check if Tailscale HTTPS is enabled + if ! /opt/tailscale status --self=true --peers=false --json \ + | jq -rce '.Self.CapMap | has("https")' > /dev/null; + then + bashio::log.error "Tailscale's HTTPS support is disabled" + bashio::exit.nok + fi + + domain=$(/opt/tailscale status --self=true --peers=false --json | jq -rc ".CertDomains[0]") + + # Checking if SSL is used + if bashio::var.true "$(bashio::core.ssl)"; then + bashio::log.error "Tailscale's HTTPS support is enabled, but Home Assistant is not accessible through plain HTTP connection" + bashio::exit.nok + fi + + # Test Home Assistant's HTTP reverse proxy configuration + if (( 200 != $(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:$(bashio::core.port)" -H "X-Forwarded-For: 127.0.0.1") )); then + bashio::log.error "Unable to connect to Home Assistant" + bashio::log.error "Please check your configuration based on the add-on's documentation under \"Option: proxy\"" + bashio::exit.nok + fi + + # Set up proxy + if ! /opt/tailscale serve "https:${https_port}" / "http://127.0.0.1:$(bashio::core.port)"; then + bashio::log.error "Unable to configure Tailscale Proxy" + bashio::exit.nok + fi + bashio::log.info "Tailscale Proxy is enabled:" + bashio::log.info " Your Home Assistant instance is available within your tailnet VPN at" + bashio::log.info " https://${domain}:${https_port}" -# Set up proxy -# Note: Configuration is persistent between reboots, though configuring the same value won't cause duplicate entries, -# but will overwite previous entry if core.port has been changed -# Note: If a tailnet node is renamed, there will be deprecated entries in status json under .Web., -# those can be removed only with reseting the serve configuration -if ! /opt/tailscale serve reset \ - || ! /opt/tailscale serve https:443 / "http://127.0.0.1:$(bashio::core.port)" -then - bashio::log.error "Unable to configure Tailscale Proxy" - bashio::exit.nok fi -bashio::log.info "Tailscale Proxy is enabled:" -bashio::log.info " Your Home Assistant instance is available within your Tailnet VPN at" -bashio::log.info " https://${domain}" 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 681d30d9..a12880c3 100755 --- a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/run +++ b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/tailscaled/run @@ -37,7 +37,7 @@ if bashio::debug ; then exec /opt/tailscaled "${options[@]}" else bashio::log.notice \ - "Tailscale logs will be suppressed after 200 lines, set add-on's "\ + "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 \ diff --git a/tailscale/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/certificate b/tailscale/rootfs/etc/s6-overlay/s6-rc.d/user/contents.d/certificate 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 9abfe064..b7972df1 100755 --- a/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh +++ b/tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh @@ -21,17 +21,15 @@ then rm /etc/s6-overlay/s6-rc.d/user/contents.d/mss-clamping fi -# Disable taildrop service when it is has been explicitly disabled +# Disable taildrop service when it has been explicitly disabled if bashio::config.false 'taildrop'; then rm /etc/s6-overlay/s6-rc.d/user/contents.d/taildrop fi -# Disable proxy service when it is has been explicitly disabled -if bashio::config.false 'proxy'; then - rm /etc/s6-overlay/s6-rc.d/user/contents.d/proxy -fi - -# Disable funnel service when it is has been explicitly disabled -if bashio::config.false 'proxy' || bashio::config.false 'funnel'; then - rm /etc/s6-overlay/s6-rc.d/user/contents.d/funnel +# Disable certificate service when it has not been configured +if ! bashio::config.true 'proxy' || \ + ! bashio::config.has_value "lets_encrypt_certfile" || \ + ! bashio::config.has_value "lets_encrypt_keyfile"; +then + rm /etc/s6-overlay/s6-rc.d/user/contents.d/certificate fi diff --git a/tailscale/rootfs/usr/bin/protect-subnet-routes b/tailscale/rootfs/usr/bin/protect-subnet-routes index 0cf79e0b..7c179787 100755 --- a/tailscale/rootfs/usr/bin/protect-subnet-routes +++ b/tailscale/rootfs/usr/bin/protect-subnet-routes @@ -26,14 +26,17 @@ if bashio::config.false "userspace_networking"; then bashio::log.error "Supervisor is unreachable" bashio::exit.nok fi - bashio::log.notice "Waiting for the supervisor to be ready..." + 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) + readarray -t routes < <(subnet-routes local) 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." + bashio::log.info "Adding local subnets to ip rules with higher priority than Tailscale's routing," + bashio::log.info "to prevent routing local subnets if the same subnet is routed within your tailnet." fi for route in "${routes[@]}"; do if [[ "${route}" =~ .*:.* ]]; then diff --git a/tailscale/rootfs/usr/bin/subnet-routes b/tailscale/rootfs/usr/bin/subnet-routes index 792792a8..ad2c72b8 100755 --- a/tailscale/rootfs/usr/bin/subnet-routes +++ b/tailscale/rootfs/usr/bin/subnet-routes @@ -15,24 +15,29 @@ function appendarray() { readarray -t -O "${#array[@]}" array } -if bashio::cache.exists 'subnet-routes'; then - readarray -t routes < <(bashio::cache.get 'subnet-routes') +if ! [[ $1 =~ ^(local|advertised)$ ]]; then + echo "Usage: subnet-routes local|advertised" 1>&2 + exit 1 +fi + +if bashio::cache.exists "subnet-routes-$1"; then + readarray -t routes < <(bashio::cache.get "subnet-routes-$1") printf -v response "%s" "${routes[@]/%/$'\n'}" else - if bashio::config.exists "advertise_routes"; then + if [[ "$1" == "advertised" ]] && bashio::config.exists "advertise_routes"; then # Configuration exists, use configured values for address in $(bashio::config "advertise_routes"); do addresses+=("${address}") done else - # Find interfaces and matching addresses from which we can extract routes to be advertised + # Find interfaces and matching addresses from which we can extract routes for interface in $(bashio::network.interfaces); do appendarray addresses < <(bashio::network.ipv4_address "${interface}") appendarray addresses < <(bashio::network.ipv6_address "${interface}") done fi - # Extract routes to be advertised + # Extract routes for address in "${addresses[@]}"; do if bashio::var.has_value "${address}"; then # Skip local link addresses @@ -58,7 +63,7 @@ else readarray -t routes < <(printf "%s" "${routes[@]/%/$'\n'}" | sort -u) printf -v response "%s" "${routes[@]/%/$'\n'}" - bashio::cache.set 'subnet-routes' "${response}" + bashio::cache.set "subnet-routes-$1" "${response}" fi printf "%s" "${response}" diff --git a/tailscale/translations/en.yaml b/tailscale/translations/en.yaml index 7f42070f..4e781098 100644 --- a/tailscale/translations/en.yaml +++ b/tailscale/translations/en.yaml @@ -26,13 +26,29 @@ configuration: your device is connected to) to other clients on your tailnet. When not set, the add-on by default will advertise routes to your subnets on all supported interfaces. + auth_key: + name: Auth key + description: >- + This options allows to couple your Home Assistant instance with your Tailscale + account using an Auth key instead of the regular authentication flow using the + Web UI. funnel: name: Tailscale Funnel description: >- This option allows you to enable Tailscale's Funnel feature to present your Home Assistant instance on the wider internet using your Tailscale domain. This requires Tailscale Proxy to be enabled. - When not set, this option is enabled by default. + When not set, this option is disabled by default. + lets_encrypt_certfile: + name: Let's Encrypt certificate chain file storage location + description: >- + The name of the certificate file generated by Tailscale Proxy using Let's Encrypt. + The file is stored in the /ssl/ folder. + lets_encrypt_keyfile: + name: Let's Encrypt certificate key file storage location + description: >- + The name of the private key file generated by Tailscale Proxy using Let's Encrypt. + The file is stored in the /ssl/ folder. log_level: name: Log level description: >- @@ -49,12 +65,12 @@ configuration: description: >- This option allows you to enable Tailscale's Proxy feature to present your Home Assistant instance on your tailnet with a valid certificate. - When not set, this option is enabled by default. + When not set, this option is disabled by default. snat_subnet_routes: name: Source NAT subnet routes description: >- This option allows subnet devices to see the traffic originating from the - subnet router, and this simplifyies routing configuration. + subnet router, and this simplifies routing configuration. To support advanced Site-to-site networking (eg. to traverse multiple networks), you can disable this functionality. When not set, this option is enabled by default. @@ -74,6 +90,11 @@ configuration: description: >- This option allows you to enable userspace networking mode. If you need to access other clients on your tailnet from your Home - Assistant instance, disable userspace networking mode, that will create a + Assistant instance, disable userspace networking mode, which will create a `tailscale0` network interface on your host. When not set, this option is enabled by default. +network: + 443/tcp: >- + This option allows you to configure the port the Tailscale Proxy and Funnel + features are accessible on. + Only port number 443, 8443 and 10000 is allowed by Tailscale Funnel.