Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

caddyhttp: Enable HTTP/3 by default #4707

Merged
merged 8 commits into from
Aug 15, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 37 additions & 21 deletions caddyconfig/httpcaddyfile/serveroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ type serverOptions struct {
WriteTimeout caddy.Duration
IdleTimeout caddy.Duration
MaxHeaderBytes int
AllowH2C bool
ExperimentalHTTP3 bool
Protocols []string
StrictSNIHost *bool
ShouldLogCredentials bool
}
Expand Down Expand Up @@ -141,22 +140,51 @@ func unmarshalCaddyfileServerOptions(d *caddyfile.Dispenser) (any, error) {
}
serverOpts.ShouldLogCredentials = true

case "protocols":
protos := d.RemainingArgs()
for _, proto := range protos {
if proto != "h1" && proto != "h2" && proto != "h2c" && proto != "h3" {
francislavoie marked this conversation as resolved.
Show resolved Hide resolved
return nil, d.Errf("unknown protocol '%s': expected h1, h2, h2c, or h3", proto)
}
if sliceContains(serverOpts.Protocols, proto) {
return nil, d.Errf("protocol %s specified more than once", proto)
}
serverOpts.Protocols = append(serverOpts.Protocols, proto)
}
if d.NextBlock(0) {
return nil, d.ArgErr()
}

case "strict_sni_host":
if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
boolVal := true
if d.Val() == "insecure_off" {
boolVal = false
}
serverOpts.StrictSNIHost = &boolVal

// TODO: DEPRECATED. (August 2022)
case "protocol":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol sub-option will be removed soon")

for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "allow_h2c":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.AllowH2C = true
caddy.Log().Named("caddyfile").Warn("DEPRECATED: allow_h2c will be removed soon; use protocols option instead")

case "experimental_http3":
if d.NextArg() {
return nil, d.ArgErr()
}
serverOpts.ExperimentalHTTP3 = true
if sliceContains(serverOpts.Protocols, "h2c") {
return nil, d.Errf("protocol h2c already specified")
}
serverOpts.Protocols = append(serverOpts.Protocols, "h2c")

case "strict_sni_host":
caddy.Log().Named("caddyfile").Warn("DEPRECATED: protocol > strict_sni_host in this position will be removed soon; move up to the servers block instead")

