Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 10, 22:30 UTC
| 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 | package caddy |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "crypto/tls" |
| 20 | "errors" |
| 21 | "fmt" |
| 22 | "io" |
| 23 | "io/fs" |
| 24 | "net" |
| 25 | "net/netip" |
| 26 | "os" |
| 27 | "strconv" |
| 28 | "strings" |
| 29 | "sync" |
| 30 | "sync/atomic" |
| 31 | |
| 32 | "github.com/quic-go/quic-go" |
| 33 | "github.com/quic-go/quic-go/http3" |
| 34 | h3qlog "github.com/quic-go/quic-go/http3/qlog" |
| 35 | "go.uber.org/zap" |
| 36 | "golang.org/x/time/rate" |
| 37 | |
| 38 | "github.com/caddyserver/caddy/v2/internal" |
| 39 | ) |
| 40 | |
| 41 | // NetworkAddress represents one or more network addresses. |
| 42 | // It contains the individual components for a parsed network |
| 43 | // address of the form accepted by ParseNetworkAddress(). |
| 44 | type NetworkAddress struct { |
| 45 | // Should be a network value accepted by Go's net package or |
| 46 | // by a plugin providing a listener for that network type. |
| 47 | Network string |
| 48 | |
| 49 | // The "main" part of the network address is the host, which |
| 50 | // often takes the form of a hostname, DNS name, IP address, |
| 51 | // or socket path. |
| 52 | Host string |
| 53 | |
| 54 | // For addresses that contain a port, ranges are given by |
| 55 | // [StartPort, EndPort]; i.e. for a single port, StartPort |
| 56 | // and EndPort are the same. For no port, they are 0. |
| 57 | StartPort uint |
| 58 | EndPort uint |
| 59 | } |
| 60 | |
| 61 | // ListenAll calls Listen for all addresses represented by this struct, i.e. all ports in the range. |
| 62 | // (If the address doesn't use ports or has 1 port only, then only 1 listener will be created.) |
| 63 | // It returns an error if any listener failed to bind, and closes any listeners opened up to that point. |
| 64 | func (na NetworkAddress) ListenAll(ctx context.Context, config net.ListenConfig) ([]any, error) { |
| 65 | var listeners []any |
| 66 | var err error |
| 67 | |
| 68 | // if one of the addresses has a failure, we need to close |
| 69 | // any that did open a socket to avoid leaking resources |
| 70 | defer func() { |
| 71 | if err == nil { |
| 72 | return |
| 73 | } |
| 74 | for _, ln := range listeners { |
| 75 | if cl, ok := ln.(io.Closer); ok { |
| 76 | cl.Close() |
| 77 | } |
| 78 | } |
| 79 | }() |
| 80 | |
| 81 | // an address can contain a port range, which represents multiple addresses; |
| 82 | // some addresses don't use ports at all and have a port range size of 1; |
| 83 | // whatever the case, iterate each address represented and bind a socket |
| 84 | for portOffset := uint(0); portOffset < na.PortRangeSize(); portOffset++ { |
| 85 | select { |
| 86 | case <-ctx.Done(): |
| 87 | return nil, ctx.Err() |
| 88 | default: |
| 89 | } |
| 90 | |
| 91 | // create (or reuse) the listener ourselves |
| 92 | var ln any |
| 93 | ln, err = na.Listen(ctx, portOffset, config) |
| 94 | if err != nil { |
| 95 | return nil, err |
| 96 | } |
| 97 | listeners = append(listeners, ln) |
| 98 | } |
| 99 | |
| 100 | return listeners, nil |
| 101 | } |
| 102 | |
| 103 | // Listen is similar to net.Listen, with a few differences: |
| 104 | // |
| 105 | // Listen announces on the network address using the port calculated by adding |
| 106 | // portOffset to the start port. (For network types that do not use ports, the |
| 107 | // portOffset is ignored.) |
| 108 | // |
| 109 | // First Listen checks if a plugin can provide a listener from this address. Otherwise, |
| 110 | // the provided ListenConfig is used to create the listener. Its Control function, |
| 111 | // if set, may be wrapped by an internally-used Control function. The provided |
| 112 | // context may be used to cancel long operations early. The context is not used |
| 113 | // to close the listener after it has been created. |
| 114 | // |
| 115 | // Caddy's listeners can overlap each other: multiple listeners may be created on |
| 116 | // the same socket at the same time. This is useful because during config changes, |
| 117 | // the new config is started while the old config is still running. How this is |
| 118 | // accomplished varies by platform and network type. For example, on Unix, SO_REUSEPORT |
| 119 | // is set except on Unix sockets, for which the file descriptor is duplicated and |
| 120 | // reused; on Windows, the close logic is virtualized using timeouts. Like normal |
| 121 | // listeners, be sure to Close() them when you are done. |
| 122 | // |
| 123 | // This method returns any type, as the implementations of listeners for various |
| 124 | // network types are not interchangeable. The type of listener returned is switched |
| 125 | // on the network type. Stream-based networks ("tcp", "unix", "unixpacket", etc.) |
| 126 | // return a net.Listener; datagram-based networks ("udp", "unixgram", etc.) return |
| 127 | // a net.PacketConn; and so forth. The actual concrete types are not guaranteed to |
| 128 | // be standard, exported types (wrapping is necessary to provide graceful reloads). |
| 129 | // |
| 130 | // Unix sockets will be unlinked before being created, to ensure we can bind to |
| 131 | // it even if the previous program using it exited uncleanly; it will also be |
| 132 | // unlinked upon a graceful exit (or when a new config does not use that socket). |
| 133 | // Listen synchronizes binds to unix domain sockets to avoid race conditions |
| 134 | // while an existing socket is unlinked. |
| 135 | func (na NetworkAddress) Listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { |
| 136 | if na.IsUnixNetwork() { |
| 137 | unixSocketsMu.Lock() |
| 138 | defer unixSocketsMu.Unlock() |
| 139 | } |
| 140 | |
| 141 | // check to see if plugin provides listener |
| 142 | if ln, err := getListenerFromPlugin(ctx, na.Network, na.Host, na.port(), portOffset, config); ln != nil || err != nil { |
| 143 | return ln, err |
| 144 | } |
| 145 | |
| 146 | // create (or reuse) the listener ourselves |
| 147 | return na.listen(ctx, portOffset, config) |
| 148 | } |
| 149 | |
| 150 | func (na NetworkAddress) listen(ctx context.Context, portOffset uint, config net.ListenConfig) (any, error) { |
| 151 | var ( |
| 152 | ln any |
| 153 | err error |
| 154 | address string |
| 155 | unixFileMode fs.FileMode |
| 156 | ) |
| 157 | |
| 158 | // split unix socket addr early so lnKey |
| 159 | // is independent of permissions bits |
| 160 | if na.IsUnixNetwork() { |
| 161 | address, unixFileMode, err = internal.SplitUnixSocketPermissionsBits(na.Host) |
| 162 | if err != nil { |
| 163 | return nil, err |
| 164 | } |
| 165 | } else if na.IsFdNetwork() { |
| 166 | address = na.Host |
| 167 | } else { |
| 168 | address = na.JoinHostPort(portOffset) |
| 169 | } |
| 170 | |
| 171 | if strings.HasPrefix(na.Network, "ip") { |
| 172 | ln, err = config.ListenPacket(ctx, na.Network, address) |
| 173 | } else { |
| 174 | if na.IsUnixNetwork() { |
| 175 | // if this is a unix socket, see if we already have it open |
| 176 | ln, err = reuseUnixSocket(na.Network, address) |
| 177 | } |
| 178 | |
| 179 | if ln == nil && err == nil { |
| 180 | // otherwise, create a new listener |
| 181 | lnKey := listenerKey(na.Network, address) |
| 182 | ln, err = internal.WithBindCapability(na.Network, address, func() (any, error) { |
| 183 | return listenReusable(ctx, lnKey, na.Network, address, config) |
| 184 | }) |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | if err != nil { |
| 189 | return nil, err |
| 190 | } |
| 191 | |
| 192 | if ln == nil { |
| 193 | return nil, fmt.Errorf("unsupported network type: %s", na.Network) |
| 194 | } |
| 195 | |
| 196 | if IsUnixNetwork(na.Network) { |
| 197 | isAbstractUnixSocket := strings.HasPrefix(address, "@") |
| 198 | if !isAbstractUnixSocket { |
| 199 | err = os.Chmod(address, unixFileMode) |
| 200 | if err != nil { |
| 201 | return nil, fmt.Errorf("unable to set permissions (%s) on %s: %v", unixFileMode, address, err) |
| 202 | } |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | return ln, nil |
| 207 | } |
| 208 | |
| 209 | // IsUnixNetwork returns true if na.Network is |
| 210 | // unix, unixgram, or unixpacket. |
| 211 | func (na NetworkAddress) IsUnixNetwork() bool { |
| 212 | return IsUnixNetwork(na.Network) |
| 213 | } |
| 214 | |
| 215 | // IsFdNetwork returns true if na.Network is |
| 216 | // fd or fdgram. |
| 217 | func (na NetworkAddress) IsFdNetwork() bool { |
| 218 | return IsFdNetwork(na.Network) |
| 219 | } |
| 220 | |
| 221 | // JoinHostPort is like net.JoinHostPort, but where the port |
| 222 | // is StartPort + offset. |
| 223 | func (na NetworkAddress) JoinHostPort(offset uint) string { |
| 224 | if na.IsUnixNetwork() || na.IsFdNetwork() { |
| 225 | return na.Host |
| 226 | } |
| 227 | return net.JoinHostPort(na.Host, strconv.FormatUint(uint64(na.StartPort+offset), 10)) |
| 228 | } |
| 229 | |
| 230 | // Expand returns one NetworkAddress for each port in the port range. |
| 231 | func (na NetworkAddress) Expand() []NetworkAddress { |
| 232 | size := na.PortRangeSize() |
| 233 | addrs := make([]NetworkAddress, size) |
| 234 | for portOffset := range size { |
| 235 | addrs[portOffset] = na.At(portOffset) |
| 236 | } |
| 237 | return addrs |
| 238 | } |
| 239 | |
| 240 | // At returns a NetworkAddress with a port range of just 1 |
| 241 | // at the given port offset; i.e. a NetworkAddress that |
| 242 | // represents precisely 1 address only. |
| 243 | func (na NetworkAddress) At(portOffset uint) NetworkAddress { |
| 244 | na2 := na |
| 245 | na2.StartPort, na2.EndPort = na.StartPort+portOffset, na.StartPort+portOffset |
| 246 | return na2 |
| 247 | } |
| 248 | |
| 249 | // PortRangeSize returns how many ports are in |
| 250 | // pa's port range. Port ranges are inclusive, |
| 251 | // so the size is the difference of start and |
| 252 | // end ports plus one. |
| 253 | func (na NetworkAddress) PortRangeSize() uint { |
| 254 | if na.EndPort < na.StartPort { |
| 255 | return 0 |
| 256 | } |
| 257 | return (na.EndPort - na.StartPort) + 1 |
| 258 | } |
| 259 | |
| 260 | func (na NetworkAddress) isLoopback() bool { |
| 261 | if na.IsUnixNetwork() || na.IsFdNetwork() { |
| 262 | return true |
| 263 | } |
| 264 | if na.Host == "localhost" { |
| 265 | return true |
| 266 | } |
| 267 | if ip, err := netip.ParseAddr(na.Host); err == nil { |
| 268 | return ip.IsLoopback() |
| 269 | } |
| 270 | return false |
| 271 | } |
| 272 | |
| 273 | func (na NetworkAddress) isWildcardInterface() bool { |
| 274 | if na.Host == "" { |
| 275 | return true |
| 276 | } |
| 277 | if ip, err := netip.ParseAddr(na.Host); err == nil { |
| 278 | return ip.IsUnspecified() |
| 279 | } |
| 280 | return false |
| 281 | } |
| 282 | |
| 283 | func (na NetworkAddress) port() string { |
| 284 | if na.StartPort == na.EndPort { |
| 285 | return strconv.FormatUint(uint64(na.StartPort), 10) |
| 286 | } |
| 287 | return fmt.Sprintf("%d-%d", na.StartPort, na.EndPort) |
| 288 | } |
| 289 | |
| 290 | // String reconstructs the address string for human display. |
| 291 | // The output can be parsed by ParseNetworkAddress(). If the |
| 292 | // address is a unix socket, any non-zero port will be dropped. |
| 293 | func (na NetworkAddress) String() string { |
| 294 | if na.Network == "tcp" && (na.Host != "" || na.port() != "") { |
| 295 | na.Network = "" // omit default network value for brevity |
| 296 | } |
| 297 | return JoinNetworkAddress(na.Network, na.Host, na.port()) |
| 298 | } |
| 299 | |
| 300 | // IsUnixNetwork returns true if the netw is a unix network. |
| 301 | func IsUnixNetwork(netw string) bool { |
| 302 | return strings.HasPrefix(netw, "unix") |
| 303 | } |
| 304 | |
| 305 | // IsFdNetwork returns true if the netw is a fd network. |
| 306 | func IsFdNetwork(netw string) bool { |
| 307 | return strings.HasPrefix(netw, "fd") |
| 308 | } |
| 309 | |
| 310 | // ParseNetworkAddress parses addr into its individual |
| 311 | // components. The input string is expected to be of |
| 312 | // the form "network/host:port-range" where any part is |
| 313 | // optional. The default network, if unspecified, is tcp. |
| 314 | // Port ranges are inclusive. |
| 315 | // |
| 316 | // Network addresses are distinct from URLs and do not |
| 317 | // use URL syntax. |
| 318 | func ParseNetworkAddress(addr string) (NetworkAddress, error) { |
| 319 | return ParseNetworkAddressWithDefaults(addr, "tcp", 0) |
| 320 | } |
| 321 | |
| 322 | // ParseNetworkAddressWithDefaults is like ParseNetworkAddress but allows |
| 323 | // the default network and port to be specified. |
| 324 | func ParseNetworkAddressWithDefaults(addr, defaultNetwork string, defaultPort uint) (NetworkAddress, error) { |
| 325 | var host, port string |
| 326 | network, host, port, err := SplitNetworkAddress(addr) |
| 327 | if err != nil { |
| 328 | return NetworkAddress{}, err |
| 329 | } |
| 330 | if network == "" { |
| 331 | network = defaultNetwork |
| 332 | } |
| 333 | if IsUnixNetwork(network) { |
| 334 | _, _, err := internal.SplitUnixSocketPermissionsBits(host) |
| 335 | return NetworkAddress{ |
| 336 | Network: network, |
| 337 | Host: host, |
| 338 | }, err |
| 339 | } |
| 340 | if IsFdNetwork(network) { |
| 341 | return NetworkAddress{ |
| 342 | Network: network, |
| 343 | Host: host, |
| 344 | }, nil |
| 345 | } |
| 346 | var start, end uint64 |
| 347 | if port == "" { |
| 348 | start = uint64(defaultPort) |
| 349 | end = uint64(defaultPort) |
| 350 | } else { |
| 351 | if len(port) > maxPortLen { |
| 352 | return NetworkAddress{}, fmt.Errorf("invalid port: longer than %d characters", maxPortLen) |
| 353 | } |
| 354 | before, after, found := strings.Cut(port, "-") |
| 355 | if !found { |
| 356 | after = before |
| 357 | } |
| 358 | start, err = strconv.ParseUint(before, 10, 16) |
| 359 | if err != nil { |
| 360 | return NetworkAddress{}, fmt.Errorf("invalid start port: %v", err) |
| 361 | } |
| 362 | end, err = strconv.ParseUint(after, 10, 16) |
| 363 | if err != nil { |
| 364 | return NetworkAddress{}, fmt.Errorf("invalid end port: %v", err) |
| 365 | } |
| 366 | if end < start { |
| 367 | return NetworkAddress{}, fmt.Errorf("end port must not be less than start port") |
| 368 | } |
| 369 | if (end-start)+1 > maxPortSpan { |
| 370 | return NetworkAddress{}, fmt.Errorf("port range exceeds %d ports", maxPortSpan) |
| 371 | } |
| 372 | } |
| 373 | return NetworkAddress{ |
| 374 | Network: network, |
| 375 | Host: host, |
| 376 | StartPort: uint(start), |
| 377 | EndPort: uint(end), |
| 378 | }, nil |
| 379 | } |
| 380 | |
| 381 | // SplitNetworkAddress splits a into its network, host, and port components. |
| 382 | // Note that port may be a port range (:X-Y), or omitted for unix sockets. |
| 383 | func SplitNetworkAddress(a string) (network, host, port string, err error) { |
| 384 | beforeSlash, afterSlash, slashFound := strings.Cut(a, "/") |
| 385 | if slashFound { |
| 386 | network = strings.ToLower(strings.TrimSpace(beforeSlash)) |
| 387 | a = afterSlash |
| 388 | if IsUnixNetwork(network) || IsFdNetwork(network) { |
| 389 | host = a |
| 390 | return network, host, port, err |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | host, port, err = net.SplitHostPort(a) |
| 395 | firstErr := err |
| 396 | |
| 397 | if err != nil { |
| 398 | // in general, if there was an error, it was likely "missing port", |
| 399 | // so try removing square brackets around an IPv6 host, adding a bogus |
| 400 | // port to take advantage of standard library's robust parser, then |
| 401 | // strip the artificial port. |
| 402 | host, _, err = net.SplitHostPort(net.JoinHostPort(strings.Trim(a, "[]"), "0")) |
| 403 | port = "" |
| 404 | } |
| 405 | |
| 406 | if err != nil { |
| 407 | err = errors.Join(firstErr, err) |
| 408 | } |
| 409 | |
| 410 | return network, host, port, err |
| 411 | } |
| 412 | |
| 413 | // JoinNetworkAddress combines network, host, and port into a single |
| 414 | // address string of the form accepted by ParseNetworkAddress(). For |
| 415 | // unix sockets, the network should be "unix" (or "unixgram" or |
| 416 | // "unixpacket") and the path to the socket should be given as the |
| 417 | // host parameter. |
| 418 | func JoinNetworkAddress(network, host, port string) string { |
| 419 | var a string |
| 420 | if network != "" { |
| 421 | a = network + "/" |
| 422 | } |
| 423 | if (host != "" && port == "") || IsUnixNetwork(network) || IsFdNetwork(network) { |
| 424 | a += host |
| 425 | } else if port != "" { |
| 426 | a += net.JoinHostPort(host, port) |
| 427 | } |
| 428 | return a |
| 429 | } |
| 430 | |
| 431 | // ListenQUICOptions holds optional parameters for ListenQUIC. |
| 432 | // A zero value matches historical defaults: 0-RTT is allowed, |
| 433 | // WebTransport QUIC capabilities are off, and no packet-conn |
| 434 | // wrappers are applied. |
| 435 | // |
| 436 | // NOTE: This API is EXPERIMENTAL and may be changed or removed. |
| 437 | type ListenQUICOptions struct { |
| 438 | TLSConfig *tls.Config |
| 439 | PacketConnWrappers []PacketConnWrapper |
| 440 | Allow0RTT *bool |
| 441 | EnableWebTransport bool |
| 442 | } |
| 443 | |
| 444 | // ListenQUIC returns a http3.QUICEarlyListener suitable for use in a Caddy module. |
| 445 | // |
| 446 | // The network will be transformed into a QUIC-compatible type if the same address can be used with |
| 447 | // different networks. Currently this just means that for tcp, udp will be used with the same |
| 448 | // address instead. |
| 449 | // |
| 450 | // ctx, portOffset, and config match Listen. Additional QUIC-specific |
| 451 | // parameters are passed in opts. |
| 452 | // |
| 453 | // The underlying listener is pooled per address: the first creator's configuration |
| 454 | // (Allow0RTT, WebTransport capabilities) wins; subsequent callers reuse that listener. |
| 455 | // |
| 456 | // NOTE: This API is EXPERIMENTAL and may be changed or removed. |
| 457 | // NOTE: user should close the returned listener twice, once to stop accepting new connections, the second time to free up the packet conn. |
| 458 | func (na NetworkAddress) ListenQUIC(ctx context.Context, portOffset uint, config net.ListenConfig, opts ListenQUICOptions) (http3.QUICListener, error) { |
| 459 | lnKey := listenerKey("quic"+na.Network, na.JoinHostPort(portOffset)) |
| 460 | |
| 461 | sharedEarlyListener, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) { |
| 462 | lnAny, err := na.Listen(ctx, portOffset, config) |
| 463 | if err != nil { |
| 464 | return nil, err |
| 465 | } |
| 466 | |
| 467 | ln := lnAny.(net.PacketConn) |
| 468 | |
| 469 | h3ln := ln |
| 470 | if len(opts.PacketConnWrappers) == 0 { |
| 471 | for { |
| 472 | // retrieve the underlying socket, so quic-go can optimize. |
| 473 | if unwrapper, ok := h3ln.(interface{ Unwrap() net.PacketConn }); ok { |
| 474 | h3ln = unwrapper.Unwrap() |
| 475 | } else { |
| 476 | break |
| 477 | } |
| 478 | } |
| 479 | } else { |
| 480 | // wrap packet conn before QUIC |
| 481 | for _, pcWrapper := range opts.PacketConnWrappers { |
| 482 | h3ln = pcWrapper.WrapPacketConn(h3ln) |
| 483 | } |
| 484 | } |
| 485 | |
| 486 | sqs := newSharedQUICState(opts.TLSConfig) |
| 487 | // http3.ConfigureTLSConfig only uses this field and tls App sets this field as well |
| 488 | //nolint:gosec |
| 489 | quicTlsConfig := &tls.Config{ |
| 490 | GetConfigForClient: sqs.getConfigForClient, |
| 491 | GetEncryptedClientHelloKeys: sqs.getEncryptedClientHelloKeys, |
| 492 | } |
| 493 | // Require clients to verify their source address when we're handling more than 1000 handshakes per second. |
| 494 | // TODO: make tunable? |
| 495 | limiter := rate.NewLimiter(1000, 1000) |
| 496 | tr := &quic.Transport{ |
| 497 | Conn: h3ln, |
| 498 | VerifySourceAddress: func(addr net.Addr) bool { return !limiter.Allow() }, |
| 499 | } |
| 500 | allow0rtt := true |
| 501 | if opts.Allow0RTT != nil { |
| 502 | allow0rtt = *opts.Allow0RTT |
| 503 | } |
| 504 | // EnableDatagrams and EnableStreamResetPartialDelivery are required by |
| 505 | // WebTransport; only enable them when WebTransport support is requested. |
| 506 | earlyLn, err := tr.ListenEarly( |
| 507 | http3.ConfigureTLSConfig(quicTlsConfig), |
| 508 | &quic.Config{ |
| 509 | InitialPacketSize: 1200, |
| 510 | Allow0RTT: allow0rtt, |
| 511 | Tracer: h3qlog.DefaultConnectionTracer, |
| 512 | EnableDatagrams: opts.EnableWebTransport, |
| 513 | EnableStreamResetPartialDelivery: opts.EnableWebTransport, |
| 514 | }, |
| 515 | ) |
| 516 | if err != nil { |
| 517 | return nil, err |
| 518 | } |
| 519 | // TODO: figure out when to close the listener and the transport |
| 520 | // using the original net.PacketConn to close them properly |
| 521 | return &sharedQuicListener{ |
| 522 | EarlyListener: earlyLn, |
| 523 | packetConn: ln, |
| 524 | sqs: sqs, |
| 525 | key: lnKey, |
| 526 | supportsWebTransport: opts.EnableWebTransport, |
| 527 | }, nil |
| 528 | }) |
| 529 | if err != nil { |
| 530 | return nil, err |
| 531 | } |
| 532 | |
| 533 | sql := sharedEarlyListener.(*sharedQuicListener) |
| 534 | // add current tls.Config to sqs, so GetConfigForClient will always return the latest tls.Config in case of context cancellation |
| 535 | ctx, cancel := sql.sqs.addState(opts.TLSConfig) |
| 536 | |
| 537 | return &fakeCloseQuicListener{ |
| 538 | sharedQuicListener: sql, |
| 539 | context: ctx, |
| 540 | contextCancel: cancel, |
| 541 | }, nil |
| 542 | } |
| 543 | |
| 544 | // ListenerUsage returns the current usage count of the given listener address. |
| 545 | func ListenerUsage(network, addr string) int { |
| 546 | count, _ := listenerPool.References(listenerKey(network, addr)) |
| 547 | return count |
| 548 | } |
| 549 | |
| 550 | // contextAndCancelFunc groups context and its cancelFunc |
| 551 | type contextAndCancelFunc struct { |
| 552 | context.Context |
| 553 | context.CancelCauseFunc |
| 554 | } |
| 555 | |
| 556 | // sharedQUICState manages GetConfigForClient |
| 557 | // see issue: https://github.com/caddyserver/caddy/pull/4849 |
| 558 | type sharedQUICState struct { |
| 559 | rmu sync.RWMutex |
| 560 | tlsConfs map[*tls.Config]contextAndCancelFunc |
| 561 | activeTlsConf *tls.Config |
| 562 | } |
| 563 | |
| 564 | // newSharedQUICState creates a new sharedQUICState |
| 565 | func newSharedQUICState(tlsConfig *tls.Config) *sharedQUICState { |
| 566 | sqtc := &sharedQUICState{ |
| 567 | tlsConfs: make(map[*tls.Config]contextAndCancelFunc), |
| 568 | activeTlsConf: tlsConfig, |
| 569 | } |
| 570 | sqtc.addState(tlsConfig) |
| 571 | return sqtc |
| 572 | } |
| 573 | |
| 574 | // getConfigForClient is used as tls.Config's GetConfigForClient field |
| 575 | func (sqs *sharedQUICState) getConfigForClient(ch *tls.ClientHelloInfo) (*tls.Config, error) { |
| 576 | sqs.rmu.RLock() |
| 577 | defer sqs.rmu.RUnlock() |
| 578 | return sqs.activeTlsConf.GetConfigForClient(ch) |
| 579 | } |
| 580 | |
| 581 | // getEncryptedClientHelloKeys is used as tls.Config's GetEncryptedClientHelloKeys field. |
| 582 | func (sqs *sharedQUICState) getEncryptedClientHelloKeys(ch *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) { |
| 583 | sqs.rmu.RLock() |
| 584 | defer sqs.rmu.RUnlock() |
| 585 | if sqs.activeTlsConf.GetEncryptedClientHelloKeys == nil { |
| 586 | return nil, nil |
| 587 | } |
| 588 | return sqs.activeTlsConf.GetEncryptedClientHelloKeys(ch) |
| 589 | } |
| 590 | |
| 591 | // addState adds tls.Config and activeRequests to the map if not present and returns the corresponding context and its cancelFunc |
| 592 | // so that when cancelled, the active tls.Config will change |
| 593 | func (sqs *sharedQUICState) addState(tlsConfig *tls.Config) (context.Context, context.CancelCauseFunc) { |
| 594 | sqs.rmu.Lock() |
| 595 | defer sqs.rmu.Unlock() |
| 596 | |
| 597 | if cacc, ok := sqs.tlsConfs[tlsConfig]; ok { |
| 598 | return cacc.Context, cacc.CancelCauseFunc |
| 599 | } |
| 600 | |
| 601 | ctx, cancel := context.WithCancelCause(context.Background()) |
| 602 | wrappedCancel := func(cause error) { |
| 603 | cancel(cause) |
| 604 | |
| 605 | sqs.rmu.Lock() |
| 606 | defer sqs.rmu.Unlock() |
| 607 | |
| 608 | delete(sqs.tlsConfs, tlsConfig) |
| 609 | if sqs.activeTlsConf == tlsConfig { |
| 610 | // select another tls.Config, if there is none, |
| 611 | // related sharedQuicListener will be destroyed anyway |
| 612 | for tc := range sqs.tlsConfs { |
| 613 | sqs.activeTlsConf = tc |
| 614 | break |
| 615 | } |
| 616 | } |
| 617 | } |
| 618 | sqs.tlsConfs[tlsConfig] = contextAndCancelFunc{ctx, wrappedCancel} |
| 619 | // there should be at most 2 tls.Configs |
| 620 | if len(sqs.tlsConfs) > 2 { |
| 621 | Log().Warn("quic listener tls configs are more than 2", zap.Int("number of configs", len(sqs.tlsConfs))) |
| 622 | } |
| 623 | return ctx, wrappedCancel |
| 624 | } |
| 625 | |
| 626 | // sharedQuicListener is like sharedListener, but for quic.EarlyListeners. |
| 627 | type sharedQuicListener struct { |
| 628 | *quic.EarlyListener |
| 629 | packetConn net.PacketConn // we have to hold these because quic-go won't close listeners it didn't create |
| 630 | sqs *sharedQUICState |
| 631 | key string |
| 632 | supportsWebTransport bool // set by the first creator; subsequent pool reuses keep this value |
| 633 | } |
| 634 | |
| 635 | // SupportsWebTransport reports whether the shared QUIC listener was created |
| 636 | // with WebTransport capabilities (DATAGRAM support and stream-reset partial |
| 637 | // delivery). The value is fixed by the first creator of a pooled listener. |
| 638 | func (sql *sharedQuicListener) SupportsWebTransport() bool { |
| 639 | return sql.supportsWebTransport |
| 640 | } |
| 641 | |
| 642 | // Destruct closes the underlying QUIC listener and its associated net.PacketConn. |
| 643 | func (sql *sharedQuicListener) Destruct() error { |
| 644 | // close EarlyListener first to stop any operations being done to the net.PacketConn |
| 645 | _ = sql.EarlyListener.Close() |
| 646 | // then close the net.PacketConn |
| 647 | return sql.packetConn.Close() |
| 648 | } |
| 649 | |
| 650 | // fakeClosedErr returns an error value that is not temporary |
| 651 | // nor a timeout, suitable for making the caller think the |
| 652 | // listener is actually closed |
| 653 | func fakeClosedErr(l interface{ Addr() net.Addr }) error { |
| 654 | return &net.OpError{ |
| 655 | Op: "accept", |
| 656 | Net: l.Addr().Network(), |
| 657 | Addr: l.Addr(), |
| 658 | Err: errFakeClosed, |
| 659 | } |
| 660 | } |
| 661 | |
| 662 | // errFakeClosed is the underlying error value returned by |
| 663 | // fakeCloseListener.Accept() after Close() has been called, |
| 664 | // indicating that it is pretending to be closed so that the |
| 665 | // server using it can terminate, while the underlying |
| 666 | // socket is actually left open. |
| 667 | var errFakeClosed = fmt.Errorf("QUIC listener 'closed' 😉") |
| 668 | |
| 669 | type fakeCloseQuicListener struct { |
| 670 | closed atomic.Int32 |
| 671 | *sharedQuicListener // embedded, so we also become a quic.EarlyListener |
| 672 | context context.Context |
| 673 | contextCancel context.CancelCauseFunc |
| 674 | } |
| 675 | |
| 676 | // Currently Accept ignores the passed context, however a situation where |
| 677 | // someone would need a hotswappable QUIC-only (not http3, since it uses context.Background here) |
| 678 | // server on which Accept would be called with non-empty contexts |
| 679 | // (mind that the default net listeners' Accept doesn't take a context argument) |
| 680 | // sounds way too rare for us to sacrifice efficiency here. |
| 681 | func (fcql *fakeCloseQuicListener) Accept(_ context.Context) (*quic.Conn, error) { |
| 682 | conn, err := fcql.sharedQuicListener.Accept(fcql.context) |
| 683 | if err == nil { |
| 684 | return conn, nil |
| 685 | } |
| 686 | |
| 687 | // if the listener is "closed", return a fake closed error instead |
| 688 | if fcql.closed.Load() == 1 && errors.Is(err, context.Canceled) { |
| 689 | return nil, fakeClosedErr(fcql) |
| 690 | } |
| 691 | return nil, err |
| 692 | } |
| 693 | |
| 694 | func (fcql *fakeCloseQuicListener) Close() error { |
| 695 | if fcql.closed.CompareAndSwap(0, 1) { |
| 696 | fcql.contextCancel(errFakeClosed) |
| 697 | } else if fcql.closed.CompareAndSwap(1, 2) { |
| 698 | _, _ = listenerPool.Delete(fcql.sharedQuicListener.key) |
| 699 | } |
| 700 | return nil |
| 701 | } |
| 702 | |
| 703 | // RegisterNetwork registers a network type with Caddy so that if a listener is |
| 704 | // created for that network type, getListener will be invoked to get the listener. |
| 705 | // This should be called during init() and will panic if the network type is standard |
| 706 | // or reserved, or if it is already registered. EXPERIMENTAL and subject to change. |
| 707 | func RegisterNetwork(network string, getListener ListenerFunc) { |
| 708 | network = strings.TrimSpace(strings.ToLower(network)) |
| 709 | |
| 710 | if network == "tcp" || network == "tcp4" || network == "tcp6" || |
| 711 | network == "udp" || network == "udp4" || network == "udp6" || |
| 712 | network == "unix" || network == "unixpacket" || network == "unixgram" || |
| 713 | strings.HasPrefix(network, "ip:") || strings.HasPrefix(network, "ip4:") || strings.HasPrefix(network, "ip6:") || |
| 714 | network == "fd" || network == "fdgram" { |
| 715 | panic("network type " + network + " is reserved") |
| 716 | } |
| 717 | |
| 718 | if _, ok := networkTypes[strings.ToLower(network)]; ok { |
| 719 | panic("network type " + network + " is already registered") |
| 720 | } |
| 721 | |
| 722 | networkTypes[network] = getListener |
| 723 | } |
| 724 | |
| 725 | var unixSocketsMu sync.Mutex |
| 726 | |
| 727 | // getListenerFromPlugin returns a listener on the given network and address |
| 728 | // if a plugin has registered the network name. It may return (nil, nil) if |
| 729 | // no plugin can provide a listener. |
| 730 | func getListenerFromPlugin(ctx context.Context, network, host, port string, portOffset uint, config net.ListenConfig) (any, error) { |
| 731 | // get listener from plugin if network type is registered |
| 732 | if getListener, ok := networkTypes[network]; ok { |
| 733 | Log().Debug("getting listener from plugin", zap.String("network", network)) |
| 734 | return getListener(ctx, network, host, port, portOffset, config) |
| 735 | } |
| 736 | |
| 737 | return nil, nil |
| 738 | } |
| 739 | |
| 740 | func listenerKey(network, addr string) string { |
| 741 | return network + "/" + addr |
| 742 | } |
| 743 | |
| 744 | // ListenerFunc is a function that can return a listener given a network and address. |
| 745 | // The listeners must be capable of overlapping: with Caddy, new configs are loaded |
| 746 | // before old ones are unloaded, so listeners may overlap briefly if the configs |
| 747 | // both need the same listener. EXPERIMENTAL and subject to change. |
| 748 | type ListenerFunc func(ctx context.Context, network, host, portRange string, portOffset uint, cfg net.ListenConfig) (any, error) |
| 749 | |
| 750 | var networkTypes = map[string]ListenerFunc{} |
| 751 | |
| 752 | // ListenerWrapper is a type that wraps a listener |
| 753 | // so it can modify the input listener's methods. |
| 754 | // Modules that implement this interface are found |
| 755 | // in the caddy.listeners namespace. Usually, to |
| 756 | // wrap a listener, you will define your own struct |
| 757 | // type that embeds the input listener, then |
| 758 | // implement your own methods that you want to wrap, |
| 759 | // calling the underlying listener's methods where |
| 760 | // appropriate. |
| 761 | type ListenerWrapper interface { |
| 762 | WrapListener(net.Listener) net.Listener |
| 763 | } |
| 764 | |
| 765 | // PacketConnWrapper is a type that wraps a packet conn |
| 766 | // so it can modify the input packet conn methods. |
| 767 | // Modules that implement this interface are found |
| 768 | // in the caddy.packetconns namespace. Usually, to |
| 769 | // wrap a packet conn, you will define your own struct |
| 770 | // type that embeds the input packet conn, then |
| 771 | // implement your own methods that you want to wrap, |
| 772 | // calling the underlying packet conn methods where |
| 773 | // appropriate. |
| 774 | type PacketConnWrapper interface { |
| 775 | WrapPacketConn(net.PacketConn) net.PacketConn |
| 776 | } |
| 777 | |
| 778 | // listenerPool stores and allows reuse of active listeners. |
| 779 | var listenerPool = NewUsagePool() |
| 780 | |
| 781 | const maxPortSpan = 65535 |
| 782 | |
| 783 | // maxPortLen is the length of the longest valid port range. |
| 784 | const maxPortLen = len("65535-65535") |