From 72e09d54d1a5a7d2af39183ee99b9dc11f1df8fb Mon Sep 17 00:00:00 2001 From: Monviech <79600909+Monviech@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:26:24 +0100 Subject: [PATCH] www/caddy: Add Layer4 openvpn, winbox and quic matcher (#4325) * www/caddy: Add CRUD for Layer4 OpenVPN matcher with mode and static key support. * www/caddy: Export static keys to the filesystem as uuid.key * www/caddy: Remove validation that checks for multiple keys, help text is enough. * www/caddy: Expand layer4 template for all supported OpenVPN modes. * www/caddy: Prevent multiple static keys for modes other than crypt2_client. Fix helptexts. * www/caddy: Add unique constraint to description of openvpn static key * www/caddy: Changelog and version bump * www/caddy: Make static key optional when choosing the tls mode in openvpn matcher * www/caddy: Prepare new Layer7 Matcher Tab for more customizable matchers in the future. * www/caddy: Add Layer4 QUIC matcher. * www/caddy: Rename matcherTab * www/caddy: Revert a4ea0cb3 since its non operational and will not be needed for a while anyway * www/caddy: Changelog --- www/caddy/Makefile | 2 +- www/caddy/pkg-descr | 12 +++ .../Caddy/Api/ReverseProxyController.php | 100 ++++++++++++------ .../OPNsense/Caddy/Layer4Controller.php | 1 + .../OPNsense/Caddy/forms/dialogLayer4.xml | 18 +++- .../Caddy/forms/dialogLayer4Openvpn.xml | 13 +++ .../mvc/app/models/OPNsense/Caddy/Caddy.php | 33 +++++- .../mvc/app/models/OPNsense/Caddy/Caddy.xml | 41 +++++++ .../mvc/app/views/OPNsense/Caddy/layer4.volt | 62 +++++++++-- .../scripts/OPNsense/Caddy/caddy_certs.php | 29 +++-- .../templates/OPNsense/Caddy/includeLayer4 | 60 +++++++++++ 11 files changed, 316 insertions(+), 55 deletions(-) create mode 100644 www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4Openvpn.xml diff --git a/www/caddy/Makefile b/www/caddy/Makefile index 1fe46006ff..bd346b669e 100644 --- a/www/caddy/Makefile +++ b/www/caddy/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= caddy -PLUGIN_VERSION= 1.7.3 +PLUGIN_VERSION= 1.7.4 PLUGIN_DEPENDS= caddy-custom PLUGIN_COMMENT= Modern Reverse Proxy with Automatic HTTPS, Dynamic DNS and Layer4 Routing PLUGIN_MAINTAINER= cedrik@pischem.com diff --git a/www/caddy/pkg-descr b/www/caddy/pkg-descr index af5015875c..42a8d507d6 100644 --- a/www/caddy/pkg-descr +++ b/www/caddy/pkg-descr @@ -13,6 +13,18 @@ DOC: https://docs.opnsense.org/manual/how-tos/caddy.html Plugin Changelog ================ +1.7.4 + +* Add: Layer4 OpenVPN matcher with mode, digest and static key support +* Add: Layer4 Winbox matcher +* Add: Layer4 QUIC matcher +* Build: Update dependency to lang/go123 +* Build: Update caddy-l4 and caddy-dynamicdns module +* Build: Fix that caddy-l4 does not stop when ssh is proxied +* Build: DNS Providers: Update porkbun, dnsmadeeasy +* Cleanup: Layer4 default route in listener_wrappers has been removed (obsolete) +* Fix: Error when same Access List is set to wildcard and subdomain + 1.7.3 * Add: Clear All button to Filter by Domain selectpicker diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php index 16bb4de37f..0e46c0504a 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Api/ReverseProxyController.php @@ -209,40 +209,6 @@ public function toggleHandleAction($uuid, $enabled = null) return $this->toggleBase("reverseproxy.handle", $uuid, $enabled); } - - // Layer4 Section - - public function searchLayer4Action() - { - return $this->searchBase("reverseproxy.layer4", null, 'description'); - } - - public function setLayer4Action($uuid) - { - return $this->setBase("layer4", "reverseproxy.layer4", $uuid); - } - - public function addLayer4Action() - { - return $this->addBase("layer4", "reverseproxy.layer4"); - } - - public function getLayer4Action($uuid = null) - { - return $this->getBase("layer4", "reverseproxy.layer4", $uuid); - } - - public function delLayer4Action($uuid) - { - return $this->delBase("reverseproxy.layer4", $uuid); - } - - public function toggleLayer4Action($uuid, $enabled = null) - { - return $this->toggleBase("reverseproxy.layer4", $uuid, $enabled); - } - - // AccessList Section public function searchAccessListAction() @@ -349,4 +315,70 @@ public function delHeaderAction($uuid) { return $this->delBase("reverseproxy.header", $uuid); } + + + // Layer4 Proxy Section + + public function searchLayer4Action() + { + return $this->searchBase("reverseproxy.layer4", null, 'description'); + } + + public function setLayer4Action($uuid) + { + return $this->setBase("layer4", "reverseproxy.layer4", $uuid); + } + + public function addLayer4Action() + { + return $this->addBase("layer4", "reverseproxy.layer4"); + } + + public function getLayer4Action($uuid = null) + { + return $this->getBase("layer4", "reverseproxy.layer4", $uuid); + } + + public function delLayer4Action($uuid) + { + return $this->delBase("reverseproxy.layer4", $uuid); + } + + public function toggleLayer4Action($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.layer4", $uuid, $enabled); + } + + + // Layer4 OpenVPN Section + + public function searchLayer4OpenvpnAction() + { + return $this->searchBase("reverseproxy.layer4openvpn", null, 'description'); + } + + public function setLayer4OpenvpnAction($uuid) + { + return $this->setBase("layer4openvpn", "reverseproxy.layer4openvpn", $uuid); + } + + public function addLayer4OpenvpnAction() + { + return $this->addBase("layer4openvpn", "reverseproxy.layer4openvpn"); + } + + public function getLayer4OpenvpnAction($uuid = null) + { + return $this->getBase("layer4openvpn", "reverseproxy.layer4openvpn", $uuid); + } + + public function delLayer4OpenvpnAction($uuid) + { + return $this->delBase("reverseproxy.layer4openvpn", $uuid); + } + + public function toggleLayer4OpenvpnAction($uuid, $enabled = null) + { + return $this->toggleBase("reverseproxy.layer4openvpn", $uuid, $enabled); + } } diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php index 00b4548e7d..1a085eff42 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/Layer4Controller.php @@ -38,5 +38,6 @@ public function indexAction() { $this->view->pick('OPNsense/Caddy/layer4'); $this->view->formDialogLayer4 = $this->getForm("dialogLayer4"); + $this->view->formDialogLayer4Openvpn = $this->getForm("dialogLayer4Openvpn"); } } diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml index a2c73f6a01..1679b056fe 100644 --- a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4.xml @@ -62,10 +62,26 @@ layer4.FromDomain select_multiple - + true + + layer4.FromOpenvpnModes + + dropdown + + + + + layer4.FromOpenvpnStaticKey + + select_multiple + + Any + 5 + + layer4.InvertMatchers diff --git a/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4Openvpn.xml b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4Openvpn.xml new file mode 100644 index 0000000000..ec225e85f3 --- /dev/null +++ b/www/caddy/src/opnsense/mvc/app/controllers/OPNsense/Caddy/forms/dialogLayer4Openvpn.xml @@ -0,0 +1,13 @@ + + + layer4openvpn.description + + text + + + layer4openvpn.StaticKey + + textbox + Paste an OpenVPN Static key. + + diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php index f738609ea9..066574cbf5 100644 --- a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.php @@ -180,7 +180,7 @@ private function checkLayer4Matchers($messages) foreach ($this->reverseproxy->layer4->iterateItems() as $item) { if ($item->isFieldChanged()) { $key = $item->__reference; - if (in_array((string)$item->Matchers, ['httphost', 'tlssni']) && empty((string)$item->FromDomain)) { + if (in_array((string)$item->Matchers, ['httphost', 'tlssni', 'quicsni']) && empty((string)$item->FromDomain)) { $messages->appendMessage(new Message( sprintf( gettext( @@ -191,7 +191,7 @@ private function checkLayer4Matchers($messages) $key . ".FromDomain" )); } elseif ( - !in_array((string)$item->Matchers, ['httphost', 'tlssni']) && + !in_array((string)$item->Matchers, ['httphost', 'tlssni', 'quicsni']) && ( !empty((string)$item->FromDomain) && (string)$item->FromDomain != '*' @@ -208,6 +208,30 @@ private function checkLayer4Matchers($messages) )); } + if ((string)$item->Matchers !== 'openvpn' && !empty((string)$item->FromOpenvpnModes)) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When "%s" matcher is selected, field must be empty.' + ), + $item->Matchers + ), + $key . ".FromOpenvpnModes" + )); + } + + if ((string)$item->Matchers !== 'openvpn' && !empty((string)$item->FromOpenvpnStaticKey)) { + $messages->appendMessage(new Message( + sprintf( + gettext( + 'When "%s" matcher is selected, field must be empty.' + ), + $item->Matchers + ), + $key . ".FromOpenvpnStaticKey" + )); + } + if ((string)$item->Type === 'global' && empty((string)$item->FromPort)) { $messages->appendMessage(new Message( sprintf( @@ -246,13 +270,14 @@ private function checkLayer4Matchers($messages) (string)$item->Type !== 'global' && ( (string)$item->Matchers == 'tls' || - (string)$item->Matchers == 'http' + (string)$item->Matchers == 'http' || + (string)$item->Matchers == 'quic' ) ) { $messages->appendMessage(new Message( sprintf( gettext( - 'When routing type is "%s", matchers "HTTP" or "TLS" cannot be chosen.' + 'When routing type is "%s", matchers "HTTP", "TLS" or "QUIC" cannot be chosen.' ), $item->Type ), diff --git a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml index 8e357ad8ee..f1ef8c65d7 100644 --- a/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml +++ b/www/caddy/src/opnsense/mvc/app/models/OPNsense/Caddy/Caddy.xml @@ -489,6 +489,29 @@ Y Please enter one or multiple hostnames or FQDNs. + + Any + + tls-auth_sha256_normal + tls-auth_sha256_inverse + tls-auth_sha512_normal + tls-auth_sha512_inverse + tls-crypt + tls-crypt2_client + tls-crypt2_server + + + + + + OPNsense.Caddy.Caddy + reverseproxy.layer4openvpn + description + %s + + + Y + Y tlssni @@ -497,14 +520,18 @@ DNS HTTP HTTP (Host Header) + OpenVPN Postgres Proxy Protocol + QUIC + QUIC (SNI Client Hello) RDP SOCKSv4 SOCKSv5 SSH TLS TLS (SNI Client Hello) + Winbox Wireguard XMPP @@ -539,6 +566,20 @@ + + + Y + + + Y + + + Description must be unique. + UniqueConstraint + + + + diff --git a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/layer4.volt b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/layer4.volt index d1bfc6cf62..b762112926 100644 --- a/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/layer4.volt +++ b/www/caddy/src/opnsense/mvc/app/views/OPNsense/Caddy/layer4.volt @@ -36,6 +36,15 @@ toggle:'/api/caddy/ReverseProxy/toggleLayer4/', }); + $("#Layer4OpenvpnGrid").UIBootgrid({ + search:'/api/caddy/ReverseProxy/searchLayer4Openvpn/', + get:'/api/caddy/ReverseProxy/getLayer4Openvpn/', + set:'/api/caddy/ReverseProxy/setLayer4Openvpn/', + add:'/api/caddy/ReverseProxy/addLayer4Openvpn/', + del:'/api/caddy/ReverseProxy/delLayer4Openvpn/', + toggle:'/api/caddy/ReverseProxy/toggleLayer4Openvpn/', + }); + /** * Displays an alert message to the user. * @@ -86,11 +95,17 @@ } }); + // Hide all elements with style_matchers initially + $(".style_matchers").closest('tr').hide(); + $("#layer4\\.Matchers").change(function() { - if ($(this).val() !== "tlssni" && $(this).val() !== "httphost") { - $(".style_matchers").closest('tr').hide(); - } else { - $(".style_matchers").closest('tr').show(); + $(".style_matchers").closest('tr').hide(); + const selectedVal = $(this).val(); + + if (selectedVal === "tlssni" || selectedVal === "httphost" || selectedVal === "quicsni") { + $(".matchers_domain").closest('tr').show(); + } else if (selectedVal === "openvpn") { + $(".matchers_openvpn").closest('tr').show(); } }); @@ -108,6 +123,7 @@
@@ -115,7 +131,7 @@

{{ lang._('Layer4 Routes') }}

-
+
@@ -125,9 +141,11 @@ - + + + @@ -152,8 +170,39 @@ + + +
+
+ +

{{ lang._('OpenVPN Static Keys') }}

+
+
{{ lang._('Routing Type') }} {{ lang._('Protocol') }} {{ lang._('Local Port') }}{{ lang._('Domain') }} {{ lang._('Matchers') }} {{ lang._('Invert Matchers') }}{{ lang._('Domain') }}{{ lang._('OpenVPN Modes') }}{{ lang._('OpenVPN Static Key') }} {{ lang._('Upstream Domain') }} {{ lang._('Upstream Port') }} {{ lang._('Remote IP') }}
+ + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+
+
+
@@ -177,3 +226,4 @@
{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4,'id':'DialogLayer4','label':lang._('Edit Layer4 Route')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4Openvpn,'id':'DialogLayer4Openvpn','label':lang._('Edit OpenVPN Static Key')])}} diff --git a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php index f65ef168cf..a8e6c36cf1 100755 --- a/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php +++ b/www/caddy/src/opnsense/scripts/OPNsense/Caddy/caddy_certs.php @@ -36,22 +36,22 @@ // Traverse through certificates foreach ($configObj->cert as $cert) { - $cert_refid = (string) $cert->refid; - $cert_content = base64_decode((string) $cert->crt); - $key_content = base64_decode((string) $cert->prv); + $cert_refid = (string)$cert->refid; + $cert_content = base64_decode((string)$cert->crt); + $key_content = base64_decode((string)$cert->prv); $cert_chain = $cert_content; // Handle CA and possible intermediate CA to create a certificate bundle if (!empty($cert->caref)) { foreach ($configObj->ca as $ca) { - if ((string) $cert->caref === (string) $ca->refid) { - $ca_content = base64_decode((string) $ca->crt); + if ((string)$cert->caref === (string)$ca->refid) { + $ca_content = base64_decode((string)$ca->crt); $cert_chain .= "\n" . $ca_content; if (!empty($ca->caref)) { foreach ($configObj->ca as $parent_ca) { - if ((string) $ca->caref === (string) $parent_ca->refid) { - $parent_ca_content = base64_decode((string) $parent_ca->crt); + if ((string)$ca->caref === (string)$parent_ca->refid) { + $parent_ca_content = base64_decode((string)$parent_ca->crt); $cert_chain .= "\n" . $parent_ca_content; break; } @@ -68,9 +68,20 @@ // Traverse through CA certificates and save them foreach ($configObj->ca as $ca) { - $ca_refid = (string) $ca->refid; - $ca_content = base64_decode((string) $ca->crt); + $ca_refid = (string)$ca->refid; + $ca_content = base64_decode((string)$ca->crt); // Save the CA certificate file_put_contents($temp_dir . $ca_refid . '.pem', $ca_content); } + +// Traverse through layer4 OpenVPN static keys and save them as files +if (isset($configObj->Pischem->caddy->reverseproxy->layer4openvpn)) { + foreach ($configObj->Pischem->caddy->reverseproxy->layer4openvpn as $openvpn) { + $uuid = (string) $openvpn['uuid']; + $static_key = (string) $openvpn->StaticKey; + + // Save the static key + file_put_contents($temp_dir . $uuid . '.key', $static_key); + } +} diff --git a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 index defaf2528f..c6058ccd70 100644 --- a/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 +++ b/www/caddy/src/opnsense/service/templates/OPNsense/Caddy/includeLayer4 @@ -70,6 +70,66 @@ {{ invert_prefix }}http host {{ layer4.FromDomain.replace(',', ' ') }} {% elif layer4.Matchers == 'tlssni' %} {{ invert_prefix }}tls sni {{ layer4.FromDomain.replace(',', ' ') }} + {% elif layer4.Matchers == 'quicsni' %} + {{ invert_prefix }}quic sni {{ layer4.FromDomain.replace(',', ' ') }} + {% elif layer4.Matchers == 'openvpn' and layer4.FromOpenvpnModes %} + {% for mode in layer4.FromOpenvpnModes.split(',') %} + {% set mode_clean = mode.strip() %} + {% if layer4.FromOpenvpnStaticKey %} + {% set key_list = layer4.FromOpenvpnStaticKey.split(',') %} + {% endif %} + {% if mode_clean.startswith('auth') %} + {% if key_list|length > 1 %} + {% set key_list = key_list[:1] %} + {% endif %} + {% set digest = 'sha256' if 'sha256' in mode_clean else 'sha512' %} + {% set direction = 'normal' if 'normal' in mode_clean else 'inverse' %} + {{ invert_prefix }}openvpn { + modes auth + auth_digest {{ digest }} + {% if layer4.FromOpenvpnStaticKey %} + group_key_direction {{ direction }} + {% for key_uuid in key_list %} + group_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% elif mode_clean == 'crypt' %} + {% if key_list|length > 1 %} + {% set key_list = key_list[:1] %} + {% endif %} + {{ invert_prefix }}openvpn { + modes crypt + {% if layer4.FromOpenvpnStaticKey %} + {% for key_uuid in key_list %} + group_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% elif mode_clean == 'crypt2_client' %} + {# Multiple keys are allowed for crypt2_client #} + {{ invert_prefix }}openvpn { + modes crypt2 + {% if layer4.FromOpenvpnStaticKey %} + {% for key_uuid in key_list %} + client_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% elif mode_clean == 'crypt2_server' %} + {% if key_list|length > 1 %} + {% set key_list = key_list[:1] %} + {% endif %} + {{ invert_prefix }}openvpn { + modes crypt2 + {% if layer4.FromOpenvpnStaticKey %} + {% for key_uuid in key_list %} + server_key_file /var/db/caddy/data/caddy/certificates/temp/{{ key_uuid.strip() }}.key + {% endfor %} + {% endif %} + } + {% endif %} + {% endfor %} {% else %} {{ invert_prefix }}{{ layer4.Matchers }} {% endif %}