Merge branch 'master' into fix/DNSHostinger

This commit is contained in:
Leandro Scardua 2026-04-04 14:50:57 +00:00 committed by GitHub
commit d814f18965
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
133 changed files with 2750 additions and 2110 deletions

28
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,28 @@
**Important notices**
Before you submit a pull request, we ask you kindly to acknowledge the following:
- [ ] I have read the contributing guidelines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md
- [ ] I opened an issue first for non-trivial changes and linked it below.
- [ ] AI tools were used to create at least part of the code submitted herewith.
If AI was used, please disclose:
- Model used:
- Extent of AI involvement:
---
**Related issue**
If this pull request relates to an issue, link it here:
---
**Describe the problem**
A clear and concise description of the problem this pull request addresses.
---
**Describe the proposed solution**
Explain what this pull request changes and why.
---

View file

@ -47,3 +47,16 @@ When creating pull request, please heed the following:
* Code review may ensue in order to help shape your proposal
* Pull request must adhere to 2-Clause BSD licensing
* Explain the problem and your proposed solution
New plugins
-----------
The pull request notes apply, but with the following additional points:
* Open an issue first to explain what you want to work on and give it time for discussion
* If you are integrating a service binary it should at least be available in FreeBSD ports
* Precompiled binaries in the plugins are not allowed
* Plugins should almost always focus on integrating an existing service and providing MVC/API GUI pages for it
* It is not possible to review and integrate plugins with a large initial codebase
* If you use AI tools in your submission please disclose their use (name and model)
* Even though you are the maintainer you effectively force burden of maintainership to the community and OPNsense developers as soon as you open your first PR

View file

@ -12,7 +12,7 @@ Copyright (c) 2021 Axelrtgs
Copyright (c) 2026 Benno Kutschenreuter
Copyright (c) 2023 Bernhard Frenking <bernhard@frenking.eu>
Copyright (c) 2023 Cannon Matthews <cannonmatthews@google.com>
Copyright (c) 2023-2025 Cedrik Pischem
Copyright (c) 2023-2026 Cedrik Pischem
Copyright (c) 2025 Christopher Linn, BackendMedia IT-Services GmbH
Copyright (c) 2005-2006 Colin Smith <ethethlay@gmail.com>
Copyright (c) 2021 Dan Lundqvist
@ -31,7 +31,7 @@ Copyright (c) 2019 Felix Matouschek <felix@matouschek.org>
Copyright (c) 2025 Florian Latifi
Copyright (c) 2024 Francisco Dimattia <info@tecnoservicio.com.ar>
Copyright (c) 2014-2025 Franco Fichtner <franco@opnsense.org>
Copyright (c) 2016-2025 Frank Wall
Copyright (c) 2016-2026 Frank Wall
Copyright (c) 2021 Github-jjw
Copyright (c) 2023 Greg Glockner <greg@glockners.net>
Copyright (c) 2024 Hasan Ucak <hasan@sunnyvalley.io>

View file

@ -1,6 +1,6 @@
PLUGIN_NAME= redis
PLUGIN_VERSION= 1.1
PLUGIN_REVISION= 3
PLUGIN_REVISION= 4
PLUGIN_COMMENT= Redis DB
PLUGIN_DEPENDS= redis72
PLUGIN_MAINTAINER= franz.fabian.94@gmail.com

View file

@ -23,6 +23,7 @@ Plugin Changelog
1.1
* Add a button to reset all databases (contributed by Michael Muenz)
* Fix service widget behaviour (contributed by sevengiants)
1.0
@ -31,6 +32,3 @@ Plugin Changelog
* Allow password protection
* Connection limits
* Performance monitoring of slow connections
WWW: http://redis.io/

View file

@ -34,12 +34,11 @@ $( document ).ready(function() {
mapDataToFormUI(data_get_map).done(function(){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// request service status on load and update status box
ajaxCall(url="/api/redis/service/status", sendData={}, callback=function(data,status) {
updateServiceStatusUI(data['status']);
});
});
// request service status on load and update status box
updateServiceControlUI('redis');
// update history on tab state and implement navigation
if(window.location.hash != "") {
$('a[href="' + window.location.hash + '"]').click()
@ -73,9 +72,7 @@ $( document ).ready(function() {
draggable: true
});
} else {
ajaxCall(url="/api/redis/service/status", sendData={}, callback=function(data,status) {
updateServiceStatusUI(data['status']);
});
updateServiceControlUI('redis');
}
});
});

View file

@ -1,5 +1,6 @@
PLUGIN_NAME= ddclient
PLUGIN_VERSION= 1.30
PLUGIN_REVISION= 2
PLUGIN_DEPENDS= ddclient py${PLUGIN_PYTHON}-boto3
PLUGIN_COMMENT= Dynamic DNS client
PLUGIN_MAINTAINER= ad@opnsense.org

View file

@ -9,6 +9,8 @@ Plugin Changelog
1.30
* Add native backend support for Hostinger (contributed by Leandro Scardua)
* Fix Hetzner existing record update (contributed by Julian Nikodemus)
* Fix PowerDNS URL validation
1.29

View file

