From c367618fee799918cf7a98cf2bcb4dbcf076ab43 Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Tue, 20 Jan 2026 18:09:36 +0100 Subject: [PATCH] net/haproxy: add support for HTTP/3 over QUIC, closes #4341 --- net/haproxy/pkg-descr | 1 + .../OPNsense/HAProxy/forms/dialogFrontend.xml | 4 +-- .../app/models/OPNsense/HAProxy/HAProxy.xml | 5 ++-- .../templates/OPNsense/HAProxy/haproxy.conf | 27 ++++++++++++++++--- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/net/haproxy/pkg-descr b/net/haproxy/pkg-descr index a94928406..2ab84561c 100644 --- a/net/haproxy/pkg-descr +++ b/net/haproxy/pkg-descr @@ -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 diff --git a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml index c40928fda..6d2ecee8a 100644 --- a/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml +++ b/net/haproxy/src/opnsense/mvc/app/controllers/OPNsense/HAProxy/forms/dialogFrontend.xml @@ -23,7 +23,7 @@ select_multiple true - + Enter address:port here. Finish with TAB. @@ -203,7 +203,7 @@ true true - + frontend.forwardFor diff --git a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml index cd2facaa2..09becb653 100644 --- a/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml +++ b/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml @@ -515,9 +515,9 @@ Y Y - /^((([0-9a-zA-Z._\-\*:\[\]]+:+[0-9]+(-[0-9]+)?|unix@[0-9a-z_\-]+)([,]){0,1}))*/u + /^((([quic4@|quic6@]*[0-9a-zA-Z._\-\*:\[\]]+:+[0-9]+(-[0-9]+)?|unix@[0-9a-z_\-]+)([,]){0,1}))*/u lower - 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. + 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. N @@ -853,6 +853,7 @@ Y Y +

HTTP/3

HTTP/2

HTTP/1.1 HTTP/1.0 diff --git a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf index 6e46f610f..5ae3c9ad5 100644 --- a/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf +++ b/net/haproxy/src/opnsense/service/templates/OPNsense/HAProxy/haproxy.conf @@ -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 %}