Skip to content

v0.3.5

Choose a tag to compare

@github-actions github-actions released this 19 Oct 17:40
· 44 commits to main since this release
Immutable release. Only release title and notes can be modified.
v0.3.5
3a19a05

What's Changed

Full Changelog: v0.3.4...v0.3.5

Added

  • A container can be the backend of several Tailscale Services, with the
    labels tslink.service.<name>.serve.<port>; each Service is advertised once
    its own configuration and certificate are in place, and all are drained
    together on stop. /ready lists each Service's state under services.
    Upgrade every node before deploying these labels: older versions ignore
    them. tslink.service with tslink.serve.<port> works as before.
  • tslink version prints 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.

  • /ready reports two more states before ready, 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, and ports-uncovered when 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 the services:read scope, from Tailscale's API. A
    latched endpoint stays ready and shows them under services.

  • tslink diag --preflight checks 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's shared.source). A deployment that passed before
    may now report "violated".

  • With a shared certificate directory, stack tasks keep their certificates
    in certs/by-stack/<stack>/, with a copy of the shared ACME account key;
    other containers keep certs/. A stack takes over a valid certificate for
    its Service from certs/ 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/lib into the plugin, which unmounts it before it serves;
    docker plugin upgrade --grant-all-permissions grants the new mount. The
    default of shared.source is now /var/lib, which sets no shared
    directory: as with the old default, the data directory, certificates stay
    in the data directory's certs/.

Removed

  • The tslink.loginserver network option. The control server is now the
    plugin setting TSLINK_LOGIN_SERVER, for every network on the host: an
    https URL, or an http URL 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_LEVEL at warn or error, 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>-N as a node-name warning 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, reported tslink (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 the tslink.loginserver option fails, whatever its credential. On one
    that an earlier version created, a container fails to start with an error
    naming TSLINK_LOGIN_SERVER, and one already running when the plugin
    restarts gets the status failed, 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