diff --git a/nixos/modules/services/web-servers/h2o/default.nix b/nixos/modules/services/web-servers/h2o/default.nix index 738bec5a80f5..74928a2552e6 100644 --- a/nixos/modules/services/web-servers/h2o/default.nix +++ b/nixos/modules/services/web-servers/h2o/default.nix @@ -221,10 +221,7 @@ let headerSet ++ [ hsts ]; } ); - in - value.settings - // headerRecAttrs - // { + listen = let identity = @@ -233,17 +230,27 @@ let key-file = "${certs.${names.cert}.directory}/key.pem"; certificate-file = "${certs.${names.cert}.directory}/fullchain.pem"; }; + + baseListen = + { + port = port.TLS; + ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // { + inherit identity; + }; + } + // lib.optionalAttrs (value.host != null) { + host = value.host; + }; + + # QUIC, if used, will duplicate the TLS over TCP directive, but + # append some extra QUIC-related settings + quicListen = lib.optional (value.tls.quic != null) (baseListen // { inherit (value.tls) quic; }); in { - port = port.TLS; - ssl = (lib.recursiveUpdate tlsRecAttrs value.tls.extraSettings) // { - inherit identity; - }; - } - // lib.optionalAttrs (value.host != null) { - host = value.host; + listen = [ baseListen ] ++ quicListen; }; - }; + in + value.settings // headerRecAttrs // listen; }; in # With a high likelihood of HTTP & ACME challenges being on the same port, diff --git a/nixos/modules/services/web-servers/h2o/vhost-options.nix b/nixos/modules/services/web-servers/h2o/vhost-options.nix index 9c8a74a62bb1..6421af115770 100644 --- a/nixos/modules/services/web-servers/h2o/vhost-options.nix +++ b/nixos/modules/services/web-servers/h2o/vhost-options.nix @@ -152,6 +152,25 @@ in ''; }; recommendations = tlsRecommendationsOption; + quic = mkOption { + type = types.nullOr types.attrs; + default = null; + description = '' + Enables HTTP/3 over QUIC on the UDP port for TLS. The attrset + provides fine-turning for QUIC behavior, but can be empty. See + . + ''; + example = + literalExpression + # nix + '' + { + amp-limit = 2; + handshake-timeout-rtt-multiplier = 300; + retry = "ON"; + } + ''; + }; extraSettings = mkOption { type = types.attrs; default = { }; diff --git a/nixos/tests/web-servers/h2o/basic.nix b/nixos/tests/web-servers/h2o/basic.nix index 82d2f339ed41..52a4b58429e8 100644 --- a/nixos/tests/web-servers/h2o/basic.nix +++ b/nixos/tests/web-servers/h2o/basic.nix @@ -43,6 +43,10 @@ in server = { pkgs, ... }: { + environment.systemPackages = [ + pkgs.curlHTTP3 + ]; + services.h2o = { enable = true; defaultHTTPListenPort = port.HTTP; @@ -60,6 +64,9 @@ in "${domain.TLS}" = { tls = { policy = "force"; + quic = { + retry = "ON"; + }; identity = [ { key-file = ../../common/acme/server/acme.test.key.pem; @@ -99,10 +106,15 @@ in ]; networking = { - firewall.allowedTCPPorts = with port; [ - HTTP - TLS - ]; + firewall = { + allowedTCPPorts = with port; [ + HTTP + TLS + ]; + allowedUDPPorts = with port; [ + TLS + ]; + }; extraHosts = '' 127.0.0.1 ${domain.HTTP} 127.0.0.1 ${domain.TLS} @@ -130,6 +142,13 @@ in assert "${sawatdi_chao_lok}" in server.succeed("curl -v --http2 --tlsv1.3 --compressed --fail-with-body 'https://${domain.TLS}:${portStrTLS}/hello_world.rst'") + quic_hello_world_head = server.succeed("curl -v --head --compressed --http3-only --fail-with-body 'https://${domain.TLS}:${portStrTLS}/hello_world.rst'").lower() + assert "http/3 200" in quic_hello_world_head + assert "server: h2o" in quic_hello_world_head + assert "content-type: text/x-rst" in quic_hello_world_head + + assert "${sawatdi_chao_lok}" in server.succeed("curl -v --http3-only --compressed --fail-with-body 'https://${domain.TLS}:${portStrTLS}/hello_world.rst'") + assert "redirected" in server.succeed("curl -v --head --fail-with-body 'http://${domain.TLS}:${portStrHTTP}/hello_world.rst'").lower() server.fail("curl --location --max-redirs 0 'http://${domain.TLS}:${portStrHTTP}/hello_world.rst'")