tsnet: ensure funnel listener cleans up after itself when closed

Previously the funnel listener would leave artifacts in the serve
config. This caused weird out-of-sync effects like the admin panel
showing that funnel was enabled for a node, but the node rejecting
packets because the listener was closed.

This change resolves these synchronization issues by ensuring that
funnel listeners clean up the serve config when closed.

See also:
https://github.com/tailscale/tailscale/commit/e109cf9fdd405153a8d8c0ec52a87d7c8ce8689b

Updates #cleanup
Signed-off-by: Harry Harpham <harry@tailscale.com>
This commit is contained in:
Harry Harpham
2026-01-09 10:02:12 -07:00
parent f9762064cf
commit 3c1be083a4
2 changed files with 143 additions and 0 deletions
+42
View File
@@ -1228,12 +1228,26 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
}
domain := st.CertDomains[0]
hp := ipn.HostPort(domain + ":" + portStr)
var cleanupOnClose func() error
if !srvConfig.AllowFunnel[hp] {
mak.Set(&srvConfig.AllowFunnel, hp, true)
srvConfig.AllowFunnel[hp] = true
if err := lc.SetServeConfig(ctx, srvConfig); err != nil {
return nil, err
}
cleanupOnClose = func() error {
sc, err := lc.GetServeConfig(ctx)
if err != nil {
return fmt.Errorf("cleaning config changes: %w", err)
}
if sc.AllowFunnel != nil {
delete(sc.AllowFunnel, hp)
}
if err := lc.SetServeConfig(ctx, sc); err != nil {
return fmt.Errorf("cleaning config changes: %w", err)
}
return nil
}
}
// Start a funnel listener.
@@ -1241,6 +1255,7 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
if err != nil {
return nil, err
}
ln = &cleanupListener{Listener: ln, cleanup: cleanupOnClose}
return tls.NewListener(ln, tlsConfig), nil
}
@@ -1449,3 +1464,30 @@ type addr struct{ ln *listener }
func (a addr) Network() string { return a.ln.keys[0].network }
func (a addr) String() string { return a.ln.addr }
// cleanupListener wraps a net.Listener with a function to be run on Close.
type cleanupListener struct {
net.Listener
cleanup func() error
cleanupOnce sync.Once
}
func (cl *cleanupListener) Close() error {
var cleanupErr error
cl.cleanupOnce.Do(func() {
if cl.cleanup != nil {
cleanupErr = cl.cleanup()
}
})
closeErr := cl.Listener.Close()
switch {
case closeErr != nil && cleanupErr != nil:
return fmt.Errorf("%w; also: %w", closeErr, cleanupErr)
case closeErr != nil:
return closeErr
case cleanupErr != nil:
return cleanupErr
default:
return nil
}
}