Skip to content

Commit 174c19a

Browse files
mholtWeidiDeng
andauthored
core: Apply SO_REUSEPORT to UDP sockets (#5725)
* core: Apply SO_REUSEPORT to UDP sockets For some reason, 10 months ago when I implemented SO_REUSEPORT for TCP, I didn't realize, or forgot, that it can be used for UDP too. It is a much better solution than using deadline hacks to reuse a socket, at least for TCP. Then mholt/caddy-l4#132 was posted, in which we see that UDP servers never actually stopped when the L4 app was stopped. I verified this using this command: $ nc -u 127.0.0.1 55353 combined with POSTing configs to the /load admin endpoint (which alternated between an echo server and a proxy server so I could tell which config was being used). I refactored the code to use SO_REUSEPORT for UDP, but of course we still need graceful reloads on all platforms, not just Unix, so I also implemented a deadline hack similar to what we used for TCP before. That implementation for TCP was not perfect, possibly having a logical (not data) race condition; but for UDP so far it seems to be working. Verified the same way I verified that SO_REUSEPORT works. I think this code is slightly cleaner and I'm fairly confident this code is effective. * Check error * Fix return * Fix var name * implement Unwrap interface and clean up * move unix packet conn to platform specific file * implement Unwrap for unix packet conn * Move sharedPacketConn into proper file * Fix Windows * move sharedPacketConn and fakeClosePacketConn to proper file --------- Co-authored-by: Weidi Deng <[email protected]>
1 parent c8559c4 commit 174c19a

File tree

3 files changed

+181
-140
lines changed

3 files changed

+181
-140
lines changed

listen.go

Lines changed: 97 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,34 @@ func reuseUnixSocket(network, addr string) (any, error) {
3030
return nil, nil
3131
}
3232

33-
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
34-
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
35-
ln, err := config.Listen(ctx, network, address)
33+
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
34+
switch network {
35+
case "udp", "udp4", "udp6", "unixgram":
36+
sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
37+
pc, err := config.ListenPacket(ctx, network, address)
38+
if err != nil {
39+
return nil, err
40+
}
41+
return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
42+
})
3643
if err != nil {
3744
return nil, err
3845
}
39-
return &sharedListener{Listener: ln, key: lnKey}, nil
40-
})
41-
if err != nil {
42-
return nil, err
46+
return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
47+
48+
default:
49+
sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
50+
ln, err := config.Listen(ctx, network, address)
51+
if err != nil {
52+
return nil, err
53+
}
54+
return &sharedListener{Listener: ln, key: lnKey}, nil
55+
})
56+
if err != nil {
57+
return nil, err
58+
}
59+
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
4360
}
44-
return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
4561
}
4662

4763
// fakeCloseListener is a private wrapper over a listener that
@@ -98,7 +114,7 @@ func (fcl *fakeCloseListener) Accept() (net.Conn, error) {
98114
// so that it's clear in the code that side-effects are shared with other
99115
// users of this listener, not just our own reference to it; we also don't
100116
// do anything with the error because all we could do is log it, but we
101-
// expliclty assign it to nothing so we don't forget it's there if needed
117+
// explicitly assign it to nothing so we don't forget it's there if needed
102118
_ = fcl.sharedListener.clearDeadline()
103119

104120
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
@@ -172,3 +188,75 @@ func (sl *sharedListener) setDeadline() error {
172188
func (sl *sharedListener) Destruct() error {
173189
return sl.Listener.Close()
174190
}
191+
192+
// fakeClosePacketConn is like fakeCloseListener, but for PacketConns,
193+
// or more specifically, *net.UDPConn
194+
type fakeClosePacketConn struct {
195+
closed int32 // accessed atomically; belongs to this struct only
196+
*sharedPacketConn // embedded, so we also become a net.PacketConn; its key is used in Close
197+
}
198+
199+
func (fcpc *fakeClosePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
200+
// if the listener is already "closed", return error
201+
if atomic.LoadInt32(&fcpc.closed) == 1 {
202+
return 0, nil, &net.OpError{
203+
Op: "readfrom",
204+
Net: fcpc.LocalAddr().Network(),
205+
Addr: fcpc.LocalAddr(),
206+
Err: errFakeClosed,
207+
}
208+
}
209+
210+
// call underlying readfrom
211+
n, addr, err = fcpc.sharedPacketConn.ReadFrom(p)
212+
if err != nil {
213+
// this server was stopped, so clear the deadline and let
214+
// any new server continue reading; but we will exit
215+
if atomic.LoadInt32(&fcpc.closed) == 1 {
216+
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
217+
if err = fcpc.SetReadDeadline(time.Time{}); err != nil {
218+
return
219+
}
220+
}
221+
}
222+
return
223+
}
224+
225+
return
226+
}
227+
228+
// Close won't close the underlying socket unless there is no more reference, then listenerPool will close it.
229+
func (fcpc *fakeClosePacketConn) Close() error {
230+
if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
231+
_ = fcpc.SetReadDeadline(time.Now()) // unblock ReadFrom() calls to kick old servers out of their loops
232+
_, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
233+
}
234+
return nil
235+
}
236+
237+
func (fcpc *fakeClosePacketConn) Unwrap() net.PacketConn {
238+
return fcpc.sharedPacketConn.PacketConn
239+
}
240+
241+
// sharedPacketConn is like sharedListener, but for net.PacketConns.
242+
type sharedPacketConn struct {
243+
net.PacketConn
244+
key string
245+
}
246+
247+
// Destruct closes the underlying socket.
248+
func (spc *sharedPacketConn) Destruct() error {
249+
return spc.PacketConn.Close()
250+
}
251+
252+
// Unwrap returns the underlying socket
253+
func (spc *sharedPacketConn) Unwrap() net.PacketConn {
254+
return spc.PacketConn
255+
}
256+
257+
// Interface guards (see https://github.com/caddyserver/caddy/issues/3998)
258+
var (
259+
_ (interface {
260+
Unwrap() net.PacketConn
261+
}) = (*fakeClosePacketConn)(nil)
262+
)

listen_unix.go

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ package caddy
2222
import (
2323
"context"
2424
"errors"
25+
"io"
2526
"io/fs"
2627
"net"
28+
"os"
2729
"sync/atomic"
2830
"syscall"
2931

@@ -87,7 +89,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
8789
return nil, nil
8890
}
8991

90-
func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
92+
func listenReusable(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (any, error) {
9193
// wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
9294
oldControl := config.Control
9395
config.Control = func(network, address string, c syscall.RawConn) error {
@@ -103,7 +105,14 @@ func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string,
103105
// we still put it in the listenerPool so we can count how many
104106
// configs are using this socket; necessary to ensure we can know
105107
// whether to enforce shutdown delays, for example (see #5393).
106-
ln, err := config.Listen(ctx, network, address)
108+
var ln io.Closer
109+
var err error
110+
switch network {
111+
case "udp", "udp4", "udp6", "unixgram":
112+
ln, err = config.ListenPacket(ctx, network, address)
113+
default:
114+
ln, err = config.Listen(ctx, network, address)
115+
}
107116
if err == nil {
108117
listenerPool.LoadOrStore(lnKey, nil)
109118
}
@@ -117,9 +126,23 @@ func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string,
117126
unixSockets[lnKey] = ln.(*unixListener)
118127
}
119128

129+
// TODO: Not 100% sure this is necessary, but we do this for net.UnixListener in listen_unix.go, so...
130+
if unix, ok := ln.(*net.UnixConn); ok {
131+
ln = &unixConn{unix, address, lnKey, &one}
132+
unixSockets[lnKey] = ln.(*unixConn)
133+
}
134+
120135
// lightly wrap the listener so that when it is closed,
121136
// we can decrement the usage pool counter
122-
return deleteListener{ln, lnKey}, err
137+
switch specificLn := ln.(type) {
138+
case net.Listener:
139+
return deleteListener{specificLn, lnKey}, err
140+
case net.PacketConn:
141+
return deletePacketConn{specificLn, lnKey}, err
142+
}
143+
144+
// other types, I guess we just return them directly
145+
return ln, err
123146
}
124147

125148
// reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
@@ -158,6 +181,36 @@ func (uln *unixListener) Close() error {
158181
return uln.UnixListener.Close()
159182
}
160183

184+
type unixConn struct {
185+
*net.UnixConn
186+
filename string
187+
mapKey string
188+
count *int32 // accessed atomically
189+
}
190+
191+
func (uc *unixConn) Close() error {
192+
newCount := atomic.AddInt32(uc.count, -1)
193+
if newCount == 0 {
194+
defer func() {
195+
unixSocketsMu.Lock()
196+
delete(unixSockets, uc.mapKey)
197+
unixSocketsMu.Unlock()
198+
_ = syscall.Unlink(uc.filename)
199+
}()
200+
}
201+
return uc.UnixConn.Close()
202+
}
203+
204+
func (uc *unixConn) Unwrap() net.PacketConn {
205+
return uc.UnixConn
206+
}
207+
208+
// unixSockets keeps track of the currently-active unix sockets
209+
// so we can transfer their FDs gracefully during reloads.
210+
var unixSockets = make(map[string]interface {
211+
File() (*os.File, error)
212+
})
213+
161214
// deleteListener is a type that simply deletes itself
162215
// from the listenerPool when it closes. It is used
163216
// solely for the purpose of reference counting (i.e.
@@ -171,3 +224,19 @@ func (dl deleteListener) Close() error {
171224
_, _ = listenerPool.Delete(dl.lnKey)
172225
return dl.Listener.Close()
173226
}
227+
228+
// deletePacketConn is like deleteListener, but
229+
// for net.PacketConns.
230+
type deletePacketConn struct {
231+
net.PacketConn
232+
lnKey string
233+
}
234+
235+
func (dl deletePacketConn) Close() error {
236+
_, _ = listenerPool.Delete(dl.lnKey)
237+
return dl.PacketConn.Close()
238+
}
239+
240+
func (dl deletePacketConn) Unwrap() net.PacketConn {
241+
return dl.PacketConn
242+
}

0 commit comments

Comments
 (0)