@ -62,7 +62,7 @@
<id>account.zone</id>
<label>Zone</label>
<type>text</type>
<style>optional_setting service_aws service_zoneedit1 service_cloudflare service_nsupdate service_gandi service_godaddy service_nfsn service_hetzner service_digitalocean service_dnspodcn service_hostinger</style>
<style>optional_setting service_aws service_zoneedit1 service_cloudflare service_nsupdate service_gandi service_godaddy service_nfsn service_hetzner service_digitalocean service_dnspodcn service_allinkl</style>
<help>Zone containing the host entry.</help>
</field>
<field>

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2021-2023 Deciso B.V.
* Copyright (C) 2021-2026 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -41,6 +41,7 @@ class DynDNS extends BaseModel
{
$messages = parent::performValidation($validateFullModel);
$validate_servers = [];
foreach ($this->getFlatNodes() as $key => $node) {
$tagName = $node->getInternalXMLTagName();
$parentNode = $node->getParentNode();
@ -51,27 +52,24 @@ class DynDNS extends BaseModel
}
}
}
foreach ($validate_servers as $key => $node) {
if ((string)$node->service == 'powerdns') {
if (empty($srv) || filter_var($srv, FILTER_VALIDATE_URL) === false) {
$messages->appendMessage(
new Message(
gettext("A valid URI is required."),
$key . ".server"
)
);
}
}
if ((string)$node->service != 'custom') {
$validate_url = false;
if ($node->service->isEqual('powerdns')) {
$validate_url = true;
} elseif (!$node->service->isEqual('custom')) {
continue;
}
$srv = (string)$node->server;
if (in_array((string)$node->protocol, ['get', 'post', 'put'])) {
if (in_array((string)$node->protocol, ['get', 'post', 'put']) || $validate_url) {
if (empty($srv) || filter_var($srv, FILTER_VALIDATE_URL) === false) {
$messages->appendMessage(
new Message(
gettext("A valid URI is required."),
$key . ".server"
gettext('A valid URI is required.'),
$key . '.server'
)
);
}
@ -79,13 +77,14 @@ class DynDNS extends BaseModel
if (empty($srv) || filter_var($srv, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) {
$messages->appendMessage(
new Message(
gettext("A valid domain is required."),
$key . ".server"
gettext('A valid domain is required.'),
$key . '.server'
)
);
}
}
}
return $messages;
}
}

View file

@ -0,0 +1,337 @@
"""
Copyright (c) 2026 Carsten Kallies
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
all-inkl.com KAS API DynDNS provider for OPNsense ddclient.
Uses the KAS SOAP API (KasApi.wsdl) to update A/AAAA records.
API endpoint: https://kasapi.kasserver.com/soap/KasApi.php
WSDL: https://kasapi.kasserver.com/soap/wsdl/KasApi.wsdl
UI fields:
username - KAS login (all-inkl username, e.g. "w0xxxxx")
password - KAS password (plaintext, transmitted over HTTPS)
hostnames - FQDN(s) to update, comma-separated (e.g. "example.com,*.example.com")
zone - DNS zone (e.g. "example.com"); derived from hostname if left empty
"""
import json
import syslog
import time
import xml.etree.ElementTree as ET
import requests
from . import BaseAccount
class AllInkl(BaseAccount):
"""all-inkl.com DynDNS via KAS SOAP API (KasApi)."""
_priority = 65535
_services = {
'allinkl': 'kasapi.kasserver.com'
}
_URL = 'https://kasapi.kasserver.com/soap/KasApi.php'
_ACTION = '"urn:xmethodsKasApi#KasApi"'
def __init__(self, account: dict):
super().__init__(account)
@staticmethod
def known_services():
return {'allinkl': 'all-inkl.com (KAS API)'}
@staticmethod
def match(account):
return account.get('service') in AllInkl._services
# ------------------------------------------------------------------
# SOAP / KAS helpers
# ------------------------------------------------------------------
def _build_envelope(self, params_dict):
"""Build a KasApi SOAP envelope. params_dict is JSON-serialised into <Params>."""
params_json = json.dumps(params_dict)
# Escape XML special characters in the JSON string
params_json = (params_json
.replace('&', '&amp;')
.replace('<', '&lt;')
.replace('>', '&gt;'))
return (
'<?xml version="1.0" encoding="utf-8"?>'
'<SOAP-ENV:Envelope'
' xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'
' xmlns:ns1="urn:xmethodsKasApi"'
' xmlns:xsd="http://www.w3.org/2001/XMLSchema"'
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
' xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"'
' SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
'<SOAP-ENV:Body>'
'<ns1:KasApi>'
'<Params xsi:type="xsd:string">' + params_json + '</Params>'
'</ns1:KasApi>'
'</SOAP-ENV:Body>'
'</SOAP-ENV:Envelope>'
)
def _kas_api(self, action, request_params):
"""Execute a KAS API action. Returns response text or None on failure."""
params = {
'kas_login': self.settings.get('username', ''),
'kas_auth_type': 'plain',
'kas_auth_data': self.settings.get('password', ''),
'kas_action': action,
'KasRequestParams': request_params,
}
envelope = self._build_envelope(params)
if self.is_verbose:
syslog.syslog(
syslog.LOG_NOTICE,
"Account %s KAS action '%s' params: %s" % (
self.description, action, json.dumps(request_params)
)
)
try:
resp = requests.post(
self._URL,
data=envelope.encode('utf-8'),
headers={
'Content-Type': 'text/xml; charset=utf-8',
'SOAPAction': self._ACTION,
'User-Agent': 'OPNsense-dyndns',
},
timeout=30
)
except requests.RequestException as exc:
syslog.syslog(
syslog.LOG_ERR,
"Account %s KAS request failed: %s" % (self.description, exc)
)
return None
if self.is_verbose:
syslog.syslog(
syslog.LOG_NOTICE,
"Account %s KAS '%s' HTTP %d: %s" % (
self.description, action, resp.status_code, resp.text[:600]
)
)
try:
root = ET.fromstring(resp.text)
except ET.ParseError:
syslog.syslog(
syslog.LOG_ERR,
"Account %s KAS '%s' invalid XML response: %s" % (
self.description, action, resp.text[:200]
)
)
return None
fault = root.find('.//{*}Fault')
if fault is not None:
faultstring = fault.find('{*}faultstring') or fault.find('faultstring')
syslog.syslog(
syslog.LOG_ERR,
"Account %s KAS '%s' SOAP fault: %s" % (
self.description, action,
faultstring.text if faultstring is not None else resp.text[:200]
)
)
return None
return resp.text
# ------------------------------------------------------------------
# Response parsing
# ------------------------------------------------------------------
def _find_record_id(self, xml_text, record_label, record_type):
"""
Parse get_dns_settings response and return record_id for the matching
record_name / record_type, or None.
The KAS response contains ns2:Map items with key/value pairs:
<item><key ...>record_name</key><value ...>dyn</value></item>
<item><key ...>record_type</key><value ...>A</value></item>
<item><key ...>record_id</key><value ...>12345</value></item>
"""
root = ET.fromstring(xml_text)
for map_item in root.findall('.//{*}KasApiResponse//{*}item'):
kv = {}
for sub in map_item.findall('{*}item'):
key_el = sub.find('{*}key')
value_el = sub.find('{*}value')
if key_el is not None and value_el is not None:
kv[key_el.text or ''] = value_el.text or ''
if kv.get('record_name') == record_label and kv.get('record_type') == record_type:
return kv.get('record_id')
return None
# ------------------------------------------------------------------
# Zone / label helpers
# ------------------------------------------------------------------
def _get_zone(self, hostname):
"""Return the DNS zone for a hostname (from config or derived)."""
zone = self.settings.get('zone', '').strip().rstrip('.')
if zone:
return zone
parts = hostname.split('.')
if len(parts) > 2:
return '.'.join(parts[1:])
return hostname
def _get_label(self, hostname, zone):
"""Return the record label (left of zone) for a hostname.
Examples:
dyn.example.com / zone example.com 'dyn'
*.example.com / zone example.com '*'
example.com / zone example.com '' (root record)
"""
if hostname == zone:
return ''
if hostname.endswith('.' + zone):
return hostname[:-len(zone) - 1]
return hostname.split('.')[0]
# ------------------------------------------------------------------
# Main entry point
# ------------------------------------------------------------------
def execute(self):
if not super().execute():
return False
record_type = "AAAA" if ':' in str(self.current_address) else "A"
hostnames_raw = self.settings.get('hostnames', '')
hostnames = [h.strip() for h in hostnames_raw.split(',') if h.strip()]
if not hostnames:
syslog.syslog(
syslog.LOG_ERR,
"Account %s no hostnames configured" % self.description
)
return False
all_success = True
last_zone = None
dns_response = None
for hostname in hostnames:
zone = self._get_zone(hostname)
zone_host = zone + '.'
label = self._get_label(hostname, zone)
if self.is_verbose:
syslog.syslog(
syslog.LOG_NOTICE,
"Account %s updating %s (zone: %s, label: '%s', type: %s) → %s" % (
self.description, hostname, zone_host,
label, record_type, self.current_address
)
)
# Fetch DNS records once per zone (cache for multiple hostnames in same zone)
if zone != last_zone:
dns_response = self._kas_api('get_dns_settings', {'zone_host': zone_host})
last_zone = zone
if dns_response is None:
syslog.syslog(
syslog.LOG_ERR,
"Account %s failed to retrieve DNS settings for %s" % (
self.description, zone_host
)
)
all_success = False
continue
# Respect KasFloodDelay between consecutive API calls
time.sleep(2)
record_id = self._find_record_id(dns_response, label, record_type)
if record_id is None:
syslog.syslog(
syslog.LOG_ERR,
"Account %s record '%s' type %s not found in zone %s" % (
self.description, label, record_type, zone_host
)
)
all_success = False
continue
if self.is_verbose:
syslog.syslog(
syslog.LOG_NOTICE,
"Account %s found record_id %s for label '%s' %s" % (
self.description, record_id, label, record_type
)
)
update_response = self._kas_api('update_dns_settings', {
'zone_host': zone_host,
'record_id': record_id,
'record_name': label,
'record_type': record_type,
'record_data': str(self.current_address),
'record_aux': '0',
})
if update_response is None:
all_success = False
continue
update_root = ET.fromstring(update_response)
if any(v.text and v.text.upper() == 'TRUE'
for v in update_root.findall('.//{*}value')):
syslog.syslog(
syslog.LOG_NOTICE,
"Account %s set new IP %s for %s" % (
self.description, self.current_address, hostname
)
)
else:
syslog.syslog(
syslog.LOG_ERR,
"Account %s update_dns_settings failed for %s: %s" % (
self.description, hostname, update_response[:300]
)
)
all_success = False
time.sleep(2)
if all_success:
self.update_state(address=self.current_address)
return True
return False

View file

@ -34,7 +34,23 @@ import requests
from . import BaseAccount
class Hetzner(BaseAccount):
class HetznerAccount(BaseAccount):
def _extract_record_name(self, hostname, zone_name):
"""Extract record name from hostname, handling FQDN format"""
hostname = hostname.rstrip('.')
if hostname.endswith('.' + zone_name):
record_name = hostname[:-len(zone_name) - 1]
elif hostname == zone_name:
record_name = '@'
else:
record_name = hostname
if not record_name or record_name == '@':
record_name = '@'
return record_name
class Hetzner(HetznerAccount):
"""
Hetzner Cloud DNS API provider
Uses the new Cloud API (api.hetzner.cloud)
@ -115,52 +131,44 @@ class Hetzner(BaseAccount):
return zone_id
def _get_record(self, headers, zone_id, record_name, record_type):
"""Get existing record by name and type"""
def _delete_record(self, headers, zone_id, record_name, record_type):
"""Delete existing record"""
url = f"{self._api_base}/zones/{zone_id}/rrsets/{record_name}/{record_type}"
response = requests.get(url, headers=headers)
if response.status_code == 404:
return None
if response.status_code != 200:
response = requests.delete(url, headers=headers)
if response.status_code not in [200, 201, 204]:
syslog.syslog(
syslog.LOG_ERR,
"Account %s error fetching record: HTTP %d - %s" % (
"Account %s error deleting record for update: HTTP %d - %s" % (
self.description, response.status_code, response.text
)
)
return None
try:
payload = response.json()
return payload.get('rrset')
except requests.exceptions.JSONDecodeError:
return False
if self.is_verbose:
syslog.syslog(
syslog.LOG_ERR,
"Account %s error parsing JSON response: %s" % (self.description, response.text)
syslog.LOG_NOTICE,
"Account %s deleted record: %s type: %s" % (
self.description, record_name, record_type
)
)
return None
return True
def _update_record(self, headers, zone_id, record_name, record_type, address):
"""Update existing record with new address"""
url = f"{self._api_base}/zones/{zone_id}/rrsets/{record_name}/{record_type}"
url = f"{self._api_base}/zones/{zone_id}/rrsets/{record_name}/{record_type}/actions/set_records"
data = {
'records': [{'value': str(address)}],
'ttl': int(self.settings.get('ttl', 300))
'records': [{
'value': str(address)
}]
}
response = requests.put(url, headers=headers, json=data)
if response.status_code != 200:
response = requests.post(url, headers=headers, json=data)
if response.status_code not in [200, 201]:
syslog.syslog(
syslog.LOG_ERR,
"Account %s error updating record: HTTP %d - %s" % (
self.description, response.status_code, response.text
)
)
return False
if self.is_verbose:
@ -205,22 +213,6 @@ class Hetzner(BaseAccount):
return True
def _extract_record_name(self, hostname, zone_name):
"""Extract record name from hostname, handling FQDN format"""
hostname = hostname.rstrip('.')
if hostname.endswith('.' + zone_name):
record_name = hostname[:-len(zone_name) - 1]
elif hostname == zone_name:
record_name = '@'
else:
record_name = hostname
if not record_name or record_name == '@':
record_name = '@'
return record_name
def execute(self):
if super().execute():
record_type = "AAAA" if ':' in str(self.current_address) else "A"
@ -253,14 +245,10 @@ class Hetzner(BaseAccount):
self.description, hostname, record_name, record_type, self.current_address
)
)
existing = self._get_record(headers, zone_id, record_name, record_type)
if existing:
success = self._update_record(
headers, zone_id, record_name, record_type, self.current_address
)
else:
success = self._update_record(
headers, zone_id, record_name, record_type, self.current_address
)
if not success:
success = self._create_record(
headers, zone_id, record_name, record_type, self.current_address
)
@ -282,7 +270,7 @@ class Hetzner(BaseAccount):
return False
class HetznerLegacy(BaseAccount):
class HetznerLegacy(HetznerAccount):
"""
Hetzner DNS Console (Legacy) API provider
Uses the old API at dns.hetzner.com - will be shut down May 2026
@ -468,22 +456,6 @@ class HetznerLegacy(BaseAccount):
return True
def _extract_record_name(self, hostname, zone_name):
"""Extract record name from hostname, handling FQDN format"""
hostname = hostname.rstrip('.')
if hostname.endswith('.' + zone_name):
record_name = hostname[:-len(zone_name) - 1]
elif hostname == zone_name:
record_name = '@'
else:
record_name = hostname
if not record_name or record_name == '@':
record_name = '@'
return record_name
def execute(self):
if super().execute():
record_type = "AAAA" if ':' in str(self.current_address) else "A"
@ -542,4 +514,4 @@ class HetznerLegacy(BaseAccount):
self.update_state(address=self.current_address)
return True
return False
return False

View file

@ -1,6 +1,6 @@
PLUGIN_NAME= dnscrypt-proxy
PLUGIN_VERSION= 1.16
PLUGIN_REVISION= 1
PLUGIN_REVISION= 2
PLUGIN_COMMENT= Flexible DNS proxy supporting DNSCrypt and DoH
PLUGIN_DEPENDS= dnscrypt-proxy2
PLUGIN_MAINTAINER= m.muenz@gmail.com

View file

@ -7,6 +7,7 @@ Plugin Changelog
1.16
* Fix ODoH servers not working (contributed by Pascal Herget)
* Fix bootstrap_resolvers with multiple comma-separated servers (contributed by Andrei Hodorog)
1.15

View file

@ -95,7 +95,7 @@ tls_disable_session_tickets = true
tls_disable_session_tickets = false
{% endif %}
bootstrap_resolvers = ['{{ OPNsense.dnscryptproxy.general.fallback_resolver }}']
bootstrap_resolvers = ['{{ OPNsense.dnscryptproxy.general.fallback_resolver.split(',') | join("','") }}']
{% if helpers.exists('OPNsense.dnscryptproxy.general.ignore_system_dns') and OPNsense.dnscryptproxy.general.ignore_system_dns == '1' %}
ignore_system_dns = true

View file

@ -1,5 +1,6 @@
PLUGIN_NAME= theme-cicada
PLUGIN_VERSION= 1.40
PLUGIN_VERSION= 1.41
PLUGIN_REVISION= 1
PLUGIN_COMMENT= The cicada theme - dark grey onyx
PLUGIN_MAINTAINER= rene@team-rebellion.net
PLUGIN_NO_ABI= yes

View file

@ -5370,7 +5370,7 @@ tbody.collapse.in {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
background-color: #191919;
}
> {
@ -10631,6 +10631,11 @@ ul.jqtree-tree {
border: 1px solid #191919 !important;
}
.rule.text-muted {
background-color: #242424 !important;
opacity: 0.4;
}
.rule.text-muted > td {
&:nth-child(1n+3) {
text-decoration: line-through;

View file

@ -3198,7 +3198,7 @@ tbody.collapse.in {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5; }
background-color: #191919; }
.dropdown-menu > li > a {
display: block;
padding: 3px 20px;
@ -6617,6 +6617,11 @@ ul.jqtree-tree .jqtree-title {
border: 1px solid #191919 !important;
}
.rule.text-muted {
background-color: #242424 !important;
opacity: 0.4;
}
.rule.text-muted > td:nth-child(1n+3) {
text-decoration: line-through;
}

View file

@ -48,7 +48,7 @@
}
.tabulator-row.tabulator-row-odd.tabulator-selectable:hover:not(.tabulator-selected) {
background-color: #242424;
background-color: #282828;
}
.tabulator-row.tabulator-row-even {
@ -56,7 +56,7 @@
}
.tabulator-row.tabulator-row-even.tabulator-selectable:hover:not(.tabulator-selected) {
background-color: #242424;
background-color: #282828;
}
.tabulator .tabulator-tableholder {
@ -210,11 +210,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
.tabulator-tableholder::after {
content: "";
display: block;

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= theme-flexcolor
PLUGIN_VERSION= 1.0
PLUGIN_VERSION= 1.1
PLUGIN_COMMENT= Theme with 3 different color schemes: black as default, light and dark-light
PLUGIN_MAINTAINER= iengels@web.de
PLUGIN_NO_ABI= yes

View file

@ -18,7 +18,7 @@
--stdborderprimary: #336CDF; /* standard border with accent*/
--stdborderinverse: #000000; /* standard border with accent*/
--stdborder50bright: #A1A1A1; /* standard border with accent*/
--badgeback: #E6E6E6; /* badge background & progress-bar & blockquote*/
--badgeback: #1F62C1; /* badge background & progress-bar & blockquote*/
--progressbar: #D4D4D4; /* progress-bar*/
--token: #AF3604; /* background token */
--highlighted: #FFFFFF; /* highlighted element */
@ -57,7 +57,7 @@
--txtboxbackhover: #000000;
--txtboxbackactive: #000000;
--txtboxbackdisabled: #000000;
--txtboxbacktoken: #E65C00;
--txtboxbacktoken: #1F62C1;
--txtboxbackdel: #FF5252; /* only tokenize, pending delete */
--txtboxbackdismiss: #F2F7FD; /* only tokenize, dismiss */
/* border */
@ -87,6 +87,8 @@
/* special characters */
--link: #FA6121; /* OPNsense text login and links */
--linkhover: #E04605; /* OPNsense text login and links hover */
--colorcheckbox: #388E3C; /* background color checkbox */
--colorradio: #FF3333; /* background color radio button */
/* accents */
--primary: #336CDF; /* primary accent */
--primaryhover: #608DE6; /* primary accent hover */
@ -97,7 +99,27 @@
--warning: #D66E12; /* warning accent */
--warninghover: #ED862B; /* warning accent hover */
--danger: #FF5252; /* danger accent */
--dangerhover: #BE2326FF8585; /* danger accent hover */
--dangerhover: #E64949; /* danger accent hover */
/* alert messages */
/* alert*/
--alertback: #000000;
--alertborder: transparent;
/* alert success */
--alertsuccessfore: #E6E6E6;
--alertsuccessback: #000000;
--alertsuccessborder: #36D93E;
/* alert info */
--alertinfofore: #E6E6E6;
--alertinfoback: #000000;
--alertinfoborder: #369DD9;
/* alert warning */
--alertwarningfore: #E6E6E6;
--alertwarningback: #000000;
--alertwarningborder: #D98236;
/* alert danger */
--alertdangerfore: #E6E6E6;
--alertdangerback: #000000;
--alertdangerborder: #D93636;
/* buttons (complete (independent) */
/* unselected */
--btnfore: #E6E6E6; /* textcolor */

View file

@ -18,7 +18,7 @@
--stdborderprimary: #FA6121; /* standard border with accent*/
--stdborderinverse: #647D9B; /* standard border with accent*/
--stdborder50bright: #A2B1C3; /* standard border with accent*/
--badgeback: #FFC59E; /* badge background & progress-bar & blockquote*/
--badgeback: #AD7A7A; /* badge background & progress-bar & blockquote*/
--progressbar: #181E25; /* progress-bar*/
--token: #FA6121; /* background token */
--highlighted: #FFFFFF; /* highlighted element */
@ -57,7 +57,7 @@
--txtboxbackhover: #F2F7FD;
--txtboxbackactive: #F2F7FD;
--txtboxbackdisabled: #181E25;
--txtboxbacktoken: #FFC59E;
--txtboxbacktoken: #AD7A7A;
--txtboxbackdel: #DB393D; /* only tokenize, pending delete */
--txtboxbackdismiss: #F2F7FD; /* only tokenize, dismiss */
/* border */
@ -87,6 +87,8 @@
/* special characters */
--link: #FA6121; /* OPNsense text login and links */
--linkhover: #E04605; /* OPNsense text login and links hover */
--colorcheckbox: #39E63C; /* background color checkbox */
--colorradio: #0089D0; /* background color radio button */
/* accents */
--primary: #FA6121; /* primary accent */
--primaryhover: #E04605; /* primary accent hover */
@ -98,6 +100,26 @@
--warninghover: #B87A00; /* warning accent hover */
--danger: #DB393D; /* danger accent */
--dangerhover: #BE2326; /* danger accent hover */
/* alert messages */
/* alert*/
--alertback: #181E25;
--alertborder: transparent;
/* alert success */
--alertsuccessfore: #00E604;
--alertsuccessback: #181E25;
--alertsuccessborder: #F2F7FD;
/* alert info */
--alertinfofore: #0099E6;
--alertinfoback: #181E25;
--alertinfoborder: #F2F7FD;
/* alert warning */
--alertwarningfore: #E69900;
--alertwarningback: #181E25;
--alertwarningborder: #F2F7FD;
/* alert danger */
--alertdangerfore: #E60004;
--alertdangerback: #181E25;
--alertdangerborder: #F2F7FD;
/* buttons (complete (independent) */
/* unselected */
--btnfore: #181E25; /* textcolor */

View file

@ -18,7 +18,7 @@
--stdborderprimary: #336CDF; /* standard border with accent*/
--stdborderinverse: #131313; /* standard border with accent*/
--stdborder50bright: #A1A1A1; /* standard border with accent*/
--badgeback: #FFC59E; /* badge background & progress-bar & blockquote*/
--badgeback: #8DA6BD; /* badge background & progress-bar & blockquote*/
--progressbar: #131313; /* progress-bar*/
--token: #71ADF4; /* background token */
--highlighted: #131313; /* highlighted element */
@ -57,7 +57,7 @@
--txtboxbackhover: #F5F5F5;
--txtboxbackactive: #F5F5F5;
--txtboxbackdisabled: #F5F5F5;
--txtboxbacktoken: #71ADF4;
--txtboxbacktoken: #8DA6BD;
--txtboxbackdel: #FC2529; /* only tokenize, pending delete */
--txtboxbackdismiss: #F5F5F5; /* only tokenize, dismiss */
/* border */
@ -87,6 +87,8 @@
/* special characters */
--link: #FA6121; /* OPNsense text login and links */
--linkhover: #E04605; /* OPNsense text login and links hover */
--colorcheckbox: #28A839; /* background color checkbox */
--colorradio: #1066CC; /* background color radio button */
/* accents */
--primary: #1066CC; /* primary accent */
--primaryhover: #0C4E9C; /* primary accent hover */
@ -98,6 +100,26 @@
--warninghover: #D97B03; /* warning accent hover */
--danger: #FC2529; /* danger accent */
--dangerhover: #EC0308; /* danger accent hover */
/* alert messages */
/* alert*/
--alertback: #F5F5F5;
--alertborder: transparent;
/* alert success */
--alertsuccessfore: #000000;
--alertsuccessback: #D4FAD9;
--alertsuccessborder: #28A839;
/* alert info */
--alertinfofore: #000000;
--alertinfoback: #D4F1FA;
--alertinfoborder: #10A6D8;
/* alert warning */
--alertwarningfore: #000000;
--alertwarningback: #F2E3CE;
--alertwarningborder: #FC9510;
/* alert danger */
--alertdangerfore: #000000;
--alertdangerback: #F2CECE;
--alertdangerborder: #FC2529;
/* buttons (complete (independent) */
/* unselected */
--btnfore: #454545; /* textcolor */

View file

@ -119,10 +119,14 @@ input::-moz-focus-inner {
padding: 0; }
input {
line-height: normal; }
input[type=checkbox],
input[type=checkbox] {
box-sizing: border-box;
padding: 0;
accent-color: var(--colorcheckbox); }
input[type=radio] {
box-sizing: border-box;
padding: 0; }
padding: 0;
accent-color: var(--colorradio); }
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
height: auto; }
@ -3996,14 +4000,11 @@ a.thumbnail:focus,
a.thumbnail.active {
border-color: var(--primary); }
.alert {
background-color: var(--pback);
color: var(--pfore);
padding: 15px;
background-color: var(--alertback);
margin-bottom: 20px;
border: 2px solid transparent;
border-radius: 3px;
-webkit-box-shadow: inset 0 1px 0 var(--boxshadow), 0 1px 0 var(--boxshadow);
box-shadow: inset 0 1px 0 var(--boxshadow), 0 1px 0 var(--boxshadow); }
border: 2px solid var(--alertborder);
border-radius: 3px; }
.alert h4 {
margin-top: 0;
color: inherit; }
@ -4024,25 +4025,33 @@ a.thumbnail.active {
right: -21px;
color: inherit; }
.alert-success {
border-color: var(--success); }
color: var(--alertsuccessfore);
background-color: var(--alertsuccessback);
border-color: var(--alertsuccessborder); }
.alert-success hr {
border-top-color: var(--pfore); }
.alert-success .alert-link {
color: var(--primary); }
.alert-info {
border-color: var(--info); }
color: var(--alertinfofore);
background-color: var(--alertinfoback);
border-color: var(--alertinfoborder); }
.alert-info hr {
border-top-color: var(--pfore); }
.alert-info .alert-link {
color: var(--info); }
.alert-warning {
border-color: var(--warning); }
color: var(--alertwarningfore);
background-color: var(--alertwarningback);
border-color: var(--alertwarningborder); }
.alert-warning hr {
border-top-color: var(--pfore); }
.alert-warning .alert-link {
color: var(--primary); }
.alert-danger {
border-color: var(--danger); }
color: var(--alertdangerfore);
background-color: var(--alertdangerback);
border-color: var(--alertdangerborder); }
.alert-danger hr {
border-top-color: var(--pfore); }
.alert-danger .alert-link {

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= theme-tukan
PLUGIN_VERSION= 1.30
PLUGIN_VERSION= 1.31
PLUGIN_COMMENT= The tukan theme - blue/white
PLUGIN_MAINTAINER= rene@team-rebellion.net
PLUGIN_NO_ABI= yes

View file

@ -40,7 +40,7 @@
.tabulator-row {
background-color: transparent;
color: #bac3ca;
color: #000;
}
.tabulator-row.tabulator-row-odd {
@ -210,11 +210,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
.tabulator-tableholder::after {
content: "";
display: block;

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= theme-vicuna
PLUGIN_VERSION= 1.50
PLUGIN_VERSION= 1.51
PLUGIN_COMMENT= The vicuna theme - blue sapphire
PLUGIN_MAINTAINER= rene@team-rebellion.net
PLUGIN_NO_ABI= yes

View file

@ -5481,7 +5481,7 @@ tbody.collapse.in {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5;
background-color: #191919;
}
> {
@ -5559,7 +5559,7 @@ tbody.collapse.in {
padding: 3px 20px;
font-size: 12px;
line-height: 1.428571429;
color: #191919;
color: #999;
white-space: nowrap;
}

View file

@ -3184,7 +3184,7 @@ tbody.collapse.in {
height: 1px;
margin: 9px 0;
overflow: hidden;
background-color: #e5e5e5; }
background-color: #191919; }
.dropdown-menu > li > a {
display: block;
padding: 3px 20px;
@ -3235,7 +3235,7 @@ tbody.collapse.in {
padding: 3px 20px;
font-size: 12px;
line-height: 1.428571429;
color: #191919;
color: #999;
white-space: nowrap; }
.dropdown-backdrop {

View file

@ -48,7 +48,7 @@
}
.tabulator-row.tabulator-row-odd.tabulator-selectable:hover:not(.tabulator-selected) {
background-color: #1b272f;
background-color: #1f2c35;
}
.tabulator-row.tabulator-row-even {
@ -56,7 +56,7 @@
}
.tabulator-row.tabulator-row-even.tabulator-selectable:hover:not(.tabulator-selected) {
background-color: #1b272f;
background-color: #1f2c35;
}
.tabulator .tabulator-tableholder {
@ -210,11 +210,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
.tabulator-tableholder::after {
content: "";
display: block;

File diff suppressed because one or more lines are too long

View file

@ -18,7 +18,7 @@ Plugin Changelog
1.12.13
* Implement memory_saving_mode formerly named enable_file_download (contributed by sopex)
* Implement memory_saving_mode formerly named enable_file_download (contributed by Konstantinos Spartalis)
1.12.12

View file

@ -1,6 +1,5 @@
PLUGIN_NAME= frr
PLUGIN_VERSION= 1.50
PLUGIN_REVISION= 1
PLUGIN_VERSION= 1.52
PLUGIN_COMMENT= The FRRouting Protocol Suite
PLUGIN_DEPENDS= frr10-pythontools
PLUGIN_MAINTAINER= ad@opnsense.org

View file

@ -12,6 +12,14 @@ WWW: https://frrouting.org/
Plugin Changelog
================
1.52
* Add BGP maximum-paths support for ECMP multipath (contributed by maxfield-allison) (opnsense/plugins#4878)
1.51
* Add per-neighbor local-as option for BGP (contributed by danohn) (opnsense/plugins/pull/5308)
1.50
* Add BGP remove-private-AS (contributed by rfrederick) (opnsense/plugins/pull/5090)

View file

@ -67,4 +67,18 @@
<type>checkbox</type>
<help>Enable extended logging of BGP neighbor changes.</help>
</field>
<field>
<id>bgp.maximumpaths</id>
<label>Maximum Paths</label>
<type>text</type>
<advanced>true</advanced>
<help>Maximum number of equal-cost paths for EBGP multipath (ECMP). Leave empty to use FRR default (1).</help>
</field>
<field>
<id>bgp.maximumpathsibgp</id>
<label>Maximum Paths (IBGP)</label>
<type>text</type>
<advanced>true</advanced>
<help>Maximum number of equal-cost paths for IBGP multipath (ECMP). Leave empty to use FRR default (1).</help>
</field>
</form>

View file

@ -36,6 +36,15 @@
<type>text</type>
<help>AS (Autonomous System) number of the neighbor, required for establishing a BGP session.</help>
</field>
<field>
<id>neighbor.localas</id>
<label>Local AS</label>
<type>text</type>
<help>Optional local AS to present to this specific neighbor.</help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
<field>
<id>neighbor.password</id>
<label>BGP MD5 Password</label>

View file

@ -48,6 +48,16 @@
</OptionValues>
<Multiple>Y</Multiple>
</bestpath>
<maximumpaths type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>128</MaximumValue>
<ValidationMessage>The value shall be between 1 and 128 or left empty to use the default.</ValidationMessage>
</maximumpaths>
<maximumpathsibgp type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>128</MaximumValue>
<ValidationMessage>The value shall be between 1 and 128 or left empty to use the default.</ValidationMessage>
</maximumpathsibgp>
<neighbors>
<neighbor type="ArrayField">
<enabled type="BooleanField">
@ -69,6 +79,10 @@
<MinimumValue>1</MinimumValue>
<MaximumValue>4294967295</MaximumValue>
</remoteas>
<localas type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>4294967295</MaximumValue>
</localas>
<password type="TextField"/>
<weight type="IntegerField">
<MinimumValue>0</MinimumValue>

View file

@ -120,6 +120,9 @@ router bgp {{ OPNsense.quagga.bgp.asnumber }}
{% else %}
neighbor {{ neighbor.address }} remote-as {{ neighbor.remote_as_mode }}
{% endif %}
{% if 'localas' in neighbor and neighbor.localas != '' %}
neighbor {{ neighbor.address }} local-as {{ neighbor.localas }}
{% endif %}
{% if neighbor.bfd|default('') == '1' %}
neighbor {{ neighbor.address }} bfd
{% endif %}
@ -167,6 +170,12 @@ router bgp {{ OPNsense.quagga.bgp.asnumber }}
{% for addressFamily in addressFamilies %}
address-family {{ addressFamily }} unicast
{% if helpers.exists('OPNsense.quagga.bgp.maximumpaths') and OPNsense.quagga.bgp.maximumpaths != '' %}
maximum-paths {{ OPNsense.quagga.bgp.maximumpaths }}
{% endif %}
{% if helpers.exists('OPNsense.quagga.bgp.maximumpathsibgp') and OPNsense.quagga.bgp.maximumpathsibgp != '' %}
maximum-paths ibgp {{ OPNsense.quagga.bgp.maximumpathsibgp }}
{% endif %}
{% for redistribution in helpers.toList('OPNsense.quagga.bgp.redistributions.redistribution') %}
{% if redistribution.enabled == '1' %}
redistribute {{ redistribution.redistribute }}{% if redistribution.linkedRoutemap %} route-map {{ helpers.getUUID(redistribution.linkedRoutemap).name }}{% endif +%}

View file

@ -1,7 +1,7 @@
{% if helpers.exists('OPNsense.quagga.general.enabled') and OPNsense.quagga.general.enabled == '1' %}
# XXX rc.d/frr splits up defunct "frr" service into frr_daemons
# and we always start "zebra" so for now this works:
zebra_setup="/usr/local/opnsense/scripts/frr/setup.sh"
watchfrr_setup="/usr/local/opnsense/scripts/frr/setup.sh"
frr_enable="YES"
{% if helpers.exists('OPNsense.quagga.general.enablecarp') and OPNsense.quagga.general.enablecarp == '1' %}
start_precmd="ifconfig | grep 'carp: MASTER'"

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= haproxy
PLUGIN_VERSION= 5.0
PLUGIN_VERSION= 5.1
PLUGIN_COMMENT= Reliable, high performance TCP/HTTP load balancer
PLUGIN_DEPENDS= haproxy py${PLUGIN_PYTHON}-haproxy-cli
PLUGIN_MAINTAINER= opnsense@moov.de

View file

@ -6,6 +6,18 @@ very high loads while needing persistence or Layer7 processing.
Plugin Changelog
================
5.1
Added:
* more conditions have support for converters
* add support for mapfile URLs (#4825)
Fixed:
* migration fails if a http-request/-response "lua" rule is configured
Changed:
* modernize UI templates
5.0
WARNING: This is a new major release, which may result in
@ -18,7 +30,7 @@ Added:
* add new condition: HTTP method
* support custom HTTP status code in "http-request deny" rules
* add new backend option to control PROXY protocol for health checks (#2909)
* add support for new map file types: beg,end,int,ip,reg,str (#3641)
* add support for new map file types: beg,end,int,ip,reg,str,sub (#3641)
* add support for more sample fetches: quic_enabled, stopping, wait_end (#3702)
* add support for HTTP compression (#4867)
* add all action keywords for http-request/-response and tcp-request/-response rules
@ -27,7 +39,8 @@ Added:
* add support for GPC/GPT/SC to conditions and rules (#1123, #5109)
* add support for SSL SNI expression to servers (#3756)
* add column "mode" to servers overview (#4632)
* add support for loading mapfiles in conditions
* add support for loading mapfiles in conditions and rules
* add support for sample fetches in rules
Fixed:
* Maintenance tab "SSL Certificates" not working with only one cert

View file

@ -266,6 +266,27 @@
<type>text</type>
<help><![CDATA[Specify the value for the URL parameter.]]></help>
</field>
<field>
<label>Parameters</label>
<type>header</type>
<style>expression_table table_var</style>
</field>
<field>
<id>acl.var_comparison</id>
<label>Comparison</label>
<type>dropdown</type>
</field>
<field>
<id>acl.var</id>
<label>Variable</label>
<type>text</type>
<help><![CDATA[The name of a variable, followed by an optional default value, e.g. (req.rate_limit) or (req.rate_limit,10).]]></help>
</field>
<field>
<id>acl.var_value</id>
<label>Value</label>
<type>text</type>
</field>
<field>
<label>Parameters</label>
<type>header</type>
@ -1788,4 +1809,10 @@
<type>dropdown</type>
<help><![CDATA[Load patterns from a map file.]]></help>
</field>
<field>
<id>acl.converter</id>
<label>Converter</label>
<type>text</type>
<help><![CDATA[Transforms or processes the output of a sample fetch (e.g. variable, mapping, arithmetic, string manipulation) and passes the result to the next converter or final evaluation. Make more complex conditions possible, e.g. acl rate_abuse var(req.rate_limit),sub(req.request_rate) lt 0.]]></help>
</field>
</form>

View file

@ -325,4 +325,22 @@
<type>text</type>
<help><![CDATA[Refers to the number of the Stick Counter, e.g. 0 or 1 when using sc0 or sc1 respectively. This value will be ignored in rules that do not support SC.]]></help>
</field>
<field>
<id>action.mapfile</id>
<label>Mapfile</label>
<type>dropdown</type>
<help><![CDATA[Load patterns from a map file.]]></help>
</field>
<field>
<id>action.map_default</id>
<label>Map default value</label>
<type>text</type>
<help><![CDATA[When a map file is specified, this is the default value.]]></help>
</field>
<field>
<id>action.sample_fetch</id>
<label>Sample fetch</label>
<type>text</type>
<help><![CDATA[Extracts a value from the request or connection context (e.g. path, header, source IP) to be used as input for converters or matching rules. May be used in combination with set-var and map files to create more complex rules, e.g.<br/>http-request set-var(req.rate_limit) path,map_beg(/path/to/mapfile,20)</br>http-request set-var(req.request_rate) base32+src,table_http_req_rate()]]></help>
</field>
</form>

View file

@ -21,6 +21,12 @@
<id>mapfile.content</id>
<label>Content</label>
<type>textbox</type>
<help><![CDATA[Paste the content of your map file here. See the <a target="_blank" href="http://docs.haproxy.org/3.2/configuration.html#map">HAProxy documentation</a> for a full description.]]></help>
<help><![CDATA[Paste the content of the map file here. See the <a target="_blank" href="http://docs.haproxy.org/3.2/configuration.html#map">HAProxy documentation</a> for a full description.]]></help>
</field>
<field>
<id>mapfile.url</id>
<label>Download URL</label>
<type>text</type>
<help><![CDATA[A URL from which the map file will be downloaded. The contents will be cached until next reload/restart of HAProxy. If the download fails, then the specified content will be used instead. In order to enable automatic updates of map files, setup a cron job that periodically reloads HAProxy.]]></help>
</field>
</form>

View file

@ -1825,133 +1825,134 @@
<expression type="OptionField">
<Required>Y</Required>
<OptionValues>
<cust_hdr_beg>hdr_beg specified HTTP Header starts with</cust_hdr_beg>
<cust_hdr_end>hdr_end specified HTTP Header ends with</cust_hdr_end>
<cust_hdr>hdr specified HTTP Header matches</cust_hdr>
<cust_hdr_reg>hdr_reg specified HTTP Header regex</cust_hdr_reg>
<cust_hdr_sub>hdr_sub specified HTTP Header contains</cust_hdr_sub>
<hdr_beg>hdr_beg HTTP Host Header starts with</hdr_beg>
<hdr_end>hdr_end HTTP Host Header ends with</hdr_end>
<hdr>hdr HTTP Host Header matches</hdr>
<hdr_reg>hdr_reg HTTP Host Header regex</hdr_reg>
<hdr_sub>hdr_sub HTTP Host Header contains</hdr_sub>
<http_auth>http_auth HTTP Basic Auth: username/password from client matches selected User/Group</http_auth>
<http_method>http_method HTTP Method</http_method>
<nbsrv>nbsrv Minimum number of usable servers in backend</nbsrv>
<path_beg>path_beg Path starts with</path_beg>
<path_dir>path_dir Path contains subdir</path_dir>
<path_end>path_end Path ends with</path_end>
<path>path Path matches</path>
<path_reg>path_reg Path regex</path_reg>
<path_sub>path_sub Path contains string</path_sub>
<quic_enabled>quic_enabled QUIC transport protocol is enabled</quic_enabled>
<traffic_is_http>req.proto_http Traffic is HTTP</traffic_is_http>
<traffic_is_ssl>req.ssl_ver Traffic is SSL (TCP request content inspection)</traffic_is_ssl>
<sc_bytes_in_rate>sc_bytes_in_rate Sticky counter: incoming bytes rate</sc_bytes_in_rate>
<sc_bytes_out_rate>sc_bytes_out_rate Sticky counter: outgoing bytes rate</sc_bytes_out_rate>
<sc_clr_gpc>sc_clr_gpc Sticky counter: clear General Purpose Counter</sc_clr_gpc>
<sc_clr_gpc0>sc_clr_gpc0 Sticky counter: clear General Purpose Counter</sc_clr_gpc0>
<sc_clr_gpc1>sc_clr_gpc1 Sticky counter: clear General Purpose Counter</sc_clr_gpc1>
<sc0_clr_gpc0>sc0_clr_gpc0 Sticky counter: clear General Purpose Counter</sc0_clr_gpc0>
<sc0_clr_gpc1>sc0_clr_gpc1 Sticky counter: clear General Purpose Counter</sc0_clr_gpc1>
<sc1_clr_gpc>sc1_clr_gpc Sticky counter: clear General Purpose Counter</sc1_clr_gpc>
<sc1_clr_gpc0>sc1_clr_gpc0 Sticky counter: clear General Purpose Counter</sc1_clr_gpc0>
<sc1_clr_gpc1>sc1_clr_gpc1 Sticky counter: clear General Purpose Counter</sc1_clr_gpc1>
<sc2_clr_gpc>sc2_clr_gpc Sticky counter: clear General Purpose Counter</sc2_clr_gpc>
<sc2_clr_gpc0>sc2_clr_gpc0 Sticky counter: clear General Purpose Counter</sc2_clr_gpc0>
<sc2_clr_gpc1>sc2_clr_gpc1 Sticky counter: clear General Purpose Counter</sc2_clr_gpc1>
<sc_conn_cnt>sc_conn_cnt Sticky counter: cumulative number of connections</sc_conn_cnt>
<sc_conn_cur>sc_conn_cur Sticky counter: concurrent connections</sc_conn_cur>
<sc_conn_rate>sc_conn_rate Sticky counter: connection rate</sc_conn_rate>
<sc_get_gpc>sc_get_gpc Sticky counter: get General Purpose Counter value</sc_get_gpc>
<sc_get_gpc0>sc_get_gpc0 Sticky counter: get General Purpose Counter value</sc_get_gpc0>
<sc_get_gpc1>sc_get_gpc1 Sticky counter: get General Purpose Counter value</sc_get_gpc1>
<sc0_get_gpc0>sc0_get_gpc0 Sticky counter: get General Purpose Counter value</sc0_get_gpc0>
<sc0_get_gpc1>sc0_get_gpc1 Sticky counter: get General Purpose Counter value</sc0_get_gpc1>
<sc1_get_gpc0>sc1_get_gpc0 Sticky counter: get General Purpose Counter value</sc1_get_gpc0>
<sc1_get_gpc1>sc1_get_gpc1 Sticky counter: get General Purpose Counter value</sc1_get_gpc1>
<sc2_get_gpc0>sc2_get_gpc0 Sticky counter: get General Purpose Counter value</sc2_get_gpc0>
<sc2_get_gpc1>sc2_get_gpc1 Sticky counter: get General Purpose Counter value</sc2_get_gpc1>
<sc_get_gpt>sc_get_gpt Sticky counter: get General Purpose Tag value</sc_get_gpt>
<sc_get_gpt0>sc_get_gpt0 Sticky counter: get General Purpose Tag value</sc_get_gpt0>
<sc0_get_gpt0>sc0_get_gpt0 Sticky counter: get General Purpose Tag value</sc0_get_gpt0>
<sc1_get_gpt0>sc1_get_gpt0 Sticky counter: get General Purpose Tag value</sc1_get_gpt0>
<sc2_get_gpt0>sc2_get_gpt0 Sticky counter: get General Purpose Tag value</sc2_get_gpt0>
<sc_glitch_cnt>sc_glitch_cnt Sticky counter: cumulative number of glitches</sc_glitch_cnt>
<sc_glitch_rate>sc_glitch_rate Sticky counter: rate of glitches</sc_glitch_rate>
<sc_gpc_rate>sc_gpc_rate Sticky counter: increment rate of General Purpose Counter</sc_gpc_rate>
<sc_gpc0_rate>sc_gpc0_rate Sticky counter: increment rate of General Purpose Counter</sc_gpc0_rate>
<sc_gpc1_rate>sc_gpc1_rate Sticky counter: increment rate of General Purpose Counter</sc_gpc1_rate>
<sc0_gpc0_rate>sc0_gpc0_rate Sticky counter: increment rate of General Purpose Counter</sc0_gpc0_rate>
<sc0_gpc1_rate>sc0_gpc1_rate Sticky counter: increment rate of General Purpose Counter</sc0_gpc1_rate>
<sc1_gpc0_rate>sc1_gpc0_rate Sticky counter: increment rate of General Purpose Counter</sc1_gpc0_rate>
<sc1_gpc1_rate>sc1_gpc1_rate Sticky counter: increment rate of General Purpose Counter</sc1_gpc1_rate>
<sc2_gpc0_rate>sc2_gpc0_rate Sticky counter: increment rate of General Purpose Counter</sc2_gpc0_rate>
<sc2_gpc1_rate>sc2_gpc1_rate Sticky counter: increment rate of General Purpose Counter</sc2_gpc1_rate>
<sc_http_err_cnt>sc_http_err_cnt Sticky counter: cumulative number of HTTP errors</sc_http_err_cnt>
<sc_http_err_rate>sc_http_err_rate Sticky counter: rate of HTTP errors</sc_http_err_rate>
<sc_http_fail_cnt>sc_http_fail_cnt Sticky counter: cumulative number of HTTP failures</sc_http_fail_cnt>
<sc_http_fail_rate>sc_http_fail_rate Sticky counter: rate of HTTP failures</sc_http_fail_rate>
<sc_http_req_cnt>sc_http_req_cnt Sticky counter: cumulative number of HTTP requests</sc_http_req_cnt>
<sc_http_req_rate>sc_http_req_rate Sticky counter: rate of HTTP requests</sc_http_req_rate>
<sc_inc_gpc>sc_inc_gpc Sticky counter: increment General Purpose Counter</sc_inc_gpc>
<sc_inc_gpc0>sc_inc_gpc0 Sticky counter: increment General Purpose Counter</sc_inc_gpc0>
<sc_inc_gpc1>sc_inc_gpc1 Sticky counter: increment General Purpose Counter</sc_inc_gpc1>
<sc0_inc_gpc0>sc0_inc_gpc0 Sticky counter: increment General Purpose Counter</sc0_inc_gpc0>
<sc0_inc_gpc1>sc0_inc_gpc1 Sticky counter: increment General Purpose Counter</sc0_inc_gpc1>
<sc1_inc_gpc0>sc1_inc_gpc0 Sticky counter: increment General Purpose Counter</sc1_inc_gpc0>
<sc1_inc_gpc1>sc1_inc_gpc1 Sticky counter: increment General Purpose Counter</sc1_inc_gpc1>
<sc2_inc_gpc0>sc2_inc_gpc0 Sticky counter: increment General Purpose Counter</sc2_inc_gpc0>
<sc2_inc_gpc1>sc2_inc_gpc1 Sticky counter: increment General Purpose Counter</sc2_inc_gpc1>
<sc_sess_cnt>sc_sess_cnt Sticky counter: cumulative number of sessions</sc_sess_cnt>
<sc_sess_rate>sc_sess_rate Sticky counter: session rate</sc_sess_rate>
<src>src Source IP matches specified IP</src>
<src_bytes_in_rate>src_bytes_in_rate Source IP: incoming bytes rate</src_bytes_in_rate>
<src_bytes_out_rate>src_bytes_out_rate Source IP: outgoing bytes rate</src_bytes_out_rate>
<src_clr_gpc>src_clr_gpc Source IP: clear General Purpose Counter</src_clr_gpc>
<src_clr_gpc0>src_clr_gpc0 Source IP: clear General Purpose Counter</src_clr_gpc0>
<src_clr_gpc1>src_clr_gpc1 Source IP: clear General Purpose Counter</src_clr_gpc1>
<src_conn_cnt>src_conn_cnt Source IP: cumulative number of connections</src_conn_cnt>
<src_conn_cur>src_conn_cur Source IP: concurrent connections</src_conn_cur>
<src_conn_rate>src_conn_rate Source IP: connection rate</src_conn_rate>
<src_get_gpc>src_get_gpc Source IP: get General Purpose Counter value</src_get_gpc>
<src_get_gpc0>src_get_gpc0 Source IP: get General Purpose Counter value</src_get_gpc0>
<src_get_gpc1>src_get_gpc1 Source IP: get General Purpose Counter value</src_get_gpc1>
<src_get_gpt>src_get_gpt Source IP: get General Purpose Tag value</src_get_gpt>
<src_glitch_cnt>src_glitch_cnt Source IP: cumulative number of glitches</src_glitch_cnt>
<src_glitch_rate>src_glitch_rate Source IP: rate of glitches</src_glitch_rate>
<src_gpc_rate>src_gpc_rate Source IP: increment rate of General Purpose Counter</src_gpc_rate>
<src_gpc0_rate>src_gpc0_rate Source IP: increment rate of General Purpose Counter</src_gpc0_rate>
<src_gpc1_rate>src_gpc1_rate Source IP: increment rate of General Purpose Counter</src_gpc1_rate>
<src_http_err_cnt>src_http_err_cnt Source IP: cumulative number of HTTP errors</src_http_err_cnt>
<src_http_err_rate>src_http_err_rate Source IP: rate of HTTP errors</src_http_err_rate>
<src_http_fail_cnt>src_http_fail_cnt Source IP: cumulative number of HTTP failures</src_http_fail_cnt>
<src_http_fail_rate>src_http_fail_rate Source IP: rate of HTTP failures</src_http_fail_rate>
<src_http_req_cnt>src_http_req_cnt Source IP: number of HTTP requests</src_http_req_cnt>
<src_http_req_rate>src_http_req_rate Source IP: rate of HTTP requests</src_http_req_rate>
<src_inc_gpc>src_inc_gpc Source IP: increment General Purpose Counter</src_inc_gpc>
<src_inc_gpc0>src_inc_gpc0 Source IP: increment General Purpose Counter</src_inc_gpc0>
<src_inc_gpc1>src_inc_gpc1 Source IP: increment General Purpose Counter</src_inc_gpc1>
<src_is_local>src_is_local Source IP is local</src_is_local>
<src_kbytes_in>src_kbytes_in Source IP: amount of data received (in kilobytes)</src_kbytes_in>
<src_kbytes_out>src_kbytes_out Source IP: amount of data sent (in kilobytes)</src_kbytes_out>
<src_port>src_port Source IP: TCP source port</src_port>
<src_sess_cnt>src_sess_cnt Source IP: cumulative number of sessions</src_sess_cnt>
<src_sess_rate>src_sess_rate Source IP: session rate</src_sess_rate>
<ssl_c_ca_commonname>ssl_c_ca_commonname SSL Client certificate issued by CA common-name</ssl_c_ca_commonname>
<ssl_c_verify_code>ssl_c_verify_code SSL Client certificate verify error result</ssl_c_verify_code>
<ssl_c_verify>ssl_c_verify SSL Client certificate is valid</ssl_c_verify>
<ssl_fc_sni>ssl_fc_sni SNI TLS extension matches (locally deciphered)</ssl_fc_sni>
<ssl_fc>ssl_fc Traffic is SSL (locally deciphered)</ssl_fc>
<ssl_hello_type>ssl_hello_type SSL Hello Type</ssl_hello_type>
<ssl_sni_beg>ssl_sni_beg SNI TLS extension starts with (TCP request content inspection)</ssl_sni_beg>
<ssl_sni_end>ssl_sni_end SNI TLS extension ends with (TCP request content inspection)</ssl_sni_end>
<ssl_sni_reg>ssl_sni_reg SNI TLS extension regex (TCP request content inspection)</ssl_sni_reg>
<ssl_sni>ssl_sni SNI TLS extension matches (TCP request content inspection)</ssl_sni>
<ssl_sni_sub>ssl_sni_sub SNI TLS extension contains (TCP request content inspection)</ssl_sni_sub>
<stopping>stopping HAProxy process is currently stopping</stopping>
<url_param>url_param URL parameter contains</url_param>
<wait_end>wait_end Inspection period is over</wait_end>
<cust_hdr_beg>hdr_beg - specified HTTP Header starts with</cust_hdr_beg>
<cust_hdr_end>hdr_end - specified HTTP Header ends with</cust_hdr_end>
<cust_hdr>hdr - specified HTTP Header matches</cust_hdr>
<cust_hdr_reg>hdr_reg - specified HTTP Header regex</cust_hdr_reg>
<cust_hdr_sub>hdr_sub - specified HTTP Header contains</cust_hdr_sub>
<hdr_beg>hdr_beg - HTTP Host Header starts with</hdr_beg>
<hdr_end>hdr_end - HTTP Host Header ends with</hdr_end>
<hdr>hdr - HTTP Host Header matches</hdr>
<hdr_reg>hdr_reg - HTTP Host Header regex</hdr_reg>
<hdr_sub>hdr_sub - HTTP Host Header contains</hdr_sub>
<http_auth>http_auth - HTTP Basic Auth: username/password from client matches selected User/Group</http_auth>
<http_method>http_method - HTTP Method</http_method>
<nbsrv>nbsrv - Minimum number of usable servers in backend</nbsrv>
<path_beg>path_beg - Path starts with</path_beg>
<path_dir>path_dir - Path contains subdir</path_dir>
<path_end>path_end - Path ends with</path_end>
<path>path - Path matches</path>
<path_reg>path_reg - Path regex</path_reg>
<path_sub>path_sub - Path contains string</path_sub>
<quic_enabled>quic_enabled - QUIC transport protocol is enabled</quic_enabled>
<traffic_is_http>req.proto_http - Traffic is HTTP</traffic_is_http>
<traffic_is_ssl>req.ssl_ver - Traffic is SSL (TCP request content inspection)</traffic_is_ssl>
<sc_bytes_in_rate>sc_bytes_in_rate - Sticky counter: incoming bytes rate</sc_bytes_in_rate>
<sc_bytes_out_rate>sc_bytes_out_rate - Sticky counter: outgoing bytes rate</sc_bytes_out_rate>
<sc_clr_gpc>sc_clr_gpc - Sticky counter: clear General Purpose Counter</sc_clr_gpc>
<sc_clr_gpc0>sc_clr_gpc0 - Sticky counter: clear General Purpose Counter</sc_clr_gpc0>
<sc_clr_gpc1>sc_clr_gpc1 - Sticky counter: clear General Purpose Counter</sc_clr_gpc1>
<sc0_clr_gpc0>sc0_clr_gpc0 - Sticky counter: clear General Purpose Counter</sc0_clr_gpc0>
<sc0_clr_gpc1>sc0_clr_gpc1 - Sticky counter: clear General Purpose Counter</sc0_clr_gpc1>
<sc1_clr_gpc>sc1_clr_gpc - Sticky counter: clear General Purpose Counter</sc1_clr_gpc>
<sc1_clr_gpc0>sc1_clr_gpc0 - Sticky counter: clear General Purpose Counter</sc1_clr_gpc0>
<sc1_clr_gpc1>sc1_clr_gpc1 - Sticky counter: clear General Purpose Counter</sc1_clr_gpc1>
<sc2_clr_gpc>sc2_clr_gpc - Sticky counter: clear General Purpose Counter</sc2_clr_gpc>
<sc2_clr_gpc0>sc2_clr_gpc0 - Sticky counter: clear General Purpose Counter</sc2_clr_gpc0>
<sc2_clr_gpc1>sc2_clr_gpc1 - Sticky counter: clear General Purpose Counter</sc2_clr_gpc1>
<sc_conn_cnt>sc_conn_cnt - Sticky counter: cumulative number of connections</sc_conn_cnt>
<sc_conn_cur>sc_conn_cur - Sticky counter: concurrent connections</sc_conn_cur>
<sc_conn_rate>sc_conn_rate - Sticky counter: connection rate</sc_conn_rate>
<sc_get_gpc>sc_get_gpc - Sticky counter: get General Purpose Counter value</sc_get_gpc>
<sc_get_gpc0>sc_get_gpc0 - Sticky counter: get General Purpose Counter value</sc_get_gpc0>
<sc_get_gpc1>sc_get_gpc1 - Sticky counter: get General Purpose Counter value</sc_get_gpc1>
<sc0_get_gpc0>sc0_get_gpc0 - Sticky counter: get General Purpose Counter value</sc0_get_gpc0>
<sc0_get_gpc1>sc0_get_gpc1 - Sticky counter: get General Purpose Counter value</sc0_get_gpc1>
<sc1_get_gpc0>sc1_get_gpc0 - Sticky counter: get General Purpose Counter value</sc1_get_gpc0>
<sc1_get_gpc1>sc1_get_gpc1 - Sticky counter: get General Purpose Counter value</sc1_get_gpc1>
<sc2_get_gpc0>sc2_get_gpc0 - Sticky counter: get General Purpose Counter value</sc2_get_gpc0>
<sc2_get_gpc1>sc2_get_gpc1 - Sticky counter: get General Purpose Counter value</sc2_get_gpc1>
<sc_get_gpt>sc_get_gpt - Sticky counter: get General Purpose Tag value</sc_get_gpt>
<sc_get_gpt0>sc_get_gpt0 - Sticky counter: get General Purpose Tag value</sc_get_gpt0>
<sc0_get_gpt0>sc0_get_gpt0 - Sticky counter: get General Purpose Tag value</sc0_get_gpt0>
<sc1_get_gpt0>sc1_get_gpt0 - Sticky counter: get General Purpose Tag value</sc1_get_gpt0>
<sc2_get_gpt0>sc2_get_gpt0 - Sticky counter: get General Purpose Tag value</sc2_get_gpt0>
<sc_glitch_cnt>sc_glitch_cnt - Sticky counter: cumulative number of glitches</sc_glitch_cnt>
<sc_glitch_rate>sc_glitch_rate - Sticky counter: rate of glitches</sc_glitch_rate>
<sc_gpc_rate>sc_gpc_rate - Sticky counter: increment rate of General Purpose Counter</sc_gpc_rate>
<sc_gpc0_rate>sc_gpc0_rate - Sticky counter: increment rate of General Purpose Counter</sc_gpc0_rate>
<sc_gpc1_rate>sc_gpc1_rate - Sticky counter: increment rate of General Purpose Counter</sc_gpc1_rate>
<sc0_gpc0_rate>sc0_gpc0_rate - Sticky counter: increment rate of General Purpose Counter</sc0_gpc0_rate>
<sc0_gpc1_rate>sc0_gpc1_rate - Sticky counter: increment rate of General Purpose Counter</sc0_gpc1_rate>
<sc1_gpc0_rate>sc1_gpc0_rate - Sticky counter: increment rate of General Purpose Counter</sc1_gpc0_rate>
<sc1_gpc1_rate>sc1_gpc1_rate - Sticky counter: increment rate of General Purpose Counter</sc1_gpc1_rate>
<sc2_gpc0_rate>sc2_gpc0_rate - Sticky counter: increment rate of General Purpose Counter</sc2_gpc0_rate>
<sc2_gpc1_rate>sc2_gpc1_rate - Sticky counter: increment rate of General Purpose Counter</sc2_gpc1_rate>
<sc_http_err_cnt>sc_http_err_cnt - Sticky counter: cumulative number of HTTP errors</sc_http_err_cnt>
<sc_http_err_rate>sc_http_err_rate - Sticky counter: rate of HTTP errors</sc_http_err_rate>
<sc_http_fail_cnt>sc_http_fail_cnt - Sticky counter: cumulative number of HTTP failures</sc_http_fail_cnt>
<sc_http_fail_rate>sc_http_fail_rate - Sticky counter: rate of HTTP failures</sc_http_fail_rate>
<sc_http_req_cnt>sc_http_req_cnt - Sticky counter: cumulative number of HTTP requests</sc_http_req_cnt>
<sc_http_req_rate>sc_http_req_rate - Sticky counter: rate of HTTP requests</sc_http_req_rate>
<sc_inc_gpc>sc_inc_gpc - Sticky counter: increment General Purpose Counter</sc_inc_gpc>
<sc_inc_gpc0>sc_inc_gpc0 - Sticky counter: increment General Purpose Counter</sc_inc_gpc0>
<sc_inc_gpc1>sc_inc_gpc1 - Sticky counter: increment General Purpose Counter</sc_inc_gpc1>
<sc0_inc_gpc0>sc0_inc_gpc0 - Sticky counter: increment General Purpose Counter</sc0_inc_gpc0>
<sc0_inc_gpc1>sc0_inc_gpc1 - Sticky counter: increment General Purpose Counter</sc0_inc_gpc1>
<sc1_inc_gpc0>sc1_inc_gpc0 - Sticky counter: increment General Purpose Counter</sc1_inc_gpc0>
<sc1_inc_gpc1>sc1_inc_gpc1 - Sticky counter: increment General Purpose Counter</sc1_inc_gpc1>
<sc2_inc_gpc0>sc2_inc_gpc0 - Sticky counter: increment General Purpose Counter</sc2_inc_gpc0>
<sc2_inc_gpc1>sc2_inc_gpc1 - Sticky counter: increment General Purpose Counter</sc2_inc_gpc1>
<sc_sess_cnt>sc_sess_cnt - Sticky counter: cumulative number of sessions</sc_sess_cnt>
<sc_sess_rate>sc_sess_rate - Sticky counter: session rate</sc_sess_rate>
<src>src - Source IP matches specified IP</src>
<src_bytes_in_rate>src_bytes_in_rate - Source IP: incoming bytes rate</src_bytes_in_rate>
<src_bytes_out_rate>src_bytes_out_rate - Source IP: outgoing bytes rate</src_bytes_out_rate>
<src_clr_gpc>src_clr_gpc - Source IP: clear General Purpose Counter</src_clr_gpc>
<src_clr_gpc0>src_clr_gpc0 - Source IP: clear General Purpose Counter</src_clr_gpc0>
<src_clr_gpc1>src_clr_gpc1 - Source IP: clear General Purpose Counter</src_clr_gpc1>
<src_conn_cnt>src_conn_cnt - Source IP: cumulative number of connections</src_conn_cnt>
<src_conn_cur>src_conn_cur - Source IP: concurrent connections</src_conn_cur>
<src_conn_rate>src_conn_rate - Source IP: connection rate</src_conn_rate>
<src_get_gpc>src_get_gpc - Source IP: get General Purpose Counter value</src_get_gpc>
<src_get_gpc0>src_get_gpc0 - Source IP: get General Purpose Counter value</src_get_gpc0>
<src_get_gpc1>src_get_gpc1 - Source IP: get General Purpose Counter value</src_get_gpc1>
<src_get_gpt>src_get_gpt - Source IP: get General Purpose Tag value</src_get_gpt>
<src_glitch_cnt>src_glitch_cnt - Source IP: cumulative number of glitches</src_glitch_cnt>
<src_glitch_rate>src_glitch_rate - Source IP: rate of glitches</src_glitch_rate>
<src_gpc_rate>src_gpc_rate - Source IP: increment rate of General Purpose Counter</src_gpc_rate>
<src_gpc0_rate>src_gpc0_rate - Source IP: increment rate of General Purpose Counter</src_gpc0_rate>
<src_gpc1_rate>src_gpc1_rate - Source IP: increment rate of General Purpose Counter</src_gpc1_rate>
<src_http_err_cnt>src_http_err_cnt - Source IP: cumulative number of HTTP errors</src_http_err_cnt>
<src_http_err_rate>src_http_err_rate - Source IP: rate of HTTP errors</src_http_err_rate>
<src_http_fail_cnt>src_http_fail_cnt - Source IP: cumulative number of HTTP failures</src_http_fail_cnt>
<src_http_fail_rate>src_http_fail_rate - Source IP: rate of HTTP failures</src_http_fail_rate>
<src_http_req_cnt>src_http_req_cnt - Source IP: number of HTTP requests</src_http_req_cnt>
<src_http_req_rate>src_http_req_rate - Source IP: rate of HTTP requests</src_http_req_rate>
<src_inc_gpc>src_inc_gpc - Source IP: increment General Purpose Counter</src_inc_gpc>
<src_inc_gpc0>src_inc_gpc0 - Source IP: increment General Purpose Counter</src_inc_gpc0>
<src_inc_gpc1>src_inc_gpc1 - Source IP: increment General Purpose Counter</src_inc_gpc1>
<src_is_local>src_is_local - Source IP is local</src_is_local>
<src_kbytes_in>src_kbytes_in - Source IP: amount of data received (in kilobytes)</src_kbytes_in>
<src_kbytes_out>src_kbytes_out - Source IP: amount of data sent (in kilobytes)</src_kbytes_out>
<src_port>src_port - Source IP: TCP source port</src_port>
<src_sess_cnt>src_sess_cnt - Source IP: cumulative number of sessions</src_sess_cnt>
<src_sess_rate>src_sess_rate - Source IP: session rate</src_sess_rate>
<ssl_c_ca_commonname>ssl_c_ca_commonname - SSL Client certificate issued by CA common-name</ssl_c_ca_commonname>
<ssl_c_verify_code>ssl_c_verify_code - SSL Client certificate verify error result</ssl_c_verify_code>
<ssl_c_verify>ssl_c_verify - SSL Client certificate is valid</ssl_c_verify>
<ssl_fc_sni>ssl_fc_sni - SNI TLS extension matches (locally deciphered)</ssl_fc_sni>
<ssl_fc>ssl_fc - Traffic is SSL (locally deciphered)</ssl_fc>
<ssl_hello_type>ssl_hello_type - SSL Hello Type</ssl_hello_type>
<ssl_sni_beg>ssl_sni_beg - SNI TLS extension starts with (TCP request content inspection)</ssl_sni_beg>
<ssl_sni_end>ssl_sni_end - SNI TLS extension ends with (TCP request content inspection)</ssl_sni_end>
<ssl_sni_reg>ssl_sni_reg - SNI TLS extension regex (TCP request content inspection)</ssl_sni_reg>
<ssl_sni>ssl_sni - SNI TLS extension matches (TCP request content inspection)</ssl_sni>
<ssl_sni_sub>ssl_sni_sub - SNI TLS extension contains (TCP request content inspection)</ssl_sni_sub>
<stopping>stopping - HAProxy process is currently stopping</stopping>
<url_param>url_param - URL parameter contains</url_param>
<var>var - Compare the value of a variable</var>
<wait_end>wait_end - Inspection period is over</wait_end>
<custom_acl>Custom condition (option pass-through)</custom_acl>
</OptionValues>
</expression>
@ -2076,6 +2077,25 @@
<Mask>/^.{1,4096}$/u</Mask>
<Required>N</Required>
</url_param_value>
<var type="TextField">
<Mask>/^.{1,4096}$/u</Mask>
<Required>N</Required>
</var>
<var_value type="TextField">
<Mask>/^.{1,4096}$/u</Mask>
<Required>N</Required>
</var_value>
<var_comparison type="OptionField">
<Required>N</Required>
<Default>gt</Default>
<OptionValues>
<gt>greater than</gt>
<ge>greater equal</ge>
<eq>equal</eq>
<lt>less than</lt>
<le>less equal</le>
</OptionValues>
</var_comparison>
<ssl_c_verify_code type="IntegerField">
<MinimumValue>0</MinimumValue>
<MaximumValue>500000</MaximumValue>
@ -3467,12 +3487,16 @@
<ValidationMessage>Related mapfile item not found</ValidationMessage>
<Required>N</Required>
</mapfile>
<converter type="TextField">
<Mask>/^.{1,4096}$/u</Mask>
<Required>N</Required>
</converter>
</acl>
</acls>
<actions>
<action type="ArrayField">
<enabled type="BooleanField">
<default>1</default>
<Default>1</Default>
<Required>Y</Required>
</enabled>
<name type="TextField">
@ -3622,6 +3646,7 @@
<do-log>do-log</do-log>
<do-resolve>do-resolve</do-resolve>
<early-hint>early-hint</early-hint>
<lua>lua</lua>
<normalize-uri>normalize-uri</normalize-uri>
<redirect>redirect</redirect>
<reject>reject</reject>
@ -3665,7 +3690,7 @@
<track-sc1>track-sc1</track-sc1>
<track-sc2>track-sc2</track-sc2>
<unset-var>unset-var</unset-var>
<use-service>use-service</use-service>
<use-service>use-service - use a lua service</use-service>
<wait-for-body>wait-for-body</wait-for-body>
<wait-for-handshake>wait-for-handshake</wait-for-handshake>
</OptionValues>
@ -3687,6 +3712,7 @@
<del-map>del-map</del-map>
<deny>deny</deny>
<do-log>do-log</do-log>
<lua>lua</lua>
<redirect>redirect</redirect>
<replace-header>replace-header</replace-header>
<replace-value>replace-value</replace-value>
@ -3780,7 +3806,7 @@
<content_track-sc1>content track-sc1</content_track-sc1>
<content_track-sc2>content track-sc2</content_track-sc2>
<content_unset-var>content unset-var</content_unset-var>
<content_use-service>content use-service</content_use-service>
<content_use-service>content use-service - use a lua service</content_use-service>
<inspect-delay>inspect-delay</inspect-delay>
<session_accept>session accept</session_accept>
<session_attach-srv>session attach-srv</session_attach-srv>
@ -4142,6 +4168,25 @@
<ValidationMessage>Please specify a value between 0 and 99.</ValidationMessage>
<Required>N</Required>
</sc_number>
<mapfile type="ModelRelationField">
<Model>
<template>
<source>OPNsense.HAProxy.HAProxy</source>
<items>mapfiles.mapfile</items>
<display>name</display>
</template>
</Model>
<ValidationMessage>Related mapfile item not found</ValidationMessage>
<Required>N</Required>
</mapfile>
<map_default type="TextField">
<Mask>/^.{1,4096}$/u</Mask>
<Required>N</Required>
</map_default>
<sample_fetch type="TextField">
<Mask>/^.{1,4096}$/u</Mask>
<Required>N</Required>
</sample_fetch>
</action>
</actions>
<luas>
@ -4309,18 +4354,23 @@
<Required>Y</Required>
<Default>dom</Default>
<OptionValues>
<beg>beg key begins with requested value</beg>
<dom>dom Domains</dom>
<end>end key ends with requested value</end>
<int>int Integers</int>
<ip>ip IPs</ip>
<reg>reg Regular Expressions</reg>
<str>str Strings</str>
<beg>beg - key begins with requested value</beg>
<dom>dom - Domains</dom>
<end>end - key ends with requested value</end>
<int>int - Integers</int>
<ip>ip - IPs</ip>
<reg>reg - Regular Expressions</reg>
<str>str - Strings</str>
<sub>sub - substring matches requested value</sub>
</OptionValues>
</type>
<content type="TextField">
<Required>Y</Required>
<Required>N</Required>
</content>
<url type="UrlField">
<Mask>/^.{1,4096}$/u</Mask>
<Required>N</Required>
</url>
</mapfile>
</mapfiles>
<groups>

View file

@ -223,7 +223,7 @@ class M5_0_0 extends BaseModelMigration
$action->http_response_option = (string)$action->http_response_set_status_code . $status_reason;
$action->http_response_set_status_code = null;
$action->http_response_set_status_reason = null;
}
}
break;
case 'http-response_set-var':
$action->type = 'http-response';

View file

@ -1,6 +1,6 @@
{#
Copyright (C) 2021 Frank Wall
Copyright (C) 2021-2026 Frank Wall
OPNsense® is Copyright © 2014 2016 by Deciso B.V.
All rights reserved.
@ -29,16 +29,17 @@ POSSIBILITY OF SUCH DAMAGE.
<script>
$( document ).ready(function() {
'use strict';
/**
* show HAProxy config
*/
function update_showconf() {
ajaxCall(url="/api/haproxy/export/config/", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/export/config/", {}, function(data, status) {
if (data['response'] && data['response'].trim()) {
$("#showconf").text(data['response']);
} else {
conf_help = "<br><span style=\"color: #000000; white-space: pre-wrap; font-family: monospace;\"> {{ lang._('Config file not found. Run a syntax check to create it.') }}</span><br>";
$("#showconfempty").append(conf_help);
$("#showconfempty").append('<br><span class="conf-message"> {{ lang._('Config file not found. Run a syntax check to create it.') }}</span><br>');
$("#showconf").hide();
}
});
@ -49,29 +50,16 @@ POSSIBILITY OF SUCH DAMAGE.
* show HAProxy config diff
*/
function update_showdiff() {
ajaxCall(url="/api/haproxy/export/diff/", sendData={}, callback=function(data,status) {
diff = '';
ajaxCall("/api/haproxy/export/diff/", {}, function(data, status) {
const diffClasses = {'+': 'diff-add', '-': 'diff-remove', '@': 'diff-hunk'};
let diff = '';
if (data['response'] && data['response'].trim()) {
var lines = data['response'].split("\n");
$.each(lines, function(n, line) {
switch(line.substring(0,1)) {
case '+':
color = '#3bbb33';
break;
case '-':
color = '#c13928';
break;
case '@':
color = '#3bb9c3';
break;
default:
color = '#000000';
}
diff += '<span style="color: ' + color + '; white-space: pre-wrap; font-family: monospace;">' + line + '</span><br>';
data['response'].split("\n").forEach(function(line) {
const cssClass = diffClasses[line.substring(0, 1)] || 'diff-context';
diff += `<span class="${cssClass}">${$('<span/>').text(line).html()}</span><br>`;
});
} else {
diff = "<br><span style=\"color: #000000; white-space: pre-wrap; font-family: monospace;\"> {{ lang._('New and old config files are identical.') }}</span><br>";
diff = '<br><span class="diff-context"> {{ lang._('New and old config files are identical.') }}</span><br>';
}
$("#showdiff").append(diff);
});
@ -83,11 +71,11 @@ POSSIBILITY OF SUCH DAMAGE.
*/
$('[id*="exportbtn"]').each(function(){
$(this).click(function(){
var type = $(this).data("type");
ajaxGet("/api/haproxy/export/download/"+type+"/", {}, function(data, status){
const type = $(this).data("type");
ajaxGet("/api/haproxy/export/download/" + type + "/", {}, function(data, status){
if (data.filename !== undefined) {
var link = $('<a></a>')
.attr('href','data:'+data.filetype+';base64,' + data.content)
const link = $('<a></a>')
.attr('href', 'data:' + data.filetype + ';base64,' + data.content)
.attr('download', data.filename)
.appendTo('body');
@ -101,18 +89,26 @@ POSSIBILITY OF SUCH DAMAGE.
});
// update history on tab state and implement navigation
if(window.location.hash != "") {
$('a[href="' + window.location.hash + '"]').click()
if (window.location.hash != "") {
$('a[href="' + window.location.hash + '"]').click();
}
$('.nav-tabs a').on('shown.bs.tab', function (e) {
history.pushState(null, null, e.target.hash);
});
$(window).on('hashchange', function(e) {
$('a[href="' + window.location.hash + '"]').click()
$('a[href="' + window.location.hash + '"]').click();
});
});
</script>
<style>
.diff-add { color: #3bbb33; white-space: pre-wrap; font-family: monospace; }
.diff-remove { color: #c13928; white-space: pre-wrap; font-family: monospace; }
.diff-hunk { color: #3bb9c3; white-space: pre-wrap; font-family: monospace; }
.diff-context { white-space: pre-wrap; font-family: monospace; }
.conf-message { white-space: pre-wrap; font-family: monospace; }
</style>
<ul class="nav nav-tabs" role="tablist" id="maintabs">
<li class="active"><a data-toggle="tab" href="#export"><b>{{ lang._('Config Export') }}</b></a></li>
<li><a data-toggle="tab" href="#diff">{{ lang._('Config Diff') }}</a></li>

View file

@ -1,6 +1,6 @@
{#
Copyright (C) 2016-2021 Frank Wall
Copyright (C) 2016-2026 Frank Wall
OPNsense® is Copyright © 2014 2015 by Deciso B.V.
All rights reserved.
@ -30,16 +30,17 @@ POSSIBILITY OF SUCH DAMAGE.
<script>
$( document ).ready(function() {
'use strict';
// get general HAProxy settings
var data_get_map = {'frm_haproxy':"/api/haproxy/settings/get"};
const data_get_map = {'frm_haproxy':"/api/haproxy/settings/get"};
// load initial data
mapDataToFormUI(data_get_map).done(function(){
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// request service status on load and update status box
ajaxCall(url="/api/haproxy/service/status", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/service/status", {}, function(data, status) {
updateServiceStatusUI(data['status']);
});
});
@ -226,7 +227,7 @@ POSSIBILITY OF SUCH DAMAGE.
// hook into on-show event for dialog to extend layout.
$('#DialogAcl').on('shown.bs.modal', function (e) {
$("#acl\\.expression").change(function(){
var service_id = 'table_' + $(this).val();
const service_id = 'table_' + $(this).val();
$(".expression_table").hide();
$("."+service_id).show();
});
@ -236,7 +237,7 @@ POSSIBILITY OF SUCH DAMAGE.
// hook into on-show event for dialog to extend layout.
$('#DialogAction').on('shown.bs.modal', function (e) {
$("#action\\.type").change(function(){
var service_id = 'table_' + $(this).val();
const service_id = 'table_' + $(this).val();
$(".type_table").hide();
$("."+service_id).show();
});
@ -246,21 +247,21 @@ POSSIBILITY OF SUCH DAMAGE.
// hook into on-show event for dialog to extend layout.
$('#DialogBackend').on('shown.bs.modal', function (e) {
$("#backend\\.mode").change(function(){
var service_id = 'table_' + $(this).val();
const service_id = 'table_' + $(this).val();
$(".mode_table").hide();
$("."+service_id).show();
});
$("#backend\\.mode").change();
$("#backend\\.healthCheckEnabled").change(function(){
var service_id = 'table_healthcheck_' + $(this).is(':checked');
const service_id = 'table_healthcheck_' + $(this).is(':checked');
$(".healthcheck_table").hide();
$("."+service_id).show();
});
$("#backend\\.healthCheckEnabled").change();
$("#backend\\.persistence").change(function(){
var persistence_id = 'table_persistence_' + $(this).val();
const persistence_id = 'table_persistence_' + $(this).val();
$(".persistence_table").hide();
$("."+persistence_id).show();
});
@ -270,7 +271,7 @@ POSSIBILITY OF SUCH DAMAGE.
// hook into on-show event for dialog to extend layout.
$('#DialogFrontend').on('shown.bs.modal', function (e) {
$("#frontend\\.mode").change(function(){
var service_id = 'table_' + $(this).val();
const service_id = 'table_' + $(this).val();
$(".mode_table").hide();
$("."+service_id).show();
});
@ -278,7 +279,7 @@ POSSIBILITY OF SUCH DAMAGE.
// show/hide SSL offloading
$("#frontend\\.ssl_enabled").change(function(){
var service_id = 'table_ssl_' + $(this).is(':checked');
const service_id = 'table_ssl_' + $(this).is(':checked');
$(".table_ssl").hide();
$("."+service_id).show();
});
@ -286,7 +287,7 @@ POSSIBILITY OF SUCH DAMAGE.
// show/hide advanced SSL settings
$("#frontend\\.ssl_advancedEnabled").change(function(){
var service_id = 'table_ssl_advanced_' + $(this).is(':checked');
const service_id = 'table_ssl_advanced_' + $(this).is(':checked');
$(".table_ssl_advanced").hide();
$("."+service_id).show();
});
@ -296,7 +297,7 @@ POSSIBILITY OF SUCH DAMAGE.
// hook into on-show event for dialog to extend layout.
$('#DialogHealthcheck').on('shown.bs.modal', function (e) {
$("#healthcheck\\.type").change(function(){
var service_id = 'table_' + $(this).val();
const service_id = 'table_' + $(this).val();
$(".type_table").hide();
$("."+service_id).show();
});
@ -306,7 +307,7 @@ POSSIBILITY OF SUCH DAMAGE.
// hook into on-show event for dialog to extend layout.
$('#DialogServer').on('shown.bs.modal', function (e) {
$("#server\\.type").change(function(){
var service_id = 'table_server_type_' + $(this).val();
const service_id = 'table_server_type_' + $(this).val();
$(".table_server_type").hide();
$("."+service_id).show();
});
@ -321,31 +322,20 @@ POSSIBILITY OF SUCH DAMAGE.
$('[id*="reconfigureAct"]').each(function(){
$(this).click(function(){
// set progress animation
$('[id*="reconfigureAct_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
$('[id*="reconfigureAct_progress"]').addClass("fa fa-spinner fa-pulse");
// first run syntax check to catch critical errors
ajaxCall(url="/api/haproxy/service/configtest", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/service/configtest", {}, function(data, status) {
// show warning in case of critical errors
if (data['result'].indexOf('ALERT') > -1) {
$('[id*="reconfigureAct_progress"]').removeClass("fa fa-spinner fa-pulse");
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('HAProxy configtest found critical errors') }}",
message: "{{ lang._('The HAProxy service may not be able to start due to critical errors. Run syntax check for further details or review the changes in the %sConfiguration Diff%s.')|format('<a href=\"/ui/haproxy/export#diff\">','</a>') }}",
buttons: [{
icon: 'fa fa-trash-o',
label: '{{ lang._('Abort') }}',
action: function(dlg){
// when done, disable progress animation
$('[id*="reconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
dlg.close();
}
}]
draggable: true
});
} else {
ajaxCall(url="/api/haproxy/service/reconfigure", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/service/reconfigure", {}, function(data, status) {
if (status != "success" || data['status'] != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
@ -356,13 +346,11 @@ POSSIBILITY OF SUCH DAMAGE.
} else {
// reload page to hide pending changes reminder
setTimeout(function () {
window.location.reload(true)
window.location.reload(true);
}, 300);
}
// when done, disable progress animation
$('[id*="reconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
$('[id*="reconfigureAct_progress"]').removeClass("fa fa-spinner fa-pulse");
});
}
});
@ -373,15 +361,11 @@ POSSIBILITY OF SUCH DAMAGE.
$('[id*="configtestAct"]').each(function(){
$(this).click(function(){
// set progress animation
$('[id*="configtestAct_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
$('[id*="configtestAct_progress"]').addClass("fa fa-spinner fa-pulse");
ajaxCall(url="/api/haproxy/service/configtest", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/service/configtest", {}, function(data, status) {
// when done, disable progress animation
$('[id*="configtestAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
$('[id*="configtestAct_progress"]').removeClass("fa fa-spinner fa-pulse");
if (data['result'].indexOf('ALERT') > -1) {
BootstrapDialog.show({
@ -413,17 +397,15 @@ POSSIBILITY OF SUCH DAMAGE.
$('[id*="saveAndTestAct"]').each(function(){
$(this).click(function(){
// extract the form id from the button id
var frm_id = "frm_" + $(this).attr("id").split('_')[1]
const frm_id = "frm_" + $(this).attr("id").split('_')[1];
// save data for this tab
saveFormToEndpoint(url="/api/haproxy/settings/set",formid=frm_id,callback_ok=function(){
saveFormToEndpoint("/api/haproxy/settings/set", frm_id, function(){
// set progress animation
$('[id*="saveAndTestAct_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
$('[id*="saveAndTestAct_progress"]').addClass("fa fa-spinner fa-pulse");
// on correct save, perform config test
ajaxCall(url="/api/haproxy/service/configtest", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/service/configtest", {}, function(data, status) {
if (data['result'].indexOf('ALERT') > -1) {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
@ -448,9 +430,7 @@ POSSIBILITY OF SUCH DAMAGE.
}
// when done, disable progress animation
$('[id*="saveAndTestAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
$('[id*="saveAndTestAct_progress"]').removeClass("fa fa-spinner fa-pulse");
});
});
});
@ -460,57 +440,36 @@ POSSIBILITY OF SUCH DAMAGE.
$('[id*="saveAndReconfigureAct"]').each(function(){
$(this).click(function(){
// extract the form id from the button id
var frm_id = "frm_" + $(this).attr("id").split('_')[1]
const frm_id = "frm_" + $(this).attr("id").split('_')[1];
// save data for this tab
saveFormToEndpoint(url="/api/haproxy/settings/set",formid=frm_id,callback_ok=function(){
saveFormToEndpoint("/api/haproxy/settings/set", frm_id, function(){
// set progress animation
$('[id*="saveAndReconfigureAct_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
$('[id*="saveAndReconfigureAct_progress"]').addClass("fa fa-spinner fa-pulse");
// on correct save, perform config test
ajaxCall(url="/api/haproxy/service/configtest", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/service/configtest", {}, function(data, status) {
// show warning in case of critical errors
if (data['result'].indexOf('ALERT') > -1) {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('HAProxy config contains critical errors') }}",
message: "{{ lang._('The HAProxy service may not be able to start due to critical errors. Try anyway?') }}",
buttons: [{
label: '{{ lang._('Continue') }}',
cssClass: 'btn-primary',
action: function(dlg){
ajaxCall(url="/api/haproxy/service/reconfigure", sendData={}, callback=function(data,status) {
if (status != "success" || data['status'] != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
title: "{{ lang._('Error reconfiguring HAProxy') }}",
message: data['status'],
draggable: true
});
}
});
// when done, disable progress animation
$('[id*="saveAndReconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
dlg.close();
}
}, {
icon: 'fa fa-trash-o',
label: '{{ lang._('Abort') }}',
action: function(dlg){
// when done, disable progress animation
$('[id*="saveAndReconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
dlg.close();
}
}]
});
$('[id*="saveAndReconfigureAct_progress"]').removeClass("fa fa-spinner fa-pulse");
stdDialogConfirm(
"{{ lang._('HAProxy config contains critical errors') }}",
"{{ lang._('The HAProxy service may not be able to start due to critical errors. Try anyway?') }}",
"{{ lang._('Continue') }}", "{{ lang._('Abort') }}", function() {
ajaxCall("/api/haproxy/service/reconfigure", {}, function(data, status) {
if (status != "success" || data['status'] != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
title: "{{ lang._('Error reconfiguring HAProxy') }}",
message: data['status'],
draggable: true
});
}
});
}
);
} else {
ajaxCall(url="/api/haproxy/service/reconfigure", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/service/reconfigure", {}, function(data, status) {
if (status != "success" || data['status'] != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_WARNING,
@ -521,13 +480,11 @@ POSSIBILITY OF SUCH DAMAGE.
} else {
// reload page to hide pending changes reminder
setTimeout(function () {
window.location.reload(true)
window.location.reload(true);
}, 300);
}
// when done, disable progress animation
$('[id*="saveAndReconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
});
$('[id*="saveAndReconfigureAct_progress"]').removeClass("fa fa-spinner fa-pulse");
});
}
});
@ -542,7 +499,7 @@ POSSIBILITY OF SUCH DAMAGE.
// show reminder when config has pending changes
function pending_changes_reminder() {
ajaxCall(url="/api/haproxy/export/diff/", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/export/diff/", {}, function(data, status) {
if (data['response'] && data['response'].trim()) {
$("#haproxyPendingReminder").show();
} else {
@ -553,14 +510,8 @@ POSSIBILITY OF SUCH DAMAGE.
pending_changes_reminder();
// show hint after every config change
function add_apply_reminder() {
hint_msg = "{{ lang._('After changing settings, please remember to test and apply them with the buttons below.') }}"
$('[id*="haproxyChangeMessage"]').each(function(){
$(this).append(hint_msg);
});
};
add_apply_reminder();
const hint_msg = "{{ lang._('After changing settings, please remember to test and apply them with the buttons below.') }}";
$('[id*="haproxyChangeMessage"]').append(hint_msg);
// show or hide the correct buttons depending on which tab is shown
// NOTE: This does not work on already shown tabs, so this event must

View file

@ -1,5 +1,6 @@
{#
Copyright (C) 2026 Frank Wall
Copyright (C) 2021 Andreas Stuerz
OPNsense® is Copyright © 2014 2016 by Deciso B.V.
All rights reserved.
@ -28,37 +29,32 @@ POSSIBILITY OF SUCH DAMAGE.
#}
<script>
$( document ).ready(function() {
'use strict';
// Get cronjobs
var cronjobs_data_get_map = {'frm_cronjobs':"/api/haproxy/maintenance/get"};
const cronjobs_data_get_map = {'frm_cronjobs':"/api/haproxy/maintenance/get"};
// load initial data
mapDataToFormUI(cronjobs_data_get_map).done(function(data){
// Add link to cron job edit page: First iterate over all cron settings.
// FIXME: Oh boy, this is ugly. Should be refactored.
$.each(data.frm_cronjobs.haproxy.maintenance.cronjobs, function(key, value) {
// Check if cron setting is enabled.
// Add link to cron job edit page for each enabled cron setting.
function addCronLink(key, cronData) {
const cron_cfg = key + 'Cron';
if (!(cron_cfg in cronData)) {
return;
}
Object.entries(cronData[cron_cfg]).forEach(function([refkey, refvalue]) {
if (refvalue.selected == 1) {
const content_id = `[id="haproxy.maintenance.cronjobs.${key}"]`;
const cron_link = `<br><a href="/ui/cron/item/open/${refkey}"><span class="fa fa-pencil"></span> {{ lang._('Configure cron job') }}</a>`;
$(content_id).closest("td").append(cron_link);
}
});
}
const cronData = data.frm_cronjobs.haproxy.maintenance.cronjobs;
Object.entries(cronData).forEach(function([key, value]) {
if (value == 1) {
// Find the matching cron job reference.
cron_cfg = key + 'Cron';
$.each(data.frm_cronjobs.haproxy.maintenance.cronjobs, function(cronkey, cronvalue) {
// Check if it is the correct entry for this cron setting.
if (cronkey == cron_cfg) {
// Get the cron job UUID.
$.each(cronvalue, function(refkey, refvalue) {
// Only the "selected" item belongs to this entry.
if (refvalue.selected == 1) {
// Find the correct container for this cron setting.
content_id = "[id=\"haproxy.maintenance.cronjobs." + key + "\"]";
$(content_id).each(function(){
// Finally add the link to the cron job edit page.
cron_link = "<br><a href=\"/ui/cron/item/open/" + refkey + "\"><span class=\"fa fa-pencil\"></span> {{ lang._('Configure cron job') }}</a>";
$(this).closest("td").append(cron_link);
});
};
});
};
});
};
addCronLink(key, cronData);
}
});
formatTokenizersUI();
@ -69,27 +65,22 @@ POSSIBILITY OF SUCH DAMAGE.
$('[id*="saveAndReconfigureAct"]').each(function(){
$(this).click(function(){
// set progress animation
$('[id*="saveAndReconfigureAct_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
$('[id*="saveAndReconfigureAct_progress"]').addClass("fa fa-spinner fa-pulse");
// extract the form id from the button id
var frm_id = "frm_" + $(this).attr("id").split('_')[1]
const frm_id = "frm_" + $(this).attr("id").split('_')[1];
// save data for this tab
saveFormToEndpoint(url="/api/haproxy/maintenance/set",formid=frm_id,callback_ok=function(){
saveFormToEndpoint("/api/haproxy/maintenance/set", frm_id, function(){
// Handle cron integration
ajaxCall(url="/api/haproxy/maintenance/fetch_cron_integration", sendData={}, callback=function(data,status) {
ajaxCall("/api/haproxy/maintenance/fetch_cron_integration", {}, function(data, status) {
});
// when done, disable progress animation
$('[id*="saveAndReconfigureAct_progress"]').each(function(){
$(this).removeClass("fa fa-spinner fa-pulse");
// reload page to show or hide links to cron edit page
setTimeout(function () {
window.location.reload(true)
}, 300);
});
// when done, disable progress animation and reload to show/hide cron links
$('[id*="saveAndReconfigureAct_progress"]').removeClass("fa fa-spinner fa-pulse");
setTimeout(function () {
window.location.reload(true);
}, 300);
});
});
});
@ -127,7 +118,7 @@ POSSIBILITY OF SUCH DAMAGE.
}
function showDiffDialog(payload) {
$.post('/api/haproxy/maintenance/cert_diff', payload, function(data) {
ajaxCall("/api/haproxy/maintenance/cert_diff", payload, function(data, status) {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_INFO,
title: "{{ lang._('Diff between configured and active SSL certificates') }}",
@ -143,19 +134,18 @@ POSSIBILITY OF SUCH DAMAGE.
}
function applyDiffDialog(payload, requested_count) {
$.post('/api/haproxy/maintenance/cert_actions', payload, function(data_actions) {
question = ''
question += `<pre>${data_actions}</pre>`;
ajaxCall("/api/haproxy/maintenance/cert_actions", payload, function(data_actions, status) {
let question = `<pre>${data_actions}</pre>`;
question += '<b>{{ lang._('Apply SSL certificates to HAProxy?') }}</b></br></br>';
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
question,
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
$.post('/api/haproxy/maintenance/cert_sync', payload, function(data) {
modified_count = data.result.add_count + data.result.remove_count + data.result.update_count;
ajaxCall("/api/haproxy/maintenance/cert_sync", payload, function(data, status) {
const modified_count = data.result.add_count + data.result.remove_count + data.result.update_count;
if (requested_count != modified_count) {
var error_msg = syncErrorMessage(data.result.modified, data.result.deleted);
const error_msg = syncErrorMessage(data.result.modified, data.result.deleted);
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('Error applying SSL certificates to HAProxy') }}",
@ -174,8 +164,7 @@ POSSIBILITY OF SUCH DAMAGE.
});
}
$("#grid-certificates").bootgrid('destroy');
var grid_certificates = $("#grid-certificates").UIBootgrid({
const grid_certificates = $("#grid-certificates").UIBootgrid({
search: '/api/haproxy/maintenance/search_certificate_diff',
options: {
ajax: true,
@ -188,83 +177,63 @@ POSSIBILITY OF SUCH DAMAGE.
},
formatters: {
"commands": function (column, row) {
buttons = ""
buttons += "<button type=\"button\" data-action=\"showDiff\" title=\"{{ lang._('Show diff') }}\" class=\"btn btn-xs btn-default\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-info-circle\"></span></button>"
buttons += " <button type=\"button\" data-action=\"applyDiff\" title=\"{{ lang._('Apply changes') }}\" class=\"btn btn-xs btn-default\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-refresh\"></span></button>"
let buttons = "";
buttons += `<button type="button" data-action="showDiff" title="{{ lang._('Show diff') }}" class="btn btn-xs btn-default" data-row-id="${row.id}"><span class="fa fa-info-circle"></span></button>`;
buttons += ` <button type="button" data-action="applyDiff" title="{{ lang._('Apply changes') }}" class="btn btn-xs btn-default" data-row-id="${row.id}"><span class="fa fa-refresh"></span></button>`;
return buttons;
},
},
}
}).on("loaded.rs.jquery.bootgrid", function(){
grid_certificates.find("*[data-action=showDiff]").off().on("click", function(e) {
var row_id = $(this).data("row-id");
var frontend_ids = row_id;
var payload = {
'frontend_ids': frontend_ids,
};
showDiffDialog(payload);
const row_id = $(this).data("row-id");
showDiffDialog({'frontend_ids': row_id});
});
grid_certificates.find("*[data-action=applyDiff]").off().on("click", function(e) {
var row_id = $(this).data("row-id");
var rows = $("#grid-certificates").bootgrid("getCurrentRows");
var row = rows.filter(function(row) {
return row.id == row_id;
const row_id = $(this).data("row-id");
const rows = $("#grid-certificates").bootgrid("getCurrentRows");
const row = rows.filter(function(r) {
return r.id == row_id;
})[0];
var requested_count = row.total_count;
var frontend_ids = row.id
var payload = {
'frontend_ids': frontend_ids,
};
applyDiffDialog(payload, requested_count);
applyDiffDialog({'frontend_ids': row.id}, row.total_count);
});
grid_certificates.find("*[data-action=showDiffBulk]").off().on("click", function(e) {
var rows = $("#grid-certificates").bootgrid("getSelectedRows");
var payload = {
'frontend_ids': rows.join()
};
const rows = $("#grid-certificates").bootgrid("getSelectedRows");
if (rows != undefined && rows.length > 0) {
showDiffDialog(payload);
showDiffDialog({'frontend_ids': rows.join()});
}
});
grid_certificates.find("*[data-action=applyDiffBulk]").off().on("click", function(e) {
var rows = $("#grid-certificates").bootgrid("getSelectedRows");
var frontend_ids = rows.join();
var all_rows = $("#grid-certificates").bootgrid("getCurrentRows");
var requested_count = 0;
const rows = $("#grid-certificates").bootgrid("getSelectedRows");
const all_rows = $("#grid-certificates").bootgrid("getCurrentRows");
let requested_count = 0;
all_rows.forEach(function(row) {
if (rows.indexOf(row.id) != -1) {
requested_count = requested_count + row.total_count;
requested_count += row.total_count;
}
});
var payload = {
'frontend_ids': frontend_ids,
};
if (rows != undefined && rows.length > 0) {
applyDiffDialog(payload, requested_count);
applyDiffDialog({'frontend_ids': rows.join()}, requested_count);
}
});
});
// Apply all changes
$("*[data-action=applyDiffAll]").off().on("click", function(e) {
$('[id*="applyDiffAll_progress"]').each(function(){
$(this).addClass("fa fa-spinner fa-pulse");
});
var all_rows = $("#grid-certificates").bootgrid("getCurrentRows");
var requested_count = 0;
$('[id*="applyDiffAll_progress"]').addClass("fa fa-spinner fa-pulse");
const all_rows = $("#grid-certificates").bootgrid("getCurrentRows");
let requested_count = 0;
all_rows.forEach(function(row) {
requested_count = requested_count + row.total_count;
requested_count += row.total_count;
});
var payload = {};
$.post('/api/haproxy/maintenance/cert_sync_bulk', payload, function(data) {
modified_count = data.result.add_count + data.result.remove_count + data.result.update_count;
ajaxCall("/api/haproxy/maintenance/cert_sync_bulk", {}, function(data, status) {
const modified_count = data.result.add_count + data.result.remove_count + data.result.update_count;
if (requested_count != modified_count) {
var error_msg = syncErrorMessage(data.result.modified, data.result.deleted);
const error_msg = syncErrorMessage(data.result.modified, data.result.deleted);
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
title: "{{ lang._('Error applying SSL certificates to HAProxy') }}",
@ -283,8 +252,7 @@ POSSIBILITY OF SUCH DAMAGE.
});
// grid-status
$("#grid-status").bootgrid('destroy');
var grid_status = $("#grid-status").UIBootgrid({
const grid_status = $("#grid-status").UIBootgrid({
search: '/api/haproxy/maintenance/search_server',
options: {
ajax: true,
@ -297,11 +265,11 @@ POSSIBILITY OF SUCH DAMAGE.
},
formatters: {
"commands": function (column, row) {
buttons = ""
buttons += "<button type=\"button\" title=\"{{ lang._('Set state to ready') }}\" class=\"btn btn-xs btn-default command-set-state\" data-state=\"ready\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-check\"></span></button>"
buttons += " <button type=\"button\" title=\"{{ lang._('Set state to drain') }}\" class=\"btn btn-xs btn-default command-set-state\" data-state=\"drain\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-sort-amount-desc\"></span></button>"
buttons += " <button type=\"button\" title=\"{{ lang._('Set state to maintenance') }}\" class=\"btn btn-xs btn-default command-set-state\" data-state=\"maint\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-wrench\"></span></button>"
buttons += " <button type=\"button\" title=\"{{ lang._('Change server weight') }}\" class=\"btn btn-xs btn-default command-set-weight\" data-weight=\"" + row.weight + "\" data-row-id=\"" + row.id + "\"><span class=\"fa fa-balance-scale\"></span></button>"
let buttons = "";
buttons += `<button type="button" title="{{ lang._('Set state to ready') }}" class="btn btn-xs btn-default command-set-state" data-state="ready" data-row-id="${row.id}"><span class="fa fa-check"></span></button>`;
buttons += ` <button type="button" title="{{ lang._('Set state to drain') }}" class="btn btn-xs btn-default command-set-state" data-state="drain" data-row-id="${row.id}"><span class="fa fa-sort-amount-desc"></span></button>`;
buttons += ` <button type="button" title="{{ lang._('Set state to maintenance') }}" class="btn btn-xs btn-default command-set-state" data-state="maint" data-row-id="${row.id}"><span class="fa fa-wrench"></span></button>`;
buttons += ` <button type="button" title="{{ lang._('Change server weight') }}" class="btn btn-xs btn-default command-set-weight" data-weight="${row.weight}" data-row-id="${row.id}"><span class="fa fa-balance-scale"></span></button>`;
return buttons;
},
},
@ -309,24 +277,20 @@ POSSIBILITY OF SUCH DAMAGE.
}).on("loaded.rs.jquery.bootgrid", function(){
// set single - server state
grid_status.find(".command-set-state").off().on("click", function(e) {
var uuid = $(this).data("row-id");
var backend = uuid.split("/")[0];
var server = uuid.split("/")[1];
var state = $(this).data("state");
var payload = {
'backend': backend,
'server': server,
'state': state
};
const uuid = $(this).data("row-id");
const backend = uuid.split("/")[0];
const server = uuid.split("/")[1];
const state = $(this).data("state");
const payload = {'backend': backend, 'server': server, 'state': state};
question = '<b>{{ lang._('Server: ') }}' + uuid + '</b></br>';
question += '<b>{{ lang._('State: ') }}' + state + '</b></br></br>';
let question = `<b>{{ lang._('Server: ') }}${uuid}</b></br>`;
question += `<b>{{ lang._('State: ') }}${state}</b></br></br>`;
question += '{{ lang._('Set administrative state for this server?') }} </br></br>';
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
question,
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
$.post('/api/haproxy/maintenance/server_state', payload, function(data) {
ajaxCall("/api/haproxy/maintenance/server_state", payload, function(data, status) {
if (data.status != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
@ -348,15 +312,15 @@ POSSIBILITY OF SUCH DAMAGE.
// set single - server weight
grid_status.find(".command-set-weight").off().on("click", function(e) {
var uuid = $(this).data("row-id");
var backend = uuid.split("/")[0];
var server = uuid.split("/")[1];
var currentWeight = $(this).data("weight");
const uuid = $(this).data("row-id");
const backend = uuid.split("/")[0];
const server = uuid.split("/")[1];
const currentWeight = $(this).data("weight");
question = '<b>{{ lang._('Server: ') }}' + uuid + '</b></br></br>';
let question = `<b>{{ lang._('Server: ') }}${uuid}</b></br></br>`;
question += '<b>{{ lang._('Weight: ') }}</b>';
question += '<div class="form-group" style="display: block;">';
question += '<input class="form-control" id="newWeight" value="' + currentWeight + '" type="text"/>';
question += `<input class="form-control" id="newWeight" value="${currentWeight}" type="text"/>`;
question += '</div>';
question += '{{ lang._('Set weight for this server?') }} </br></br>';
@ -364,13 +328,13 @@ POSSIBILITY OF SUCH DAMAGE.
question,
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
var payload = {
'backend': backend,
'server': server,
'weight': $("#newWeight").val()
const payload = {
'backend': backend,
'server': server,
'weight': $("#newWeight").val()
};
$.post('/api/haproxy/maintenance/server_weight', payload, function(data) {
ajaxCall("/api/haproxy/maintenance/server_weight", payload, function(data, status) {
if (data.status != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
@ -392,28 +356,23 @@ POSSIBILITY OF SUCH DAMAGE.
// set bulk - server state
grid_status.find("*[data-action=setStateBulk]").off().on("click", function(e) {
var rows = $("#grid-status").bootgrid("getSelectedRows");
var server_ids = rows.join()
var state = $(this).data("state");
var payload = {
'server_ids': server_ids,
'state': state
};
const rows = $("#grid-status").bootgrid("getSelectedRows");
const state = $(this).data("state");
const payload = {'server_ids': rows.join(), 'state': state};
if (rows != undefined && rows.length > 0) {
question = '<b>{{ lang._('Selected server: ') }}</b></br>';
question += '<ul>';
$.each(rows, function(key, id){
question += '<li>' + id + '</li>';
let question = '<b>{{ lang._('Selected server: ') }}</b></br><ul>';
rows.forEach(function(id) {
question += `<li>${id}</li>`;
});
question += '</ul>';
question += '<b>{{ lang._('State: ') }}' + state + '</b></br></br>';
question += `<b>{{ lang._('State: ') }}${state}</b></br></br>`;
question += '{{ lang._('Set administrative state for all selected servers?') }} </br></br>';
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
question,
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
$.post('/api/haproxy/maintenance/server_state_bulk', payload, function(data) {
ajaxCall("/api/haproxy/maintenance/server_state_bulk", payload, function(data, status) {
if (data.status != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
@ -439,14 +398,13 @@ POSSIBILITY OF SUCH DAMAGE.
// set bulk - server weight
grid_status.find("*[data-action=setWeightBulk]").off().on("click", function(e) {
var rows = $("#grid-status").bootgrid("getSelectedRows");
var server_ids = rows.join()
const rows = $("#grid-status").bootgrid("getSelectedRows");
const server_ids = rows.join();
if (rows != undefined && rows.length > 0) {
question = '<b>{{ lang._('Selected server: ') }}</b></br>';
question += '<ul>';
$.each(rows, function(key, id){
question += '<li>' + id + '</li>';
let question = '<b>{{ lang._('Selected server: ') }}</b></br><ul>';
rows.forEach(function(id) {
question += `<li>${id}</li>`;
});
question += '</ul>';
question += '<b>{{ lang._('Weight: ') }}</b>';
@ -458,12 +416,12 @@ POSSIBILITY OF SUCH DAMAGE.
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
question,
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
var payload = {
'server_ids': server_ids,
'weight': $("#newBulkWeight").val()
const payload = {
'server_ids': server_ids,
'weight': $("#newBulkWeight").val()
};
$.post('/api/haproxy/maintenance/server_weight_bulk', payload, function(data) {
ajaxCall("/api/haproxy/maintenance/server_weight_bulk", payload, function(data, status) {
if (data.status != 'ok') {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_DANGER,
@ -475,7 +433,6 @@ POSSIBILITY OF SUCH DAMAGE.
dialog.close();
// reload - because some are successfully executed
$("#grid-status").bootgrid("reload");
}
}]
});

View file

@ -1,6 +1,6 @@
{#
Copyright (C) 2016 Frank Wall
Copyright (C) 2016-2026 Frank Wall
OPNsense® is Copyright © 2014 2016 by Deciso B.V.
All rights reserved.
@ -29,7 +29,9 @@ POSSIBILITY OF SUCH DAMAGE.
<script>
$( document ).ready(function() {
var gridopt = {
'use strict';
const gridopt = {
ajax: false,
selection: false,
multiSelect: false
@ -37,118 +39,96 @@ POSSIBILITY OF SUCH DAMAGE.
$("#grid-status").bootgrid('destroy');
$("#grid-status").bootgrid(gridopt);
// build table rows safely from key/value data
function buildInfoRows(data) {
return Object.entries(data).map(function([key, value]) {
return $("<tr/>").append(
$("<td/>").text(key),
$("<td/>").text(value)
);
});
}
// build table rows from an array of objects using a field list
function buildGridRows(data, fields) {
return Object.values(data).map(function(value) {
const $tr = $("<tr/>");
fields.forEach(function(field) {
$("<td/>").text(value[field] != null ? value[field] : '').appendTo($tr);
});
return $tr;
});
}
// update info
$("#update-info").click(function() {
$('#processing-dialog').modal('show');
$('#updatelist').empty();
ajaxGet(url = "/api/haproxy/statistics/info/", sendData={},
callback = function (data, status) {
$("#infolist > tbody").empty();
$("#infolist > thead").hide();
if (status == "success") {
$("#infolist > thead").show();
$.each(data, function (key, value) {
$('#infolist > tbody').append('<tr><td>'+key+'</td>' +
"<td>"+value+"</td></tr>");
});
} else {
$("#infolist > tbody").append("<tr><td colspan=2 style='text-align:center;'><br/>{{ lang._('The statistics could not be fetched. Is HAProxy running?') }}<br/><br/></td></tr>");
}
$('#processing-dialog').modal('hide');
ajaxGet("/api/haproxy/statistics/info/", {},
function (data, status) {
$("#infolist > tbody").empty();
$("#infolist > thead").hide();
if (status == "success") {
$("#infolist > thead").show();
$("#infolist > tbody").append(buildInfoRows(data));
} else {
$("<tr/>").append(
$("<td/>").attr("colspan", 2).css("text-align", "center")
.html("<br/>{{ lang._('The statistics could not be fetched. Is HAProxy running?') }}<br/><br/>")
).appendTo("#infolist > tbody");
}
$('#processing-dialog').modal('hide');
}
);
});
// update status
$("#update-status").click(function() {
$('#processing-dialog').modal('show');
ajaxGet(url = "/api/haproxy/statistics/counters/", sendData={},
callback = function (data, status) {
if (status == "success") {
// status
$("#status_nav").show();
$("#grid-status").bootgrid('destroy');
var html = [];
$.each(data, function (key, value) {
var fields = ["id", "pxname", "svname", "status", "lastchg", "weight", "act", "downtime"];
tr_str = '<tr>';
for (var i = 0; i < fields.length; i++) {
if (value[fields[i]] != null) {
tr_str += '<td>' + value[fields[i]] + '</td>';
} else {
tr_str += '<td></td>';
}
}
tr_str += '</tr>';
html.push(tr_str);
});
$("#grid-status > tbody").html(html.join(''));
$("#grid-status").bootgrid(gridopt);
}
$('#processing-dialog').modal('hide');
ajaxGet("/api/haproxy/statistics/counters/", {},
function (data, status) {
if (status == "success") {
const fields = ["id", "pxname", "svname", "status", "lastchg", "weight", "act", "downtime"];
$("#status_nav").show();
$("#grid-status").bootgrid('destroy');
$("#grid-status > tbody").empty().append(buildGridRows(data, fields));
$("#grid-status").bootgrid(gridopt);
}
$('#processing-dialog').modal('hide');
}
);
});
// update counters
$("#update-counters").click(function() {
$('#processing-dialog').modal('show');
ajaxGet(url = "/api/haproxy/statistics/counters/", sendData={},
callback = function (data, status) {
if (status == "success") {
// counters
$("#counters_nav").show();
$("#grid-counters").bootgrid('destroy');
var html = [];
$.each(data, function (key, value) {
var fields = ["id", "pxname", "svname", "qcur", "qmax", "qlimit", "rate", "rate_max", "rate_lim", "scur", "smax", "slim", "stot", "bin", "bout", "dreq", "dresp", "ereq", "econ", "eresp", "wretr", "wredis"];
tr_str = '<tr>';
for (var i = 0; i < fields.length; i++) {
if (value[fields[i]] != null) {
tr_str += '<td>' + value[fields[i]] + '</td>';
} else {
tr_str += '<td></td>';
}
}
tr_str += '</tr>';
html.push(tr_str);
});
$("#grid-counters> tbody").html(html.join(''));
$("#grid-counters").bootgrid(gridopt);
}
$('#processing-dialog').modal('hide');
ajaxGet("/api/haproxy/statistics/counters/", {},
function (data, status) {
if (status == "success") {
const fields = ["id", "pxname", "svname", "qcur", "qmax", "qlimit", "rate", "rate_max", "rate_lim", "scur", "smax", "slim", "stot", "bin", "bout", "dreq", "dresp", "ereq", "econ", "eresp", "wretr", "wredis"];
$("#counters_nav").show();
$("#grid-counters").bootgrid('destroy');
$("#grid-counters > tbody").empty().append(buildGridRows(data, fields));
$("#grid-counters").bootgrid(gridopt);
}
$('#processing-dialog').modal('hide');
}
);
});
// update tables
$("#update-tables").click(function() {
$('#processing-dialog').modal('show');
ajaxGet(url = "/api/haproxy/statistics/tables/", sendData={},
callback = function (data, status) {
if (status == "success") {
// tables
$("#tables_nav").show();
$("#grid-tables").bootgrid('destroy');
var html = [];
$.each(data, function (key, value) {
var fields = ["table", "type", "size", "used"];
tr_str = '<tr>';
for (var i = 0; i < fields.length; i++) {
if (value[fields[i]] != null) {
tr_str += '<td>' + value[fields[i]] + '</td>';
} else {
tr_str += '<td></td>';
}
}
tr_str += '</tr>';
html.push(tr_str);
});
$("#grid-tables> tbody").html(html.join(''));
$("#grid-tables").bootgrid(gridopt);
}
$('#processing-dialog').modal('hide');
ajaxGet("/api/haproxy/statistics/tables/", {},
function (data, status) {
if (status == "success") {
const fields = ["table", "type", "size", "used"];
$("#tables_nav").show();
$("#grid-tables").bootgrid('destroy');
$("#grid-tables > tbody").empty().append(buildGridRows(data, fields));
$("#grid-tables").bootgrid(gridopt);
}
$('#processing-dialog').modal('hide');
}
);
});

View file

@ -2,7 +2,7 @@
<?php
/*
* Copyright (C) 2016-2018 Frank Wall
* Copyright (C) 2016-2026 Frank Wall
* Copyright (C) 2015 Deciso B.V.
* All rights reserved.
*
@ -41,12 +41,56 @@ if (isset($configObj->OPNsense->HAProxy->mapfiles)) {
foreach ($configObj->OPNsense->HAProxy->mapfiles->children() as $mapfile) {
$mf_name = (string)$mapfile->name;
$mf_id = (string)$mapfile->id;
$mf_url = (string)$mapfile->url;
if ($mf_id != "") {
$mf_content = htmlspecialchars_decode(str_replace("\r", "", (string)$mapfile->content));
$mf_filename = $export_path . $mf_id . ".txt";
file_put_contents($mf_filename, $mf_content);
chmod($mf_filename, 0600);
echo "map file exported to " . $mf_filename . "\n";
// Download file from URL (if URL was provided).
try {
if ($mf_url == "") {
throw new \Exception("no URL provided");
}
$fp = fopen($mf_filename, 'wb');
if ($fp === false) {
throw new \Exception("unable to open {$mf_filename} for writing");
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $mf_url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_FILE, $fp);
if (!curl_exec($ch)) {
throw new \Exception("download error: " . curl_error($ch));
}
echo "map file downloaded to " . $mf_filename . "\n";
} catch (\Exception $e) {
// Show error message only if URL was specified.
if ($mf_url != "") {
echo "download of map file failed, error: " . $e->getMessage() . "\n";
echo "trying to fill map file with fallback content\n";
$mf_content = "# NOTE: Download failed, this is the fallback content.\n";
} else {
$mf_content = '';
}
// Write contents to map file.
// This is also used as a fallback if map file download fails.
$mf_content = $mf_content . htmlspecialchars_decode(str_replace("\r", "", (string)$mapfile->content));
file_put_contents($mf_filename, $mf_content);
echo "map file exported to " . $mf_filename . "\n";
} finally {
if (isset($ch)) {
curl_close($ch);
}
if (isset($fp) && is_resource($fp)) {
fclose($fp);
}
chmod($mf_filename, 0600);
chown($mf_filename, 'www');
}
}
}
}

View file

@ -171,59 +171,44 @@
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'hdr' %}
{% do acl_options.append('hdr(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.hdr|default("") != "" %}
{% do acl_options.append('hdr(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.hdr) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'hdr_beg' %}
{% do acl_options.append('hdr_beg(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.hdr_beg|default("") != "" %}
{% do acl_options.append('hdr_beg(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.hdr_beg) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'hdr_end' %}
{% do acl_options.append('hdr_end(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.hdr_end|default("") != "" %}
{% do acl_options.append('hdr_end(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.hdr_end) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'hdr_reg' %}
{% do acl_options.append('hdr_reg(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.hdr_reg|default("") != "" %}
{% do acl_options.append('hdr_reg(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.hdr_reg) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'hdr_sub' %}
{% do acl_options.append('hdr_sub(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.hdr_sub|default("") != "" %}
{% do acl_options.append('hdr_sub(host)') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.hdr_sub) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'http_auth' %}
{% if acl_data.allowedUsers|default("") != "" or acl_data.allowedGroups|default("") != "" %}
@ -253,70 +238,52 @@
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'path' %}
{% do acl_options.append('path') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.path|default("") != "" %}
{% do acl_options.append('path') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.path) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'path_beg' %}
{% do acl_options.append('path_beg') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.path_beg|default("") != "" %}
{% do acl_options.append('path_beg') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.path_beg) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'path_dir' %}
{% do acl_options.append('path_dir') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.path_dur|default("") != "" %}
{% do acl_options.append('path_dir') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.path_dir) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'path_end' %}
{% do acl_options.append('path_end') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.path_end|default("") != "" %}
{% do acl_options.append('path_end') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.path_end) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'path_reg' %}
{% do acl_options.append('path_reg') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.path_reg|default("") != "" %}
{% do acl_options.append('path_reg') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.path_reg) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'path_sub' %}
{% do acl_options.append('path_sub') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% if acl_data.path_sub|default("") != "" %}
{% do acl_options.append('path_sub') %}
{% if acl_data.caseSensitive|default('0') == '0' %}
{% do acl_options.append('-i') %}
{% endif %}
{% do acl_options.append(acl_data.path_sub) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'sc_bytes_in_rate' %}
{% if acl_data.sc_number|default("") != "" and acl_data.sc_bytes_in_rate|default("") != "" %}
@ -481,7 +448,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_conn_cnt(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_conn_cnt_comparison ~ ' ' ~ acl_data.sc_conn_cnt) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_conn_cnt(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_conn_cnt_comparison ~ ' ' ~ acl_data.sc_conn_cnt) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -493,7 +465,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_conn_cur(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_conn_cur_comparison ~ ' ' ~ acl_data.sc_conn_cur) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_conn_cur(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_conn_cur_comparison ~ ' ' ~ acl_data.sc_conn_cur) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -505,7 +482,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_conn_rate(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_conn_rate_comparison ~ ' ' ~ acl_data.sc_conn_rate) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_conn_rate(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_conn_rate_comparison ~ ' ' ~ acl_data.sc_conn_rate) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -817,7 +799,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_err_cnt(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_http_err_cnt_comparison ~ ' ' ~ acl_data.sc_http_err_cnt) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_err_cnt(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_http_err_cnt_comparison ~ ' ' ~ acl_data.sc_http_err_cnt) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -829,7 +816,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_err_rate(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_http_err_rate_comparison ~ ' ' ~ acl_data.sc_http_err_rate) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_err_rate(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_http_err_rate_comparison ~ ' ' ~ acl_data.sc_http_err_rate) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -841,7 +833,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_fail_cnt(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_http_fail_cnt_comparison ~ ' ' ~ acl_data.sc_http_fail_cnt) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_fail_cnt(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_http_fail_cnt_comparison ~ ' ' ~ acl_data.sc_http_fail_cnt) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -853,7 +850,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_fail_rate(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_http_fail_rate_comparison ~ ' ' ~ acl_data.sc_http_fail_rate) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_fail_rate(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_http_fail_rate_comparison ~ ' ' ~ acl_data.sc_http_fail_rate) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -865,7 +867,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_req_cnt(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_http_req_cnt_comparison ~ ' ' ~ acl_data.sc_http_req_cnt) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_req_cnt(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_http_req_cnt_comparison ~ ' ' ~ acl_data.sc_http_req_cnt) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -877,7 +884,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_req_rate(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_http_req_rate_comparison ~ ' ' ~ acl_data.sc_http_req_rate) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_http_req_rate(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_http_req_rate_comparison ~ ' ' ~ acl_data.sc_http_req_rate) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -997,7 +1009,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_sess_cnt(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_sess_cnt_comparison ~ ' ' ~ acl_data.sc_sess_cnt) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_sess_cnt(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_sess_cnt_comparison ~ ' ' ~ acl_data.sc_sess_cnt) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -1009,7 +1026,12 @@
{% else %}
{% set table_data = '' %}
{% endif %}
{% do acl_options.append('sc_sess_rate(' ~ acl_data.sc_number ~ table_data ~ ') ' ~ acl_data.sc_sess_rate_comparison ~ ' ' ~ acl_data.sc_sess_rate) %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('sc_sess_rate(' ~ acl_data.sc_number ~ table_data ~ ')' ~ converter_data ~ ' ' ~ acl_data.sc_sess_rate_comparison ~ ' ' ~ acl_data.sc_sess_rate) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
@ -1414,6 +1436,18 @@
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{% elif acl_data.expression == 'var' %}
{% if acl_data.var|default("") != "" and acl_data.var_value|default("") != "" %}
{% if acl_data.converter|default("") != "" %}
{% set converter_data = ',' ~ acl_data.converter %}
{% else %}
{% set converter_data = '' %}
{% endif %}
{% do acl_options.append('var' ~ acl_data.var ~ converter_data ~ ' ' ~ acl_data.var_comparison ~ ' ' ~ acl_data.var_value) %}
{% else %}
{% set acl_enabled = '0' %}
# ERROR: missing parameters
{% endif %}
{# # handle boolean ACL types that do not require any input #}
{% elif acl_data.expression in acl_boolean_types %}
{% do acl_options.append(acl_data.expression) %}
@ -1520,6 +1554,8 @@
{% set action_keyword_data = 'lua.' ~ action_data.http_request_option %}
{% elif action_data.http_request_action == 'set-var' %}
{% set action_keyword_data = 'set-var' ~ action_data.http_request_option %}
{% elif action_data.http_request_action == 'set-var-fmt' %}
{% set action_keyword_data = 'set-var-fmt' ~ action_data.http_request_option %}
{% elif action_data.http_request_action == 'use-service' %}
{% set action_keyword_data = 'use-service lua.' ~ action_data.http_request_option %}
{% else %}
@ -1670,6 +1706,22 @@
{% set action_enabled = '0' %}
{% do global_action_options.append('# ERROR: unsupported rule type ' ~ action_data.type) %}
{% endif %}
{# # Add sample fetch to map file config. #}
{% if action_data.mapfile|default("") != "" %}
{% set mapfile_data = helpers.getUUID(action_data.mapfile) %}
{% set mapfile_path = '/tmp/haproxy/mapfiles/' ~ mapfile_data.id ~ '.txt' %}
{% set mapfile_config = 'map_' ~ mapfile_data.type %}
{% if action_data.map_default|default("") != "" %}
{% set mapfile_default = ',' ~ action_data.map_default %}
{% endif %}
{% if action_data.sample_fetch|default("") != "" %}
{% set mapfile_sf = action_data.sample_fetch ~ ',' %}
{% endif %}
{% do action_options.append(mapfile_sf ~ mapfile_config ~ '(' ~ mapfile_path ~ mapfile_default ~ ')') %}
{# # Add/append sample fetch. #}
{% elif action_data.sample_fetch|default("") != "" %}
{% do action_options.append(action_data.sample_fetch) %}
{% endif %}
{# # Is this rule enabled in the GUI? #}
{% if action_data.enabled|default('') == '1' %}
{# # check if action is valid #}

View file

@ -1,6 +1,6 @@
PLUGIN_NAME= isc-dhcp
PLUGIN_VERSION= 1.0
PLUGIN_REVISION= 3
PLUGIN_REVISION= 4
PLUGIN_COMMENT= ISC DHCPv4/v6 server
PLUGIN_DEPENDS= isc-dhcp44-server
PLUGIN_MAINTAINER= franco@opnsense.org

View file

@ -1325,3 +1325,21 @@ function dhcpd_staticarp($interface, $ifconfig_details)
interfaces_neighbors_configure($interface, $ifconfig_details);
}
}
function dhcpd_ip_in_interface_alias_subnet($interface, $ipalias)
{
if (empty($interface) || !is_ipaddr($ipalias)) {
return false;
}
foreach (config_read_array('virtualip', 'vip', false) as $vip) {
if ($vip['mode'] == 'ipalias' && $vip['interface'] == $interface) {
$subnet = is_ipaddrv6($ipalias) ? gen_subnetv6($vip['subnet'], $vip['subnet_bits']) : gen_subnet($vip['subnet'], $vip['subnet_bits']);
if (ip_in_subnet($ipalias, "{$subnet}/{$vip['subnet_bits']}")) {
return true;
}
}
}
return false;
}

View file

@ -0,0 +1,58 @@
<?php
/*
* Copyright (C) 2023 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Interfaces\Neighbor;
use OPNsense\Core\Config;
class dhcpd
{
public function collect()
{
$result = [];
$intfmap = [];
$config = Config::getInstance()->object();
if ($config->dhcpd->count() > 0) {
foreach ($config->dhcpd->children() as $intf => $node) {
foreach ($node->children() as $key => $data) {
if ($key == 'staticmap') {
if (!empty($data->arp_table_static_entry) || !empty($node->staticarp)) {
$result[] = [
'etheraddr' => (string)$data->mac,
'ipaddress' => (string)$data->ipaddr,
'descr' => (string)$data->descr,
'source' => sprintf('dhcpd-%s', $intf)
];
}
}
}
}
}
return $result;
}
}

View file

@ -175,7 +175,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
list (, $parent_net) = interfaces_primary_address($pconfig['if']);
if (is_subnetv4($parent_net) && $pconfig['gateway'] && $pconfig['gateway'] != "none") {
if (!ip_in_subnet($pconfig['gateway'], $parent_net) && !ip_in_interface_alias_subnet($pconfig['if'], $pconfig['gateway'])) {
if (!ip_in_subnet($pconfig['gateway'], $parent_net) && !dhcpd_ip_in_interface_alias_subnet($pconfig['if'], $pconfig['gateway'])) {
$input_errors[] = sprintf(gettext("The gateway address %s does not lie within the chosen interface's subnet."), $pconfig['gateway']);
}
}

View file

@ -167,7 +167,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
if (is_subnetv4($parent_net) && $pconfig['gateway'] != "none" && !empty($pconfig['gateway'])) {
if (!ip_in_subnet($pconfig['gateway'], $parent_net) && !ip_in_interface_alias_subnet($if, $pconfig['gateway'])) {
if (!ip_in_subnet($pconfig['gateway'], $parent_net) && !dhcpd_ip_in_interface_alias_subnet($if, $pconfig['gateway'])) {
$input_errors[] = sprintf(gettext("The gateway address %s does not lie within the chosen interface's subnet."), $_POST['gateway']);
}
}

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= tayga
PLUGIN_VERSION= 1.4
PLUGIN_VERSION= 1.5
PLUGIN_COMMENT= Tayga NAT64
PLUGIN_DEPENDS= tayga
PLUGIN_MAINTAINER= m.muenz@gmail.com

View file

@ -7,6 +7,10 @@ networks where dedicated NAT64 hardware would be overkill.
Plugin Changelog
================
1.5
* Allow non-global IPv4 addresses when using 64:ff9b::/96 (contributed by Maurice Walker)
1.4
* Enable forwarding of UDP packets with zero checksum (contributed by Maurice Walker)

View file

@ -3,6 +3,7 @@
tun-device nat64
data-dir /var/db/tayga
udp-cksum-mode fwd
wkpf-strict no
ipv4-addr {{ OPNsense.tayga.general.v4address }}
{% if helpers.exists('OPNsense.tayga.general.v6address') and OPNsense.tayga.general.v6address != '' %}

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= upnp
PLUGIN_VERSION= 1.8
PLUGIN_VERSION= 1.9
PLUGIN_DEPENDS= miniupnpd
PLUGIN_COMMENT= UPnP IGD & PCP/NAT-PMP Service
PLUGIN_MAINTAINER= franco@opnsense.org

View file

@ -7,6 +7,24 @@ WWW: https://miniupnp.tuxfamily.org/
Plugin Changelog
================
1.9
* Separate service log file and log level UI option
* More specific allow third-party mapping UI option
* Impove help/wording and update missed changelog
* Add daemon patch to improve logging
(all contributed by Self-Hosting-Group)
1.8
* New UI options: disable IPv6 mapping, allow third-party mapping, UPnP IGD compatibility, router/friendly name; remove option: report system uptime (bug)
* List IPv6 maps and keep active maps when reconfiguring/restarting service, clearer added via / description field
* New UI sections, rewording plugin, set allow-filtered with STUN to workaround CGNAT test limitation, clean up daemon config
* Update daemon to 2.3.9, add build options (e.g. IGDv2 support), add daemon patch to improve UPnP IGDv2 compatibility
(all contributed by Self-Hosting-Group)
1.7
* Add option to allow arbitrary number of UPnP/NAT-PMP rules (contributed by Kreeblah)

View file

@ -26,6 +26,11 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
function miniupnpd_syslog()
{
return ['miniupnpd' => ['facility' => ['miniupnpd']]];
}
function miniupnpd_enabled()
{
global $config;
@ -39,10 +44,12 @@ function miniupnpd_firewall($fw)
return;
}
/* required for IPv4: */
$fw->registerAnchor('miniupnpd', 'rdr');
/* required for IPv6: */
$fw->registerAnchor('miniupnpd', 'fw');
/* required for IPv4 NAT hairpinning: */
$fw->registerAnchor('miniupnpd', 'nat', 0, 'head');
$fw->registerAnchor('miniupnpd', 'binat');
}
function miniupnpd_services()
@ -67,6 +74,8 @@ function miniupnpd_services()
function miniupnpd_start()
{
global $config;
if (!miniupnpd_enabled()) {
return;
}
@ -75,15 +84,29 @@ function miniupnpd_start()
return;
}
mwexecfb('/usr/local/sbin/miniupnpd -f %s -P %s', [ '/var/etc/miniupnpd.conf', '/var/run/miniupnpd.pid']);
$cmd_frmt = ['/usr/local/sbin/miniupnpd -f %s -P %s'];
$cmd_args = ['/var/etc/miniupnpd.conf', '/var/run/miniupnpd.pid'];
switch ($config['installedpackages']['miniupnpd']['config'][0]['log_level'] ?? '') {
case 'debug':
$cmd_frmt[] = '-vv';
break;
case 'info':
$cmd_frmt[] = '-v';
break;
default:
break;
}
mwexecfb($cmd_frmt, $cmd_args);
}
function miniupnpd_stop()
{
killbypid('/var/run/miniupnpd.pid');
mwexecf('/sbin/pfctl -a miniupnpd -Fr');
mwexecf('/sbin/pfctl -a miniupnpd -Fn');
mwexecf('/sbin/pfctl -a miniupnpd -F rules');
mwexecf('/sbin/pfctl -a miniupnpd -F nat');
}
function miniupnpd_configure()
@ -198,11 +221,15 @@ function miniupnpd_configure_do($verbose = false)
$config_text .= "bitrate_up={$upload}\n";
}
if (!empty($upnp_config['allow_third_party_mapping'])) {
if (in_array($upnp_config['allow_third_party_mapping'] ?? '', ['1', 'upnp-igd'])) {
$config_text .= "secure_mode=no\n";
$config_text .= "pcp_allow_thirdparty=yes\n";
} else {
$config_text .= "secure_mode=yes\n";
}
if (in_array($upnp_config['allow_third_party_mapping'] ?? '', ['1', 'pcp'])) {
$config_text .= "pcp_allow_thirdparty=yes\n";
} else {
$config_text .= "pcp_allow_thirdparty=no\n";
}
@ -217,7 +244,8 @@ function miniupnpd_configure_do($verbose = false)
/* enable system uptime instead of miniupnpd uptime */
if (!empty($upnp_config['sysuptime'])) {
$config_text .= "system_uptime=yes\n";
/* Disable system uptime to workaround daemon bug with PCP/NAT-PMP epoch on BSD */
//$config_text .= "system_uptime=yes\n";
}
/* set webgui url */

View file

@ -11,4 +11,11 @@
<pattern>status_upnp.php*</pattern>
</patterns>
</page-status-upnpstatus>
<page-diagnostics-logs-upnp>
<name>Services: UPnP IGD &amp; PCP: Log File</name>
<patterns>
<pattern>ui/diagnostics/log/core/miniupnpd/*</pattern>
<pattern>api/diagnostics/log/core/miniupnpd/*</pattern>
</patterns>
</page-diagnostics-logs-upnp>
</acl>

View file

@ -5,6 +5,7 @@
<Edit url="/services_upnp.php?*" visibility="hidden"/>
</Settings>
<ActiveMaps VisibleName="Active Maps" order="20" url="/status_upnp.php"/>
<LogFile VisibleName="Log File" order="30" url="/ui/diagnostics/log/core/miniupnpd"/>
</UPnP>
</Services>
</menu>

View file

@ -0,0 +1,6 @@
###################################################################
# Local syslog-ng configuration filter definition [miniupnpd].
###################################################################
filter f_local_miniupnpd {
program("miniupnpd");
};

View file

@ -78,6 +78,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
'friendly_name',
'iface_array',
'ipv6_disable',
'log_level',
'logpackets',
'overridesubnet',
'overridewanip',
@ -178,7 +179,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// save form data
$upnp = [];
// boolean types
foreach (['enable', 'enable_upnp', 'enable_natpmp', 'logpackets', 'sysuptime', 'permdefault', 'allow_third_party_mapping', 'ipv6_disable'] as $fieldname) {
foreach (['enable', 'enable_upnp', 'enable_natpmp', 'logpackets', 'sysuptime', 'permdefault', 'ipv6_disable'] as $fieldname) {
$upnp[$fieldname] = !empty($pconfig[$fieldname]);
}
// numeric types
@ -186,7 +187,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$upnp['num_permuser'] = $pconfig['num_permuser'];
}
// text field types
foreach (['ext_iface', 'download', 'upload', 'overridewanip', 'overridesubnet', 'stun_host', 'stun_port', 'friendly_name', 'upnp_igd_compat'] as $fieldname) {
foreach (['allow_third_party_mapping', 'download', 'ext_iface', 'friendly_name', 'log_level', 'overridesubnet', 'overridewanip', 'stun_host', 'stun_port', 'upload', 'upnp_igd_compat'] as $fieldname) {
$upnp[$fieldname] = $pconfig[$fieldname];
}
foreach (miniupnpd_permuser_list() as $fieldname) {
@ -233,16 +234,16 @@ include("head.inc");
</thead>
<tbody>
<tr>
<td><a id="help_for_enable" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Enable");?></td>
<td><a id="help_for_enable" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Enabled");?></td>
<td>
<input name="enable" type="checkbox" value="yes" <?=!empty($pconfig['enable']) ? "checked=\"checked\"" : ""; ?> />
<div class="hidden" data-for="help_for_enable">
<?=gettext("Start the autonomous port mapping service.");?>
<?=gettext("Enable the autonomous port mapping service.");?>
</div>
</td>
</tr>
<tr>
<td><a id="help_for_enable_upnp" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Allow UPnP IGD Port Mapping");?></td>
<td><a id="help_for_enable_upnp" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Enable UPnP IGD protocol");?></td>
<td>
<input name="enable_upnp" type="checkbox" value="yes" <?=!empty($pconfig['enable_upnp']) ? "checked=\"checked\"" : ""; ?> />
<div class="hidden" data-for="help_for_enable_upnp">
@ -251,7 +252,7 @@ include("head.inc");
</td>
</tr>
<tr>
<td><a id="help_for_enable_natpmp" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Allow PCP/NAT-PMP Port Mapping");?></td>
<td><a id="help_for_enable_natpmp" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Enable PCP/NAT-PMP protocols");?></td>
<td>
<input name="enable_natpmp" type="checkbox" value="yes" <?=!empty($pconfig['enable_natpmp']) ? "checked=\"checked\"" : ""; ?> />
<div class="hidden" data-for="help_for_enable_natpmp">
@ -324,16 +325,19 @@ include("head.inc");
</td>
</tr>
<tr>
<td><i class="fa fa-info-circle text-muted"></i> <?=gettext("Override external IPv4");?></td>
<td><a id="help_for_overridewanip" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Override external IPv4");?></td>
<td>
<input name="overridewanip" type="text" value="<?=$pconfig['overridewanip'];?>" />
<div class="hidden" data-for="help_for_overridewanip">
<?=gettext('Report custom public/external (WAN) IPv4 address.');?>
</div>
</td>
</tr>
<tr>
<td><a id="help_for_overridesubnet" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Internal interface IPv4 subnet override");?></td>
<td>
<select name="overridesubnet" class="selectpicker" id="overridesubnet">
<option value="" <?= empty($pconfig['overridesubnet']) ? 'selected="selected"' : '' ?>><?= gettext('default') ?></option>
<option value="" <?= empty($pconfig['overridesubnet']) ? 'selected="selected"' : '' ?>><?= gettext('Default') ?></option>
<?php for ($i = 32; $i >= 1; $i--): ?>
<option value="<?= $i ?>" <?=!empty($pconfig['overridesubnet']) && $pconfig['overridesubnet'] == $i ? 'selected="selected"' : '' ?>><?= $i ?></option>
<?php endfor ?>
@ -346,9 +350,14 @@ include("head.inc");
<tr>
<td><a id="help_for_allow_third_party_mapping" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Allow third-party mapping");?></td>
<td>
<input name="allow_third_party_mapping" type="checkbox" value="yes" <?=!empty($pconfig['allow_third_party_mapping']) ? "checked=\"checked\"" : ""; ?> />
<select name="allow_third_party_mapping">
<option value="0" <?= ($pconfig['allow_third_party_mapping'] ?? '') == '0' ? 'selected="selected"' : '' ?> ><?= gettext('Disabled (recommended)') ?></option>
<option value="1" <?= ($pconfig['allow_third_party_mapping'] ?? '') == '1' ? 'selected="selected"' : '' ?> ><?= gettext('Enabled') ?></option>
<option value="upnp-igd" <?= ($pconfig['allow_third_party_mapping'] ?? '') == 'upnp-igd' ? 'selected="selected"' : '' ?> ><?= gettext('Enabled (UPnP IGD only)') ?></option>
<option value="pcp" <?= ($pconfig['allow_third_party_mapping'] ?? '') == 'pcp' ? 'selected="selected"' : '' ?> ><?= gettext('Enabled (PCP only)') ?></option>
</select>
<div class="hidden" data-for="help_for_allow_third_party_mapping">
<?=gettext("Allow adding port maps for non-requesting IP addresses.");?>
<?=gettext("Allow adding port maps for non-requesting IP addresses; use with care.");?>
</div>
</td>
</tr>
@ -367,6 +376,16 @@ include("head.inc");
</div>
</td>
</tr> -->
<tr>
<td><i class="fa fa-info-circle text-muted"></i> <?= gettext('Log level') ?></td>
<td>
<select name="log_level">
<option value="" <?= ($pconfig['log_level'] ?? '') == '' ? 'selected="selected"' : '' ?> ><?= gettext('Default') ?></option>
<option value="info" <?= ($pconfig['log_level'] ?? '') == 'info' ? 'selected="selected"' : '' ?> ><?= gettext('Info') ?></option>
<option value="debug" <?= ($pconfig['log_level'] ?? '') == 'debug' ? 'selected="selected"' : '' ?> ><?= gettext('Debug') ?></option>
</select>
</td>
</tr>
<tr>
<td><a id="help_for_logpackets" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Firewall logs");?></td>
<td>
@ -393,12 +412,15 @@ include("head.inc");
</thead>
<tbody>
<tr>
<td><i class="fa fa-info-circle text-muted"></i> <?= gettext('UPnP IGD compatibility mode') ?></td>
<td><a id="help_for_upnp_igd_compat" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?= gettext('UPnP IGD compatibility') ?></td>
<td>
<select name="upnp_igd_compat">
<option value="igdv1" <?= ($pconfig['upnp_igd_compat'] ?? '') == 'igdv1' ? 'selected="selected"' : '' ?> ><?= gettext('IGDv1 (IPv4 only)') ?></option>
<option value="igdv2" <?= ($pconfig['upnp_igd_compat'] ?? '') == 'igdv2' ? 'selected="selected"' : '' ?> ><?= gettext('IGDv2 (with workarounds)') ?></option>
</select>
<div class="hidden" data-for="help_for_upnp_igd_compat">
<?=sprintf(gettext('Set compatibility mode (act as device) to workaround IGDv2-incompatible clients; %s are known to only work with %s.'), 'Sony PS, Activision CoD…', 'IGDv1');?>
</div>
</td>
</tr>
<tr>
@ -436,7 +458,7 @@ include("head.inc");
<table class="table table-striped opnsense_standard_table_form">
<thead>
<tr>
<th colspan="2"><?=gettext("Custom Access Control List");?></th>
<th colspan="2"><?=gettext("Access Control List");?></th>
</tr>
</thead>
<tbody>

View file

@ -71,8 +71,8 @@ include("head.inc");
<th><?=gettext("Port")?></th>
<th><?=gettext("External port")?></th>
<th><?=gettext("Protocol")?></th>
<th><?=gettext("Source IP")?></th>
<th><?=gettext("Source port")?></th>
<th><?=gettext("Remote IP")?></th>
<th><?=gettext("Remote port")?></th>
<th><?=gettext("Added via / description")?></th>
</tr>
</thead>
@ -83,8 +83,8 @@ include("head.inc");
!preg_match('/on (?P<iface>.+) inet6 proto (?P<proto>.+) from (?P<srcaddr>[^ ]+) (port = (?P<srcport>.+) )?to (?P<intaddr>.+) port = (?P<intport>\d+) (flags [^ ]+ )?keep state (label "(?P<descr>.+)" )?rtable [0-9]/', $rdr_entry, $matches)) {
continue;
}
if (preg_match('/PCP ([A-Z]+) ([0-9a-f]{24})$/', $matches['descr'], $descrmatch) === 1) {
$descr = "PCP ({$descrmatch[1]} nonce {$descrmatch[2]})";
if (preg_match('/PCP [A-Z]+ ([0-9a-f]{24})$/', $matches['descr'], $descrmatch) === 1) {
$descr = "PCP (nonce {$descrmatch[1]})";
} elseif (preg_match('/^NAT-PMP \d+ \w+$/', $matches['descr'], $descrmatch) === 1) {
$descr = 'NAT-PMP';
} elseif (preg_match('/^pinhole-(\d+).*IGD2 pinhole$/', $matches['descr'], $descrmatch) === 1) {

View file

@ -1,6 +1,6 @@
PLUGIN_NAME= wol
PLUGIN_VERSION= 2.5
PLUGIN_REVISION= 3
PLUGIN_REVISION= 4
PLUGIN_DEPENDS= wol
PLUGIN_COMMENT= Wake on LAN Service

View file

@ -4,6 +4,7 @@
<patterns>
<pattern>ui/wol/*</pattern>
<pattern>api/wol/wol/*</pattern>
<pattern>api/diagnostics/interface/get_arp*</pattern>
</patterns>
</page-services-wakeonlan>
</acl>

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= acme-client
PLUGIN_VERSION= 4.13
PLUGIN_VERSION= 4.15
PLUGIN_COMMENT= ACME Client
PLUGIN_MAINTAINER= opnsense@moov.de
PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon

View file

@ -8,6 +8,21 @@ WWW: https://github.com/acmesh-official/acme.sh
Plugin Changelog
================
4.15
Added:
* add support for deploy hook "truenas_ws" (#5309)
Changed:
* always use configured cert name in cert description (#5282)
4.14
Fixed:
* fix class name of Google Domains DNS API (to make PHP linter happy)
* parameters for TrueNAS automation not visible (#5210)
* multiple buttons not working (#5123)
4.13
Added:

View file

@ -377,6 +377,47 @@
<type>header</type>
<style>method_table method_table_acme_truenas</style>
</field>
<field>
<id>action.acme_truenas_apikey</id>
<label>TrueNAS API key</label>
<type>text</type>
<help>API key generated in the TrueNAS web UI.</help>
</field>
<field>
<id>action.acme_truenas_hostname</id>
<label>TrueNAS hostname</label>
<type>text</type>
<help>Hostname or IP address of TrueNAS Server.</help>
</field>
<field>
<id>action.acme_truenas_scheme</id>
<label>TrueNAS scheme</label>
<type>dropdown</type>
<help>Connection scheme that will be used when uploading certificates to TrueNAS Core Server.</help>
</field>
<field>
<label>Required Parameters</label>
<type>header</type>
<style>method_table method_table_acme_truenasws</style>
</field>
<field>
<id>action.acme_truenasws_apikey</id>
<label>TrueNAS API key</label>
<type>text</type>
<help>API key generated in the TrueNAS web UI.</help>
</field>
<field>
<id>action.acme_truenasws_hostname</id>
<label>TrueNAS hostname</label>
<type>text</type>
<help>Hostname or IP address of TrueNAS Server.</help>
</field>
<field>
<id>action.acme_truenasws_protocol</id>
<label>TrueNAS protocol</label>
<type>dropdown</type>
<help>Connection scheme that will be used when uploading certificates to TrueNAS Server.</help>
</field>
<field>
<label>Required Parameters</label>
<type>header</type>
@ -399,24 +440,6 @@
<label>Ruckus Password</label>
<type>password</type>
</field>
<field>
<id>action.acme_truenas_apikey</id>
<label>TrueNAS API key</label>
<type>text</type>
<help>API key generated in the TrueNAS web UI.</help>
</field>
<field>
<id>action.acme_truenas_hostname</id>
<label>TrueNAS hostname</label>
<type>text</type>
<help>Hostname or IP address of TrueNAS Core Server.</help>
</field>
<field>
<id>action.acme_truenas_scheme</id>
<label>TrueNAS scheme</label>
<type>dropdown</type>
<help>Connection scheme that will be used when uploading certificates to TrueNAS Core Server.</help>
</field>
<field>
<label>Required Parameters</label>
<type>header</type>

View file

@ -0,0 +1,48 @@
<?php
/*
* Copyright (C) 2026 Konstantinos Spartalis (cspartalis@potatonetworks.com)
* Copyright (C) 2023 Jan Winkler
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\AcmeClient\LeAutomation;
use OPNsense\AcmeClient\LeAutomationInterface;
/**
* Run acme.sh deploy hook truenas_ws
* @package OPNsense\AcmeClient
*/
class AcmeTruenasWS extends Base implements LeAutomationInterface
{
public function prepare()
{
$this->acme_env['DEPLOY_TRUENAS_APIKEY'] = (string)$this->config->acme_truenasws_apikey;
$this->acme_env['DEPLOY_TRUENAS_HOSTNAME'] = (string)$this->config->acme_truenasws_hostname;
$this->acme_env['DEPLOY_TRUENAS_PROTOCOL'] = (string)$this->config->acme_truenasws_protocol;
$this->acme_args[] = '--deploy-hook truenas_ws --insecure';
return true;
}
}

View file

@ -203,7 +203,6 @@ class LeCertificate extends LeCommon
$cert_details = CertStore::parseX509($cert_content);
$cert_subject = $cert_details['name'];
$cert_serial = $cert_details['serialNumber'];
$cert_cn = $cert_details['commonname'];
$cert_issuer = implode(",", $cert_details['issuer']);
} else {
LeUtils::log_error('unable to read certificate content from file');
@ -226,12 +225,7 @@ class LeCertificate extends LeCommon
$cert = array();
$cert['refid'] = uniqid();
$cert['caref'] = (string)$ca['refid'];
if (empty($cert_cn)) {
// Fallback to configured name if Common Name is empty (e.g. for IP certificates)
$cert['descr'] = (string)$this->config->name . ' (ACME Client)';
} else {
$cert['descr'] = (string)$cert_cn . ' (ACME Client)';
}
$cert['descr'] = (string)$this->config->name . ' (ACME Client)';
$import_log_message = 'imported';
$cert_found = false;
@ -273,7 +267,7 @@ class LeCertificate extends LeCommon
$newcert->crt = base64_encode($cert_content);
$newcert->prv = base64_encode($key_content);
}
LeUtils::log("{$import_log_message} ACME X.509 certificate: {$cert_cn} ({$cert['refid']})");
LeUtils::log("{$import_log_message} ACME X.509 certificate: {$this->config->name} ({$cert['refid']})");
// Serialize to config and save
// Skip validation because the current in-memory model may not

View file

@ -34,7 +34,7 @@ use OPNsense\Core\Config;
* Google Domains DNS API
* @package OPNsense\AcmeClient
*/
class DnsGoogleDomains extends Base implements LeValidationInterface
class DnsGoogledomains extends Base implements LeValidationInterface
{
public function prepare()
{

View file

@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/AcmeClient</mount>
<version>4.3.0</version>
<version>4.3.1</version>
<description>A secure ACME Client plugin</description>
<items>
<settings>
@ -1430,7 +1430,8 @@
<acme_ruckus>Upload certificate to Ruckus controller</acme_ruckus>
<acme_vault>Upload certificate to HashiCorp Vault</acme_vault>
<acme_synology_dsm>Upload certificate to Synology DSM</acme_synology_dsm>
<acme_truenas>Upload certificate to TrueNAS Core Server</acme_truenas>
<acme_truenas>Upload certificate to TrueNAS Server (deprecated API)</acme_truenas>
<acme_truenasws>Upload certificate to TrueNAS Server (Websocket API)</acme_truenasws>
<acme_zyxel_gs1900>Upload certificate to Zyxel GS1900 series switches</acme_zyxel_gs1900>
<acme_unifi>Update local Unifi keystore</acme_unifi>
<configd_generic>System or Plugin Command</configd_generic>
@ -1744,6 +1745,25 @@
<https>HTTPS</https>
</OptionValues>
</acme_truenas_scheme>
<acme_truenasws_apikey type="TextField">
<Required>N</Required>
<Mask>/^.{1,1024}$/u</Mask>
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
</acme_truenasws_apikey>
<acme_truenasws_hostname type="HostnameField">
<Default>localhost</Default>
<Required>N</Required>
<Mask>/^.{1,1024}$/u</Mask>
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
</acme_truenasws_hostname>
<acme_truenasws_protocol type="OptionField">
<Default>ws</Default>
<Required>N</Required>
<OptionValues>
<ws>ws [default]</ws>
<wss>wss</wss>
</OptionValues>
</acme_truenasws_protocol>
<acme_unifi_keystore type="TextField">
<Default>/usr/local/share/java/unifi/data/keystore</Default>
<Required>N</Required>

View file

@ -99,224 +99,38 @@ POSSIBILITY OF SUCH DAMAGE.
},
};
/**
* reload bootgrid, return to current selected page
*/
function std_bootgrid_reload(gridId) {
var currentpage = $("#"+gridId).bootgrid("getCurrentPage");
$("#"+gridId).bootgrid("reload");
// absolutely not perfect, bootgrid.reload doesn't seem to support when().done()
setTimeout(function(){
$('#'+gridId+'-footer a[data-page="'+currentpage+'"]').click();
}, 400);
}
/**
* copy actions for selected items from opnsense_bootgrid_plugin.js
*/
const grid_accounts = $("#grid-accounts").UIBootgrid($.extend(gridParams, { options: gridopt }));
$("#grid-accounts").on("loaded.rs.jquery.bootgrid", function (e)
{
// toggle all rendered tooltips (once for all)
$('.bootgrid-tooltip').tooltip();
// scale footer on resize
$(this).find("tfoot td:first-child").attr('colspan',$(this).find("th").length - 1);
$(this).find('tr[data-row-id]').each(function(){
if ($(this).find('[class*="command-toggle"]').first().data("value") == "0") {
$(this).addClass("text-muted");
const grid_accounts = $("#grid-accounts").UIBootgrid($.extend(gridParams, {
options: gridopt,
commands: {
register: {
method: function(event, cell) {
var uuid = $(this).data("row-id");
stdDialogConfirm(
'{{ lang._('Confirmation Required') }}',
'{{ lang._('Register the selected account with the configured ACME CA?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}',
function() {
ajaxCall(gridParams['register'] + uuid, {}, function() {
grid_accounts.bootgrid("reload");
});
}
);
},
classname: 'fa fa-address-book-o',
title: '{{ lang._('Register account') }}',
sequence: 510
}
});
// edit dialog id to use
var editDlg = $(this).attr('data-editDialog');
var gridId = $(this).attr('id');
// link Add new to child button with data-action = add
$(this).find("*[data-action=add]").click(function(){
if ( gridParams['get'] != undefined && gridParams['add'] != undefined) {
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'];
mapDataToFormUI(urlMap).done(function(){
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
//
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
saveFormToEndpoint(url=gridParams['add'],
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
$("#"+gridId).bootgrid("reload");
}, true);
});
} else {
console.log("[grid] action add missing")
},
tabulatorOptions: {
rowFormatter: function(row) {
if (parseInt(row.getData()['enabled'], 2) !== 1) {
$(row.getElement()).addClass('text-muted');
} else {
$(row.getElement()).removeClass('text-muted');
}
}
});
// link delete selected items action
$(this).find("*[data-action=deleteSelected]").click(function(){
if ( gridParams['del'] != undefined) {
stdDialogConfirm('{{ lang._('Confirm removal') }}',
'{{ lang._('Do you want to remove the selected item?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function () {
var rows =$("#"+gridId).bootgrid('getSelectedRows');
if (rows != undefined){
var deferreds = [];
$.each(rows, function(key,uuid){
deferreds.push(ajaxCall(url=gridParams['del'] + uuid, sendData={},null));
});
// refresh after load
$.when.apply(null, deferreds).done(function(){
std_bootgrid_reload(gridId);
});
}
});
} else {
console.log("[grid] action del missing")
}
});
});
/**
* copy actions for items from opnsense_bootgrid_plugin.js
*/
grid_accounts.on("loaded.rs.jquery.bootgrid", function(){
// edit dialog id to use
var editDlg = $(this).attr('data-editDialog');
var gridId = $(this).attr('id');
// edit item
grid_accounts.find(".command-edit").on("click", function(e)
{
if (editDlg != undefined && gridParams['get'] != undefined) {
var uuid = $(this).data("row-id");
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'] + uuid;
mapDataToFormUI(urlMap).done(function () {
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for pipe edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
// define save action
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
if (gridParams['set'] != undefined) {
saveFormToEndpoint(url=gridParams['set']+uuid,
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
std_bootgrid_reload(gridId);
}, true);
} else {
console.log("[grid] action set missing")
}
});
} else {
console.log("[grid] action get or data-editDialog missing")
}
});
// copy item, save as new
grid_accounts.find(".command-copy").on("click", function(e)
{
if (editDlg != undefined && gridParams['get'] != undefined) {
var uuid = $(this).data("row-id");
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'] + uuid;
mapDataToFormUI(urlMap).done(function () {
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for pipe edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
// define save action
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
if (gridParams['add'] != undefined) {
saveFormToEndpoint(url=gridParams['add'],
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
std_bootgrid_reload(gridId);
}, true);
} else {
console.log("[grid] action add missing")
}
});
} else {
console.log("[grid] action get or data-editDialog missing")
}
});
// delete item
grid_accounts.find(".command-delete").on("click", function(e)
{
if (gridParams['del'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirm removal') }}',
'{{ lang._('Do you want to remove the selected item?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function () {
ajaxCall(url=gridParams['del'] + uuid,
sendData={},callback=function(data,status){
// reload grid after delete
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action del missing")
}
});
// toggle item
grid_accounts.find(".command-toggle").on("click", function(e)
{
if (gridParams['toggle'] != undefined) {
var uuid=$(this).data("row-id");
$(this).addClass("fa-spinner fa-pulse");
ajaxCall(url=gridParams['toggle'] + uuid,
sendData={},callback=function(data,status){
// reload grid after toggle
std_bootgrid_reload(gridId);
});
} else {
console.log("[grid] action toggle missing")
}
});
// register account
grid_accounts.find(".command-register").on("click", function(e)
{
if (gridParams['register'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
'{{ lang._('Register the selected account with the configured ACME CA?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
ajaxCall(url=gridParams['register'] + uuid,sendData={},callback=function(data,status){
// reload grid afterwards
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action register missing")
}
});
});
}
}));
// hook into on-show event for dialog to extend layout.
$('#DialogAccount').on('shown.bs.modal', function (e) {

View file

@ -124,302 +124,116 @@ POSSIBILITY OF SUCH DAMAGE.
},
};
/**
* reload bootgrid, return to current selected page
*/
function std_bootgrid_reload(gridId) {
var currentpage = $("#"+gridId).bootgrid("getCurrentPage");
$("#"+gridId).bootgrid("reload");
// absolutely not perfect, bootgrid.reload doesn't seem to support when().done()
setTimeout(function(){
$('#'+gridId+'-footer a[data-page="'+currentpage+'"]').click();
}, 400);
}
/**
* copy actions for selected items from opnsense_bootgrid_plugin.js
*/
const grid_certificates = $("#grid-certificates").UIBootgrid($.extend(gridParams, { options: gridopt }));
$("#grid_certificates").on("loaded.rs.jquery.bootgrid", function (e)
{
// toggle all rendered tooltips (once for all)
$('.bootgrid-tooltip').tooltip();
// scale footer on resize
$(this).find("tfoot td:first-child").attr('colspan',$(this).find("th").length - 1);
$(this).find('tr[data-row-id]').each(function(){
if ($(this).find('[class*="command-toggle"]').first().data("value") == "0") {
$(this).addClass("text-muted");
const grid_certificates = $("#grid-certificates").UIBootgrid($.extend(gridParams, {
options: gridopt,
commands: {
sign: {
method: function(event, cell) {
var uuid = $(this).data("row-id");
stdDialogConfirm(
'{{ lang._('Confirmation Required') }}',
'{{ lang._('Forcefully issue or renew the selected certificate?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}',
function() {
// Handle HAProxy integration (no-op if not applicable)
ajaxCall("/api/acmeclient/settings/fetch_ha_proxy_integration",
{}, function(data, status) {
ajaxCall(gridParams['sign'] + uuid, {}, function() {
grid_certificates.bootgrid("reload");
});
});
}
);
},
classname: 'fa fa-repeat',
title: '{{ lang._('Issue or renew certificate') }}',
sequence: 510
},
import: {
method: function(event, cell) {
var uuid = $(this).data("row-id");
stdDialogConfirm(
'{{ lang._('Confirmation Required') }}',
'{{ lang._('(Re-) import the selected certificate and associated CA certificates into the trust storage?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}',
function() {
ajaxCall(gridParams['import'] + uuid, {}, function() {
grid_certificates.bootgrid("reload");
});
}
);
},
classname: 'fa fa-certificate',
title: '{{ lang._('(Re-) Import certificate') }}',
sequence: 520
},
automation: {
method: function(event, cell) {
var uuid = $(this).data("row-id");
stdDialogConfirm(
'{{ lang._('Confirmation Required') }}',
'{{ lang._('Rerun all automations for the selected certificate?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}',
function() {
ajaxCall(gridParams['automation'] + uuid, {}, function() {
grid_certificates.bootgrid("reload");
});
}
);
},
classname: 'fa fa-paper-plane',
title: '{{ lang._('Run automations') }}',
sequence: 530
},
revoke: {
method: function(event, cell) {
var uuid = $(this).data("row-id");
stdDialogConfirm(
'{{ lang._('Confirmation Required') }}',
'{{ lang._('Revoke selected certificate?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}',
function() {
ajaxCall(gridParams['revoke'] + uuid, {}, function() {
grid_certificates.bootgrid("reload");
});
},
'danger'
);
},
classname: 'fa fa-power-off',
title: '{{ lang._('Revoke certificate') }}',
sequence: 540
},
removekey: {
method: function(event, cell) {
var uuid = $(this).data("row-id");
stdDialogConfirm(
'{{ lang._('Confirmation Required') }}',
'{{ lang._('Really remove the private key?%s%sThe certificate will be completely reset. This is useful when the private key has been compromised or when you have changed the key options and want to regenerate the private key.%sNote that you have to revalidate the certificate afterwards in order to create a new private key and a matching certificate.') | format('<br/>', '<br/>', '<br/>') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}',
function() {
ajaxCall(gridParams['removekey'] + uuid, {}, function() {
grid_certificates.bootgrid("reload");
});
},
'danger'
);
},
classname: 'fa fa-history',
title: '{{ lang._('Reset certificate') }}',
sequence: 550
}
});
// edit dialog id to use
var editDlg = $(this).attr('data-editDialog');
var gridId = $(this).attr('id');
// link Add new to child button with data-action = add
$(this).find("*[data-action=add]").click(function(){
if ( gridParams['get'] != undefined && gridParams['add'] != undefined) {
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'];
mapDataToFormUI(urlMap).done(function(){
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
//
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
saveFormToEndpoint(url=gridParams['add'],
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
$("#"+gridId).bootgrid("reload");
}, true);
});
} else {
console.log("[grid] action add missing")
},
tabulatorOptions: {
rowFormatter: function(row) {
if (parseInt(row.getData()['enabled'], 2) !== 1) {
$(row.getElement()).addClass('text-muted');
} else {
$(row.getElement()).removeClass('text-muted');
}
}
});
// link delete selected items action
$(this).find("*[data-action=deleteSelected]").click(function(){
if ( gridParams['del'] != undefined) {
stdDialogConfirm('{{ lang._('Confirm removal') }}',
'{{ lang._('Do you want to remove the selected item?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function () {
var rows =$("#"+gridId).bootgrid('getSelectedRows');
if (rows != undefined){
var deferreds = [];
$.each(rows, function(key,uuid){
deferreds.push(ajaxCall(url=gridParams['del'] + uuid, sendData={},null));
});
// refresh after load
$.when.apply(null, deferreds).done(function(){
std_bootgrid_reload(gridId);
});
}
});
} else {
console.log("[grid] action del missing")
}
});
});
/**
* copy actions for items from opnsense_bootgrid_plugin.js
*/
grid_certificates.on("loaded.rs.jquery.bootgrid", function(){
// edit dialog id to use
var editDlg = $(this).attr('data-editDialog');
var gridId = $(this).attr('id');
// edit item
grid_certificates.find(".command-edit").on("click", function(e)
{
if (editDlg != undefined && gridParams['get'] != undefined) {
var uuid = $(this).data("row-id");
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'] + uuid;
mapDataToFormUI(urlMap).done(function () {
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for pipe edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
// define save action
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
if (gridParams['set'] != undefined) {
saveFormToEndpoint(url=gridParams['set']+uuid,
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
std_bootgrid_reload(gridId);
}, true);
} else {
console.log("[grid] action set missing")
}
});
} else {
console.log("[grid] action get or data-editDialog missing")
}
});
// copy item, save as new
grid_certificates.find(".command-copy").on("click", function(e)
{
if (editDlg != undefined && gridParams['get'] != undefined) {
var uuid = $(this).data("row-id");
var urlMap = {};
urlMap['frm_' + editDlg] = gridParams['get'] + uuid;
mapDataToFormUI(urlMap).done(function () {
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
});
// show dialog for pipe edit
$('#'+editDlg).modal({backdrop: 'static', keyboard: false});
// define save action
$("#btn_"+editDlg+"_save").unbind('click').click(function(){
if (gridParams['add'] != undefined) {
saveFormToEndpoint(url=gridParams['add'],
formid='frm_' + editDlg, callback_ok=function(){
$("#"+editDlg).modal('hide');
std_bootgrid_reload(gridId);
}, true);
} else {
console.log("[grid] action add missing")
}
});
} else {
console.log("[grid] action get or data-editDialog missing")
}
});
// delete item
grid_certificates.find(".command-delete").on("click", function(e)
{
if (gridParams['del'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirm removal') }}',
'{{ lang._('Do you want to remove the selected item?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function () {
ajaxCall(url=gridParams['del'] + uuid,
sendData={},callback=function(data,status){
// reload grid after delete
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action del missing")
}
});
// toggle item
grid_certificates.find(".command-toggle").on("click", function(e)
{
if (gridParams['toggle'] != undefined) {
var uuid=$(this).data("row-id");
$(this).addClass("fa-spinner fa-pulse");
ajaxCall(url=gridParams['toggle'] + uuid,
sendData={},callback=function(data,status){
// reload grid after toggle
std_bootgrid_reload(gridId);
});
} else {
console.log("[grid] action toggle missing")
}
});
// sign cert
grid_certificates.find(".command-sign").on("click", function(e)
{
if (gridParams['sign'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
'{{ lang._('Forcefully issue or renew the selected certificate?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
// Handle HAProxy integration (no-op if not applicable)
ajaxCall(url="/api/acmeclient/settings/fetch_ha_proxy_integration", sendData={}, callback=function(data,status) {
ajaxCall(url=gridParams['sign'] + uuid,sendData={},callback=function(data,status){
// reload grid after sign
$("#"+gridId).bootgrid("reload");
});
});
});
} else {
console.log("[grid] action sign missing")
}
});
// revoke cert
grid_certificates.find(".command-revoke").on("click", function(e)
{
if (gridParams['revoke'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
'{{ lang._('Revoke selected certificate?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
ajaxCall(url=gridParams['revoke'] + uuid,
sendData={},callback=function(data,status){
// reload grid after sign
$("#"+gridId).bootgrid("reload");
});
}, 'danger');
} else {
console.log("[grid] action revoke missing")
}
});
// remove private key
grid_certificates.find(".command-removekey").on("click", function(e)
{
if (gridParams['removekey'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
'{{ lang._('Really remove the private key?%s%sThe certificate will be completely reset. This is useful when the private key has been compromised or when you have changed the key options and want to regenerate the private key.%sNote that you have to revalidate the certificate afterwards in order to create a new private key and a matching certificate.') | format('<br/>', '<br/>', '<br/>') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
ajaxCall(url=gridParams['removekey'] + uuid,
sendData={},callback=function(data,status){
// reload grid after sign
$("#"+gridId).bootgrid("reload");
});
}, 'danger');
} else {
console.log("[grid] action removekey missing")
}
});
// run automation
grid_certificates.find(".command-automation").on("click", function(e)
{
if (gridParams['automation'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
'{{ lang._('Rerun all automations for the selected certificate?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
ajaxCall(url=gridParams['automation'] + uuid,
sendData={},callback=function(data,status){
// reload grid after sign
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action automation missing")
}
});
// import certificate into trust storage
grid_certificates.find(".command-import").on("click", function(e)
{
if (gridParams['import'] != undefined) {
var uuid=$(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirmation Required') }}',
'{{ lang._('(Re-) import the selected certificate and associated CA certificates into the trust storage?') }}',
'{{ lang._('Yes') }}', '{{ lang._('Cancel') }}', function() {
ajaxCall(url=gridParams['import'] + uuid,
sendData={},callback=function(data,status){
// reload grid after sign
$("#"+gridId).bootgrid("reload");
});
});
} else {
console.log("[grid] action import missing")
}
});
});
}
}));
// Hide options that are irrelevant in this context.
$('#DialogCertificate').on('shown.bs.modal', function (e) {

View file

@ -54,31 +54,6 @@
search:'/api/diagnostics/log/core/acmeclient'
});
grid_systemlog.on("loaded.rs.jquery.bootgrid", function(){
$(".action-page").click(function(event){
event.preventDefault();
$("#grid-systemlog").bootgrid("search", "");
let new_page = parseInt((parseInt($(this).data('row-id')) / $("#grid-log").bootgrid("getRowCount")))+1;
$("input.search-field").val("");
// XXX: a bit ugly, but clearing the filter triggers a load event.
setTimeout(function(){
$("ul.pagination > li:last > a").data('page', new_page).click();
}, 100);
});
});
grid_acmelog.on("loaded.rs.jquery.bootgrid", function(){
$(".action-page").click(function(event){
event.preventDefault();
$("#grid-acmelog").bootgrid("search", "");
let new_page = parseInt((parseInt($(this).data('row-id')) / $("#grid-log").bootgrid("getRowCount")))+1;
$("input.search-field").val("");
// XXX: a bit ugly, but clearing the filter triggers a load event.
setTimeout(function(){
$("ul.pagination > li:last > a").data('page', new_page).click();
}, 100);
});
});
});
</script>

View file

@ -11,7 +11,7 @@ Plugin Changelog
1.8.1
* Fixed detect broken executables option (contributed by sopex)
* Fixed detect broken executables option (contributed by Konstantinos Spartalis)
1.8

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= netbird
PLUGIN_VERSION= 1.1
PLUGIN_VERSION= 1.2
PLUGIN_DEPENDS= netbird
PLUGIN_COMMENT= Peer-to-peer VPN that seamlessly connects your devices
PLUGIN_MAINTAINER= dev@netbird.io

View file

@ -42,6 +42,36 @@
<type>checkbox</type>
<help>Allows incoming SSH connections</help>
</field>
<field>
<id>settings.ssh.enableRoot</id>
<label>Enable Root Login</label>
<type>checkbox</type>
<help>Allow root user login</help>
</field>
<field>
<id>settings.ssh.enableSFTP</id>
<label>Enable SFTP</label>
<type>checkbox</type>
<help>Enable SFTP subsystem for file transfers</help>
</field>
<field>
<id>settings.ssh.enableLocalPortForwarding</id>
<label>Enable Local Port Forwarding</label>
<type>checkbox</type>
<help>Allow clients to forward local ports through the server</help>
</field>
<field>
<id>settings.ssh.enableRemotePortForwarding</id>
<label>Enable Remote Port Forwarding</label>
<type>checkbox</type>
<help>Allow clients to request remote port forwarding</help>
</field>
<field>
<id>settings.ssh.enableAuth</id>
<label>Enable SSH Authentication</label>
<type>checkbox</type>
<help>Enable JWT authentication for SSH connections. When disabled, allows any peer with network access</help>
</field>
<field>
<type>header</type>
<label>DNS</label>

View file

@ -46,6 +46,11 @@ class Settings extends BaseModel
$config["WgPort"] = (int)$this->general->wireguardPort->__toString();
$config["ServerSSHAllowed"] = $this->ssh->enable->__toString() == 1;
$config["EnableSSHRoot"] = $this->ssh->enableRoot->__toString() == 1;
$config["EnableSSHSFTP"] = $this->ssh->enableSFTP->__toString() == 1;
$config["EnableSSHLocalPortForwarding"] = $this->ssh->enableLocalPortForwarding->__toString() == 1;
$config["EnableSSHRemotePortForwarding"] = $this->ssh->enableRemotePortForwarding->__toString() == 1;
$config["DisableSSHAuth"] = $this->ssh->enableAuth->__toString() != 1;
$config["DisableFirewall"] = $this->firewall->allowConfig->__toString() != 1;
$config["BlockInbound"] = $this->firewall->blockInboundConnection->__toString() == 1;
$config["DisableDNS"] = $this->dns->enable->__toString() != 1;

View file

@ -1,7 +1,7 @@
<model>
<mount>//OPNsense/netbird/settings</mount>
<description>NetBird settings</description>
<version>1.1.0</version>
<version>1.2.0</version>
<items>
<general>
<enable type="BooleanField">
@ -31,6 +31,26 @@
<Default>0</Default>
<Required>Y</Required>
</enable>
<enableRoot type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</enableRoot>
<enableSFTP type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</enableSFTP>
<enableLocalPortForwarding type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</enableLocalPortForwarding>
<enableRemotePortForwarding type="BooleanField">
<Default>0</Default>
<Required>Y</Required>
</enableRemotePortForwarding>
<enableAuth type="BooleanField">
<Default>1</Default>
<Required>Y</Required>
</enableAuth>
</ssh>
<dns>
<enable type="BooleanField">

View file

@ -1,5 +1,6 @@
PLUGIN_NAME= openvpn-legacy
PLUGIN_VERSION= 1.0
PLUGIN_REVISION= 1
PLUGIN_COMMENT= OpenVPN legacy support
PLUGIN_DEPENDS= # openvpn
PLUGIN_MAINTAINER= ad@opnsense.org

View file

@ -1474,7 +1474,7 @@ $( document ).ready(function() {
</span><br>
<select name='netbios_ntype' class="selectpicker">
<?php
foreach ($netbios_nodetypes as $type => $name) :
foreach (['0' => 'none', '1' => 'b-node', '2' => 'p-node', '4' => 'm-node', '5' => 'h-node'] as $type => $name):
$selected = "";
if ($pconfig['netbios_ntype'] == $type) {
$selected = "selected=\"selected\"";

View file

@ -1,5 +1,6 @@
PLUGIN_NAME= q-feeds-connector
PLUGIN_VERSION= 1.4
PLUGIN_VERSION= 1.5
PLUGIN_REVISION= 1
PLUGIN_COMMENT= Connector for Q-Feeds threat intel
PLUGIN_MAINTAINER= devel@qfeeds.com
PLUGIN_TIER= 2

View file

@ -3,9 +3,18 @@ Connector for Q-Feeds threat intel
Plugin Changelog
================
1.5
* Feature: Add passlist option for unbound
* Feature: Add effective networks for unbound
* Feature: Add NXDOMAIN option for unbound
* Feature: Add dest address for unbound
* Bugfix: Invalidate alias cache on reconfigure
1.4
* Feature: Added DNSCrypt-Proxy integration
* Bugfix: Track loaded lists when deselected and reload Unbounds blocklist in that case
1.3

View file

@ -34,6 +34,7 @@ use OPNsense\Base\ApiMutableModelControllerBase;
use OPNsense\Base\UserException;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Firewall\Alias;
class SettingsController extends ApiMutableModelControllerBase
{
@ -51,6 +52,8 @@ class SettingsController extends ApiMutableModelControllerBase
if (strpos($res, 'EXIT OK') === false) {
throw new UserException($res);
}
/* as we register new dynamic aliases, we're also responsible for invalidating an existing cache */
Alias::flushCacheData();
return ['status' => 'ok', 'output' => $res];
}

View file

@ -13,6 +13,44 @@
<id>connect.general.enable_unbound_bl</id>
<label>Register domain feeds</label>
<type>checkbox</type>
<help>Use domain feeds in Unbound DNS blocklist, requires blocklists to be enabled in order to have effect</help>
<help>Use domain feeds in Unbound DNS and DNScrypt-proxy blocklists, requires blocklists to be enabled in order to have effect</help>
</field>
<field>
<type>header</type>
<label>Unbound blocklist settings</label>
<style>unbound_options</style>
</field>
<field>
<id>connect.unbound.allowlists</id>
<label>Allowlist Domains</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>List of domains to allow. You can use regular expressions. This allow list only applies to blocklist matches on items in this policy.</help>
</field>
<field>
<id>connect.unbound.source_nets</id>
<label>Source Net(s)</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>Source networks to apply policy on. Examples are 192.168.1.0/24 or 192.168.1.1. Leave empty to apply on everything. All specified networks should use the same protocol family and have equal sizes to avoid priority issues. </help>
</field>
<field>
<id>connect.unbound.address</id>
<label>Destination Address</label>
<type>text</type>
<advanced>true</advanced>
<help>
Destination ip address for entries in the blocklist (leave empty to use default: 0.0.0.0).
Not used when "Return NXDOMAIN" is checked.
</help>
</field>
<field>
<id>connect.unbound.nxdomain</id>
<label>Return NXDOMAIN</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>Use the DNS response code NXDOMAIN instead of a destination address.</help>
</field>
</form>

View file

@ -7,5 +7,21 @@
<apikey type="TextField"/>
<enable_unbound_bl type="BooleanField"/>
</general>
<unbound>
<allowlists type="CSVListField"/>
<source_nets type="NetworkField">
<Multiple>Y</Multiple>
<Strict>Y</Strict>
<ValidationMessage>Please specify a valid network segment or address (IPv4/IPv6). If a mask is provided, please omit the host bits.</ValidationMessage>
<WildcardEnabled>N</WildcardEnabled>
<NetMaskRequired>N</NetMaskRequired>
<AsList>Y</AsList>
</source_nets>
<address type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<AddressFamily>ipv4</AddressFamily>
</address>
<nxdomain type="BooleanField"/>
</unbound>
</items>
</model>

View file

@ -99,6 +99,13 @@ POSSIBILITY OF SUCH DAMAGE.
}
});
$("#connect\\.general\\.enable_unbound_bl").change(function(){
if ($(this).is(':checked')) {
$(".unbound_options").closest('table').show();
} else {
$(".unbound_options").closest('table').hide();
}
});
let selected_tab = window.location.hash != "" ? window.location.hash : "#settings";
$('a[href="' +selected_tab + '"]').tab('show');

View file

@ -37,7 +37,7 @@ class QFeedsConfig:
cnf = ConfigParser()
cnf.read(config_filename)
if cnf.has_section('api') and cnf.has_option('api', 'key'):
self.api_key = cnf.get('api', 'key')
self.api_key = cnf.get('api', 'key').strip()
class Api:

View file

@ -61,6 +61,8 @@ class PFLogCrawler:
# quick scan for datetime, interface, direction, source, dest, source_port, dest_port
parts = line.split()
fw_line = parts[-1].split(',') # strip syslog
if fw_line[6] == 'pass':
return []
ip_addresses = [x for x in fw_line if is_ip_address(x)]
# Find destination IP position to get ports from next fields (only if numeric)
dest_idx = fw_line.index(ip_addresses[1]) if len(ip_addresses) > 1 else len(fw_line)
@ -77,8 +79,10 @@ class PFLogCrawler:
for idx, line in enumerate(f_in):
for rule_id in self._rule_ids:
if rule_id in line:
result.append(self._parse_log_line(line))
rows_processed +=1
lline = self._parse_log_line(line)
if lline:
result.append(lline)
rows_processed +=1
break # inner loop
if (idx % 100000 == 0 and time.time() - start_time > max_time) or rows_processed >= max_results:
return result

View file

@ -1,7 +1,7 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2025-2026 Deciso B.V.
Copyright (c) 2025 Deciso B.V.
All rights reserved.
Redistribution and use in source and binary forms, with or without

View file

@ -1,7 +1,7 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2025 Deciso B.V.
Copyright (c) 2025-2026 Deciso B.V.
All rights reserved.
Redistribution and use in source and binary forms, with or without
@ -27,12 +27,19 @@
"""
import os
import re
import syslog
import uuid
from . import BaseBlocklistHandler
class DefaultBlocklistHandler(BaseBlocklistHandler):
class QFeedsBlocklistHandler(BaseBlocklistHandler):
def __init__(self):
super().__init__('/usr/local/etc/unbound/qfeeds-blocklists.conf')
self.priority = 50
self._compat_id = str(uuid.uuid4())
def _is_enabled(self):
return self.cnf and self.cnf.has_section('settings') and self.cnf.has_option('settings', 'filenames')
def get_config(self):
# do not use, unbound worker settings
@ -41,11 +48,10 @@ class DefaultBlocklistHandler(BaseBlocklistHandler):
def get_blocklist(self):
# Only return domains if integration is enabled (filenames are offered)
qfeeds_filenames = []
if self.cnf and self.cnf.has_section('settings'):
if self.cnf.has_option('settings', 'filenames'):
qfeeds_filenames = self.cnf.get('settings', 'filenames').split(',')
# touch a file to help qfeedsctl detect the current instance uses its list
open('/tmp/qfeeds-unbound-bl.stat', 'w').write('')
if self._is_enabled():
qfeeds_filenames = self.cnf.get('settings', 'filenames').split(',')
# touch a file to help qfeedsctl detect the current instance uses its list
open('/tmp/qfeeds-unbound-bl.stat', 'w').write('')
result = {}
for filename in qfeeds_filenames:
@ -58,3 +64,35 @@ class DefaultBlocklistHandler(BaseBlocklistHandler):
def get_passlist_patterns(self):
return []
def get_policies(self):
if not self._is_enabled():
return []
cfg = {
'source_nets': [],
'address': '',
'rcode': '',
'id': self._compat_id,
'allowlists': []
}
for k,v in self.cnf['settings'].items():
if k in cfg and v.strip() != '':
if type(cfg[k]) is list:
cfg[k] = v.split(',')
else:
cfg[k] = v.strip()
if cfg['allowlists']:
compiled_passlist = set()
for pattern in cfg['allowlists']:
try:
re.compile(pattern, re.IGNORECASE)
compiled_passlist.add(pattern)
except re.error:
syslog.syslog(syslog.LOG_ERR,'Q-Feeds : skip invalid whitelist exclude pattern "%s"' % pattern)
cfg['passlist'] = '|'.join(compiled_passlist)
del cfg['allowlists']
return [cfg]

View file

@ -2,4 +2,11 @@
not helpers.empty('OPNsense.QFeedsConnector.general.enable_unbound_bl') %}
[settings]
filenames=/var/db/qfeeds-tables/malware_domains.txt
{% if not helpers.empty('OPNsense.QFeedsConnector.unbound') %}
allowlists={{OPNsense.QFeedsConnector.unbound.allowlists|default('')}}
source_nets={{OPNsense.QFeedsConnector.unbound.source_nets|default('')}}
address={{OPNsense.QFeedsConnector.unbound.address|default('0.0.0.0')}}
rcode={% if OPNsense.QFeedsConnector.unbound.nxdomain|default('0') == '1' %}NXDOMAIN{%else%}NOERROR{%endif +%}
cache_ttl={{OPNsense.QFeedsConnector.unbound.cache_ttl|default('72000')}}
{% endif %}
{% endif %}

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= tailscale
PLUGIN_VERSION= 1.3
PLUGIN_VERSION= 1.4
PLUGIN_COMMENT= VPN mesh securely connecting clients using WireGuard
PLUGIN_DEPENDS= tailscale
PLUGIN_MAINTAINER= sam@sheridan.uk

Some files were not shown because too many files have changed in this diff Show more