Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions tailscale/DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

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!

local target address, a protocol, and a port to expose it on. The `svc:` prefix

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the svc: prefix there in the first place, but after configuring it multiple times during testing I felt it annoying. But for parity to the advertise_tags I can revert this change. But honestly I would like the tags also more without the prefix. But that's just a personal preference. If it makes it more clean I'll revert. Just say. ;)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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. :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions tailscale/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: []
Expand All @@ -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])$)"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path: "match(^/.*$)?"
snat_subnet_routes: bool
stateful_filtering: bool
tags:
Expand Down
117 changes: 117 additions & 0 deletions tailscale/rootfs/etc/s6-overlay/s6-rc.d/share-services/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/command/with-contenv bashio
# shellcheck shell=bash
export LOG_FD

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bashio:jq ?

name="${name#svc:}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have svc: prefix there?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That will be not necessary anymore, when we make svc: mandatory.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean name="${name#svc:}" currently it is not needed, there is no svc: in services.name.

configured_names+=("${name}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

post-tailscaled already waited for this

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use bashio::jq() ?
-e and dev/null and ||true, we don't need this, use bashio::jq IMHO


# Remove services that are no longer configured in this app
for service_name in "${current_names[@]}"; do
if [[ "${service_name}" != svc:* ]]; then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do wee need this? just asking! what else can be there?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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}")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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}")

@lmagyar lmagyar Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ! "${protocol}" = @(http|https|tcp|tls-terminated-tcp); then
exit.nok "Invalid value '${protocol}' for services.protocol"
fi

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very good in bash. Thanks for the advice.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
https://github.com/hassio-addons/app-tailscale/blob/main/tailscale/rootfs/etc/s6-overlay/s6-rc.d/share-homeassistant/run#L24

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

@lmagyar lmagyar Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
do we need --yes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it's not anymore. Earlier tailscale serve used to ask interactively when invoking with a service. I tried manually. It's no longer the case. I'll remove it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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
1 change: 1 addition & 0 deletions tailscale/rootfs/etc/s6-overlay/s6-rc.d/share-services/up
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/etc/s6-overlay/s6-rc.d/share-services/run
Empty file.
5 changes: 5 additions & 0 deletions tailscale/rootfs/etc/s6-overlay/scripts/stage2_hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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!!!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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 :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

fi
8 changes: 8 additions & 0 deletions tailscale/translations/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ configuration:
internet.
Only ports 443, 8443, and 10000 are allowed by Tailscale.
Port 443 is used by default.
services:
name: Service hosts
description: >-
This option allows you to advertise other local services running on this
device as Tailscale Services. Each service needs a name, a local target
address, a protocol, and a port to expose it on. The `svc:` prefix is added
automatically; do not include it in the configuration.
These options are disabled by default.
snat_subnet_routes:
name: Source NAT subnet routes
description: >-
Expand Down