@marcel91200 / caddy Mirror · read-only / commits / d483d781
Fork 0

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 +}