From 1d919aafc5a14bd9c70481e59449acfd11b1b5c8 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 | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/pkg/client/tunnel.go b/pkg/client/tunnel.go index 1332204..9d9d2a2 100644 --- a/pkg/client/tunnel.go +++ b/pkg/client/tunnel.go @@ -6,6 +6,7 @@ import ( "io" "math/rand" "sync" + "sync/atomic" "time" ) @@ -29,8 +30,8 @@ const ( // SoftEther VPN session. Each "block" is one Ethernet frame. type Tunnel struct { sess *Session - writeCh chan []byte // serialized messages queued for the single writer - writeErr error // last write error + writeCh chan []byte // serialized messages queued for the single writer + writeErr atomic.Value // stores error from writeLoop stopCh chan struct{} stopped sync.Once } @@ -50,20 +51,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.Store(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() } @@ -124,7 +128,10 @@ func (t *Tunnel) WriteFrames(frames [][]byte) error { select { case t.writeCh <- buf: - return t.writeErr + if err, ok := t.writeErr.Load().(error); ok { + return err + } + return nil case <-t.stopCh: return fmt.Errorf("tunnel closed") }