-
Notifications
You must be signed in to change notification settings - Fork 127
Add support for advertising Tailscale Services #715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,7 @@ log_level: info | |
| login_server: "https://controlplane.tailscale.com" | ||
| share_homeassistant: disabled | ||
| share_on_port: 443 | ||
| services: [] | ||
| snat_subnet_routes: true | ||
| stateful_filtering: false | ||
| tags: | ||
|
|
@@ -317,6 +318,47 @@ Only ports 443, 8443, and 10000 are allowed by Tailscale. | |
|
|
||
| Port 443 is used by default. | ||
|
|
||
| ### Option: `services` | ||
|
|
||
| This option allows you to advertise other local services running on this device | ||
| as [Tailscale Services][tailscale_info_services]. Each service needs a name, a | ||
| local target address, a protocol, and a port to expose it on. The `svc:` prefix | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The advertise_tags option requires the tag: prefix. Maybe we should require svc: here also. What do you think?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree, we should remove tag: also, in a separate PR. If this PR is accepted, I will make a tag: removing PR. :)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would keep it in both places here and do another PR removing them together. So there is no mixed state in between.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is your decision. :) |
||
| is added automatically by the app; do not include it in the configuration. | ||
|
|
||
| You can use this to expose apps running on your Home Assistant instance, such as | ||
| an audiobookshelf add-on, to your tailnet using a stable MagicDNS name. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no add-ons anymore, use apps please |
||
|
|
||
| ```yaml | ||
| services: | ||
| - name: audiobookshelf | ||
| target: http://127.0.0.1:13378 | ||
| protocol: http | ||
| port: 80 | ||
| path: / | ||
| ``` | ||
|
|
||
| Supported protocols: | ||
|
|
||
| - `http`: Expose the service as an HTTP server on the configured port. | ||
| - `https`: Expose the service as an HTTPS server on the configured port. | ||
| You must enable MagicDNS and HTTPS certificates for your tailnet in the | ||
| Tailscale admin console first. Once enabled, Tailscale automatically | ||
| provisions a TLS certificate for the service. | ||
| - `tcp`: Forward raw TCP packets to the configured target. | ||
| - `tls-terminated-tcp`: Forward TLS-terminated TCP packets to the configured | ||
| target. | ||
|
|
||
| The `target` must be a local address reachable from this app. Use `http://` or | ||
| `https://` targets for HTTP/HTTPS protocols, and `tcp://` targets for TCP and | ||
| tls-terminated-tcp protocols, for example `http://127.0.0.1:13378`. The `path` | ||
| option is optional and defaults to `/`. | ||
|
|
||
| Before a service can accept traffic, you must define the Service in the Tailscale | ||
| admin console and approve this device as a Service host. More information: | ||
| [Services][tailscale_info_services]. | ||
|
|
||
| This option is disabled by default. | ||
|
|
||
| ### Option: `snat_subnet_routes` | ||
|
|
||
| This option allows subnet devices to see the traffic originating from the subnet | ||
|
|
@@ -545,6 +587,7 @@ SOFTWARE. | |
| [tailscale_info_pi_hole]: https://tailscale.com/docs/solutions/block-ads-all-devices-anywhere-using-raspberry-pi | ||
| [tailscale_info_quad100]: https://tailscale.com/docs/reference/quad100 | ||
| [tailscale_info_serve]: https://tailscale.com/docs/features/tailscale-serve | ||
| [tailscale_info_services]: https://tailscale.com/docs/features/tailscale-services | ||
| [tailscale_info_site_to_site]: https://tailscale.com/docs/features/site-to-site | ||
| [tailscale_info_subnets]: https://tailscale.com/docs/features/subnet-routers | ||
| [tailscale_info_tags]: https://tailscale.com/docs/features/tags | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,6 +52,7 @@ options: | |
| login_server: "https://controlplane.tailscale.com" | ||
| share_homeassistant: disabled | ||
| share_on_port: 443 | ||
| services: [] | ||
| snat_subnet_routes: true | ||
| stateful_filtering: false | ||
| tags: [] | ||
|
|
@@ -78,6 +79,12 @@ schema: | |
| login_server: url | ||
| share_homeassistant: list(disabled|serve|funnel) | ||
| share_on_port: match(^(?:443|8443|10000)$) | ||
| services: | ||
| - name: "match(^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$)" | ||
| target: "match(^(?:https?|tcp)://127\\.0\\.0\\.1:([1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$)" | ||
| protocol: "list(http|https|tcp|tls-terminated-tcp)" | ||
| port: "match(^([1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$)" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is a "port" type, see https://developers.home-assistant.io/docs/apps/configuration#options--schema |
||
| path: "match(^/.*$)?" | ||
| snat_subnet_routes: bool | ||
| stateful_filtering: bool | ||
| tags: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| #!/command/with-contenv bashio | ||
| # shellcheck shell=bash | ||
| export LOG_FD | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is not needed with the new base image with the new bashio. Will be removed in all other places later, with all the temporary workarounds.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't know that. |
||
| # ============================================================================== | ||
| # Home Assistant Community App: Tailscale | ||
| # Advertises configured Tailscale Services from this host | ||
| # ============================================================================== | ||
|
|
||
| declare current_config | ||
|
|
||
| declare service | ||
| declare name | ||
| declare service_name | ||
|
|
||
| declare protocol | ||
|
|
||
| declare port | ||
|
|
||
| declare target | ||
|
|
||
| declare path | ||
|
|
||
| declare -a options=() | ||
| declare -a configured_services=() | ||
| declare -a configured_names=() | ||
| declare -a current_names=() | ||
|
|
||
| # Nothing to do when no services are configured | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nope, we still have to remove removed services!!! |
||
| if ! bashio::config 'services' | jq -e '. | length > 0' > /dev/null 2>&1; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Read configured services | ||
| # Note: bashio::config emits one JSON object per line for array options | ||
| readarray -t configured_services < <(bashio::config 'services') | ||
|
|
||
| # Build a list of configured service names (without the svc: prefix) | ||
| for service in "${configured_services[@]}"; do | ||
| name=$(jq -re '.name' <<< "${service}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bashio:jq ? |
||
| name="${name#svc:}" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have svc: prefix there?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That will be not necessary anymore, when we make
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean |
||
| configured_names+=("${name}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more thing, to get configured_names, you can use bashio::config 'services.names' (if I remember correctly), and add some jq or grep to delete the svc: prefixes if needed, so this whole loop can be a one-liner I think. |
||
| done | ||
|
|
||
| bashio::log.info "Configured service hosts: ${configured_names[*]}" | ||
|
|
||
| # Wait for tailscaled to be available | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. post-tailscaled already waited for this |
||
| while ! bashio::fs.socket_exists "/var/run/tailscale/tailscaled.sock"; do | ||
| sleep 1 | ||
| done | ||
|
|
||
| while ! /opt/tailscale status --json --peers=false --self=false \ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. post-tailscaled already waited for this
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, okay that's useful. I haven't caught this. |
||
| | jq --exit-status '.BackendState == "Running"' > /dev/null; do | ||
| sleep 2 | ||
| done | ||
|
|
||
| # Read currently advertised services from the tailscale serve config | ||
| if ! current_config=$(/opt/tailscale serve get-config --all 2>/dev/null); then | ||
| current_config='{}' | ||
| fi | ||
|
|
||
| # Build a list of currently configured service names | ||
| readarray -t current_names < <(jq -re '.services | keys[]' <<< "${current_config}" 2>/dev/null || true) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not use bashio::jq() ? |
||
|
|
||
| # Remove services that are no longer configured in this app | ||
| for service_name in "${current_names[@]}"; do | ||
| if [[ "${service_name}" != svc:* ]]; then | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do wee need this? just asking! what else can be there?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was ment as a safe guard agains when Tailscale should decide to put other things in the .service section then actual services. But you are questioning this correctly. Why should they do it. I'll remove that guard. |
||
| continue | ||
| fi | ||
| # Compare without the svc: prefix | ||
| if ! printf '%s\n' "${configured_names[@]}" | grep -qx "${service_name#svc:}"; then | ||
| bashio::log.info "Removing stale service host configuration for ${service_name}" | ||
| /opt/tailscale serve drain "${service_name}" >/dev/null 2>&1 || true | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should log at least a warning on error (an exit.nok would stop the app, so let it start up, but don't hide the error) |
||
| /opt/tailscale serve clear "${service_name}" >/dev/null 2>&1 || true | ||
| fi | ||
| done | ||
|
|
||
| # Configure and advertise each service | ||
| for service in "${configured_services[@]}"; do | ||
| name=$(jq -re '.name' <<< "${service}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bashio:jq ? |
||
| name="${name#svc:}" | ||
| service_name="svc:${name}" | ||
| protocol=$(jq -re '.protocol' <<< "${service}") | ||
| port=$(jq -re '.port' <<< "${service}") | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My last question: can we use the same eg. 443 port here, as the share_homeassistant service uses? Services are running on a virtual tailnet name/IP, so it shouldn't be a problem. I'm asking, because I'm lazy to test it myself. :D
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes that should be possible. I will test this once i have implemented the changes. |
||
| target=$(jq -re '.target' <<< "${service}") | ||
| path=$(jq -re '.path // empty' <<< "${service}") | ||
|
|
||
| options=() | ||
| case "${protocol}" in | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. protocol's value is restricted by the config schema, validate only that it is in the accepted list, exit.nok if not (fail during development), then use it directly in a single line
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if ! "${protocol}" = @(http|https|tcp|tls-terminated-tcp); then
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not very good in bash. Thanks for the advice.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another idea. If https or tls-terminated-tcp is used, shouldn't we check once (!!!) that https support is enabled? Like in Though we can say, that share_homeassistant is for the less experienced users, with more checks. :D |
||
| http) | ||
| options+=("--http=${port}") | ||
| ;; | ||
| https) | ||
| options+=("--https=${port}") | ||
| ;; | ||
| tcp) | ||
| options+=("--tcp=${port}") | ||
| ;; | ||
| tls-terminated-tcp) | ||
| options+=("--tls-terminated-tcp=${port}") | ||
| ;; | ||
| esac | ||
|
|
||
| if [[ -n "${path}" ]]; then | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bashio::var.has_value |
||
| options+=("--set-path=${path}") | ||
| fi | ||
|
|
||
| # Clean any existing configuration for this service before reconfiguring | ||
| /opt/tailscale serve drain "${service_name}" >/dev/null 2>&1 || true | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. at least a warning should be logged when removing, otherwise when the serve below fails during recreation, nobody will know the root cause |
||
| /opt/tailscale serve clear "${service_name}" >/dev/null 2>&1 || true | ||
|
|
||
| bashio::log.info "Advertising service host for ${service_name}" | ||
| if ! /opt/tailscale serve --service="${service_name}" --yes "${options[@]}" "${target}"; then | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would add "serve" and "target" to the options array above, this is how it is implemented everywhere else
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it's not anymore. Earlier
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't they use it to check if https is enabled and ask whether they should enable it if not? AFAIK they don't do it in a non-interactive cli and just emit an error, but I'm not 100% sure about this. :( |
||
| bashio::exit.nok "Failed to advertise service host for ${service_name}" | ||
| fi | ||
| done | ||
|
|
||
| exit 0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| oneshot |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /etc/s6-overlay/s6-rc.d/share-services/run |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -133,3 +133,8 @@ fi | |
| if bashio::config.equals 'share_homeassistant' 'disabled'; then | ||
| rm /etc/s6-overlay/s6-rc.d/user/contents.d/share-homeassistant | ||
| fi | ||
|
|
||
| # Disable share-services service when no services are configured | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nope, it always has to start, to be able to remove unused services!!!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True! |
||
| if ! bashio::config 'services' | jq -e '. | length > 0' > /dev/null; then | ||
| rm -f /etc/s6-overlay/s6-rc.d/user/contents.d/share-services | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't follow every advice from an AI, you don't need -f, if it is not there, fail during development :)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. |
||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At all other places in DOCS the reference to the TS docs is in a separate line as "More information: ..." This is "important", because the device must to use tags!