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
This commit is contained in:
Aaron Paterson 2024-09-30 12:55:03 -04:00 committed by GitHub
parent 1a345b4fa6
commit 4b1a9b6cc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 946 additions and 364 deletions

View file

@ -18,6 +18,7 @@ import (
"context"
"crypto/tls"
"fmt"
"maps"
"net"
"net/http"
"strconv"
@ -203,17 +204,75 @@ func (app *App) Provision(ctx caddy.Context) error {
}
}
// 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")) {
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"}
}
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 !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 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
// client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI
@ -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)
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)
protocols := srv.Protocols
if srv.ListenProtocols != nil && srv.ListenProtocols[lnIndex] != nil {
protocols = srv.ListenProtocols[lnIndex]
}
// 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)
}
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++ {
hostport := listenAddr.JoinHostPort(portOffset)
// 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
}
}
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))
}
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)
}
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))
srv.listeners = append(srv.listeners, ln)
// enable HTTP/1 if configured
if h1ok {
//nolint:errcheck
go srv.server.Serve(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 useTLS {
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.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))
srv.listeners = append(srv.listeners, 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))
}
}
}

View file

@ -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

View file

@ -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"
}
}

View file

@ -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

View file

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

View file

@ -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))