Skip to content

BUG: local listener keeps accepting after upstream tsnet session dies — forwarded connections fail at kex with no liveness signal #208

Description

@mlorentedev

Summary

After the upstream tsnet session goes away (auth-key expiry/revocation, control-plane drop,
network change), ts-bridge connect keeps its local TCP listener up and accepting, but every
forwarded connection now dies immediately at the remote handshake. There is no signal that the
tunnel is functionally dead — the port still listens, so any health check that only probes "is
something listening on the port?" passes while the bridge is useless.

Evidence

A bridge that had been forwarding fine (an SSH pull completed through it) later produced, with the
local listener still LISTENING, the same reset on every attempt:

kex_exchange_identification: read: Connection reset by peer
Connection reset by 127.0.0.1 port <port>

netstat confirmed 127.0.0.1:<port> was still LISTENING (same PID) at the moment of the resets.
A fresh connect then failed at startup with:

auth key rejected by control plane (likely expired, revoked, or single-use already consumed)
tailscale init failed (...): tsnet.Up: backend: invalid key

— i.e. the upstream identity had lapsed, but the already-running bridge never reflected that on the
listener side; it just kept resetting forwarded connections.

Impact

  • Silent, hard-to-diagnose failures: clients see "connection reset" / "kex" errors with no hint
    that the bridge's upstream is dead.
  • Port-based liveness checks (and wrappers/automation that wait for the listener to come up) get a
    false "healthy" reading and proceed against a dead tunnel.

Expected

The local listener's health should track the upstream tsnet session. On upstream loss the bridge
should do at least one of:

  • stop accepting new local connections (callers fail fast with connection-refused, which is
    detectable), and/or
  • exit non-zero with the upstream reason, and/or
  • surface the dead state via status / --health-addr so a supervisor can restart it.

Notes

Related but distinct from #203 (READY line at startup) and #204 (structured exit reason when
connect fails at startup): those cover startup signaling. This is runtime upstream death on an
already-running bridge whose local listener stays up.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggoPull requests that update go code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions