aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules
diff options
context:
space:
mode:
authorAaron Paterson <[email protected]>2024-09-30 12:55:03 -0400
committerGitHub <[email protected]>2024-09-30 10:55:03 -0600
commit4b1a9b6cc1aa521e21289afa276d29952a97d8f3 (patch)
treec4e28391860ed003aebaca340deb764b6f532e94 /modules
parent1a345b4fa620dfb0909a3b086bd76e35dfdbefa5 (diff)
downloadcaddy-4b1a9b6cc1aa521e21289afa276d29952a97d8f3.tar.gz
caddy-4b1a9b6cc1aa521e21289afa276d29952a97d8f3.zip
core: Implement socket activation listeners (#6573)
* caddy adapt for listen_protocols * adapt listen_socket * allow multiple listen sockets for port ranges and readd socket fd listen logic * readd logic to start servers according to listener protocols * gofmt * adapt caddytest * gosec * fmt and rename listen to listenWithSocket * fmt and rename listen to listenWithSocket * more consistent error msg * non unix listenReusableWithSocketFile * remove unused func * doc comment typo * nonosec * commit * doc comments * more doc comments * comment was misleading, cardinality did not change * addressesWithProtocols * update test * fd/ and fdgram/ * rm addr * actually write... * i guess we doin' "skip": now * wrong var in placeholder * wrong var in placeholder II * update param name in comment * dont save nil file pointers * windows * key -> parsedKey * osx * multiple default_bind with protocols * check for h1 and h2 listener netw
Diffstat (limited to 'modules')
-rw-r--r--modules/caddyhttp/app.go232
-rw-r--r--modules/caddyhttp/proxyprotocol/listenerwrapper.go2
-rw-r--r--modules/caddyhttp/reverseproxy/addresses.go2
-rw-r--r--modules/caddyhttp/reverseproxy/healthchecks.go4
-rw-r--r--modules/caddyhttp/server.go44
-rw-r--r--modules/caddyhttp/staticresp.go2
6 files changed, 195 insertions, 91 deletions
diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go
index 5dbecf9b2..673ebcb8e 100644
--- a/modules/caddyhttp/app.go
+++ b/modules/caddyhttp/app.go
@@ -18,6 +18,7 @@ import (
"context"
"crypto/tls"
"fmt"
+ "maps"
"net"
"net/http"
"strconv"
@@ -203,15 +204,73 @@ func (app *App) Provision(ctx caddy.Context) error {
}
}
+ // if no protocols configured explicitly, enable all except h2c
+ if len(srv.Protocols) == 0 {
+ srv.Protocols = []string{"h1", "h2", "h3"}
+ }
+
+ srvProtocolsUnique := map[string]struct{}{}
+ for _, srvProtocol := range srv.Protocols {
+ srvProtocolsUnique[srvProtocol] = struct{}{}
+ }
+ _, h1ok := srvProtocolsUnique["h1"]
+ _, h2ok := srvProtocolsUnique["h2"]
+ _, h2cok := srvProtocolsUnique["h2c"]
+
// the Go standard library does not let us serve only HTTP/2 using
// http.Server; we would probably need to write our own server
- if !srv.protocol("h1") && (srv.protocol("h2") || srv.protocol("h2c")) {
+ if !h1ok && (h2ok || h2cok) {
return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
}
- // if no protocols configured explicitly, enable all except h2c
- if len(srv.Protocols) == 0 {
- srv.Protocols = []string{"h1", "h2", "h3"}
+ if srv.ListenProtocols != nil {
+ if len(srv.ListenProtocols) != len(srv.Listen) {
+ return fmt.Errorf("server %s: listener protocols count does not match address count: %d != %d",
+ srvName, len(srv.ListenProtocols), len(srv.Listen))
+ }
+
+ for i, lnProtocols := range srv.ListenProtocols {
+ if lnProtocols != nil {
+ // populate empty listen protocols with server protocols
+ lnProtocolsDefault := false
+ var lnProtocolsInclude []string
+ srvProtocolsInclude := maps.Clone(srvProtocolsUnique)
+
+ // keep existing listener protocols unless they are empty
+ for _, lnProtocol := range lnProtocols {
+ if lnProtocol == "" {
+ lnProtocolsDefault = true
+ } else {
+ lnProtocolsInclude = append(lnProtocolsInclude, lnProtocol)
+ delete(srvProtocolsInclude, lnProtocol)
+ }
+ }
+
+ // append server protocols to listener protocols if any listener protocols were empty
+ if lnProtocolsDefault {
+ for _, srvProtocol := range srv.Protocols {
+ if _, ok := srvProtocolsInclude[srvProtocol]; ok {
+ lnProtocolsInclude = append(lnProtocolsInclude, srvProtocol)
+ }
+ }
+ }
+
+ lnProtocolsIncludeUnique := map[string]struct{}{}
+ for _, lnProtocol := range lnProtocolsInclude {
+ lnProtocolsIncludeUnique[lnProtocol] = struct{}{}
+ }
+ _, h1ok := lnProtocolsIncludeUnique["h1"]
+ _, h2ok := lnProtocolsIncludeUnique["h2"]
+ _, h2cok := lnProtocolsIncludeUnique["h2c"]
+
+ // check if any listener protocols contain h2 or h2c without h1
+ if !h1ok && (h2ok || h2cok) {
+ return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i)
+ }
+
+ srv.ListenProtocols[i] = lnProtocolsInclude
+ }
+ }
}
// if not explicitly configured by the user, disallow TLS
@@ -344,7 +403,7 @@ func (app *App) Validate() error {
// check that every address in the port range is unique to this server;
// we do not use <= here because PortRangeSize() adds 1 to EndPort for us
for i := uint(0); i < listenAddr.PortRangeSize(); i++ {
- addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.Itoa(int(listenAddr.StartPort+i)))
+ addr := caddy.JoinNetworkAddress(listenAddr.Network, listenAddr.Host, strconv.FormatUint(uint64(listenAddr.StartPort+i), 10))
if sn, ok := lnAddrs[addr]; ok {
return fmt.Errorf("server %s: listener address repeated: %s (already claimed by server '%s')", srvName, addr, sn)
}
@@ -422,99 +481,118 @@ func (app *App) Start() error {
srv.server.Handler = h2c.NewHandler(srv, h2server)
}
- for _, lnAddr := range srv.Listen {
+ for lnIndex, lnAddr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
return fmt.Errorf("%s: parsing listen address '%s': %v", srvName, lnAddr, err)
}
+
srv.addresses = append(srv.addresses, listenAddr)
+ protocols := srv.Protocols
+ if srv.ListenProtocols != nil && srv.ListenProtocols[lnIndex] != nil {
+ protocols = srv.ListenProtocols[lnIndex]
+ }
+
+ protocolsUnique := map[string]struct{}{}
+ for _, protocol := range protocols {
+ protocolsUnique[protocol] = struct{}{}
+ }
+ _, h1ok := protocolsUnique["h1"]
+ _, h2ok := protocolsUnique["h2"]
+ _, h2cok := protocolsUnique["h2c"]
+ _, h3ok := protocolsUnique["h3"]
+
for portOffset := uint(0); portOffset < listenAddr.PortRangeSize(); portOffset++ {
- // create the listener for this socket
hostport := listenAddr.JoinHostPort(portOffset)
- lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
- if err != nil {
- return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
- }
- ln := lnAny.(net.Listener)
-
- // wrap listener before TLS (up to the TLS placeholder wrapper)
- var lnWrapperIdx int
- for i, lnWrapper := range srv.listenerWrappers {
- if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok {
- lnWrapperIdx = i + 1 // mark the next wrapper's spot
- break
- }
- ln = lnWrapper.WrapListener(ln)
- }
// enable TLS if there is a policy and if this is not the HTTP port
useTLS := len(srv.TLSConnPolicies) > 0 && int(listenAddr.StartPort+portOffset) != app.httpPort()
- if useTLS {
- // create TLS listener - this enables and terminates TLS
- ln = tls.NewListener(ln, tlsCfg)
-
- // enable HTTP/3 if configured
- if srv.protocol("h3") {
- // Can't serve HTTP/3 on the same socket as HTTP/1 and 2 because it uses
- // a different transport mechanism... which is fine, but the OS doesn't
- // differentiate between a SOCK_STREAM file and a SOCK_DGRAM file; they
- // are still one file on the system. So even though "unixpacket" and
- // "unixgram" are different network types just as "tcp" and "udp" are,
- // the OS will not let us use the same file as both STREAM and DGRAM.
- if len(srv.Protocols) > 1 && listenAddr.IsUnixNetwork() {
- app.logger.Warn("HTTP/3 disabled because Unix can't multiplex STREAM and DGRAM on same socket",
- zap.String("file", hostport))
- for i := range srv.Protocols {
- if srv.Protocols[i] == "h3" {
- srv.Protocols = append(srv.Protocols[:i], srv.Protocols[i+1:]...)
- break
- }
- }
- } else {
- app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
- if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
- return err
- }
- }
+
+ // enable HTTP/3 if configured
+ if h3ok && useTLS {
+ app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
+ if err := srv.serveHTTP3(listenAddr.At(portOffset), tlsCfg); err != nil {
+ return err
}
}
- // finish wrapping listener where we left off before TLS
- for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
- ln = srv.listenerWrappers[i].WrapListener(ln)
+ if h3ok && !useTLS {
+ // Can only serve h3 with TLS enabled
+ app.logger.Warn("HTTP/3 skipped because it requires TLS",
+ zap.String("network", listenAddr.Network),
+ zap.String("addr", hostport))
}
- // handle http2 if use tls listener wrapper
- if useTLS {
- http2lnWrapper := &http2Listener{
- Listener: ln,
- server: srv.server,
- h2server: h2server,
+ if h1ok || h2ok && useTLS || h2cok {
+ // create the listener for this socket
+ lnAny, err := listenAddr.Listen(app.ctx, portOffset, net.ListenConfig{KeepAlive: time.Duration(srv.KeepAliveInterval)})
+ if err != nil {
+ return fmt.Errorf("listening on %s: %v", listenAddr.At(portOffset), err)
+ }
+ ln, ok := lnAny.(net.Listener)
+ if !ok {
+ return fmt.Errorf("network '%s' cannot handle HTTP/1 or HTTP/2 connections", listenAddr.Network)
}
- srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
- ln = http2lnWrapper
- }
- // if binding to port 0, the OS chooses a port for us;
- // but the user won't know the port unless we print it
- if !listenAddr.IsUnixNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
- app.logger.Info("port 0 listener",
- zap.String("input_address", lnAddr),
- zap.String("actual_address", ln.Addr().String()))
- }
+ if useTLS {
+ // create TLS listener - this enables and terminates TLS
+ ln = tls.NewListener(ln, tlsCfg)
+ }
+
+ // wrap listener before TLS (up to the TLS placeholder wrapper)
+ var lnWrapperIdx int
+ for i, lnWrapper := range srv.listenerWrappers {
+ if _, ok := lnWrapper.(*tlsPlaceholderWrapper); ok {
+ lnWrapperIdx = i + 1 // mark the next wrapper's spot
+ break
+ }
+ ln = lnWrapper.WrapListener(ln)
+ }
+
+ // finish wrapping listener where we left off before TLS
+ for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
+ ln = srv.listenerWrappers[i].WrapListener(ln)
+ }
+
+ // handle http2 if use tls listener wrapper
+ if h2ok {
+ http2lnWrapper := &http2Listener{
+ Listener: ln,
+ server: srv.server,
+ h2server: h2server,
+ }
+ srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
+ ln = http2lnWrapper
+ }
+
+ // if binding to port 0, the OS chooses a port for us;
+ // but the user won't know the port unless we print it
+ if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() && listenAddr.StartPort == 0 && listenAddr.EndPort == 0 {
+ app.logger.Info("port 0 listener",
+ zap.String("input_address", lnAddr),
+ zap.String("actual_address", ln.Addr().String()))
+ }
- app.logger.Debug("starting server loop",
- zap.String("address", ln.Addr().String()),
- zap.Bool("tls", useTLS),
- zap.Bool("http3", srv.h3server != nil))
+ app.logger.Debug("starting server loop",
+ zap.String("address", ln.Addr().String()),
+ zap.Bool("tls", useTLS),
+ zap.Bool("http3", srv.h3server != nil))
- srv.listeners = append(srv.listeners, ln)
+ srv.listeners = append(srv.listeners, ln)
+
+ // enable HTTP/1 if configured
+ if h1ok {
+ //nolint:errcheck
+ go srv.server.Serve(ln)
+ }
+ }
- // enable HTTP/1 if configured
- if srv.protocol("h1") {
- //nolint:errcheck
- go srv.server.Serve(ln)
+ if h2ok && !useTLS {
+ // Can only serve h2 with TLS enabled
+ app.logger.Warn("HTTP/2 skipped because it requires TLS",
+ zap.String("network", listenAddr.Network),
+ zap.String("addr", hostport))
}
}
}
diff --git a/modules/caddyhttp/proxyprotocol/listenerwrapper.go b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
index 440e70710..f5f2099ce 100644
--- a/modules/caddyhttp/proxyprotocol/listenerwrapper.go
+++ b/modules/caddyhttp/proxyprotocol/listenerwrapper.go
@@ -72,7 +72,7 @@ func (pp *ListenerWrapper) Provision(ctx caddy.Context) error {
pp.policy = func(options goproxy.ConnPolicyOptions) (goproxy.Policy, error) {
// trust unix sockets
- if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) {
+ if network := options.Upstream.Network(); caddy.IsUnixNetwork(network) || caddy.IsFdNetwork(network) {
return goproxy.USE, nil
}
ret := pp.FallbackPolicy
diff --git a/modules/caddyhttp/reverseproxy/addresses.go b/modules/caddyhttp/reverseproxy/addresses.go
index 82c1c7994..31f4aeb35 100644
--- a/modules/caddyhttp/reverseproxy/addresses.go
+++ b/modules/caddyhttp/reverseproxy/addresses.go
@@ -137,7 +137,7 @@ func parseUpstreamDialAddress(upstreamAddr string) (parsedAddr, error) {
}
// we can assume a port if only a hostname is specified, but use of a
// placeholder without a port likely means a port will be filled in
- if port == "" && !strings.Contains(host, "{") && !caddy.IsUnixNetwork(network) {
+ if port == "" && !strings.Contains(host, "{") && !caddy.IsUnixNetwork(network) && !caddy.IsFdNetwork(network) {
port = "80"
}
}
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index 319cc9248..1735e45a4 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -330,7 +330,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
return
}
if hcp := uint(upstream.activeHealthCheckPort); hcp != 0 {
- if addr.IsUnixNetwork() {
+ if addr.IsUnixNetwork() || addr.IsFdNetwork() {
addr.Network = "tcp" // I guess we just assume TCP since we are using a port??
}
addr.StartPort, addr.EndPort = hcp, hcp
@@ -345,7 +345,7 @@ func (h *Handler) doActiveHealthCheckForAllHosts() {
}
hostAddr := addr.JoinHostPort(0)
dialAddr := hostAddr
- if addr.IsUnixNetwork() {
+ if addr.IsUnixNetwork() || addr.IsFdNetwork() {
// this will be used as the Host portion of a http.Request URL, and
// paths to socket files would produce an error when creating URL,
// so use a fake Host value instead; unix sockets are usually local
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index f5478cb37..5aa7e0f63 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -220,6 +220,10 @@ type Server struct {
// Default: `[h1 h2 h3]`
Protocols []string `json:"protocols,omitempty"`
+ // ListenProtocols overrides Protocols for each parallel address in Listen.
+ // A nil value or element indicates that Protocols will be used instead.
+ ListenProtocols [][]string `json:"listen_protocols,omitempty"`
+
// If set, metrics observations will be enabled.
// This setting is EXPERIMENTAL and subject to change.
Metrics *Metrics `json:"metrics,omitempty"`
@@ -597,7 +601,11 @@ func (s *Server) findLastRouteWithHostMatcher() int {
// not already done, and then uses that server to serve HTTP/3 over
// the listener, with Server s as the handler.
func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error {
- addr.Network = getHTTP3Network(addr.Network)
+ h3net, err := getHTTP3Network(addr.Network)
+ if err != nil {
+ return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
+ }
+ addr.Network = h3net
h3ln, err := addr.ListenQUIC(s.ctx, 0, net.ListenConfig{}, tlsCfg)
if err != nil {
return fmt.Errorf("starting HTTP/3 QUIC listener: %v", err)
@@ -849,7 +857,21 @@ func (s *Server) logRequest(
// protocol returns true if the protocol proto is configured/enabled.
func (s *Server) protocol(proto string) bool {
- return slices.Contains(s.Protocols, proto)
+ if s.ListenProtocols == nil {
+ if slices.Contains(s.Protocols, proto) {
+ return true
+ }
+ } else {
+ for _, lnProtocols := range s.ListenProtocols {
+ for _, lnProtocol := range lnProtocols {
+ if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
+ return true
+ }
+ }
+ }
+ }
+
+ return false
}
// Listeners returns the server's listeners. These are active listeners,
@@ -1089,9 +1111,14 @@ const (
)
var networkTypesHTTP3 = map[string]string{
- "unix": "unixgram",
- "tcp4": "udp4",
- "tcp6": "udp6",
+ "unixgram": "unixgram",
+ "udp": "udp",
+ "udp4": "udp4",
+ "udp6": "udp6",
+ "tcp": "udp",
+ "tcp4": "udp4",
+ "tcp6": "udp6",
+ "fdgram": "fdgram",
}
// RegisterNetworkHTTP3 registers a mapping from non-HTTP/3 network to HTTP/3
@@ -1106,11 +1133,10 @@ func RegisterNetworkHTTP3(originalNetwork, h3Network string) {
networkTypesHTTP3[originalNetwork] = h3Network
}
-func getHTTP3Network(originalNetwork string) string {
+func getHTTP3Network(originalNetwork string) (string, error) {
h3Network, ok := networkTypesHTTP3[strings.ToLower(originalNetwork)]
if !ok {
- // TODO: Maybe a better default is to not enable HTTP/3 if we do not know the network?
- return "udp"
+ return "", fmt.Errorf("network '%s' cannot handle HTTP/3 connections", originalNetwork)
}
- return h3Network
+ return h3Network, nil
}
diff --git a/modules/caddyhttp/staticresp.go b/modules/caddyhttp/staticresp.go
index 1fea6978f..1b93ede4b 100644
--- a/modules/caddyhttp/staticresp.go
+++ b/modules/caddyhttp/staticresp.go
@@ -387,7 +387,7 @@ func cmdRespond(fl caddycmd.Flags) (int, error) {
return caddy.ExitCodeFailedStartup, err
}
- if !listenAddr.IsUnixNetwork() {
+ if !listenAddr.IsUnixNetwork() && !listenAddr.IsFdNetwork() {
listenAddrs := make([]string, 0, listenAddr.PortRangeSize())
for offset := uint(0); offset < listenAddr.PortRangeSize(); offset++ {
listenAddrs = append(listenAddrs, listenAddr.JoinHostPort(offset))