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