@marcel91200 / caddy Mirror · read-only / commits / e3e8aabb
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 13, 10:30 UTC

core: Refactor and improve listener logic (#5089)

* core: Refactor, improve listener logic Deprecate: - caddy.Listen - caddy.ListenTimeout - caddy.ListenPacket Prefer caddy.NetworkAddress.Listen() instead. Change: - caddy.ListenQUIC (hopefully to remove later) - caddy.ListenerFunc signature (add context and ListenConfig) - Don't emit Alt-Svc header advertising h3 over HTTP/3 - Use quic.ListenEarly instead of quic.ListenEarlyAddr; this gives us more flexibility (e.g. possibility of HTTP/3 over UDS) but also introduces a new issue: https://github.com/lucas-clemente/quic-go/issues/3560#issuecomment-1258959608 - Unlink unix socket before and after use * Appease the linter * Keep ListenAll

Matt Holt committed Sep 28, 2022 at 13:35 UTC e3e8aabbcf65d37516bb97f9dc0f77df52f8cf55
7 files changed +562 -292
admin.go
+4 -3
@@ -382,7 +382,7 @@ func replaceLocalAdminServer(cfg *Config) error {
382
383 handler := cfg.Admin.newAdminHandler(addr, false)
384
385 - ln, err := Listen(addr.Network, addr.JoinHostPort(0))
385 + ln, err := addr.Listen(context.TODO(), 0, net.ListenConfig{})
386 if err != nil {
387 return err
388 }
@@ -403,7 +403,7 @@ func replaceLocalAdminServer(cfg *Config) error {
403 serverMu.Lock()
404 server := localAdminServer
405 serverMu.Unlock()
406 - if err := server.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
406 + if err := server.Serve(ln.(net.Listener)); !errors.Is(err, http.ErrServerClosed) {
407 adminLogger.Error("admin server shutdown for unknown reason", zap.Error(err))
408 }
409 }()
@@ -549,10 +549,11 @@ func replaceRemoteAdminServer(ctx Context, cfg *Config) error {
549 serverMu.Unlock()
550
551 // start listener
552 - ln, err := Listen(addr.Network, addr.JoinHostPort(0))
552 + lnAny, err := addr.Listen(ctx, 0, net.ListenConfig{})
553 if err != nil {
554 return err
555 }
556 + ln := lnAny.(net.Listener)
557 ln = tls.NewListener(ln, tlsConfig)
558
559 go func() {
listen.go
+7 -19
@@ -20,7 +20,7 @@
20 package caddy
21
22 import (
23 - "fmt"
23 + "context"
24 "net"
25 "sync"
26 "sync/atomic"
@@ -29,21 +29,14 @@ import (
29 "go.uber.org/zap"
30 )
31
32 -func ListenTimeout(network, addr string, keepAlivePeriod time.Duration) (net.Listener, error) {
33 - // check to see if plugin provides listener
34 - if ln, err := getListenerFromPlugin(network, addr); err != nil || ln != nil {
35 - return ln, err
36 - }
37 -
38 - lnKey := listenerKey(network, addr)
32 +func reuseUnixSocket(network, addr string) (any, error) {
33 + return nil, nil
34 +}
35
36 +func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
37 sharedLn, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
41 - ln, err := net.Listen(network, addr)
38 + ln, err := config.Listen(ctx, network, address)
39 if err != nil {
43 - // https://github.com/caddyserver/caddy/pull/4534
44 - if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) {
45 - return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err)
46 - }
40 return nil, err
41 }
42 return &sharedListener{Listener: ln, key: lnKey}, nil
@@ -51,8 +44,7 @@ func ListenTimeout(network, addr string, keepAlivePeriod time.Duration) (net.Lis
44 if err != nil {
45 return nil, err
46 }
54 -
55 - return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: keepAlivePeriod}, nil
47 + return &fakeCloseListener{sharedListener: sharedLn.(*sharedListener), keepAlivePeriod: config.KeepAlive}, nil
48 }
49
50 // fakeCloseListener is a private wrapper over a listener that
@@ -156,8 +148,6 @@ func (sl *sharedListener) clearDeadline() error {
148 switch ln := sl.Listener.(type) {
149 case *net.TCPListener:
150 err = ln.SetDeadline(time.Time{})
159 - case *net.UnixListener:
160 - err = ln.SetDeadline(time.Time{})
151 }
152 sl.deadline = false
153 }
@@ -173,8 +163,6 @@ func (sl *sharedListener) setDeadline() error {
163 switch ln := sl.Listener.(type) {
164 case *net.TCPListener:
165 err = ln.SetDeadline(timeInPast)
176 - case *net.UnixListener:
177 - err = ln.SetDeadline(timeInPast)
166 }
167 sl.deadline = true
168 }
listen_unix.go
+53 -50
@@ -24,78 +24,88 @@ import (
24 "errors"
25 "io/fs"
26 "net"
27 - "sync"
27 + "sync/atomic"
28 "syscall"
29 - "time"
29
30 "go.uber.org/zap"
31 "golang.org/x/sys/unix"
32 )
33
35 -// ListenTimeout is the same as Listen, but with a configurable keep-alive timeout duration.
36 -func ListenTimeout(network, addr string, keepalivePeriod time.Duration) (net.Listener, error) {
37 - // check to see if plugin provides listener
38 - if ln, err := getListenerFromPlugin(network, addr); err != nil || ln != nil {
39 - return ln, err
34 +// reuseUnixSocket copies and reuses the unix domain socket (UDS) if we already
35 +// have it open; if not, unlink it so we can have it. No-op if not a unix network.
36 +func reuseUnixSocket(network, addr string) (any, error) {
37 + if !isUnixNetwork(network) {
38 + return nil, nil
39 }
40
41 socketKey := listenerKey(network, addr)
43 - if isUnixNetwork(network) {
44 - unixSocketsMu.Lock()
45 - defer unixSocketsMu.Unlock()
42
47 - socket, exists := unixSockets[socketKey]
48 - if exists {
49 - // make copy of file descriptor
50 - socketFile, err := socket.File() // dup() deep down
43 + socket, exists := unixSockets[socketKey]
44 + if exists {
45 + // make copy of file descriptor
46 + socketFile, err := socket.File() // does dup() deep down
47 + if err != nil {
48 + return nil, err
49 + }
50 +
51 + // use copied fd to make new Listener or PacketConn, then replace
52 + // it in the map so that future copies always come from the most
53 + // recent fd (as the previous ones will be closed, and we'd get
54 + // "use of closed network connection" errors) -- note that we
55 + // preserve the *pointer* to the counter (not just the value) so
56 + // that all socket wrappers will refer to the same value
57 + switch unixSocket := socket.(type) {
58 + case *unixListener:
59 + ln, err := net.FileListener(socketFile)
60 if err != nil {
61 return nil, err
62 }
63 + atomic.AddInt32(unixSocket.count, 1)
64 + unixSockets[socketKey] = &unixListener{ln.(*net.UnixListener), socketKey, unixSocket.count}
65
55 - // use copy to make new listener
56 - ln, err := net.FileListener(socketFile)
66 + case *unixConn:
67 + pc, err := net.FilePacketConn(socketFile)
68 if err != nil {
69 return nil, err
70 }
60 -
61 - // the old socket fd will likely be closed soon, so replace it in the map
62 - unixSockets[socketKey] = ln.(*net.UnixListener)
63 -
64 - return ln.(*net.UnixListener), nil
71 + atomic.AddInt32(unixSocket.count, 1)
72 + unixSockets[socketKey] = &unixConn{pc.(*net.UnixConn), addr, socketKey, unixSocket.count}
73 }
74
67 - // from what I can tell after some quick research, it's quite common for programs to
68 - // leave their socket file behind after they close, so the typical pattern is to
69 - // unlink it before you bind to it -- this is often crucial if the last program using
70 - // it was killed forcefully without a chance to clean up the socket, but there is a
71 - // race, as the comment in net.UnixListener.close() explains... oh well?
72 - if err := syscall.Unlink(addr); err != nil && !errors.Is(err, fs.ErrNotExist) {
73 - return nil, err
74 - }
75 + return unixSockets[socketKey], nil
76 }
77
77 - config := &net.ListenConfig{Control: reusePort, KeepAlive: keepalivePeriod}
78 -
79 - ln, err := config.Listen(context.Background(), network, addr)
80 - if err != nil {
78 + // from what I can tell after some quick research, it's quite common for programs to
79 + // leave their socket file behind after they close, so the typical pattern is to
80 + // unlink it before you bind to it -- this is often crucial if the last program using
81 + // it was killed forcefully without a chance to clean up the socket, but there is a
82 + // race, as the comment in net.UnixListener.close() explains... oh well, I guess?
83 + if err := syscall.Unlink(addr); err != nil && !errors.Is(err, fs.ErrNotExist) {
84 return nil, err
85 }
86
84 - if uln, ok := ln.(*net.UnixListener); ok {
85 - // TODO: ideally, we should unlink the socket once we know we're done using it
86 - // (i.e. either on exit or a new config that doesn't use this socket; in UsagePool
87 - // terms, when the reference count reaches 0), but given that we unlink existing
88 - // socket before we create the new one anyway (see above), we don't necessarily
89 - // need to clean up after ourselves; still, doing so would probably be more tidy
90 - uln.SetUnlinkOnClose(false)
91 - unixSockets[socketKey] = uln
92 - }
87 + return nil, nil
88 +}
89
94 - return ln, nil
90 +func listenTCPOrUnix(ctx context.Context, lnKey string, network, address string, config net.ListenConfig) (net.Listener, error) {
91 + // wrap any Control function set by the user so we can also add our reusePort control without clobbering theirs
92 + oldControl := config.Control
93 + config.Control = func(network, address string, c syscall.RawConn) error {
94 + if oldControl != nil {
95 + if err := oldControl(network, address, c); err != nil {
96 + return err
97 + }
98 + }
99 + return reusePort(network, address, c)
100 + }
101 + return config.Listen(ctx, network, address)
102 }
103
104 // reusePort sets SO_REUSEPORT. Ineffective for unix sockets.
105 func reusePort(network, address string, conn syscall.RawConn) error {
106 + if isUnixNetwork(network) {
107 + return nil
108 + }
109 return conn.Control(func(descriptor uintptr) {
110 if err := unix.SetsockoptInt(int(descriptor), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
111 Log().Error("setting SO_REUSEPORT",
@@ -106,10 +116,3 @@ func reusePort(network, address string, conn syscall.RawConn) error {
116 }
117 })
118 }
109 -
110 -// unixSockets keeps track of the currently-active unix sockets
111 -// so we can transfer their FDs gracefully during reloads.
112 -var (
113 - unixSockets = make(map[string]*net.UnixListener)
114 - unixSocketsMu sync.Mutex
115 -)
listeners.go
+424 -202
@@ -19,230 +19,187 @@ import (
19 "crypto/tls"
20 "errors"
21 "fmt"
22 + "io"
23 "net"
24 "net/netip"
25 "os"
26 "strconv"
27 "strings"
28 + "sync"
29 "sync/atomic"
30 "syscall"
31 + "time"
32
33 "github.com/lucas-clemente/quic-go"
34 "github.com/lucas-clemente/quic-go/http3"
35 "go.uber.org/zap"
36 )
37
35 -// Listen is like net.Listen, except Caddy's listeners can overlap
36 -// each other: multiple listeners may be created on the same socket
37 -// at the same time. This is useful because during config changes,
38 -// the new config is started while the old config is still running.
39 -// When Caddy listeners are closed, the closing logic is virtualized
40 -// so the underlying socket isn't actually closed until all uses of
41 -// the socket have been finished. Always be sure to close listeners
42 -// when you are done with them, just like normal listeners.
43 -func Listen(network, addr string) (net.Listener, error) {
44 - // a 0 timeout means Go uses its default
45 - return ListenTimeout(network, addr, 0)
46 -}
47 -
48 -// getListenerFromPlugin returns a listener on the given network and address
49 -// if a plugin has registered the network name. It may return (nil, nil) if
50 -// no plugin can provide a listener.
51 -func getListenerFromPlugin(network, addr string) (net.Listener, error) {
52 - network = strings.TrimSpace(strings.ToLower(network))
53 -
54 - // get listener from plugin if network type is registered
55 - if getListener, ok := networkTypes[network]; ok {
56 - Log().Debug("getting listener from plugin", zap.String("network", network))
57 - return getListener(network, addr)
58 - }
59 -
60 - return nil, nil
38 +// NetworkAddress represents one or more network addresses.
39 +// It contains the individual components for a parsed network
40 +// address of the form accepted by ParseNetworkAddress().
41 +type NetworkAddress struct {
42 + // Should be a network value accepted by Go's net package or
43 + // by a plugin providing a listener for that network type.
44 + Network string
45 +
46 + // The "main" part of the network address is the host, which
47 + // often takes the form of a hostname, DNS name, IP address,
48 + // or socket path.
49 + Host string
50 +
51 + // For addresses that contain a port, ranges are given by
52 + // [StartPort, EndPort]; i.e. for a single port, StartPort
53 + // and EndPort are the same. For no port, they are 0.
54 + StartPort uint
55 + EndPort uint
56 }
57
63 -// ListenPacket returns a net.PacketConn suitable for use in a Caddy module.
64 -// It is like Listen except for PacketConns.
65 -// Always be sure to close the PacketConn when you are done.
66 -func ListenPacket(network, addr string) (net.PacketConn, error) {
67 - lnKey := listenerKey(network, addr)
58 +// ListenAll calls Listen() for all addresses represented by this struct, i.e. all ports in the range.
59 +// (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.)
60 +// It returns an error if any listener failed to bind, and closes any listeners opened up to that point.
61 +//
62 +// TODO: Experimental API: subject to change or removal.
63 +func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) {
64 + var listeners []any
65 + var err error
66 +
67 + // if one of the addresses has a failure, we need to close
68 + // any that did open a socket to avoid leaking resources
69 + defer func() {
70 + if err == nil {
71 + return
72 + }
73 + for _, ln := range listeners {
74 + if cl, ok := ln.(io.Closer); ok {
75 + cl.Close()
76 + }
77 + }
78 + }()
79 +
80 + // an address can contain a port range, which represents multiple addresses;
81 + // some addresses don't use ports at all and have a port range size of 1;
82 + // whatever the case, iterate each address represented and bind a socket
83 + for portOffset := uint(0); portOffset < na.PortRangeSize(); portOffset++ {
84 + select {
85 + case <-ctx.Done():
86 + return nil, ctx.Err()
87 + default:
88 + }
89
69 - sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
70 - pc, err := net.ListenPacket(network, addr)
90 + // create (or reuse) the listener ourselves
91 + var ln any
92 + ln, err = na.Listen(ctx, portOffset, config)
93 if err != nil {
72 - // https://github.com/caddyserver/caddy/pull/4534
73 - if isUnixNetwork(network) && isListenBindAddressAlreadyInUseError(err) {
74 - return nil, fmt.Errorf("%w: this can happen if Caddy was forcefully killed", err)
75 - }
94 return nil, err
95 }
78 - return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
79 - })
80 - if err != nil {
81 - return nil, err
96 + listeners = append(listeners, ln)
97 }
98
84 - return &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}, nil
99 + return listeners, nil
100 }
101
87 -// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
88 -// Note that the context passed to Accept is currently ignored, so using
89 -// a context other than context.Background is meaningless.
90 -// This API is EXPERIMENTAL and may change.
91 -func ListenQUIC(addr string, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
92 - lnKey := listenerKey("udp", addr)
93 -
94 - sharedEl, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
95 - el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{
96 - RequireAddressValidation: func(clientAddr net.Addr) bool {
97 - var highLoad bool
98 - if activeRequests != nil {
99 - highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
100 - }
101 - return highLoad
102 - },
103 - })
104 - if err != nil {
105 - return nil, err
106 - }
107 - return &sharedQuicListener{EarlyListener: el, key: lnKey}, nil
108 - })
109 - if err != nil {
110 - return nil, err
102 +// Listen is similar to net.Listen, with a few differences:
103 +//
104 +// Listen announces on the network address using the port calculated by adding
105 +// portOffset to the start port. (For network types that do not use ports, the
106 +// portOffset is ignored.)
107 +//
108 +// The provided ListenConfig is used to create the listener. Its Control function,
109 +// if set, may be wrapped by an internally-used Control function. The provided
110 +// context may be used to cancel long operations early. The context is not used
111 +// to close the listener after it has been created.
112 +//
113 +// Caddy's listeners can overlap each other: multiple listeners may be created on
114 +// the same socket at the same time. This is useful because during config changes,
115 +// the new config is started while the old config is still running. How this is
116 +// accomplished varies by platform and network type. For example, on Unix, SO_REUSEPORT
117 +// is set except on Unix sockets, for which the file descriptor is duplicated and
118 +// reused; on Windows, the close logic is virtualized using timeouts. Like normal
119 +// listeners, be sure to Close() them when you are done.
120 +//
121 +// This method returns any type, as the implementations of listeners for various
122 +// network types are not interchangeable. The type of listener returned is switched
123 +// on the network type. Stream-based networks ("tcp", "unix", "unixpacket", etc.)
124 +// return a net.Listener; datagram-based networks ("udp", "unixgram", etc.) return
125 +// a net.PacketConn; and so forth. The actual concrete types are not guaranteed to
126 +// be standard, exported types (wrapping is necessary to provide graceful reloads).
127 +//
128 +// Unix sockets will be unlinked before being created, to ensure we can bind to
129 +// it even if the previous program using it exited uncleanly; it will also be
130 +// unlinked upon a graceful exit (or when a new config does not use that socket).
131 +//
132 +// TODO: Experimental API: subject to change or removal.
133 +func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
134 + if na.IsUnixNetwork() {
135 + unixSocketsMu.Lock()
136 + defer unixSocketsMu.Unlock()
137 }
138
113 - ctx, cancel := context.WithCancel(context.Background())
114 - return &fakeCloseQuicListener{
115 - sharedQuicListener: sharedEl.(*sharedQuicListener),
116 - context: ctx,
117 - contextCancel: cancel,
118 - }, nil
119 -}
139 + // check to see if plugin provides listener
140 + if ln, err := getListenerFromPlugin(ctx, na.Network, na.JoinHostPort(portOffset), config); ln != nil || err != nil {
141 + return ln, err
142 + }
143
121 -// ListenerUsage returns the current usage count of the given listener address.
122 -func ListenerUsage(network, addr string) int {
123 - count, _ := listenerPool.References(listenerKey(network, addr))
124 - return count
144 + // create (or reuse) the listener ourselves
145 + return na.listen(ctx, portOffset, config)
146 }
147
127 -func listenerKey(network, addr string) string {
128 - return network + "/" + addr
129 -}
148 +func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) {
149 + var ln any
150 + var err error
151
131 -type fakeCloseQuicListener struct {
132 - closed int32 // accessed atomically; belongs to this struct only
133 - *sharedQuicListener // embedded, so we also become a quic.EarlyListener
134 - context context.Context
135 - contextCancel context.CancelFunc
136 -}
152 + address := na.JoinHostPort(portOffset)
153
138 -// Currently Accept ignores the passed context, however a situation where
139 -// someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here)
140 -// server on which Accept would be called with non-empty contexts
141 -// (mind that the default net listeners' Accept doesn't take a context argument)
142 -// sounds way too rare for us to sacrifice efficiency here.
143 -func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) {
144 - conn, err := fcql.sharedQuicListener.Accept(fcql.context)
145 - if err == nil {
146 - return conn, nil
154 + // if this is a unix socket, see if we already have it open
155 + if socket, err := reuseUnixSocket(na.Network, address); socket != nil || err != nil {
156 + return socket, err
157 }
158
149 - // if the listener is "closed", return a fake closed error instead
150 - if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
151 - return nil, fakeClosedErr(fcql)
152 - }
153 - return nil, err
154 -}
159 + lnKey := listenerKey(na.Network, address)
160
156 -func (fcql *fakeCloseQuicListener) Close() error {
157 - if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
158 - fcql.contextCancel()
159 - _, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
161 + switch na.Network {
162 + case "tcp", "tcp4", "tcp6", "unix", "unixpacket":
163 + ln, err = listenTCPOrUnix(ctx, lnKey, na.Network, address, config)
164 + case "unixgram":
165 + ln, err = config.ListenPacket(ctx, na.Network, address)
166 + case "udp", "udp4", "udp6":
167 + sharedPc, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
168 + pc, err := config.ListenPacket(ctx, na.Network, address)
169 + if err != nil {
170 + return nil, err
171 + }
172 + return &sharedPacketConn{PacketConn: pc, key: lnKey}, nil
173 + })
174 + if err != nil {
175 + return nil, err
176 + }
177 + ln = &fakeClosePacketConn{sharedPacketConn: sharedPc.(*sharedPacketConn)}
178 }
161 - return nil
162 -}
163 -
164 -// fakeClosedErr returns an error value that is not temporary
165 -// nor a timeout, suitable for making the caller think the
166 -// listener is actually closed
167 -func fakeClosedErr(l interface{ Addr() net.Addr }) error {
168 - return &net.OpError{
169 - Op: "accept",
170 - Net: l.Addr().Network(),
171 - Addr: l.Addr(),
172 - Err: errFakeClosed,
179 + if strings.HasPrefix(na.Network, "ip") {
180 + ln, err = config.ListenPacket(ctx, na.Network, address)
181 }
174 -}
175 -
176 -// ErrFakeClosed is the underlying error value returned by
177 -// fakeCloseListener.Accept() after Close() has been called,
178 -// indicating that it is pretending to be closed so that the
179 -// server using it can terminate, while the underlying
180 -// socket is actually left open.
181 -var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
182 -
183 -// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
184 -type fakeClosePacketConn struct {
185 - closed int32 // accessed atomically; belongs to this struct only
186 - *sharedPacketConn // embedded, so we also become a net.PacketConn
187 -}
188 -
189 -func (fcpc *fakeClosePacketConn) Close() error {
190 - if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
191 - _, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
182 + if err != nil {
183 + return nil, err
184 }
193 - return nil
194 -}
195 -
196 -// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
197 -func (fcpc fakeClosePacketConn) SetReadBuffer(bytes int) error {
198 - if conn, ok := fcpc.PacketConn.(interface{ SetReadBuffer(int) error }); ok {
199 - return conn.SetReadBuffer(bytes)
185 + if ln == nil {
186 + return nil, fmt.Errorf("unsupported network type: %s", na.Network)
187 }
201 - return fmt.Errorf("SetReadBuffer() not implemented for %T", fcpc.PacketConn)
202 -}
188
204 -// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
205 -func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) {
206 - if conn, ok := fcpc.PacketConn.(interface {
207 - SyscallConn() (syscall.RawConn, error)
208 - }); ok {
209 - return conn.SyscallConn()
189 + // if new listener is a unix socket, make sure we can reuse it later
190 + // (we do our own "unlink on close" -- not required, but more tidy)
191 + one := int32(1)
192 + switch unix := ln.(type) {
193 + case *net.UnixListener:
194 + unix.SetUnlinkOnClose(false)
195 + ln = &unixListener{unix, lnKey, &one}
196 + unixSockets[lnKey] = ln.(*unixListener)
197 + case *net.UnixConn:
198 + ln = &unixConn{unix, address, lnKey, &one}
199 + unixSockets[lnKey] = ln.(*unixConn)
200 }
211 - return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn)
212 -}
213 -
214 -// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
215 -type sharedQuicListener struct {
216 - quic.EarlyListener
217 - key string
218 -}
219 -
220 -// Destruct closes the underlying QUIC listener.
221 -func (sql *sharedQuicListener) Destruct() error {
222 - return sql.EarlyListener.Close()
223 -}
224 -
225 -// sharedPacketConn is like sharedListener, but for net.PacketConns.
226 -type sharedPacketConn struct {
227 - net.PacketConn
228 - key string
229 -}
230 -
231 -// Destruct closes the underlying socket.
232 -func (spc *sharedPacketConn) Destruct() error {
233 - return spc.PacketConn.Close()
234 -}
201
236 -// NetworkAddress contains the individual components
237 -// for a parsed network address of the form accepted
238 -// by ParseNetworkAddress(). Network should be a
239 -// network value accepted by Go's net package. Port
240 -// ranges are given by [StartPort, EndPort].
241 -type NetworkAddress struct {
242 - Network string
243 - Host string
244 - StartPort uint
245 - EndPort uint
202 + return ln, nil
203 }
204
205 // IsUnixNetwork returns true if na.Network is
@@ -260,17 +217,27 @@ func (na NetworkAddress) JoinHostPort(offset uint) string {
217 return net.JoinHostPort(na.Host, strconv.Itoa(int(na.StartPort+offset)))
218 }
219
220 +// Expand returns one NetworkAddress for each port in the port range.
221 +//
222 +// This is EXPERIMENTAL and subject to change or removal.
223 func (na NetworkAddress) Expand() []NetworkAddress {
224 size := na.PortRangeSize()
225 addrs := make([]NetworkAddress, size)
226 for portOffset := uint(0); portOffset < size; portOffset++ {
267 - na2 := na
268 - na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset
269 - addrs[portOffset] = na2
227 + addrs[portOffset] = na.At(portOffset)
228 }
229 return addrs
230 }
231
232 +// At returns a NetworkAddress with a port range of just 1
233 +// at the given port offset; i.e. a NetworkAddress that
234 +// represents precisely 1 address only.
235 +func (na NetworkAddress) At(portOffset uint) NetworkAddress {
236 + na2 := na
237 + na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset
238 + return na2
239 +}
240 +
241 // PortRangeSize returns how many ports are in
242 // pa's port range. Port ranges are inclusive,
243 // so the size is the difference of start and
@@ -326,20 +293,6 @@ func isUnixNetwork(netw string) bool {
293 return netw == "unix" || netw == "unixgram" || netw == "unixpacket"
294 }
295
329 -func isListenBindAddressAlreadyInUseError(err error) bool {
330 - switch networkOperationError := err.(type) {
331 - case *net.OpError:
332 - switch syscallError := networkOperationError.Err.(type) {
333 - case *os.SyscallError:
334 - if syscallError.Syscall == "bind" {
335 - return true
336 - }
337 - }
338 - }
339 -
340 - return false
341 -}
342 -
296 // ParseNetworkAddress parses addr into its individual
297 // components. The input string is expected to be of
298 // the form "network/host:port-range" where any part is
@@ -439,6 +392,209 @@ func JoinNetworkAddress(network, host, port string) string {
392 return a
393 }
394
395 +// DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future.
396 +func Listen(network, addr string) (net.Listener, error) {
397 + // a 0 timeout means Go uses its default
398 + return ListenTimeout(network, addr, 0)
399 +}
400 +
401 +// DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future.
402 +func ListenTimeout(network, addr string, keepalivePeriod time.Duration) (net.Listener, error) {
403 + netAddr, err := ParseNetworkAddress(JoinNetworkAddress(network, addr, ""))
404 + if err != nil {
405 + return nil, err
406 + }
407 +
408 + ln, err := netAddr.Listen(context.TODO(), 0, net.ListenConfig{KeepAlive: keepalivePeriod})
409 + if err != nil {
410 + return nil, err
411 + }
412 +
413 + return ln.(net.Listener), nil
414 +}
415 +
416 +// DEPRECATED: Use NetworkAddress.Listen instead. This function will likely be changed or removed in the future.
417 +func ListenPacket(network, addr string) (net.PacketConn, error) {
418 + netAddr, err := ParseNetworkAddress(JoinNetworkAddress(network, addr, ""))
419 + if err != nil {
420 + return nil, err
421 + }
422 +
423 + ln, err := netAddr.Listen(context.TODO(), 0, net.ListenConfig{})
424 + if err != nil {
425 + return nil, err
426 + }
427 +
428 + return ln.(net.PacketConn), nil
429 +}
430 +
431 +// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
432 +// The network will be transformed into a QUIC-compatible type (if unix, then
433 +// unixgram will be used; otherwise, udp will be used).
434 +//
435 +// NOTE: This API is EXPERIMENTAL and may be changed or removed.
436 +//
437 +// TODO: See if we can find a more elegant solution closer to the new NetworkAddress.Listen API.
438 +func ListenQUIC(ln net.PacketConn, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
439 + lnKey := listenerKey(ln.LocalAddr().Network(), ln.LocalAddr().String())
440 +
441 + sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
442 + earlyLn, err := quic.ListenEarly(ln, http3.ConfigureTLSConfig(tlsConf), &quic.Config{
443 + RequireAddressValidation: func(clientAddr net.Addr) bool {
444 + var highLoad bool
445 + if activeRequests != nil {
446 + highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
447 + }
448 + return highLoad
449 + },
450 + })
451 + if err != nil {
452 + return nil, err
453 + }
454 +
455 + return &sharedQuicListener{EarlyListener: earlyLn, key: lnKey}, nil
456 + })
457 + if err != nil {
458 + return nil, err
459 + }
460 +
461 + // TODO: to serve QUIC over a unix socket, currently we need to hold onto
462 + // the underlying net.PacketConn (which we wrap as unixConn to keep count
463 + // of closes) because closing the quic.EarlyListener doesn't actually close
464 + // the underlying PacketConn, but we need to for unix sockets since we dup
465 + // the file descriptor and thus need to close the original; track issue:
466 + // https://github.com/lucas-clemente/quic-go/issues/3560#issuecomment-1258959608
467 + var unix *unixConn
468 + if uc, ok := ln.(*unixConn); ok {
469 + unix = uc
470 + }
471 +
472 + ctx, cancel := context.WithCancel(context.Background())
473 + return &fakeCloseQuicListener{
474 + sharedQuicListener: sharedEarlyListener.(*sharedQuicListener),
475 + uc: unix,
476 + context: ctx,
477 + contextCancel: cancel,
478 + }, nil
479 +}
480 +
481 +// ListenerUsage returns the current usage count of the given listener address.
482 +func ListenerUsage(network, addr string) int {
483 + count, _ := listenerPool.References(listenerKey(network, addr))
484 + return count
485 +}
486 +
487 +// sharedQuicListener is like sharedListener, but for quic.EarlyListeners.
488 +type sharedQuicListener struct {
489 + quic.EarlyListener
490 + key string
491 +}
492 +
493 +// Destruct closes the underlying QUIC listener.
494 +func (sql *sharedQuicListener) Destruct() error {
495 + return sql.EarlyListener.Close()
496 +}
497 +
498 +// sharedPacketConn is like sharedListener, but for net.PacketConns.
499 +type sharedPacketConn struct {
500 + net.PacketConn
501 + key string
502 +}
503 +
504 +// Destruct closes the underlying socket.
505 +func (spc *sharedPacketConn) Destruct() error {
506 + return spc.PacketConn.Close()
507 +}
508 +
509 +// fakeClosedErr returns an error value that is not temporary
510 +// nor a timeout, suitable for making the caller think the
511 +// listener is actually closed
512 +func fakeClosedErr(l interface{ Addr() net.Addr }) error {
513 + return &net.OpError{
514 + Op: "accept",
515 + Net: l.Addr().Network(),
516 + Addr: l.Addr(),
517 + Err: errFakeClosed,
518 + }
519 +}
520 +
521 +// errFakeClosed is the underlying error value returned by
522 +// fakeCloseListener.Accept() after Close() has been called,
523 +// indicating that it is pretending to be closed so that the
524 +// server using it can terminate, while the underlying
525 +// socket is actually left open.
526 +var errFakeClosed = fmt.Errorf("listener 'closed' 😉")
527 +
528 +// fakeClosePacketConn is like fakeCloseListener, but for PacketConns.
529 +type fakeClosePacketConn struct {
530 + closed int32 // accessed atomically; belongs to this struct only
531 + *sharedPacketConn // embedded, so we also become a net.PacketConn
532 +}
533 +
534 +func (fcpc *fakeClosePacketConn) Close() error {
535 + if atomic.CompareAndSwapInt32(&fcpc.closed, 0, 1) {
536 + _, _ = listenerPool.Delete(fcpc.sharedPacketConn.key)
537 + }
538 + return nil
539 +}
540 +
541 +// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
542 +func (fcpc fakeClosePacketConn) SetReadBuffer(bytes int) error {
543 + if conn, ok := fcpc.PacketConn.(interface{ SetReadBuffer(int) error }); ok {
544 + return conn.SetReadBuffer(bytes)
545 + }
546 + return fmt.Errorf("SetReadBuffer() not implemented for %T", fcpc.PacketConn)
547 +}
548 +
549 +// Supports QUIC implementation: https://github.com/caddyserver/caddy/issues/3998
550 +func (fcpc fakeClosePacketConn) SyscallConn() (syscall.RawConn, error) {
551 + if conn, ok := fcpc.PacketConn.(interface {
552 + SyscallConn() (syscall.RawConn, error)
553 + }); ok {
554 + return conn.SyscallConn()
555 + }
556 + return nil, fmt.Errorf("SyscallConn() not implemented for %T", fcpc.PacketConn)
557 +}
558 +
559 +type fakeCloseQuicListener struct {
560 + closed int32 // accessed atomically; belongs to this struct only
561 + *sharedQuicListener // embedded, so we also become a quic.EarlyListener
562 + uc *unixConn // underlying unix socket, if UDS
563 + context context.Context
564 + contextCancel context.CancelFunc
565 +}
566 +
567 +// Currently Accept ignores the passed context, however a situation where
568 +// someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here)
569 +// server on which Accept would be called with non-empty contexts
570 +// (mind that the default net listeners' Accept doesn't take a context argument)
571 +// sounds way too rare for us to sacrifice efficiency here.
572 +func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (quic.EarlyConnection, error) {
573 + conn, err := fcql.sharedQuicListener.Accept(fcql.context)
574 + if err == nil {
575 + return conn, nil
576 + }
577 +
578 + // if the listener is "closed", return a fake closed error instead
579 + if atomic.LoadInt32(&fcql.closed) == 1 && errors.Is(err, context.Canceled) {
580 + return nil, fakeClosedErr(fcql)
581 + }
582 + return nil, err
583 +}
584 +
585 +func (fcql *fakeCloseQuicListener) Close() error {
586 + if atomic.CompareAndSwapInt32(&fcql.closed, 0, 1) {
587 + fcql.contextCancel()
588 + _, _ = listenerPool.Delete(fcql.sharedQuicListener.key)
589 + if fcql.uc != nil {
590 + // unix sockets need to be closed ourselves because we dup() the file
591 + // descriptor when we reuse them, so this avoids a resource leak
592 + fcql.uc.Close()
593 + }
594 + }
595 + return nil
596 +}
597 +
598 // RegisterNetwork registers a network type with Caddy so that if a listener is
599 // created for that network type, getListener will be invoked to get the listener.
600 // This should be called during init() and will panic if the network type is standard
@@ -460,11 +616,77 @@ func RegisterNetwork(network string, getListener ListenerFunc) {
616 networkTypes[network] = getListener
617 }
618
619 +type unixListener struct {
620 + *net.UnixListener
621 + mapKey string
622 + count *int32 // accessed atomically
623 +}
624 +
625 +func (uln *unixListener) Close() error {
626 + newCount := atomic.AddInt32(uln.count, -1)
627 + if newCount == 0 {
628 + defer func() {
629 + addr := uln.Addr().String()
630 + unixSocketsMu.Lock()
631 + delete(unixSockets, uln.mapKey)
632 + unixSocketsMu.Unlock()
633 + _ = syscall.Unlink(addr)
634 + }()
635 + }
636 + return uln.UnixListener.Close()
637 +}
638 +
639 +type unixConn struct {
640 + *net.UnixConn
641 + filename string
642 + mapKey string
643 + count *int32 // accessed atomically
644 +}
645 +
646 +func (uc *unixConn) Close() error {
647 + newCount := atomic.AddInt32(uc.count, -1)
648 + if newCount == 0 {
649 + defer func() {
650 + unixSocketsMu.Lock()
651 + delete(unixSockets, uc.mapKey)
652 + unixSocketsMu.Unlock()
653 + _ = syscall.Unlink(uc.filename)
654 + }()
655 + }
656 + return uc.UnixConn.Close()
657 +}
658 +
659 +// unixSockets keeps track of the currently-active unix sockets
660 +// so we can transfer their FDs gracefully during reloads.
661 +var (
662 + unixSockets = make(map[string]interface {
663 + File() (*os.File, error)
664 + })
665 + unixSocketsMu sync.Mutex
666 +)
667 +
668 +// getListenerFromPlugin returns a listener on the given network and address
669 +// if a plugin has registered the network name. It may return (nil, nil) if
670 +// no plugin can provide a listener.
671 +func getListenerFromPlugin(ctx context.Context, network, addr string, config net.ListenConfig) (any, error) {
672 + // get listener from plugin if network type is registered
673 + if getListener, ok := networkTypes[network]; ok {
674 + Log().Debug("getting listener from plugin", zap.String("network", network))
675 + return getListener(ctx, network, addr, config)
676 + }
677 +
678 + return nil, nil
679 +}
680 +
681 +func listenerKey(network, addr string) string {
682 + return network + "/" + addr
683 +}
684 +
685 // ListenerFunc is a function that can return a listener given a network and address.
686 // The listeners must be capable of overlapping: with Caddy, new configs are loaded
687 // before old ones are unloaded, so listeners may overlap briefly if the configs
688 // both need the same listener. EXPERIMENTAL and subject to change.
467 -type ListenerFunc func(network, addr string) (net.Listener, error)
689 +type ListenerFunc func(ctx context.Context, network, addr string, cfg net.ListenConfig) (any, error)
690
691 var networkTypes = map[string]ListenerFunc{}
692
modules/caddyhttp/app.go
+39 -9
@@ -18,6 +18,7 @@ import (
18 "context"
19 "crypto/tls"
20 "fmt"
21 + "net"
22 "net/http"
23 "strconv"
24 "sync"
@@ -387,10 +388,11 @@ func (app *App) Start() error {
388 for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
389 // create the listener for this socket
390 hostport := listenAddr.JoinHostPort(portOffset)
390 - ln, err := caddy.ListenTimeout(listenAddr.Network, hostport, time.Duration(srv.KeepAliveInterval))
391 + lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
392 if err != nil {
392 - return fmt.Errorf("%s: listening on %s: %v", listenAddr.Network, hostport, err)
393 + return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
394 }
395 + ln := lnAny.(net.Listener)
396
397 // wrap listener before TLS (up to the TLS placeholder wrapper)
398 var lnWrapperIdx int
@@ -409,10 +411,27 @@ func (app *App) Start() error {
411 ln = tls.NewListener(ln, tlsCfg)
412
413 // enable HTTP/3 if configured
412 - if srv.protocol("h3") && !listenAddr.IsUnixNetwork() {
413 - app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
414 - if err := srv.serveHTTP3(hostport, tlsCfg); err != nil {
415 - return err
414 + if srv.protocol("h3") {
415 + // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
416 + // a different transport mechanism... which is fine, but the OS doesn't
417 + // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
418 + // are still one file on the system. So even though "unixpacket" and
419 + // "unixgram" are different network types just as "tcp" and "udp" are,
420 + // the OS will not let us use the same file as both STREAM and DGRAM.
421 + if len(srv.Protocols) > 1 && listenAddr.IsUnixNetwork() {
422 + app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
423 + zap.String("file", hostport))
424 + for i := range srv.Protocols {
425 + if srv.Protocols[i] == "h3" {
426 + srv.Protocols = append(srv.Protocols[:i], srv.Protocols[i+1:]...)
427 + break
428 + }
429 + }
430 + } else {
431 + app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
432 + if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
433 + return err
434 + }
435 }
436 }
437 }
@@ -424,11 +443,10 @@ func (app *App) Start() error {
443
444 // if binding to port 0, the OS chooses a port for us;
445 // but the user won't know the port unless we print it
427 - if listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
446 + if !listenAddr.IsUnixNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
447 app.logger.Info("port 0 listener",
448 zap.String("input_address", lnAddr),
430 - zap.String("actual_address", ln.Addr().String()),
431 - )
449 + zap.String("actual_address", ln.Addr().String()))
450 }
451
452 app.logger.Debug("starting server loop",
@@ -533,6 +551,18 @@ func (app *App) Stop() error {
551 if server.h3server == nil {
552 return
553 }
554 +
555 + // TODO: we have to manually close our listeners because quic-go won't
556 + // close listeners it didn't create along with the server itself...
557 + // see https://github.com/lucas-clemente/quic-go/issues/3560
558 + for _, el := range server.h3listeners {
559 + if err := el.Close(); err != nil {
560 + app.logger.Error("HTTP/3 listener close",
561 + zap.Error(err),
562 + zap.String("address", el.LocalAddr().String()))
563 + }
564 + }
565 +
566 // TODO: CloseGracefully, once implemented upstream (see https://github.com/lucas-clemente/quic-go/issues/2103)
567 if err := server.h3server.Close(); err != nil {
568 app.logger.Error("HTTP/3 server shutdown",
modules/caddyhttp/matchers.go
+3 -1
@@ -156,7 +156,9 @@ type (
156 MatchHeaderRE map[string]*MatchRegexp
157
158 // MatchProtocol matches requests by protocol. Recognized values are
159 - // "http", "https", and "grpc".
159 + // "http", "https", and "grpc" for broad protocol matches, or specific
160 + // HTTP versions can be specified like so: "http/1", "http/1.1",
161 + // "http/2", "http/3", or minimum versions: "http/2+", etc.
162 MatchProtocol string
163
164 // MatchRemoteIP matches requests by client IP (or CIDR range).
modules/caddyhttp/server.go
+32 -8
@@ -170,9 +170,10 @@ type Server struct {
170 errorLogger *zap.Logger
171 ctx caddy.Context
172
173 - server *http.Server
174 - h3server *http3.Server
175 - addresses []caddy.NetworkAddress
173 + server *http.Server
174 + h3server *http3.Server
175 + h3listeners []net.PacketConn // TODO: we have to hold these because quic-go won't close listeners it didn't create
176 + addresses []caddy.NetworkAddress
177
178 shutdownAt time.Time
179 shutdownAtMu *sync.RWMutex
@@ -193,9 +194,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
194 atomic.AddInt64(&s.activeRequests, 1)
195 defer atomic.AddInt64(&s.activeRequests, -1)
196
196 - err := s.h3server.SetQuicHeaders(w.Header())
197 - if err != nil {
198 - s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
197 + if r.ProtoMajor < 3 {
198 + err := s.h3server.SetQuicHeaders(w.Header())
199 + if err != nil {
200 + s.logger.Error("setting HTTP/3 Alt-Svc header", zap.Error(err))
201 + }
202 }
203 }
204
@@ -493,8 +496,27 @@ func (s *Server) findLastRouteWithHostMatcher() int {
496 // serveHTTP3 creates a QUIC listener, configures an HTTP/3 server if
497 // not already done, and then uses that server to serve HTTP/3 over
498 // the listener, with Server s as the handler.
496 -func (s *Server) serveHTTP3(hostport string, tlsCfg *tls.Config) error {
497 - h3ln, err := caddy.ListenQUIC(hostport, tlsCfg, &s.activeRequests)
499 +func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error {
500 + switch addr.Network {
501 + case "unix":
502 + addr.Network = "unixgram"
503 + case "tcp":
504 + addr.Network = "udp"
505 + case "tcp4":
506 + addr.Network = "udp4"
507 + case "tcp6":
508 + addr.Network = "udp6"
509 + default:
510 + return fmt.Errorf("unsure what network to use for HTTP/3 given network type: %s", addr.Network)
511 + }
512 +
513 + lnAny, err := addr.Listen(s.ctx, 0, net.ListenConfig{})
514 + if err != nil {
515 + return err
516 + }
517 + ln := lnAny.(net.PacketConn)
518 +
519 + h3ln, err := caddy.ListenQUIC(ln, tlsCfg, &s.activeRequests)
520 if err != nil {
521 return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
522 }
@@ -512,6 +534,8 @@ func (s *Server) serveHTTP3(hostport string, tlsCfg *tls.Config) error {
534 }
535 }
536
537 + s.h3listeners = append(s.h3listeners, lnAny.(net.PacketConn))
538 +
539 //nolint:errcheck
540 go s.h3server.ServeListener(h3ln)
541