Skip to content

Commit b2c7806

Browse files
committed
fix(stun): harden packet decode and listener shutdown
Fixes three crash paths in STUN discovery, one observed in the wild and two found while tracing its error path. ReadPacketData can return an empty buffer with a nil error, so slicing at payloadOff panicked with "slice bounds out of range [32:0]" on darwin/bsd. Guard the length before slicing, and guard the equivalent buf[8:] on Linux. Parse returns nil when the reply carries no XOR-MAPPED-ADDRESS, which Connect then dereferenced. An RFC 3489-era server answering with only MAPPED-ADDRESS would crash the daemon. Return ErrNoMappedAddress instead so the resolver falls through to the next configured server. Stop and the listener goroutines shared no shutdown signal. A listener parked handing off a reply that arrived after Read timed out would race Stop's close of packetChan and panic on Linux, or block Stop's waitGroup.Wait until context cancellation on darwin/bsd. Add a done channel and stop closing packetChan: Read already selects with a timeout, so the close bought nothing and only opened the race. This also stops the Linux listener spinning on a closed socket until its timeout. Not covered by new tests: the short-packet and shutdown paths need a raw socket or pcap handle, so they are verified by construction and the existing suite only. Signed-off-by: Date Huang <tjjh89017@hotmail.com>
1 parent 83495ac commit b2c7806

3 files changed

Lines changed: 65 additions & 2 deletions

File tree

internal/stun/helper.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
var (
1212
ErrResponseMessage = errors.New("error reading from response message channel")
1313
ErrTimeout = errors.New("timed out waiting for response")
14+
// ErrNoMappedAddress means the reply decoded cleanly but carried no
15+
// XOR-MAPPED-ADDRESS, so there is no reflexive endpoint to report. An
16+
// RFC 3489-era server answering with only MAPPED-ADDRESS lands here.
17+
ErrNoMappedAddress = errors.New("stun response has no XOR-MAPPED-ADDRESS")
1418
)
1519

1620
const BindingPacketHeaderSize = 8

internal/stun/stun_darwinbsd.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ type Stun struct {
3838
once sync.Once
3939
packetChan chan *stun.Message
4040
waitGroup sync.WaitGroup
41+
// done is closed by Stop to release a listener goroutine parked trying to
42+
// hand off a packet nobody is waiting for any more. Without it Stop's
43+
// waitGroup.Wait blocks until the context is cancelled, hanging Resolve's
44+
// deferred cleanup.
45+
done chan struct{}
46+
stopOnce sync.Once
4147
}
4248

