tunnel: fix panic on send to closed writeCh during disconnect

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) <noreply@anthropic.com>
This commit is contained in:
Git Sagar 2026-06-07 16:29:08 +05:30
parent 51824b830e
commit 1d919aafc5

View file

@ -6,6 +6,7 @@ import (
"io" "io"
"math/rand" "math/rand"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
@ -30,7 +31,7 @@ const (
type Tunnel struct { type Tunnel struct {
sess *Session sess *Session
writeCh chan []byte // serialized messages queued for the single writer writeCh chan []byte // serialized messages queued for the single writer
writeErr error // last write error writeErr atomic.Value // stores error from writeLoop
stopCh chan struct{} stopCh chan struct{}
stopped sync.Once stopped sync.Once
} }
@ -50,20 +51,23 @@ func NewTunnel(sess *Session) *Tunnel {
// writeLoop is the single goroutine that writes to the connection. // writeLoop is the single goroutine that writes to the connection.
// All writes are serialized through writeCh — no mutex needed. // All writes are serialized through writeCh — no mutex needed.
func (t *Tunnel) writeLoop() { func (t *Tunnel) writeLoop() {
for buf := range t.writeCh { for {
select {
case buf := <-t.writeCh:
if _, err := t.sess.Conn.Write(buf); err != nil { if _, err := t.sess.Conn.Write(buf); err != nil {
t.writeErr = err t.writeErr.Store(err)
return
}
case <-t.stopCh:
return return
} }
} }
} }
// Close stops the keepalive goroutine and closes the underlying connection. // 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 { func (t *Tunnel) Close() error {
t.stopped.Do(func() { t.stopped.Do(func() { close(t.stopCh) })
close(t.stopCh)
close(t.writeCh)
})
return t.sess.Conn.Close() return t.sess.Conn.Close()
} }
@ -124,7 +128,10 @@ func (t *Tunnel) WriteFrames(frames [][]byte) error {
select { select {
case t.writeCh <- buf: case t.writeCh <- buf:
return t.writeErr if err, ok := t.writeErr.Load().(error); ok {
return err
}
return nil
case <-t.stopCh: case <-t.stopCh:
return fmt.Errorf("tunnel closed") return fmt.Errorf("tunnel closed")
} }