Repository navigation
v0.3.5
What's Changed
- Probe the host's tailnet isolation across a start without Docker by Moritz Mazetti (@Radiergummi) in #130
- Several Tailscale Services per container by Moritz Mazetti (@Radiergummi) in #125
- Test campaign: security model, preflight, cluster suite and red-team probes by Moritz Mazetti (@Radiergummi) in #126
- Version command, unknown arguments, and the upgrade prompt by Moritz Mazetti (@Radiergummi) in #131
- Advertise a Service anew when control does not fetch it by Moritz Mazetti (@Radiergummi) in #133
- Cluster suite fixes from the X1 bed run by Moritz Mazetti (@Radiergummi) in #132
- Wait for a slow registration instead of restarting tailscaled by Moritz Mazetti (@Radiergummi) in #134
- Document known limitations: Tailnet Lock, Services fetches, DNS-01 failures by Moritz Mazetti (@Radiergummi) in #135
- Make TestCertWaiterStartsOnce deterministic by Moritz Mazetti (@Radiergummi) in #136
- Warn when control names a node -N by Moritz Mazetti (@Radiergummi) in #137
- Keep each stack's certificates in a directory of its own by Moritz Mazetti (@Radiergummi) in #139
- Report Services control does not route to as unready by Moritz Mazetti (@Radiergummi) in #138
- Retry a reset GET after the connection flood by Moritz Mazetti (@Radiergummi) in #141
- Start only as a Docker plugin, create the data directory, stamp the version by Moritz Mazetti (@Radiergummi) in #140
- Log the plugin's start line once the log level applies by Moritz Mazetti (@Radiergummi) in #143
- Cluster suite: manual approval, mixed DNS, partial ports, R7 timeline by Moritz Mazetti (@Radiergummi) in #142
- Make the control server a plugin setting (GHSA-28r2-gm83-4qwq) by Moritz Mazetti (@Radiergummi) in #144
- Release v0.3.5 by Moritz Mazetti (@Radiergummi) in #145
Full Changelog: v0.3.4...v0.3.5
Added
- A container can be the backend of several Tailscale Services, with the
labelstslink.service.<name>.serve.<port>; each Service is advertised once
its own configuration and certificate are in place, and all are drained
together on stop./readylists each Service's state underservices.
Upgrade every node before deploying these labels: older versions ignore
them.tslink.servicewithtslink.serve.<port>works as before. tslink versionprints the versions of tslink and its bundled Tailscale.
Changed
-
A Service whose serve configuration fails is retried in place, with a
warning in the endpoint's status until it succeeds; before, tailscaled
restarted, without draining what it advertised. -
At start, tslink removes Services in the node's state that the container's
labels no longer name, and a drain clears every Service the node advertises.
A node that had such Services removed, or could not have them removed, is
drained when it stops, even if its container names no Service. -
A readiness endpoint (
tslink.health) accepts at most 16 connections at a
time, and closes a connection that has not sent its request headers within 2 s
(was 5 s), so a flood of connections cannot hold it. -
/readyreports two more states beforeready, so a healthcheck on it
now waits until control routes traffic to the host:awaiting-control
until control has fetched the node's Service list after tslink advertised
a Service, andports-uncoveredwhen the host does not serve every port
of the Service's definition, which control treats as partially configured
and sends nothing to. The status file warns of both (control/<service>,
ports/<service>). The ports come from the node's capabilities or, for an
OAuth client with theservices:readscope, from Tailscale's API. A
latched endpoint stays ready and shows them underservices. -
tslink diag --preflightchecks more: E1 also flags a container that
enables forwarding by sysctl or runs without seccomp or AppArmor
confinement, and E2 a container that mounts the shared certificate
directory (the plugin'sshared.source). A deployment that passed before
may now report "violated". -
With a shared certificate directory, stack tasks keep their certificates
incerts/by-stack/<stack>/, with a copy of the shared ACME account key;
other containers keepcerts/. A stack takes over a valid certificate for
its Service fromcerts/instead of getting a new one, but a Service
moved to another stack, or a renamed stack, gets a new certificate, which
counts against Let's Encrypt's limit of 5 for each name in a week. -
The documentation states that tslink is not tested with Tailnet Lock, and
is expected to work under it only with a pre-signed auth key. It also
describes failed DNS-01 validations of HTTPS certificates,
Services that control does not fetch, and drains that control fetches
after tslink's 10 s limit. -
The plugin creates its data directory,
/var/lib/docker-plugins/tailscale,
as it starts: it no longer needs to exist before the install, and a host
without it no longer leaves the plugin disabled. For this, Docker mounts
the host's/var/libinto the plugin, which unmounts it before it serves;
docker plugin upgrade --grant-all-permissionsgrants the new mount. The
default ofshared.sourceis now/var/lib, which sets no shared
directory: as with the old default, the data directory, certificates stay
in the data directory'scerts/.
Removed
- The
tslink.loginservernetwork option. The control server is now the
plugin settingTSLINK_LOGIN_SERVER, for every network on the host: an
httpsURL, or anhttpURL with an IP address, since tailscaled resolves
names with the container's DNS servers and plain http does not
authenticate the server. Any other value keeps the plugin from starting.
Recreate networks that set the option without it, with the same auth key:
with another key, their containers register as new nodes.
Fixed
- With
TSLINK_LOG_LEVELatwarnorerror, the plugin still logged its
start line at info, before the setting applied. - A login that control answered after more than 60 s, as during a rollout
of many tasks, restarted tailscaled, which then registered a second node:
control named it<hostname>-1, and the first, offline node kept the
name (#127). tslink now warns at 60 s and waits up to 4 more minutes for
the registration in progress. A registration answered even later still
leaves the orphaned first node; tslink now reports a node named
<hostname>-Nas anode-namewarning in the status and the log. - After a plugin restart, which drains each Service backend on shutdown and
advertises it again on start, control sometimes never fetched the node's
new Service list: the host stayed inactive, and a Service with no other
host unreachable, until the next change. tslink now advertises a Service
anew when control has not fetched it within 15 s, up to 3 times. - The plugin binary, run with arguments it does not know, exits with usage
instead of starting a plugin instance, which on the host could install
isolation chains the host's settings turn off. - Without arguments, the plugin binary starts a plugin instance only inside a
Docker plugin, which it recognizes by the plugin socket directory Docker
mounts. On a host or in a container, it exits with usage (#96). - Builds from a source tree without Git metadata, such as the cluster test
bed's, reportedtslink (devel). The builds now stamp the version
(scripts/version.sh) into the binary, and the plugin logs it at start. - With a shared certificate directory (
shared.source), a stack that named
another stack's Service took its certificate lease, which control never
let it use, and the other stack's replicas waited for the lease: its
certificate was neither issued nor renewed while the first stack ran.
Each stack now keeps its certificates and leases in a directory of its
own.
Security
- A network that names a control server of its own is refused: creating one
with thetslink.loginserveroption fails, whatever its credential. On one
that an earlier version created, a container fails to start with an error
namingTSLINK_LOGIN_SERVER, and one already running when the plugin
restarts gets the statusfailed, naming it. A network could
otherwise have nodes present their credential, one minted from the cluster
credential included, to a server it chose
(GHSA-28r2-gm83-4qwq;
affects v0.2.0 to v0.3.4).
Release checks
On 3a19a05 (docs/releasing.md): CI green (34 checks on the release PR and on #144); weekly fuzz run within 7 days (2026-10-19); SECURITY.md has no manual checks. The red team ran on 2026-10-19 on commits whose product code (pkg, cmd, docker, go.mod, go.sum) is identical to this release: on a fresh single-host testbed (b401da1, Docker 29.9.1, kernel 6.8) and on a five-node Swarm bed against a real tailnet (aa8cf8a), with all probes passing and their control runs getting through; the cluster suite (R1 to R18), an upgrade from v0.3.2 and disruptions (Docker restart and kill, reboot, power off) passed on the same bed.
Guarantees and their tests
- G1: pkg/docker/driver_test.go:824 pkg/docker/isolation_test.go:50 pkg/docker/isolation_test.go:78 pkg/docker/isolation_test.go:124 pkg/docker/isolation_test.go:157 pkg/docker/isolation_test.go:201 pkg/netutil/bypass_test.go:12 pkg/netutil/container_test.go:56 pkg/netutil/hoststate_test.go:59 pkg/netutil/isolate_test.go:91 pkg/netutil/isolate_test.go:179 pkg/netutil/isolate_test.go:198 pkg/netutil/isolate_traffic_test.go:256 pkg/netutil/procs_test.go:212 pkg/netutil/repair_traffic_test.go:132 pkg/netutil/tailnet_test.go:83 test/integration/run.sh:440 test/integration/run.sh:442 test/integration/run.sh:444 test/integration/run.sh:446 test/integration/run.sh:449 test/integration/run.sh:488 test/security/redteam/G1-during-recovery.sh:2 test/security/redteam/G1-during-recovery.sh:61 test/security/redteam/G1-ipv6-through-host.sh:2 test/security/redteam/G1-ipv6-through-host.sh:80 test/security/redteam/G1-neighbour-gateway.sh:2 test/security/redteam/G1-neighbour-gateway.sh:81
- G2: pkg/netutil/isolate_test.go:225 pkg/netutil/isolate_traffic_test.go:179 pkg/netutil/isolate_traffic_test.go:211 pkg/netutil/procs_test.go:212 pkg/netutil/repair_traffic_test.go:132 test/integration/run.sh:558 test/integration/run.sh:559 test/integration/run.sh:562 test/security/redteam/G2-lan-router.sh:2 test/security/redteam/G2-lan-router.sh:67 test/security/redteam/G2-lan-router.sh:68
- G3: pkg/core/config_fuzz_test.go:17 pkg/docker/serve_fuzz_test.go:15 pkg/docker/serve_fuzz_test.go:61 pkg/tailscale/resolver_fuzz_test.go:19 pkg/tailscale/serve_fuzz_test.go:24
- G4: pkg/core/config_test.go:143 pkg/core/endpoint_test.go:58 pkg/core/loginserver_test.go:17 pkg/core/loginserver_test.go:47 pkg/core/loginserver_test.go:73 pkg/core/loginserver_test.go:113 pkg/core/network_test.go:309 pkg/core/network_test.go:354 pkg/core/statedir_fuzz_test.go:111 pkg/core/statedir_fuzz_test.go:121 pkg/core/statedir_fuzz_test.go:131 pkg/core/statedir_fuzz_test.go:152 pkg/docker/driver_test.go:93 pkg/docker/networks_test.go:81 pkg/tailscale/certs_test.go:321 test/security/redteam/G4-foreign-network.sh:2 test/security/redteam/G4-foreign-network.sh:59 test/security/redteam/G4-foreign-service-lease.sh:2 test/security/redteam/G4-foreign-service-lease.sh:109 test/security/redteam/G4-login-server.sh:2 test/security/redteam/G4-login-server.sh:120 test/security/redteam/G4-login-server.sh:121 test/security/redteam/G4-tag-scope.sh:2 test/security/redteam/G4-tag-scope.sh:46
- G5: pkg/docker/credentials_test.go:41 pkg/tailscale/authkey_test.go:144 pkg/tailscale/authkey_test.go:163 pkg/tailscale/logout_test.go:262 pkg/tailscale/resolver_test.go:264 pkg/tailscale/up_test.go:70 pkg/tailscale/up_test.go:111 pkg/tailscale/up_test.go:135 test/integration/run.sh:699 test/integration/run.sh:700 test/integration/run.sh:701 test/integration/run.sh:702 test/integration/run.sh:703 test/security/redteam/G5-canaries.sh:2 test/security/redteam/G5-canaries.sh:171 test/security/redteam/G5-canaries.sh:172 test/security/redteam/G5-canaries.sh:173 test/security/redteam/G5-canaries.sh:174 test/security/redteam/G5-canaries.sh:175
- G6: pkg/core/statedir_fuzz_test.go:63 pkg/core/statedir_fuzz_test.go:74 pkg/tailscale/certs_fuzz_test.go:11 test/security/redteam/G6-abstract-sockets.sh:2 test/security/redteam/G6-abstract-sockets.sh:72