4349
func calculatePayloadOffset(linkType uint32, protocol string) uint32 {
@@ -153,6 +159,7 @@ func New(ctx context.Context, excludeInterface string, port uint16, protocol str
153159
protocol: protocol,
154160
handles: handles,
155161
packetChan: make(chan *stun.Message),
162+
done: make(chan struct{}),
156163
}
157164

158165
if err := setPacketConn(s, c); err != nil {
@@ -198,9 +205,13 @@ func setPacketConn(s *Stun, c net.PacketConn) error {
198205
return nil
199206
}
200207

208+
// Stop signals the per-interface listeners, waits for them, then closes the
209+
// socket. packetChan is deliberately left open: Read already selects with a
210+
// timeout, so closing it buys nothing and only opens a send-on-closed-channel
211+
// race. The channel is garbage collected along with the Stun.
201212
func (s *Stun) Stop() error {
213+
s.stopOnce.Do(func() { close(s.done) })
202214
s.waitGroup.Wait()
203-
close(s.packetChan)
204215
if s.protocol == "ipv6" {
205216
return s.conn6.Close()
206217
}
@@ -227,6 +238,8 @@ func (s *Stun) Start(ctx context.Context) {
227238
select {
228239
case <-ctx.Done():
229240
return
241+
case <-s.done:
242+
return
230243
case <-timeout:
231244
return
232245
default:
@@ -239,6 +252,14 @@ func (s *Stun) Start(ctx context.Context) {
239252
logger.Trace().Msgf("fail to read packet data from %s, err %v", handle.name, err)
240253
continue
241254
}
255+
// ReadPacketData can hand back a short or empty
256+
// buffer with a nil error (a timed-out read yields
257+
// len 0), so the payload offset is not safe to slice
258+
// with until it is known to be in range.
259+
if uint32(len(buf)) < handle.payloadOff {
260+
logger.Trace().Msgf("short packet (%d bytes, need %d) from %s", len(buf), handle.payloadOff, handle.name)
261+
continue
262+
}
242263
// decode STUN
243264
m := &stun.Message{
244265
Raw: buf[handle.payloadOff:],
@@ -251,6 +272,8 @@ func (s *Stun) Start(ctx context.Context) {
251272
case s.packetChan <- m:
252273
case <-ctx.Done():
253274
return
275+
case <-s.done:
276+
return
254277
}
255278
return
256279
}
@@ -298,7 +321,12 @@ func (s *Stun) Connect(ctx context.Context, stunAddr string) (_ string, _ int, e
298321
return "", 0, err
299322
}
300323

324+
// Parse returns nil when the reply carries no XOR-MAPPED-ADDRESS; the
325+
// resolver treats the error as "this server failed" and moves to the next.
301326
replyAddr := Parse(ctx, reply)
327+
if replyAddr == nil {
328+
return "", 0, ErrNoMappedAddress
329+
}
302330

303331
return replyAddr.IP.String(), replyAddr.Port, nil
304332
}

internal/stun/stun_linux.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package stun
44
import (
55
"context"
66
"encoding/binary"
7+
"fmt"
78
"net"
89
"sync"
910
"syscall"
@@ -25,6 +26,12 @@ type Stun struct {
2526
conn6 *ipv6.PacketConn
2627
once sync.Once
2728
packetChan chan []byte
29+
// done is closed by Stop to release a listener goroutine that is parked
30+
// trying to hand off a packet nobody is waiting for any more (a reply that
31+
// arrived after Read already timed out). Without it Stop's close of
32+
// packetChan would race that send and panic.
33+
done chan struct{}
34+
stopOnce sync.Once
2835
}
2936

3037
// markControl stamps firewallMark on the socket before it is bound, so the
@@ -116,6 +123,7 @@ func New(ctx context.Context, excludeInterface string, port uint16, protocol str
116123
port: port,
117124
protocol: protocol,
118125
packetChan: make(chan []byte),
126+
done: make(chan struct{}),
119127
}
120128

121129
if err := setPacketConn(s, c, filter); err != nil {
@@ -125,8 +133,12 @@ func New(ctx context.Context, excludeInterface string, port uint16, protocol str
125133
return s, nil
126134
}
127135

136+
// Stop releases the listener goroutine and closes the socket. packetChan is
137+
// deliberately left open: Read already selects with a timeout, so closing it
138+
// buys nothing and only opens a send-on-closed-channel race with a listener
139+
// still holding a late reply. The channel is garbage collected with the Stun.
128140
func (s *Stun) Stop() error {
129-
close(s.packetChan)
141+
s.stopOnce.Do(func() { close(s.done) })
130142
if s.protocol == "ipv6" {
131143
return s.conn6.Close()
132144
}
@@ -150,19 +162,30 @@ func (s *Stun) Start(ctx context.Context) {
150162
select {
151163
case <-ctx.Done():
152164
return
165+
case <-s.done:
166+
return
153167
case <-timeout:
154168
return
155169
default:
156170
buf := make([]byte, PacketSize)
157171
n, err := s.readFrom(buf)
158172
if err != nil {
173+
// Stop closed the socket out from under us; leave
174+
// rather than spin on a dead fd until timeout.
175+
select {
176+
case <-s.done:
177+
return
178+
default:
179+
}
159180
continue
160181
}
161182
select {
162183
case s.packetChan <- buf[:n]:
163184
return
164185
case <-ctx.Done():
165186
return
187+
case <-s.done:
188+
return
166189
}
167190
}
168191
}
@@ -218,7 +241,12 @@ func (s *Stun) Connect(ctx context.Context, stunAddr string) (string, int, error
218241
return "", 0, err
219242
}
220243

244+
// Parse returns nil when the reply carries no XOR-MAPPED-ADDRESS; the
245+
// resolver treats the error as "this server failed" and moves to the next.
221246
replyAddr := Parse(ctx, reply)
247+
if replyAddr == nil {
248+
return "", 0, ErrNoMappedAddress
249+
}
222250

223251
return replyAddr.IP.String(), replyAddr.Port, nil
224252
}
@@ -229,6 +257,9 @@ func (s *Stun) Read(ctx context.Context) (*stun.Message, error) {
229257
// Linux kernel strips IP headers for both IPv4 and IPv6 raw sockets
230258
// We only receive: UDP header (8 bytes) + STUN payload
231259
// Note: BPF filter runs before IP header stripping, so it needs different offsets
260+
if len(buf) < 8 {
261+
return nil, fmt.Errorf("short packet: %d bytes, need at least a UDP header", len(buf))
262+
}
232263
m := &stun.Message{
233264
Raw: buf[8:], // Skip UDP header
234265
}

0 commit comments

Comments
 (0)