Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 04:45 UTC
listeners: close unix socket immediately and unlink file on reload (#8061)
Co-authored-by: Zen Dodd <mail@steadytao.com>
Little Fedya committed
Oct 3, 2026 at 08:45 UTC
d483d7816f7a28fc104a21a2e33f0723261fc758
2 files changed
+462
-30
listen_unix.go
+54
-30
@@ -27,8 +27,10 @@ import (
27
"io/fs"
28
"net"
29
"os"
30
+ "runtime"
31
"slices"
32
"strconv"
33
+ "strings"
34
"sync"
35
"sync/atomic"
36
"syscall"
@@ -50,6 +52,7 @@ func reuseUnixSocket(network, addr string) (any, error) {
52
if err != nil {
53
return nil, err
54
}
55
+ defer socketFile.Close()
56
57
// use copied fd to make new Listener or PacketConn, then replace
58
// it in the map so that future copies always come from the most
@@ -64,7 +67,11 @@ func reuseUnixSocket(network, addr string) (any, error) {
67
return nil, err
68
}
69
unixSocket.count.Add(1)
67
- unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
70
+ unixSockets[socketKey] = &unixListener{
71
+ UnixListener: ln.(*net.UnixListener),
72
+ mapKey: socketKey,
73
+ count: unixSocket.count,
74
+ }
75
76
case *unixConn:
77
pc, err := net.FilePacketConn(socketFile)
@@ -72,11 +79,18 @@ func reuseUnixSocket(network, addr string) (any, error) {
79
return nil, err
80
}
81
unixSocket.count.Add(1)
75
- unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), socketKey, unixSocket.count}
82
+ unixSockets[socketKey] = &unixConn{
83
+ UnixConn: pc.(*net.UnixConn),
84
+ mapKey: socketKey,
85
+ count: unixSocket.count,
86
+ }
87
}
88
89
return unixSockets[socketKey], nil
90
}
91
+ if isAbstractUnixSocket(addr) {
92
+ return nil, nil
93
+ }
94
95
// from what I can tell after some quick research, it's quite common for programs to
96
// leave their socket file behind after they close, so the typical pattern is to
@@ -167,7 +181,7 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
181
if unix, ok := ln.(*net.UnixConn); ok {
182
cnt := new(atomic.Int32)
183
cnt.Store(1)
170
- ln = &unixConn{unix, lnKey, cnt}
184
+ ln = &unixConn{UnixConn: unix, mapKey: lnKey, count: cnt}
185
unixSockets[lnKey] = ln.(*unixConn)
186
}
187
}
@@ -184,7 +198,7 @@ func listenReusable(ctx context.Context, lnKey string, network, address string,
198
unix.SetUnlinkOnClose(false)
199
cnt := new(atomic.Int32)
200
cnt.Store(1)
187
- ln = &unixListener{unix, lnKey, cnt}
201
+ ln = &unixListener{UnixListener: unix, mapKey: lnKey, count: cnt}
202
unixSockets[lnKey] = ln.(*unixListener)
203
}
204
}
@@ -219,52 +233,62 @@ type unixListener struct {
233
*net.UnixListener
234
mapKey string
235
count *atomic.Int32
236
+ closed atomic.Bool
237
}
238
239
func (uln *unixListener) Close() error {
240
+ if !uln.closed.CompareAndSwap(false, true) {
241
+ return net.ErrClosed
242
+ }
243
+ unixSocketsMu.Lock()
244
+ defer unixSocketsMu.Unlock()
245
+
246
+ var name string
247
+ if addr := uln.Addr(); addr != nil {
248
+ name = addr.String()
249
+ }
250
newCount := uln.count.Add(-1)
251
+ err := uln.UnixListener.Close()
252
if newCount == 0 {
227
- file, err := uln.File()
228
- var name string
229
- if err == nil {
230
- name = file.Name()
253
+ delete(unixSockets, uln.mapKey)
254
+ if name != "" && !isAbstractUnixSocket(name) {
255
+ _ = syscall.Unlink(name)
256
}
232
- defer func() {
233
- unixSocketsMu.Lock()
234
- delete(unixSockets, uln.mapKey)
235
- unixSocketsMu.Unlock()
236
- if err == nil {
237
- _ = syscall.Unlink(name)
238
- }
239
- }()
257
}
241
- return uln.UnixListener.Close()
258
+ return err
259
}
260
261
type unixConn struct {
262
*net.UnixConn
263
mapKey string
264
count *atomic.Int32
265
+ closed atomic.Bool
266
}
267
268
func (uc *unixConn) Close() error {
269
+ if !uc.closed.CompareAndSwap(false, true) {
270
+ return net.ErrClosed
271
+ }
272
+ unixSocketsMu.Lock()
273
+ defer unixSocketsMu.Unlock()
274
+
275
+ var name string
276
+ if addr := uc.LocalAddr(); addr != nil {
277
+ name = addr.String()
278
+ }
279
newCount := uc.count.Add(-1)
280
+ err := uc.UnixConn.Close()
281
if newCount == 0 {
253
- file, err := uc.File()
254
- var name string
255
- if err == nil {
256
- name = file.Name()
282
+ delete(unixSockets, uc.mapKey)
283
+ if name != "" && !isAbstractUnixSocket(name) {
284
+ _ = syscall.Unlink(name)
285
}
258
- defer func() {
259
- unixSocketsMu.Lock()
260
- delete(unixSockets, uc.mapKey)
261
- unixSocketsMu.Unlock()
262
- if err == nil {
263
- _ = syscall.Unlink(name)
264
- }
265
- }()
286
}
267
- return uc.UnixConn.Close()
287
+ return err
288
+}
289
+
290
+func isAbstractUnixSocket(name string) bool {
291
+ return (runtime.GOOS == "linux" || runtime.GOOS == "android") && strings.HasPrefix(name, "@")
292
}
293
294
func (uc *unixConn) Unwrap() net.PacketConn {
listen_unix_test.go
new
+408
@@ -0,0 +1,408 @@
1
+// Copyright 2015 Matthew Holt and The Caddy Authors
2
+//
3
+// Licensed under the Apache License, Version 2.0 (the "License");
4
+// you may not use this file except in compliance with the License.
5
+// You may obtain a copy of the License at
6
+//
7
+// http://www.apache.org/licenses/LICENSE-2.0
8
+//
9
+// Unless required by applicable law or agreed to in writing, software
10
+// distributed under the License is distributed on an "AS IS" BASIS,
11
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+// See the License for the specific language governing permissions and
13
+// limitations under the License.
14
+
15
+//go:build unix && !solaris
16
+
17
+package caddy
18
+
19
+import (
20
+ "context"
21
+ "errors"
22
+ "io"
23
+ "net"
24
+ "os"
25
+ "path/filepath"
26
+ "runtime"
27
+ "sync"
28
+ "testing"
29
+ "time"
30
+)
31
+
32
+func TestAbstractUnixSocketsDoNotUnlinkFilesystemPaths(t *testing.T) {
33
+ if runtime.GOOS != "linux" {
34
+ t.Skip("abstract Unix sockets are Linux-specific")
35
+ }
36
+
37
+ for _, network := range []string{"unix", "unixgram"} {
38
+ t.Run(network, func(t *testing.T) {
39
+ sentinel, err := os.CreateTemp(".", "@caddy-abstract-*")
40
+ if err != nil {
41
+ t.Fatal(err)
42
+ }
43
+ name := filepath.Base(sentinel.Name())
44
+ if err := sentinel.Close(); err != nil {
45
+ t.Fatal(err)
46
+ }
47
+ t.Cleanup(func() { _ = os.Remove(name) })
48
+
49
+ ln, err := (NetworkAddress{Network: network, Host: name}).Listen(
50
+ context.Background(), 0, net.ListenConfig{})
51
+ if err != nil {
52
+ t.Fatal(err)
53
+ }
54
+ closed := false
55
+ t.Cleanup(func() {
56
+ if !closed {
57
+ _ = ln.(io.Closer).Close()
58
+ }
59
+ })
60
+ if _, err := os.Stat(name); err != nil {
61
+ t.Fatalf("abstract listen removed sentinel: %v", err)
62
+ }
63
+ err = ln.(io.Closer).Close()
64
+ closed = true
65
+ if err != nil {
66
+ t.Fatal(err)
67
+ }
68
+ if _, err := os.Stat(name); err != nil {
69
+ t.Fatalf("abstract close removed sentinel: %v", err)
70
+ }
71
+ })
72
+ }
73
+}
74
+
75
+func TestUnixListenerDoubleCloseDoesNotReleaseAnotherReference(t *testing.T) {
76
+ socketPath := shortTempSocket(t)
77
+ na := NetworkAddress{Network: "unix", Host: socketPath}
78
+
79
+ firstAny, err := na.Listen(context.Background(), 0, net.ListenConfig{})
80
+ if err != nil {
81
+ t.Fatal(err)
82
+ }
83
+ firstOuter := firstAny.(deleteListener)
84
+ first := firstOuter.Listener.(*unixListener)
85
+ var second *unixListener
86
+ t.Cleanup(func() {
87
+ _ = firstOuter.Close()
88
+ if second != nil {
89
+ _ = second.Close()
90
+ }
91
+ })
92
+ secondAny, err := na.Listen(context.Background(), 0, net.ListenConfig{})
93
+ if err != nil {
94
+ t.Fatal(err)
95
+ }
96
+ second = secondAny.(*unixListener)
97
+
98
+ if err := first.Close(); err != nil {
99
+ t.Fatal(err)
100
+ }
101
+ if err := first.Close(); !errors.Is(err, net.ErrClosed) {
102
+ t.Fatalf("second Close() error = %v; want net.ErrClosed", err)
103
+ }
104
+ if got := first.count.Load(); got != 1 {
105
+ t.Fatalf("reference count = %d; want 1", got)
106
+ }
107
+ if _, err := os.Stat(socketPath); err != nil {
108
+ t.Fatalf("socket used by second reference was removed: %v", err)
109
+ }
110
+
111
+ if err := second.Close(); err != nil {
112
+ t.Fatal(err)
113
+ }
114
+}
115
+
116
+func TestUnixConnDoubleCloseDoesNotReleaseAnotherReference(t *testing.T) {
117
+ socketPath := shortTempSocket(t)
118
+ na := NetworkAddress{Network: "unixgram", Host: socketPath}
119
+
120
+ firstAny, err := na.Listen(context.Background(), 0, net.ListenConfig{})
121
+ if err != nil {
122
+ t.Fatal(err)
123
+ }
124
+ firstOuter := firstAny.(deletePacketConn)
125
+ first := firstOuter.PacketConn.(*unixConn)
126
+ var second *unixConn
127
+ t.Cleanup(func() {
128
+ _ = firstOuter.Close()
129
+ if second != nil {
130
+ _ = second.Close()
131
+ }
132
+ })
133
+ secondAny, err := na.Listen(context.Background(), 0, net.ListenConfig{})
134
+ if err != nil {
135
+ t.Fatal(err)
136
+ }
137
+ second = secondAny.(*unixConn)
138
+
139
+ if err := first.Close(); err != nil {
140
+ t.Fatal(err)
141
+ }
142
+ if err := first.Close(); !errors.Is(err, net.ErrClosed) {
143
+ t.Fatalf("second Close() error = %v; want net.ErrClosed", err)
144
+ }
145
+ if got := first.count.Load(); got != 1 {
146
+ t.Fatalf("reference count = %d; want 1", got)
147
+ }
148
+ if _, err := os.Stat(socketPath); err != nil {
149
+ t.Fatalf("socket used by second reference was removed: %v", err)
150
+ }
151
+
152
+ if err := second.Close(); err != nil {
153
+ t.Fatal(err)
154
+ }
155
+}
156
+
157
+// shortTempSocket returns a short unix socket path to avoid sockaddr_un
158
+// path length limits (104 bytes on Darwin/BSD, 108 bytes on Linux),
159
+// which can be exceeded by t.TempDir() in deeply nested temp directories.
160
+func shortTempSocket(t *testing.T) string {
161
+ t.Helper()
162
+ f, err := os.CreateTemp("", "c-*.sock")
163
+ if err != nil {
164
+ t.Fatalf("failed to create temp socket path: %v", err)
165
+ }
166
+ path := f.Name()
167
+ _ = f.Close()
168
+ _ = os.Remove(path)
169
+ t.Cleanup(func() {
170
+ _ = os.Remove(path)
171
+ })
172
+ return path
173
+}
174
+
175
+func TestUnixListenerClosesImmediatelyAndUnlinks(t *testing.T) {
176
+ socketPath := shortTempSocket(t)
177
+ lnKey := listenerKey("unix", socketPath)
178
+
179
+ ctx := context.Background()
180
+ rawLn, err := listenReusable(ctx, lnKey, "unix", socketPath, net.ListenConfig{})
181
+ if err != nil {
182
+ t.Fatalf("listenReusable failed: %v", err)
183
+ }
184
+
185
+ ln, ok := rawLn.(net.Listener)
186
+ if !ok {
187
+ t.Fatalf("expected net.Listener, got %T", rawLn)
188
+ }
189
+
190
+ // Verify socket file was created on disk
191
+ if _, err := os.Stat(socketPath); err != nil {
192
+ t.Fatalf("expected socket file to exist on disk: %v", err)
193
+ }
194
+
195
+ // Verify we can connect and accept
196
+ done := make(chan struct{})
197
+ go func() {
198
+ defer close(done)
199
+ conn, err := ln.Accept()
200
+ if err == nil {
201
+ _ = conn.Close()
202
+ }
203
+ }()
204
+
205
+ clientConn, err := net.DialTimeout("unix", socketPath, 2*time.Second)
206
+ if err != nil {
207
+ t.Fatalf("failed to dial unix socket: %v", err)
208
+ }
209
+ _ = clientConn.Close()
210
+ select {
211
+ case <-done:
212
+ case <-time.After(2 * time.Second):
213
+ t.Fatal("timed out waiting for accept")
214
+ }
215
+
216
+ // Close the listener
217
+ if err := ln.Close(); err != nil {
218
+ t.Fatalf("failed to close listener: %v", err)
219
+ }
220
+
221
+ // The socket file must be removed immediately
222
+ if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
223
+ t.Fatalf("expected socket file to be unlinked after Close, but stat returned: %v", err)
224
+ }
225
+
226
+ // Dialing the closed socket path must fail immediately and not hang
227
+ dialErrChan := make(chan error, 1)
228
+ go func() {
229
+ c, err := net.DialTimeout("unix", socketPath, 500*time.Millisecond)
230
+ if err == nil {
231
+ _ = c.Close()
232
+ }
233
+ dialErrChan <- err
234
+ }()
235
+
236
+ select {
237
+ case err := <-dialErrChan:
238
+ if err == nil {
239
+ t.Fatal("expected dial to fail on closed socket, but succeeded")
240
+ }
241
+ case <-time.After(1 * time.Second):
242
+ t.Fatal("dialing closed socket hung instead of failing immediately")
243
+ }
244
+}
245
+
246
+func TestUnixConnClosesImmediatelyAndUnlinks(t *testing.T) {
247
+ socketPath := shortTempSocket(t)
248
+ lnKey := listenerKey("unixgram", socketPath)
249
+
250
+ ctx := context.Background()
251
+ rawConn, err := listenReusable(ctx, lnKey, "unixgram", socketPath, net.ListenConfig{})
252
+ if err != nil {
253
+ t.Fatalf("listenReusable failed: %v", err)
254
+ }
255
+
256
+ closer, ok := rawConn.(io.Closer)
257
+ if !ok {
258
+ t.Fatalf("expected io.Closer, got %T", rawConn)
259
+ }
260
+
261
+ // Verify socket file was created on disk
262
+ if _, err := os.Stat(socketPath); err != nil {
263
+ t.Fatalf("expected socket file to exist on disk: %v", err)
264
+ }
265
+
266
+ // Close the datagram conn
267
+ if err := closer.Close(); err != nil {
268
+ t.Fatalf("failed to close unixgram connection: %v", err)
269
+ }
270
+
271
+ // The socket file must be removed immediately
272
+ if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
273
+ t.Fatalf("expected socket file to be unlinked after Close, but stat returned: %v", err)
274
+ }
275
+}
276
+
277
+func TestUnixListenerReuseAndUnlinkOnlyWhenZeroCount(t *testing.T) {
278
+ socketPath := shortTempSocket(t)
279
+ lnKey := listenerKey("unix", socketPath)
280
+
281
+ ctx := context.Background()
282
+ rawLn1, err := listenReusable(ctx, lnKey, "unix", socketPath, net.ListenConfig{})
283
+ if err != nil {
284
+ t.Fatalf("listenReusable failed: %v", err)
285
+ }
286
+ ln1 := rawLn1.(net.Listener)
287
+
288
+ // Reuse socket
289
+ rawLn2, err := reuseUnixSocket("unix", socketPath)
290
+ if err != nil {
291
+ t.Fatalf("reuseUnixSocket failed: %v", err)
292
+ }
293
+ ln2 := rawLn2.(net.Listener)
294
+
295
+ // Close ln1: count drops to 1, socket must still exist
296
+ if err := ln1.Close(); err != nil {
297
+ t.Fatalf("ln1.Close() failed: %v", err)
298
+ }
299
+ if _, err := os.Stat(socketPath); err != nil {
300
+ t.Fatalf("expected socket file to still exist after closing 1 of 2 references: %v", err)
301
+ }
302
+
303
+ // Close ln2: count drops to 0, socket file must be unlinked
304
+ if err := ln2.Close(); err != nil {
305
+ t.Fatalf("ln2.Close() failed: %v", err)
306
+ }
307
+ if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
308
+ t.Fatalf("expected socket file to be unlinked after closing all references, got: %v", err)
309
+ }
310
+}
311
+
312
+func TestUnixListenerConcurrentCloseAndRelisten(t *testing.T) {
313
+ socketPath := shortTempSocket(t)
314
+ na := NetworkAddress{Network: "unix", Host: socketPath}
315
+
316
+ for i := 0; i < 20; i++ {
317
+ ln, err := na.Listen(context.Background(), 0, net.ListenConfig{})
318
+ if err != nil {
319
+ t.Fatalf("iteration %d: initial listen failed: %v", i, err)
320
+ }
321
+ listener := ln.(net.Listener)
322
+
323
+ var wg sync.WaitGroup
324
+ wg.Add(2)
325
+
326
+ var newLn net.Listener
327
+ var listenErr error
328
+
329
+ go func() {
330
+ defer wg.Done()
331
+ _ = listener.Close()
332
+ }()
333
+
334
+ go func() {
335
+ defer wg.Done()
336
+ ln2, err := na.Listen(context.Background(), 0, net.ListenConfig{})
337
+ if err != nil {
338
+ listenErr = err
339
+ return
340
+ }
341
+ newLn = ln2.(net.Listener)
342
+ }()
343
+
344
+ wg.Wait()
345
+
346
+ if listenErr != nil {
347
+ t.Fatalf("iteration %d: concurrent listen failed: %v", i, listenErr)
348
+ }
349
+
350
+ if _, err := os.Stat(socketPath); err != nil {
351
+ t.Fatalf("iteration %d: socket file does not exist after concurrent relisten: %v", i, err)
352
+ }
353
+
354
+ clientConn, err := net.DialTimeout("unix", socketPath, 500*time.Millisecond)
355
+ if err != nil {
356
+ t.Fatalf("iteration %d: dialing concurrent listener failed: %v", i, err)
357
+ }
358
+ _ = clientConn.Close()
359
+
360
+ _ = newLn.Close()
361
+ }
362
+}
363
+
364
+func TestUnixConnConcurrentCloseAndRelisten(t *testing.T) {
365
+ socketPath := shortTempSocket(t)
366
+ na := NetworkAddress{Network: "unixgram", Host: socketPath}
367
+
368
+ for i := 0; i < 20; i++ {
369
+ pc, err := na.Listen(context.Background(), 0, net.ListenConfig{})
370
+ if err != nil {
371
+ t.Fatalf("iteration %d: initial listen failed: %v", i, err)
372
+ }
373
+ packetConn := pc.(net.PacketConn)
374
+
375
+ var wg sync.WaitGroup
376
+ wg.Add(2)
377
+
378
+ var newPc net.PacketConn
379
+ var listenErr error
380
+
381
+ go func() {
382
+ defer wg.Done()
383
+ _ = packetConn.Close()
384
+ }()
385
+
386
+ go func() {
387
+ defer wg.Done()
388
+ pc2, err := na.Listen(context.Background(), 0, net.ListenConfig{})
389
+ if err != nil {
390
+ listenErr = err
391
+ return
392
+ }
393
+ newPc = pc2.(net.PacketConn)
394
+ }()
395
+
396
+ wg.Wait()
397
+
398
+ if listenErr != nil {
399
+ t.Fatalf("iteration %d: concurrent listen failed: %v", i, listenErr)
400
+ }
401
+
402
+ if _, err := os.Stat(socketPath); err != nil {
403
+ t.Fatalf("iteration %d: socket file does not exist after concurrent relisten: %v", i, err)
404
+ }
405
+
406
+ _ = newPc.Close()
407
+ }
408
+}