www/caddy: Layer4 support in listener_wrapper to route TLS traffic without termination (#4112)

This commit is contained in:
Monviech 2024-07-30 12:12:18 +02:00 committed by GitHub
parent 13a9db5d5f
commit f30007b60c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 455 additions and 147 deletions

View file

@ -9,16 +9,19 @@ WWW: https://caddyserver.com/
Main features of this plugin:
* Easy to configure and reliable! Reverse Proxy any HTTP/HTTPS or WebSocket application in minutes.
* Hard to break! Extensive validations of the configuration on each save and apply.
* Automatic Let's Encrypt and ZeroSSL Certificates with HTTP-01 and TLS-ALPN-01 challenge
* Automatic Let's Encrypt and ZeroSSL certificates with HTTP-01 and TLS-ALPN-01 challenge
* DNS-01 challenge and Dynamic DNS with supported DNS Providers built right in
* Use custom certificates from OPNsense certificate store
* Wildcard Domain and Subdomain support
* Access Lists to restrict access based on static networks
* Basic Auth to restrict access by username and password
* Forward Auth with Authelia
* Syslog-ng integration and HTTP Access Log
* Header manipulation with header_up and header_down
* NTLM Transport
* Header manipulation
* Simple load balancing with passive health check
* Widgets for OPNsense Dashboard (24.7 and later)
* Layer4 SNI based routing of TCP/UDP
DOC: https://docs.opnsense.org/manual/how-tos/caddy.html
@ -27,172 +30,172 @@ Plugin Changelog
1.6.2
* Add Authentik as authentication provider (contributed by Tim-Sc)
* Add: Authentik as authentication provider (contributed by Tim-Sc)
* Add: Feature Preview - Layer4 routing to proxy traffic without TLS termination. Can be enabled in advanced mode of "General Settings"
1.6.1
Add: Run Caddy as "www" user and group, by enabling "Disable Superuser" in General Settings.
Cleanup: Validations in general.volt are all appended to their form keys, instead of triggering a Bootstrap Dialog.
Change: Description Fields are optional now, reducing the steps needed to create new entries.
Add: Shortcuts to add new Domains and Handlers.
Change: Subdomain Tab only appears when a wildcard domain has been configured.
* Add: Run Caddy as "www" user and group, by enabling "Disable Superuser" in General Settings
* Add: Shortcuts to add new Domains and Handlers
* Change: Description Fields are optional now, reducing the steps needed to create new entries
* Change: Subdomain Tab only appears when a wildcard domain has been configured
* Cleanup: Validations in general.volt are all appended to their form keys, instead of triggering a Bootstrap Dialog
1.6.0
* Add: New Dashboard widgets for 24.7, showing domain status and certificate validity status.
* Cleanup: PHP files refactored for PSR-12, Python files refactored for PEP-8.
* Cleanup: Templates Caddyfile and rc.conf.d/caddy refactored for maintainability.
* Cleanup: Spurious keys removed from Caddy.xml model.
* Cleanup: Unused code removed from Caddy.php.
* Fix: Caddyfile template fixed when IPv6 addresses and ports are used in Upstream. IPv6 address wraps into brackets now.
* Add: forward_auth directive with Authelia as Authz Provider.
* Add: Default HTTP and HTTPS ports can be changed in general settings.
* Add: Introduce HTTP version to handler. HTTP/1.1, HTTP/2 and HTTP/3 can be chosen.
* Add: HTTP Keepalive can be set in a handler.
* Change: Option "tls_trusted_ca_certs" is now "tls_trust_pool".
* Build: Update caddy-dns Cloudflare and Route53. Route53 field names changed due to upstream changes.
* Add: TLS can be deactivated in a domain.
* Add: New Dashboard widgets for 24.7, showing domain status and certificate validity status
* Add: forward_auth directive with Authelia as Authz Provider
* Add: Default HTTP and HTTPS ports can be changed in general settings
* Add: Introduce HTTP version to handler. HTTP/1.1, HTTP/2 and HTTP/3 can be chosen
* Add: HTTP Keepalive can be set in a handler
* Add: TLS can be deactivated in a domain
* Build: Update caddy-dns Cloudflare and Route53. Route53 field names changed due to upstream changes
* Change: Option "tls_trusted_ca_certs" is now "tls_trust_pool"
* Cleanup: PHP files refactored for PSR-12, Python files refactored for PEP-8
* Cleanup: Templates Caddyfile and rc.conf.d/caddy refactored for maintainability
* Cleanup: Spurious keys removed from Caddy.xml model
* Cleanup: Unused code removed from Caddy.php
* Fix: Caddyfile template fixed when IPv6 addresses and ports are used in Upstream. IPv6 address wraps into brackets now
1.5.7
* Build: Update to Caddy v2.8.4 + caddy-dns plugins updated to latest upstream versions
* Add: Error message when OPNsense WebGUI settings conflict with Auto HTTPS.
* Add: Error message when Auto HTTPS is enabled, and ACME email field is empty, for caddy v2.8.4
* Add: Error message when OPNsense WebGUI settings conflict with Auto HTTPS
* Add: Error message when Auto HTTPS is enabled, and ACME email field is empty, for caddy v2.8.
* Add: Dynamic DNS now supports "Update Only", only updating existing records without creating new ones
* Add: DNS Providers: dnsmadeeasy, bunny, civo, scaleway, acmeproxy, inwx, namedotcom, easydns, infomaniak, directadmin, hosttech, vult
* Build: Update to Caddy v2.8.4 + caddy-dns plugins updated to latest upstream version
* Change: Dynamic DNS "TTL" and "Check Interval" have been changed to seconds. Existing values have been reset to use the defaults of the implementation
* Cleanup: Fix crash of searchAction when reverseUuids is null
* Cleanup: basicauth directive is now basic_auth in the Caddyfile template, for caddy v2.8.4
* Change: Dynamic DNS "TTL" and "Check Interval" have been changed to seconds. Existing values have been reset to use the defaults of the implementation.
* Add: Dynamic DNS now supports "Update Only", only updating existing records without creating new ones.
* Fix: The subdomain port field has been removed, since it is unsupported. Subdomains track their ports from their parent wildcard domain.
* Add: DNS Providers: dnsmadeeasy, bunny, civo, scaleway, acmeproxy, inwx, namedotcom, easydns, infomaniak, directadmin, hosttech, vultr
* Remove: DNS Providers: godaddy
* Cleanup: basicauth directive is now basic_auth in the Caddyfile template, for caddy v2.8
* Cleanup: Refactor dns provider configuration in Caddyfile template
* Fix: The subdomain port field has been removed, since it is unsupported. Subdomains track their ports from their parent wildcard domain
* Remove: DNS Providers: godaddy
1.5.6
* Fix: Wildcard domains with activated "Dynamic DNS" update their base domain with * instead of @.
* Add: DNS Providers: Netcup, RFC2136
* Fix: Wildcard domains with activated "Dynamic DNS" update their base domain with * instead of @
1.5.5
* Fix: "Apply" could hang when websockets are in use by clients. A grace period of 10s has been added in General Settings that forces to close all connections on config changes.
* Add: In Reverse Proxy, a new dropdown can select one or multiple domains, filtering the Bootgrids of Domains, Subdomains and Handlers for the selected Domain.
* Add: Global Log Level can be set in Log Settings.
* Fix: "Apply" will always read all certificates from the filesystem, even if the Caddy configuration has remained unchanged. "reload" has been changed to "reloadssl".
* Change: ACME Email should be filled out since it's a requirement for ZeroSSL.
* Fix: "Save" and "Apply" buttons in General Settings have been improved to reliably trigger validation of form.
* Cleanup: Javascript variables have been changed from var to let to reduce scope.
* Fix: Template has been fixed to allow any TLS option in Handlers to appear independant when filled out. This increases flexibility with the "tls_server_name" option.
* Add: Diagnostics view added where the current Caddyfile and JSON configuration can be displayed, validated and downloaded.
* Add: HTTP-01 Challenge Redirection can also be configured for subdomains.
* Cleanup: lang() and gettext() functions added for translations.
* Cleanup: Rewritten most help texts in forms for consistency.
* Fix: The newly introduced "configctl caddy reload" action, which calls the "service caddy reloadssl" command, will now also trigger the setup.sh script.
* Add: In Reverse Proxy, a new dropdown can select one or multiple domains, filtering the Bootgrids of Domains, Subdomains and Handlers for the selected Domain
* Add: Global Log Level can be set in Log Settings
* Add: Diagnostics view added where the current Caddyfile and JSON configuration can be displayed, validated and downloaded
* Add: HTTP-01 Challenge Redirection can also be configured for subdomains
* Change: ACME Email should be filled out since it's a requirement for ZeroSSL
* Cleanup: Javascript variables have been changed from var to let to reduce scope
* Cleanup: lang() and gettext() functions added for translations
* Cleanup: Rewritten most help texts in forms for consistency
* Fix: "Apply" could hang when websockets are in use by clients. A grace period of 10s has been added in General Settings that forces to close all connections on config changes
* Fix: "Apply" will always read all certificates from the filesystem, even if the Caddy configuration has remained unchanged. "reload" has been changed to "reloadssl"
* Fix: "Save" and "Apply" buttons in General Settings have been improved to reliably trigger validation of form
* Fix: Template has been fixed to allow any TLS option in Handlers to appear independant when filled out. This increases flexibility with the "tls_server_name" option
* Fix: The newly introduced "configctl caddy reload" action, which calls the "service caddy reloadssl" command, will now also trigger the setup.sh script
1.5.4
* Fix: When pressing Apply, the Caddy service will be reloaded instead of restarted. This fixes long restart times and service interruptions.
* Change: All Description Fields are now required to be populated.
* Change: Model Relation Fields now display two values instead of one to make most options appear unique.
* Add: HTTP response code and HTTP response message can be set per access list in advanced mode.
* Add: Header functionality added. Multiple header manipulations can be set per handler.
* Cleanup: Update searchBase() in ReverseProxyController.php for easier maintainability.
* Fix: Move selectpicker empty option to model in general.volt, using BlankDesc. This fixes the option IPv4+IPv6 not appearing in Dynamic DNS.
* Add: Simple Load Balancing support with the default random policy, by allowing to add multiple Upstream Domains in Handlers.
* Add: Passive Health check for load balancing (Upstream Fail Duration) in Handlers.
* Fix: Input validation so a base domain like "example.com" and a wildcard domain like "*.example.com" can now be created at the same time in domains.
* Add: Simple Load Balancing support with the default random policy, by allowing to add multiple Upstream Domains in Handlers
* Add: Passive Health check for load balancing (Upstream Fail Duration) in Handlers
* Add: HTTP response code and HTTP response message can be set per access list in advanced mode
* Add: Header functionality added. Multiple header manipulations can be set per handler
* Change: All Description Fields are now required to be populated
* Change: Model Relation Fields now display two values instead of one to make most options appear unique
* Cleanup: Update searchBase() in ReverseProxyController.php for easier maintainability
* Fix: When pressing Apply, the Caddy service will be reloaded instead of restarted. This fixes long restart times and service interruptions
* Fix: Move selectpicker empty option to model in general.volt, using BlankDesc. This fixes the option IPv4+IPv6 not appearing in Dynamic DNS
* Fix: Input validation so a base domain like "example.com" and a wildcard domain like "*.example.com" can now be created at the same time in domains
1.5.3
* Change from "Phalcon Messages" to "OPNsense Messages" in Caddy.php.
* Change default storage location from /usr/local/etc/caddy to /var/db/caddy/data/caddy/.
* Change description from "TextField" to "DescriptionField" in Caddy.xml model.
* Add tls_insecure_skip_verify to handlers.
* Add possibility to restart Caddy with the ACME Client by using "Automations - Run Command - System or Plugin Command".
* Add option to redirect the ACME HTTP-01 challenge to an upstream destination as advanced option in domains.
* Remove unmaintained DNS Providers: dnspod, hetzner, namesilo, vercel, alidns, metaname, openstack-designate.
* Cleanup dialogs and UI to present all options better.
* Add: tls_insecure_skip_verify to handlers
* Add: Possibility to restart Caddy with the ACME Client by using "Automations - Run Command - System or Plugin Command"
* Add: Option to redirect the ACME HTTP-01 challenge to an upstream destination as advanced option in domains
* Change: From "Phalcon Messages" to "OPNsense Messages" in Caddy.php
* Change: Default storage location from /usr/local/etc/caddy to /var/db/caddy/data/caddy/
* Change: Description from "TextField" to "DescriptionField" in Caddy.xml model
* Cleanup: Dialogs and UI to present all options better
* Remove: Unmaintained DNS Providers: dnspod, hetzner, namesilo, vercel, alidns, metaname, openstack-designate
1.5.2
* Increased timeout of message area in reverse_proxy.volt and general.volt to 15 seconds.
* When pressing Apply, the form is saved automatically before the reconfigure action.
* Cleaned up Caddy.xml model to satisfy make lint.
* When selecting an interface in Dynamic DNS, at most one IPv6 GUA and IPv4 non-RFC1918 address will be extracted. Fixes all IP addresses being read.
* Build: When selecting an interface in Dynamic DNS, at most one IPv6 GUA and IPv4 non-RFC1918 address will be extracted. Fixes all IP addresses being read
* Cleanup: Increased timeout of message area in reverse_proxy.volt and general.volt to 15 seconds
* Cleanup: Caddy.xml model to satisfy make lint
* Fix: When pressing Apply, the form is saved automatically before the reconfigure action
1.5.1
* More DNS Providers added: netlify, namesilo, njalla, vercel, googleclouddns, alidns, powerdns, tencentcloud, dinahosting, metaname, hexonet, ddnss, linode, mailinabox, ovh, namecheap, azure, openstack-designate.
* More input fields and better documentation added for the DNS Provider API Keys.
* Changed rc.d script to standard freebsd poudriere one packaged with the caddy-custom binary, included setup.sh script to rc.conf.d/caddy.
* Updated dependancy to caddy-custom instead of caddy.
* Removed +POST_DEINSTALL.post and +POST_INSTALL.post.
* Turned syslog-ng configuration from template to static file.
* A few typos in the general.volt and reverse_proxy.volt corrected.
* The RealInterfaceField custom Fieldtype was removed and replaced with an OPNsense integrated template function to read the interface name.
* Enable $internalModelUseSafeDelete in ReverseProxyController.php - Items can only be deleted when they are not referenced by other items, making deleting in the GUI safer since there can't be any orphaned configuration left behind.
* Migration script M1_1_3 from "Description" to "description" added. Lower case description is needed to be in line with some OPNsense integrated functions.
* Add: More DNS Providers: netlify, namesilo, njalla, vercel, googleclouddns, alidns, powerdns, tencentcloud, dinahosting, metaname, hexonet, ddnss, linode, mailinabox, ovh, namecheap, azure, openstack-designate
* Add: More input fields and better documentation for the DNS Provider API Keys
* Change: rc.d script to standard freebsd poudriere one packaged with the caddy-custom binary, included setup.sh script to rc.conf.d/caddy
* Change: Updated dependancy to caddy-custom instead of caddy
* Change: Enable $internalModelUseSafeDelete in ReverseProxyController.php - Items can only be deleted when they are not referenced by other items, making deleting in the GUI safer since there can't be any orphaned configuration left behind
* Cleanup: Turned syslog-ng configuration from template to static file
* Cleanup: A few typos in the general.volt and reverse_proxy.volt corrected
* Cleanup: The RealInterfaceField custom Fieldtype was removed and replaced with an OPNsense integrated template function to read the interface name
* Cleanup: Migration script M1_1_3 from "Description" to "description" added. Lower case description is needed to be in line with some OPNsense integrated functions
* Remove: +POST_DEINSTALL.post and +POST_INSTALL.post
1.5.0
* Omit vultr from DNS-Providers since it can't be built without errors.
* General view cleanup by seperating options into different tabs.
* Added ACME-DNS Provider for custom ACME Server support.
* When pressing save, a hint will appear that changes should be applied. Apply and Save now give feedback on success.
* Create ACL for Caddy
* Code consistency improved.
* Add: ACME-DNS Provider for custom ACME Server support
* Add: When pressing save, a hint will appear that changes should be applied. Apply and Save now give feedback on success
* Add: Create ACL for Caddy
* Cleanup: General view cleanup by seperating options into different tabs
* Cleanup: Code consistency improved
* Remove: vultr from DNS-Providers since it can't be built without errors
1.4.5
* New validate api action when pressing apply that validates the Caddyfile + Validation model fix.
* Added configuration option to log HTTP access to plain JSON files. [contributed by @pmhausen]
* Added backend path prepend feature to handler configuration. [contributed by @pmhausen]
* Add: New validate api action when pressing apply that validates the Caddyfile + Validation model fix
* Add: Configuration option to log HTTP access to plain JSON files (contributed by pmhausen)
* Add: Backend path prepend feature to handler configuration (contributed by pmhausen)
1.4.4
* Route53 DNS Provider added
* Dark Mode GUI fix
* Add: Route53 DNS Provider
* Cleanup: Dark Mode GUI
1.4.2
* Added Basic Auth as additional access restriction, multiple users can be set per domain and subdomain.
* Made views cleaner: Seperated General Settings and DNS Provider Settings, joined Access List and Basic Auth in new Access Tab.
* Fixed template generation of Caddyfile for new DNS Providers deSEC.
* Added Porkbun DNS Provider for GUI configuration with additional DNS Secret Api Key input field.
* Cleaned up some code and fixed some typos.
* Add: Basic Auth as additional access restriction, multiple users can be set per domain and subdomain
* Add: Porkbun DNS Provider for GUI configuration with additional DNS Secret Api Key input field
* Cleanup: Improve views with seperated General Settings and DNS Provider Settings, joined Access List and Basic Auth in new Access Tab
* Cleanup: Fixed some typos in forms.
* Fix: Template generation of Caddyfile for new DNS Provider deSEC
1.4.0
* DynDNS (Dynamic DNS) Feature added.
* Logging refactored to syslog-ng.
* HTTP Access Logs can be enabled.
* More DNS Providers added: Cloudflare, Duck DNS, DigitalOcean, DNSPod, Hetzner, GoDaddy, Gandi, Vultr, IONOS, deSEC
* Add: DynDNS (Dynamic DNS) Feature
* Add: HTTP Access Logs
* Add: More DNS Providers: Cloudflare, Duck DNS, DigitalOcean, DNSPod, Hetzner, GoDaddy, Gandi, Vultr, IONOS, deSEC
* Cleanup: Logging refactored to syslog-ng.
1.3.4
* Added abort statement to close all connections that don't match any handle.
* Added tls_server_name to model
* Fixes DNS Challenge not adhering to the status of the DNS-01 Checkbox
* Add: abort statement to close all connections that do not match any handle
* Add: tls_server_name to model
* Fix: DNS Challenge not adhering to the status of the DNS-01 Checkbox
1.3.3
* Swapped processing order in template from wildcard domains -> subdomains to subdomains -> wildcard domains
* Change: Swapped processing order in template from wildcard domains -> subdomains to subdomains -> wildcard domains
1.3.2
* Added inline loop logic to template to always print empty handles last in the Caddyfile before specific handles.
* Add: Inline loop logic to template to always print empty handles last in the Caddyfile before specific handles
1.3.1
* Added AccessList functionality.
* Small constraint to handle added. Handles have to start at least with / when they're not empty, or Caddy won't start.
* Add reload for the ServiceControlUI after pressing Apply.
* Add: Access Lists
* Add: Reload for the ServiceControlUI after pressing Apply
* Fix: Small constraint to handle added. Handles have to start at least with / when they're not empty, or Caddy won't start
1.3.0
1.3.0 - Initial release
* Initial first stable release.
* Create easy Reverse Proxy entries, use the HTTP or DNS-01 Challenge to get quick and easy ACME Certificates with Let's Encrypt.
* Control Caddy and view the Logfile in the OPNsense GUI.
* Create more complicated nested handle structures with wildcard domains, subdomains, handles and catch all handles.
* Use your own certificates where needed.
* Use TLS, NTLM and CA certificates to communicate with your Backend Servers.
* Use Caddy with two OPNsense Firewalls in HA - only fully supported with custom certificates from OPNsense trust store.
* Create easy Reverse Proxy entries, use the HTTP or DNS-01 Challenge to get quick and easy ACME Certificates with Let's Encrypt
* Control Caddy and view the Logfile in the OPNsense GUI
* Create more complicated nested handle structures with wildcard domains, subdomains, handles and catch all handles
* Use your own certificates where needed
* Use TLS, NTLM and CA certificates to communicate with your Backend Servers
* Use Caddy with two OPNsense Firewalls in HA - only fully supported with custom certificates from OPNsense trust store

View file

@ -210,6 +210,39 @@ class ReverseProxyController extends ApiMutableModelControllerBase
}
// 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()

View file

@ -41,6 +41,7 @@ class ReverseProxyController extends IndexController
$this->view->formDialogReverseProxy = $this->getForm("dialogReverseProxy");
$this->view->formDialogSubdomain = $this->getForm("dialogSubdomain");
$this->view->formDialogHandle = $this->getForm("dialogHandle");
$this->view->formDialogLayer4 = $this->getForm("dialogLayer4");
$this->view->formDialogAccessList = $this->getForm("dialogAccessList");
$this->view->formDialogBasicAuth = $this->getForm("dialogBasicAuth");
$this->view->formDialogHeader = $this->getForm("dialogHeader");

View file

@ -0,0 +1,64 @@
<form>
<field>
<id>layer4.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help><![CDATA[Enable this Layer4 route.]]></help>
</field>
<field>
<id>layer4.FromDomain</id>
<label>Domain</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[Enter one or multiple domains to route via SNI or Host Header. The domain will be added to the "listener_wrapper". Essentially, all traffic that Caddy receives on its frontend listeners (most likely the default HTTP and HTTPS ports) is funnelled through this wrapper to be processed and routed. Wildcard domains are allowed, e.g. "*.example.com". Host wildcards are allowed too, e.g. "*".]]></help>
</field>
<field>
<id>layer4.Matchers</id>
<label>Matchers</label>
<type>dropdown</type>
<help><![CDATA[Match the traffic of the selected domains. The TCP/UDP packets will be routed to the selected upstream domains without terminating TLS or altering the traffic. Only protocols that send a "Client Hello" (like TLS), or a "Host Header" (like HTTP) can be routed here. Routing Precedence: 1. "Host", 2. "SNI", 3. "not SNI", 4. "HTTP Handlers" (hidden default route for all unmatched traffic). The "not" operator will invert the match of the selected domains.]]></help>
</field>
<field>
<id>layer4.ToDomain</id>
<label>Upstream Domain</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help><![CDATA[Enter a domain name or IP address of the upstream destination. If multiple are chosen, they will be load balanced with the default random policy.]]></help>
</field>
<field>
<id>layer4.ToPort</id>
<label>Upstream Port</label>
<type>text</type>
<help><![CDATA[Choose a custom port for the upstream destination.]]></help>
</field>
<field>
<id>layer4.description</id>
<label>Description</label>
<type>text</type>
<help><![CDATA[Enter a description for this Layer4 route.]]></help>
</field>
<field>
<type>header</type>
<label>Health Check</label>
<collapse>true</collapse>
</field>
<field>
<id>layer4.PassiveHealthFailDuration</id>
<label>Upstream Fail Duration</label>
<type>text</type>
<help><![CDATA[Enables a passive health check when multiple destinations in "Upstream Domain" are set. "Fail Duration" is a value that defines how long to remember a failed request. A duration of 1 or more seconds enables passive health checking; the default is empty (off). A reasonable starting point might be 30s to balance error rates with responsiveness when bringing an unhealthy upstream back online.]]></help>
</field>
<field>
<type>header</type>
<label>Proxy Protocol</label>
<collapse>true</collapse>
</field>
<field>
<id>layer4.ProxyProtocol</id>
<label>Proxy Protocol</label>
<type>dropdown</type>
<help><![CDATA[Add the HA Proxy Protocol header. Either version 1 or 2 can be chosen. The default is off, since it is only needed when the upstream can use the Proxy Protocol header.]]></help>
</field>
</form>

View file

@ -5,6 +5,13 @@
<type>checkbox</type>
<help><![CDATA[Enable or disable the Caddy web server.]]></help>
</field>
<field>
<id>caddy.general.EnableLayer4</id>
<label>(Feature Preview) Enable Layer4</label>
<type>checkbox</type>
<help><![CDATA[Enable Layer4 support to stream TCP/UDP. WARNING: This feature is in active developement and the configuration can break or change in the future. The Layer4 app matches before the HTTP app. Deactivating this option will remove all Layer4 functionality completely. More information: https://github.com/mholt/caddy-l4]]></help>
<advanced>true</advanced>
</field>
<field>
<id>caddy.general.HttpPort</id>
<label>HTTP Port</label>

View file

@ -1,13 +1,14 @@
<model>
<mount>//Pischem/caddy</mount>
<description>A GUI model for configuring a reverse proxy in the Caddy web server.</description>
<version>1.2.2</version>
<description>Caddy Reverse Proxy</description>
<version>1.3.0</version>
<items>
<general>
<enabled type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</enabled>
<EnableLayer4 type="BooleanField"/>
<HttpPort type="PortField"/>
<HttpsPort type="PortField"/>
<TlsEmail type="EmailField"/>
@ -386,6 +387,52 @@
</HeaderReplace>
<description type="DescriptionField"/>
</header>
<layer4 type="ArrayField">
<enabled type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</enabled>
<FromDomain type="HostnameField">
<Required>Y</Required>
<IpAllowed>N</IpAllowed>
<FqdnWildcardAllowed>Y</FqdnWildcardAllowed>
<HostWildcardAllowed>Y</HostWildcardAllowed>
<FieldSeparator>,</FieldSeparator>
<AsList>Y</AsList>
<ValidationMessage>Please enter one or multiple hostnames or FQDNs.</ValidationMessage>
</FromDomain>
<Matchers type="OptionField">
<Required>Y</Required>
<Default>tlssni</Default>
<OptionValues>
<httphost>Host (HTTP)</httphost>
<tlssni>SNI (TLS)</tlssni>
<nottlssni>not SNI (TLS)</nottlssni>
</OptionValues>
</Matchers>
<ToDomain type="HostnameField">
<Required>Y</Required>
<FieldSeparator>,</FieldSeparator>
<AsList>Y</AsList>
<ValidationMessage>Please enter one or multiple valid IP addresses, hostnames or FQDNs.</ValidationMessage>
</ToDomain>
<ToPort type="PortField">
<Required>Y</Required>
</ToPort>
<PassiveHealthFailDuration type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>100</MaximumValue>
<ValidationMessage>Please enter a value between 1 to 100.</ValidationMessage>
</PassiveHealthFailDuration>
<ProxyProtocol type="OptionField">
<BlankDesc>Off (default)</BlankDesc>
<OptionValues>
<v1>v1</v1>
<v2>v2</v2>
</OptionValues>
</ProxyProtocol>
<description type="DescriptionField"/>
</layer4>
</reverseproxy>
</items>
</model>

View file

@ -69,6 +69,7 @@
// These fields do not need the validation workaround, they get their validation messages from core.
let validationExceptions = [
"caddy.general.enabled",
"caddy.general.EnableLayer4",
"caddy.general.DisableSuperuser",
"caddy.general.HttpPort",
"caddy.general.HttpsPort",

View file

@ -61,6 +61,15 @@
}
});
$("#reverseLayer4Grid").UIBootgrid({
search:'/api/caddy/ReverseProxy/searchLayer4/',
get:'/api/caddy/ReverseProxy/getLayer4/',
set:'/api/caddy/ReverseProxy/setLayer4/',
add:'/api/caddy/ReverseProxy/addLayer4/',
del:'/api/caddy/ReverseProxy/delLayer4/',
toggle:'/api/caddy/ReverseProxy/toggleLayer4/',
});
$("#reverseHandleGrid").UIBootgrid({
search:'/api/caddy/ReverseProxy/searchHandle/',
get:'/api/caddy/ReverseProxy/getHandle/',
@ -152,10 +161,11 @@
onAction: function(data, status) {
// Check if the action was successful
if (status === "success" && data && data['status'].toLowerCase() === 'ok') {
// Update only the service control UI for 'caddy'
showAlert("{{ lang._('Configuration applied successfully.') }}", "{{ lang._('Apply Success') }}");
// Update the service control UI for 'caddy'
updateServiceControlUI('caddy');
checkAndToggleSubdomainsTab();
// Update the Tab visibility
initializeTabs()
} else {
console.error("{{ lang._('Action was not successful or an error occurred:') }}", data);
}
@ -214,7 +224,7 @@
toggleSelectPicker(currentTab);
});
// Add click event listener for "Add Upstream" button
// Add click event listener for "Add HTTP Handler" button
$("#addHandleBtn").on("click", function() {
if ($('#maintabs .active a').attr('href') === "#handlesTab") {
// Directly open the dialog if already in the Handles tab
@ -238,37 +248,44 @@
}
});
// Check and set the visibility of the Subdomains tab on initial load
checkAndToggleSubdomainsTab();
// Function to check and toggle Subdomains tab
function checkAndToggleSubdomainsTab() {
// Perform an API call to get data and check tabs' visibility on initial load
function initializeTabs() {
$.ajax({
url: '/api/caddy/ReverseProxy/getAllReverseDomains',
url: '/api/caddy/reverse_proxy/get',
type: 'GET',
dataType: 'json',
success: function(response) {
let hasWildcard = response.rows.some(domain => domain.domainPort.startsWith('*'));
toggleSubdomainsTab(hasWildcard);
// Check for wildcards in domains to toggle Subdomains tab
const hasWildcard = Object.values(response.caddy.reverseproxy.reverse).some(entry => entry.FromDomain.startsWith('*'));
toggleTabVisibility('#tab-subdomains', hasWildcard);
// Check if Layer 4 is enabled to toggle the Layer 4 tab
const enableLayer4 = response.caddy.general.EnableLayer4 === '1';
toggleTabVisibility('#tab-layer4', enableLayer4);
},
error: function() {
console.error("{{ lang._('Failed to load domain data from getAllReverseDomains') }}");
console.error("{{ lang._('Failed to load data from /api/caddy/reverse_proxy/get') }}");
}
});
}
// Function to show or hide the Subdomains tab
function toggleSubdomainsTab(visible) {
let subdomainsTab = $('#maintabs a[href="#subdomainsTab"]').parent();
// Generic function to show or hide a tab and switch to another tab if the current one is hidden
function toggleTabVisibility(tabSelector, visible) {
let tab = $(tabSelector);
if (visible) {
subdomainsTab.show();
tab.show();
} else {
subdomainsTab.hide();
if (subdomainsTab.hasClass('active')) {
$('#maintabs a[href="#domainsTab"]').tab('show');
tab.hide();
// Switch to 'Domains' tab if the currently active tab is being hidden
if (tab.hasClass('active')) {
$('#tab-domains a').tab('show');
}
}
}
// Initialize tabs on load
initializeTabs();
});
</script>
@ -284,14 +301,16 @@
font-size: 16px;
font-style: italic;
}
</style>
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#domainsTab">{{ lang._('Domains') }}</a></li>
<li><a data-toggle="tab" href="#subdomainsTab">{{ lang._('Subdomains') }}</a></li>
<li><a data-toggle="tab" href="#handlesTab">{{ lang._('Handlers') }}</a></li>
<li><a data-toggle="tab" href="#accessTab">{{ lang._('Access') }}</a></li>
<li><a data-toggle="tab" href="#headerTab">{{ lang._('Headers') }}</a></li>
<li id="tab-layer4" style="display: none;"><a data-toggle="tab" href="#layer4Tab">{{ lang._('Layer4 Routes') }}</a></li>
<li id="tab-domains" class="active"><a data-toggle="tab" href="#domainsTab">{{ lang._('Domains') }}</a></li>
<li id="tab-subdomains" style="display: none;"><a data-toggle="tab" href="#subdomainsTab">{{ lang._('Subdomains') }}</a></li>
<li id="tab-handlers"><a data-toggle="tab" href="#handlesTab">{{ lang._('HTTP Handlers') }}</a></li>
<li id="tab-access"><a data-toggle="tab" href="#accessTab">{{ lang._('HTTP Access') }}</a></li>
<li id="tab-headers"><a data-toggle="tab" href="#headerTab">{{ lang._('HTTP Headers') }}</a></li>
</ul>
<div class="tab-content content-box">
@ -300,7 +319,7 @@
<!-- Button group on the left -->
<div>
<button id="addDomainBtn" type="button" class="btn btn-secondary">{{ lang._('Step 1: Add Domain') }}</button>
<button id="addHandleBtn" type="button" class="btn btn-secondary">{{ lang._('Step 2: Add Upstream') }}</button>
<button id="addHandleBtn" type="button" class="btn btn-secondary">{{ lang._('Step 2: Add HTTP Handler') }}</button>
</div>
<!-- Selectpicker on the right -->
<select id="reverseFilter" class="selectpicker form-control" multiple data-live-search="true" data-width="348px" data-size="7" title="{{ lang._('Filter by Domain') }}">
@ -388,7 +407,7 @@
<!-- Handle Tab -->
<div id="handlesTab" class="tab-pane fade">
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Handlers') }}</h1>
<h1 class="custom-header">{{ lang._('HTTP Handlers') }}</h1>
<div style="display: block;"> <!-- Common container -->
<table id="reverseHandleGrid" class="table table-condensed table-hover table-striped" data-editDialog="DialogHandle" data-editAlert="ConfigurationChangeMessage">
<thead>
@ -498,7 +517,7 @@
<!-- Header Tab -->
<div id="headerTab" class="tab-pane fade">
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Headers') }}</h1>
<h1 class="custom-header">{{ lang._('HTTP Headers') }}</h1>
<div style="display: block;"> <!-- Common container -->
<table id="reverseHeaderGrid" class="table table-condensed table-hover table-striped" data-editDialog="DialogHeader" data-editAlert="ConfigurationChangeMessage">
<thead>
@ -527,6 +546,42 @@
</div>
</div>
</div>
<!-- Layer4 Tab -->
<div id="layer4Tab" class="tab-pane fade">
<div style="padding-left: 16px;">
<h1 class="custom-header">{{ lang._('Layer4 Routes') }}</h1>
<div style="display: block;"> <!-- Common container -->
<table id="reverseLayer4Grid" class="table table-condensed table-hover table-striped" data-editDialog="DialogLayer4" data-editAlert="ConfigurationChangeMessage">
<thead>
<tr>
<th data-column-id="uuid" data-type="string" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="enabled" data-width="6em" data-type="boolean" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
<th data-column-id="FromDomain" data-type="string">{{ lang._('Domain') }}</th>
<th data-column-id="Matchers" data-type="string">{{ lang._('Matcher') }}</th>
<th data-column-id="ToDomain" data-type="string">{{ lang._('Upstream Domain') }}</th>
<th data-column-id="ToPort" data-type="string">{{ lang._('Upstream Port') }}</th>
<th data-column-id="PassiveHealthFailDuration" data-type="string" data-visible="false">{{ lang._('Fail Duration') }}</th>
<th data-column-id="ProxyProtocol" data-type="string" data-visible="false">{{ lang._('Proxy Protocol') }}</th>
<th data-column-id="description" data-type="string">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button id="addReverseLayer4Btn" data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<!-- Reconfigure Button -->
@ -551,9 +606,10 @@
</div>
</section>
{{ partial("layout_partials/base_dialog",['fields':formDialogReverseProxy,'id':'DialogReverseProxy','label':lang._('Edit Reverse Proxy Domain')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogSubdomain,'id':'DialogSubdomain','label':lang._('Edit Reverse Proxy Subdomain')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogHandle,'id':'DialogHandle','label':lang._('Edit Handler')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogReverseProxy,'id':'DialogReverseProxy','label':lang._('Edit Domain')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogSubdomain,'id':'DialogSubdomain','label':lang._('Edit Subdomain')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogHandle,'id':'DialogHandle','label':lang._('Edit HTTP Handler')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogAccessList,'id':'DialogAccessList','label':lang._('Edit Access List')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogBasicAuth,'id':'DialogBasicAuth','label':lang._('Edit Basic Auth')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogHeader,'id':'DialogHeader','label':lang._('Edit Header')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogHeader,'id':'DialogHeader','label':lang._('Edit HTTP Header')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogLayer4,'id':'DialogLayer4','label':lang._('Edit Layer4 Route')])}}

View file

@ -78,9 +78,11 @@
#}
{% set accessListUuid = generalSettings.accesslist %}
{% set logCredentials = generalSettings.LogCredentials %}
{% set enableLayer4 = generalSettings.EnableLayer4 %}
{% set hasAccessList = false %}
{% set hasLogCredentials = false %}
{% set hasEnableLayer4 = false %}
{% if accessListUuid %}
{% set accessList = helpers.toList('Pischem.caddy.reverseproxy.accesslist') | selectattr('@uuid', 'equalto', accessListUuid) | first %}
@ -93,7 +95,11 @@
{% set hasLogCredentials = true %}
{% endif %}
{% if hasAccessList or hasLogCredentials %}
{% if enableLayer4 == '1' %}
{% set hasEnableLayer4 = true %}
{% endif %}
{% if hasAccessList or hasLogCredentials or hasEnableLayer4 %}
servers {
{% if hasAccessList %}
trusted_proxies static {{ accessList.clientIps.split(',') | join(' ') }}
@ -101,6 +107,13 @@
{% if hasLogCredentials %}
log_credentials
{% endif %}
{% if hasEnableLayer4 %}
listener_wrappers {
{# Plug the Layer 4 template in #}
{% include "OPNsense/Caddy/includeLayer4" %}
}
{% endif %}
}
{% endif %}
@ -234,6 +247,32 @@
# Reverse Proxy Configuration
{#
# When Layer4 is active, set default ports.
# Caddy does not set them up automatically under certain conditions.
# For example when AutoHTTPS would be off, or no domains have been configured.
# There are no regressions to set these ports up.
#}
{% if enableLayer4|default("0") == "1" %}
# Layer4 default HTTP port
{% if httpPort %}
:{{ httpPort }} {
}
{% else %}
:80 {
}
{% endif %}
# Layer4 default HTTPS port
{% if httpsPort %}
:{{ httpsPort }} {
}
{% else %}
:443 {
}
{% endif %}
{% endif %}
{#
# Section: HTTP-01 Challenge Redirection
# Purpose: A small premade reverse_proxy section

View file

@ -0,0 +1,57 @@
{#
# This file sets up the listener_wrapper for layer 4 routing support.
# - Section: Servers Global Configuration
# Also allows for custom configurations with the import statement.
#}
{% set layer4_configs = helpers.toList('Pischem.caddy.reverseproxy.layer4') %}
{# Define a macro for setting up the proxy #}
{% macro setup_proxy(to_domains, to_port, fail_duration, proxy_protocol) %}
proxy {% for domain in to_domains.split(',') %}
{% set is_ipv6 = (':' in domain) %} {# Check if the domain contains a colon, typical in IPv6 addresses #}
{{ '[' if is_ipv6 }}{{ domain }}{{ ']' if is_ipv6 }}:{{ to_port }}{% if not loop.last %} {% endif %}
{% endfor %} {
{% if fail_duration %}
fail_duration {{ fail_duration }}s
{% endif %}
{% if proxy_protocol %}
proxy_protocol {{ proxy_protocol }}
{% endif %}
}
{% endmacro %}
{# Set up Layer4 App #}
layer4 {
import /usr/local/etc/caddy/caddy.d/*.layer4
{# 1. loop to handle http host matchers #}
{% for layer4 in layer4_configs %}
{% if layer4.enabled == "1" and layer4.Matchers == 'httphost' %}
@{{ layer4['@uuid'] }} http host {{ layer4.FromDomain.replace(',', ' ') }}
route @{{ layer4['@uuid'] }} {
{{ setup_proxy(layer4.ToDomain, layer4.ToPort, layer4.PassiveHealthFailDuration, layer4.ProxyProtocol) }}
}
{% endif %}
{% endfor %}
{# 2. loop to handle tls sni matchers #}
{% for layer4 in layer4_configs %}
{% if layer4.enabled == "1" and layer4.Matchers == 'tlssni' %}
@{{ layer4['@uuid'] }} tls sni {{ layer4.FromDomain.replace(',', ' ') }}
route @{{ layer4['@uuid'] }} {
{{ setup_proxy(layer4.ToDomain, layer4.ToPort, layer4.PassiveHealthFailDuration, layer4.ProxyProtocol) }}
}
{% endif %}
{% endfor %}
{# 3. loop to handle not tls sni matchers #}
{% for layer4 in layer4_configs %}
{% if layer4.enabled == "1" and layer4.Matchers == 'nottlssni' %}
@{{ layer4['@uuid'] }} not tls sni {{ layer4.FromDomain.replace(',', ' ') }}
route @{{ layer4['@uuid'] }} {
{{ setup_proxy(layer4.ToDomain, layer4.ToPort, layer4.PassiveHealthFailDuration, layer4.ProxyProtocol) }}
}
{% endif %}
{% endfor %}
{# Empty Route that catches all other traffic #}
route
}
{# Route all other traffic to HTTP App #}
tls