net/haproxy: add support for HTTP/3 over QUIC, closes #4341

This commit is contained in:
Frank Wall 2026-01-20 18:09:36 +01:00
parent 512a24fb58
commit c367618fee
4 changed files with 29 additions and 8 deletions

View file

@ -9,6 +9,7 @@ Plugin Changelog
4.7
Added:
* add support for HTTP/3 over QUIC to frontends (#4341)
* add new rule: http-request silent-drop
* add new condition: HTTP method
* support custom HTTP status code in "http-request deny" rules

View file

@ -23,7 +23,7 @@
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[Configure listen addresses for this Public Service, i.e. 127.0.0.1:8080 or www.example.com:443 or unix@socket-name. Use TAB key to complete typing a listen address.]]></help>
<help><![CDATA[Configure listen addresses for this Public Service, i.e. 127.0.0.1:8080 or www.example.com:443 or unix@socket-name. Add quic4@ or quic6@ to enable QUIC for IPv4/IPv6, e.g. quic4@www.example.com:443. Use TAB key to complete typing a listen address.]]></help>
<hint>Enter address:port here. Finish with TAB.</hint>
</field>
<field>
@ -203,7 +203,7 @@
<style>tokenize</style>
<allownew>true</allownew>
<sortable>true</sortable>
<help><![CDATA[When using the TLS ALPN extension, HAProxy advertises the specified protocol list as supported on top of ALPN. SSL offloading must be enabled.]]></help>
<help><![CDATA[When using the TLS ALPN extension, HAProxy advertises the specified protocol list as supported on top of ALPN. SSL offloading must be enabled. Note that HTTP/3 will only be added to QUIC-compatible Listening Addresses.]]></help>
</field>
<field>
<id>frontend.forwardFor</id>

View file

@ -515,9 +515,9 @@
<bind type="CSVListField">
<Required>Y</Required>
<Multiple>Y</Multiple>
<Mask>/^((([0-9a-zA-Z._\-\*:\[\]]+:+[0-9]+(-[0-9]+)?|unix@[0-9a-z_\-]+)([,]){0,1}))*/u</Mask>
<Mask>/^((([quic4@|quic6@]*[0-9a-zA-Z._\-\*:\[\]]+:+[0-9]+(-[0-9]+)?|unix@[0-9a-z_\-]+)([,]){0,1}))*/u</Mask>
<ChangeCase>lower</ChangeCase>
<ValidationMessage>Please provide a valid listen address, i.e. 127.0.0.1:8080, [::1]:8080, www.example.com:443 or unix@socket-name. Port range as start-end, i.e. 127.0.0.1:1220-1240.</ValidationMessage>
<ValidationMessage>Please provide a valid listen address, i.e. 127.0.0.1:8080, [::1]:8080, www.example.com:443, quic4@www.example.com or unix@socket-name. Port range as start-end, i.e. 127.0.0.1:1220-1240.</ValidationMessage>
</bind>
<bindOptions type="TextField">
<Required>N</Required>
@ -853,6 +853,7 @@
<Sorted>Y</Sorted>
<Multiple>Y</Multiple>
<OptionValues>
<h3>HTTP/3</h3>
<h2>HTTP/2</h2>
<http11>HTTP/1.1</http11>
<http10>HTTP/1.0</http10>

View file

@ -1013,6 +1013,8 @@ global
{% if helpers.exists('OPNsense.HAProxy.general.tuning.maxConnections') %}
maxconn {{OPNsense.HAProxy.general.tuning.maxConnections}}
{% endif %}
{# # TODO: remove this option when OpenSSL 3.5 is available on OPNsense #}
limited-quic
{# # check if OCSP is enabled #}
{% if OPNsense.HAProxy.general.tuning.ocspUpdateEnabled|default('') == '1' %}
{% if helpers.exists('OPNsense.HAProxy.general.tuning.ocspUpdateMinDelay') %}
@ -1420,9 +1422,8 @@ frontend {{frontend.name}}
{% endif %}
{# # HTTP/2 with TLS enabled #}
{% if frontend.http2Enabled|default("") == '1' and frontend.advertised_protocols|default("") != "" %}
{# # convert protocols to HAProxy-compatible format #}
{% set alpn_options = frontend.advertised_protocols|replace('http10', 'http/1.0')|replace('http11', 'http/1.1') %}
{% do ssl_options.append('alpn ' ~ alpn_options) %}
{# # To ensure proper handling of each HTTP protocol, these #}
{# # entries will be processed when parsing individual bind lines. #}
{% else %}
{# # disable ALPN to enforce the GUI settings #}
{% do ssl_options.append('no-alpn') %}
@ -1448,6 +1449,8 @@ frontend {{frontend.name}}
{# # bind/listen configuration #}
{% if frontend.bind|default("") != "" %}
{% for bind in frontend.bind.split(",") %}
{# # alpn advertisements are specific to each bind line #}
{% set alpn_options = [] %}
{# # check if this is a unix socket #}
{% set unix_bind = bind | regex_replace ("^unix@.*","TRUE") %}
{% if unix_bind == "TRUE" %}
@ -1459,7 +1462,23 @@ frontend {{frontend.name}}
{% set bind_address = bind %}
{% set bind_name = bind %}
{% endif %}
bind {{bind_address}} name {{bind_name}} {% if frontend.bindOptions|default("") != "" %}{{ frontend.bindOptions }} {% endif %}{% if frontend.ssl_enabled == '1' and ssl_certs|default("") != "" %}ssl {{ ssl_options|join(' ') }} {{ ssl_certs|join(' ') }} {% endif %}{% if adv_options|length > 0 %} {{ adv_options|join(' ') }} {% endif %}
{# # handle incompatible alpn advertisements #}
{% if bind.startswith('quic4@') or bind.startswith('quic6@') %}
{# # strip incompatible advertisement for QUIC bind lines #}
{% set alpn_incompatible = ['h2', 'http11', 'http10'] %}
{% set alpn_filtered = frontend.advertised_protocols.split(',') | reject('in', alpn_incompatible) | join(',') %}
{% else %}
{# # strip incompatible advertisement for non-QUIC bind lines #}
{% set alpn_incompatible = ['h3'] %}
{% set alpn_filtered = frontend.advertised_protocols.split(',') | reject('in', alpn_incompatible) | join(',') %}
{% endif %}
{# # add alpn advertisements #}
{% if alpn_filtered|default("") != "" %}
{# # convert alpn protocols to HAProxy-compatible format #}
{% set alpn_conv = alpn_filtered|replace('http10', 'http/1.0')|replace('http11', 'http/1.1') %}
{% do alpn_options.append('alpn ' ~ alpn_conv) %}
{% endif %}
bind {{bind_address}} name {{bind_name}} {% if frontend.bindOptions|default("") != "" %}{{ frontend.bindOptions }} {% endif %}{% if frontend.ssl_enabled == '1' and ssl_certs|default("") != "" %}ssl {{ ssl_options|join(' ') }} {{ alpn_options|join(' ') }} {{ ssl_certs|join(' ') }} {% endif %}{% if adv_options|length > 0 %} {{ adv_options|join(' ') }} {% endif %}
{% endfor %}
{% endif %}