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:
parent
51824b830e
commit
1d919aafc5
1 changed files with 17 additions and 10 deletions
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -29,8 +30,8 @@ const (
|
||||||
// SoftEther VPN session. Each "block" is one Ethernet frame.
|
// SoftEther VPN session. Each "block" is one Ethernet frame.
|
||||||
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 {
|
||||||
if _, err := t.sess.Conn.Write(buf); err != nil {
|
select {
|
||||||
t.writeErr = err
|
case buf := <-t.writeCh:
|
||||||
|
if _, err := t.sess.Conn.Write(buf); err != nil {
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue