From 3bafe683cb3a221c033b523bc49cc48e69c7e95a Mon Sep 17 00:00:00 2001 From: Git Sagar Date: Sun, 7 Jun 2026 16:29:08 +0530 Subject: [PATCH] tunnel: fix panic on send to closed writeCh during disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the server disconnects, Close() was closing both stopCh and writeCh. The TAP→Server goroutine could race and send to the closed writeCh, causing a panic. Fix: don't close writeCh in Close(), let writeLoop exit via stopCh instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- pkg/client/tunnel.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pkg/client/tunnel.go b/pkg/client/tunnel.go index 1332204..fd3558a 100644 --- a/pkg/client/tunnel.go +++ b/pkg/client/tunnel.go @@ -50,20 +50,23 @@ func NewTunnel(sess *Session) *Tunnel { // writeLoop is the single goroutine that writes to the connection. // All writes are serialized through writeCh — no mutex needed. func (t *Tunnel) writeLoop() { - for buf := range t.writeCh { - if _, err := t.sess.Conn.Write(buf); err != nil { - t.writeErr = err + for { + select { + case buf := <-t.writeCh: + if _, err := t.sess.Conn.Write(buf); err != nil { + t.writeErr = err + return + } + case <-t.stopCh: return } } } // Close stops the keepalive goroutine and closes the underlying connection. +// The writeCh is not closed here — writeLoop exits when the connection write fails. func (t *Tunnel) Close() error { - t.stopped.Do(func() { - close(t.stopCh) - close(t.writeCh) - }) + t.stopped.Do(func() { close(t.stopCh) }) return t.sess.Conn.Close() }