if d.NextArg() && d.Val() != "insecure_off" && d.Val() != "on" {
return nil, d.Errf("strict_sni_host only supports 'on' or 'insecure_off', got '%s'", d.Val())
}
Expand Down Expand Up @@ -185,17 +213,6 @@ func applyServerOptions(
options map[string]any,
warnings *[]caddyconfig.Warning,
) error {
// If experimental HTTP/3 is enabled, enable it on each server.
// We already know there won't be a conflict with serverOptions because
// we validated earlier that "experimental_http3" cannot be set at the same
// time as "servers"
if enableH3, ok := options["experimental_http3"].(bool); ok && enableH3 {
*warnings = append(*warnings, caddyconfig.Warning{Message: "the 'experimental_http3' global option is deprecated, please use the 'servers > protocol > experimental_http3' option instead"})
for _, srv := range servers {
srv.ExperimentalHTTP3 = true
}
}

serverOpts, ok := options["servers"].([]serverOptions)
if !ok {
return nil
Expand Down Expand Up @@ -229,8 +246,7 @@ func applyServerOptions(
server.WriteTimeout = opts.WriteTimeout
server.IdleTimeout = opts.IdleTimeout
server.MaxHeaderBytes = opts.MaxHeaderBytes
server.AllowH2C = opts.AllowH2C
server.ExperimentalHTTP3 = opts.ExperimentalHTTP3
server.Protocols = opts.Protocols
server.StrictSNIHost = opts.StrictSNIHost
if opts.ShouldLogCredentials {
if server.Logs == nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@
}
max_header_size 100MB
log_credentials
protocol {
allow_h2c
experimental_http3
strict_sni_host
}
strict_sni_host
protocols h1 h2 h2c h3
}
}

Expand Down Expand Up @@ -61,8 +58,12 @@ foo.com {
"logs": {
"should_log_credentials": true
},
"experimental_http3": true,
"allow_h2c": true
"protocols": [
"h1",
"h2",
"h2c",
"h3"
]
}
}
}
Expand Down
19 changes: 17 additions & 2 deletions listeners.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,26 @@ func ListenPacket(network, addr string) (net.PacketConn, error) {
// ListenQUIC returns a quic.EarlyListener suitable for use in a Caddy module.
// Note that the context passed to Accept is currently ignored, so using
// a context other than context.Background is meaningless.
func ListenQUIC(addr string, tlsConf *tls.Config) (quic.EarlyListener, error) {
func ListenQUIC(addr string, tlsConf *tls.Config, activeRequests *int64) (quic.EarlyListener, error) {
lnKey := listenerKey("udp", addr)

sharedEl, _, err := listenerPool.LoadOrNew(lnKey, func() (Destructor, error) {
el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{})
el, err := quic.ListenAddrEarly(addr, http3.ConfigureTLSConfig(tlsConf), &quic.Config{
AcceptToken: func(clientAddr net.Addr, token *quic.Token) bool {
mholt marked this conversation as resolved.
Show resolved Hide resolved
var highLoad bool
if activeRequests != nil {
highLoad = atomic.LoadInt64(activeRequests) > 1000 // TODO: make tunable?
}
if !highLoad {
return true
}
if token == nil {
return false
}
// TODO: validate the token (can we just use quic.defaultAcceptToken? - it also does the nil check btw...)
return true
},
})
if err != nil {
return nil, err
}
Expand Down
84 changes: 55 additions & 29 deletions modules/caddyhttp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (

"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddytls"
"github.com/lucas-clemente/quic-go/http3"
"go.uber.org/zap"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
Expand Down Expand Up @@ -185,6 +184,17 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.accessLogger = app.logger.Named("log.access")
}

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

// if not explicitly configured by the user, disallow TLS
// client auth bypass (domain fronting) which could
// otherwise be exploited by sending an unprotected SNI
Expand All @@ -196,8 +206,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// based on hostname
if srv.StrictSNIHost == nil && srv.hasTLSClientAuth() {
app.logger.Warn("enabling strict SNI-Host enforcement because TLS client auth is configured",
zap.String("server_id", srvName),
)
zap.String("server_id", srvName))
trueBool := true
srv.StrictSNIHost = &trueBool
}
Expand All @@ -206,8 +215,7 @@ func (app *App) Provision(ctx caddy.Context) error {
for i := range srv.Listen {
lnOut, err := repl.ReplaceOrErr(srv.Listen[i], true, true)
if err != nil {
return fmt.Errorf("server %s, listener %d: %v",
srvName, i, err)
return fmt.Errorf("server %s, listener %d: %v", srvName, i, err)
}
srv.Listen[i] = lnOut
}
Expand Down Expand Up @@ -324,10 +332,34 @@ func (app *App) Start() error {
Handler: srv,
ErrorLog: serverLogger,
}

// disable HTTP/2, which we enabled by default during provisioning
if !srv.protocol("h2") {
srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
for _, cp := range srv.TLSConnPolicies {
// the TLSConfig was already provisioned, so... manually remove it
for i, np := range cp.TLSConfig.NextProtos {
if np == "h2" {
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
break
}
}
// remove it from the parent connection policy too, just to keep things tidy
for i, alpn := range cp.ALPN {
if alpn == "h2" {
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
break
}
}
}
}

// this TLS config is used by the std lib to choose the actual TLS config for connections
// by looking through the connection policies to find the first one that matches
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)

// enable h2c if configured
if srv.AllowH2C {
// enable H2C if configured
if srv.protocol("h2c") {
h2server := &http2.Server{
IdleTimeout: time.Duration(srv.IdleTimeout),
}
Expand Down Expand Up @@ -362,27 +394,15 @@ func (app *App) Start() error {
// 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
// create TLS listener - this enables and terminates TLS
ln = tls.NewListener(ln, tlsCfg)

// TODO: HTTP/3 support is experimental for now
if srv.ExperimentalHTTP3 {
mholt marked this conversation as resolved.
Show resolved Hide resolved
if srv.h3server == nil {
srv.h3server = &http3.Server{
Handler: srv,
TLSConfig: tlsCfg,
MaxHeaderBytes: srv.MaxHeaderBytes,
}
// enable HTTP/3 if configured
if srv.protocol("h3") {
app.logger.Info("enabling HTTP/3 listener", zap.String("addr", hostport))
if err := srv.serveHTTP3(hostport, tlsCfg); err != nil {
return err
}

app.logger.Info("enabling experimental HTTP/3 listener", zap.String("addr", hostport))
h3ln, err := caddy.ListenQUIC(hostport, tlsCfg)
if err != nil {
return fmt.Errorf("getting HTTP/3 QUIC listener: %v", err)
}

//nolint:errcheck
go srv.h3server.ServeListener(h3ln)
}
}

Expand All @@ -402,16 +422,22 @@ func (app *App) Start() error {

app.logger.Debug("starting server loop",
zap.String("address", ln.Addr().String()),
zap.Bool("http3", srv.ExperimentalHTTP3),
zap.Bool("tls", useTLS),
)
zap.Bool("http3", srv.h3server != nil))

srv.listeners = append(srv.listeners, ln)

//nolint:errcheck
go srv.server.Serve(ln)
// enable HTTP/1 if configured
if srv.protocol("h1") {
//nolint:errcheck
go srv.server.Serve(ln)
}
}
}

srv.logger.Info("server running",
zap.String("name", srvName),
zap.Strings("protocols", srv.Protocols))
}

// finish automatic HTTPS by finally beginning
Expand Down