Compare commits

...

74 Commits

Author SHA1 Message Date
Franco Fichtner
cbc09e7c5a firewall: well known ports added to filter rule selection; closes #9692 2026-02-05 09:12:17 +01:00
Franco Fichtner
700f590383 firewall: undefined is also "*" 2026-02-05 08:34:43 +01:00
Franco Fichtner
4912a671be interfaces: fix wlanmode usage part 2 #9727 2026-02-05 07:01:27 +01:00
Monviech
d4eb6235ae
Firewall: Rules [new]: Add tcpflags_any for parity with legacy rules (#9720) 2026-02-04 15:58:03 +01:00
Monviech
d43b14ef9b
Firewall: Rules [new]: Exclude loopback from interface selectpicker (#9723) 2026-02-04 14:13:25 +01:00
Monviech
6dce1de829
Firewall - Rules [new]: Add all rules option to interface selectpicker and make it default selection (#9713)
* Firewall - Rules [new]: Add all rules option to interface selectpicker and select it by default
* Interface select default only on null or no match, not on empty string
* To fix URL hash weirdness, it's best to special case __floating and __any in the frontend
* Lower diff in controller by folding null case into is_if
* firewall: tweak the interface selector

Show group name and description.  Could be inconvenient when
the description is long, but better for the auto-groups.
That also removes the hint at the end.  See note below.

Use fixed width logos for the interface groupings.

Remove muted from the any selection.

TODO/Remarks:

The groupings being shown as muted is a bit inconvenient also
as it muddies the perspective of what this does and how important
it is.  Would be nicer to not have it in this particular selector.

When an interface is selected it would be perfect if the grouping
icon would show so people see better what they selected.  Coloring
is nice so that would take it to the next level.

Badges with rule counts are not overly nice in front of the selection
and also don't update on rule delete.  While it's nice to have this
the question is if this is really needed.  A counter already exists
when clicking on them (Showing xxxx).

---------

Co-authored-by: Franco Fichtner <franco@opnsense.org>
2026-02-04 11:10:55 +01:00
Ad Schellevis
7a8f6bee11 mvc - ui: fix jquery glitch when using "options" instead of val(), closes https://github.com/opnsense/core/pull/9717 2026-02-04 10:01:07 +01:00
Ad Schellevis
cf0341f139 mvc - ui: move refresh of selectpicker types into setFormData() and improve type detection.
When we are using a selectpicker, the original one will be moved inside a div containing the "bootstrap-select" class.

If this fixes https://github.com/opnsense/core/pull/9717, we need to remove the console output as that is merely for debugging.
2026-02-04 08:51:16 +01:00
Ad Schellevis
40cb82128d mvc: BaseModel - improve legacy mapper support when parent item doesn't exist.
Fixes:

ErrorException: Undefined array key 0 in /usr/local/opnsense/mvc/app/models/OPNsense/Base/BaseModel.php:755
Stack trace:

If we can't find the specified root node, we should create one, which is similar to non legacy mapper nodes.
2026-02-03 16:28:06 +01:00
Monviech
5276f51dc0
dnsmasq: Compare lower case strings only in leases to fix edge cases in is_reserved detection (#9714) 2026-02-03 15:42:37 +01:00
Franco Fichtner
7ae42d9584 firmware: do not fail upgrade if new kernel is already booted
PR: https://forum.opnsense.org/index.php?topic=50654.0
2026-02-03 15:04:51 +01:00
Franco Fichtner
c3dd6d56f1 openssh: minor style tweak 2026-02-03 08:29:52 +01:00
Franco Fichtner
e771a800d5 ipsec: same same, switch class name not file name 2026-02-03 08:24:04 +01:00
Franco Fichtner
b95c81d08d interfaces: class name was wrong, fix UI page link 2026-02-03 08:18:31 +01:00
Franco Fichtner
e2d95ad672 firewall: double check the theory of 4c559a63d42
While here add the proper translation and safeguarding.
2026-02-03 07:59:05 +01:00
Franco Fichtner
45597a976c interfaces: fix wlan creation when $mode is empty
We don't currently have a way to specify if-empty-do-not-quote
since strict quoting is often much more effective in bubbling up
errors.  It could be useful to have it but the recent improvement
of mwexecf() and friends regarding array-based format strings can
account for this too.

PR: https://forum.opnsense.org/index.php?topic=50561.msg258926#msg258926
2026-02-02 21:17:15 +01:00
Ad Schellevis
39fcbddb05 mvc: ApiControllerBase->exportCsv(), mark content safe so escaping is disabled. closes https://github.com/opnsense/core/issues/9694 2026-02-02 21:11:36 +01:00
Monviech
ce432fa769
Firewall: Rules [new]: Remove schedule formatter from group and automatic rule rows (#9708) 2026-02-02 18:13:32 +01:00
Monviech
d260467553
Firewall: Rules [new]: Add link to states and put it first in list (#9707) 2026-02-02 17:16:40 +01:00
Monviech
0f6d82af34
Firewall: Rules [new]: Change toggle_log icon to help visibility of enabled/disabled status (#9704) 2026-02-02 16:14:33 +01:00
Monviech
9aaf675694
Firewall: Rules [new]: Statistics column is responsive now (#9679) 2026-02-02 11:14:39 +01:00
Stephan de Wit
7333fba07a bootgrid: cleanup previous 2026-02-02 11:02:01 +01:00
Stephan de Wit
3ce73ff043 bootgrid: searchable column selectors (fixes https://github.com/opnsense/core/issues/9698) 2026-02-02 11:00:14 +01:00
Stephan de Wit
b5cf3f7410 bootgrid: split toggle-selected into enable/disable-selected (https://github.com/opnsense/core/issues/9678) 2026-02-02 10:19:04 +01:00
Stephan de Wit
f7f0857ca9 bootgrid: introduce toggle-selected command (fixes https://github.com/opnsense/core/issues/9678)
This will only render if selection && multiSelect are true, and
stickySelect is disabled.
2026-02-02 09:23:02 +01:00
Franco Fichtner
4c559a63d4 firewall: ancient copy+paste error in scrub rules 2026-02-02 08:41:09 +01:00
Ad Schellevis
a5fed616a5 Firewall: Schedule - add missing schedules support in "Firewall: Rules [new]" and refactor existing usage to avoid duplication of logic. closes https://github.com/opnsense/core/issues/9690
This commit moves the schedule logic out of filter_core_rules_user() where it didn't belong in the first place.
Since we need legacy code to determine schedule behavior, we cannot move it to the plugin classes easily, instead sweep all registered rules after registration so we can process "sched" for all of them in the same way.

We can next add a simple action into the model to ask if there actually is a schedule, which pf_cron() needs to schedule the rule updates.

Finally add an icon and link into the mvc page to refer to the schedule itself.
2026-02-01 13:27:33 +01:00
Monviech
3bcdae70f7
radvd: When Base6Interface constructor is used, use its primary address for ifcfgipv6 (#9689) 2026-02-01 13:23:36 +01:00
Franco Fichtner
1727592311 firewall: style for previous 2026-02-01 12:15:04 +01:00
Ad Schellevis
c6540bf6fa Interfaces: Diagnostics: Ping - add optional interval (seconds), closes https://github.com/opnsense/core/issues/9695 2026-02-01 11:25:45 +01:00
Franco Fichtner
d31faf7f7c mvc: shield exec_safe() against "fatal" type errors
Allows the system to boot in the worst case and replaces the
command with a simple dummy command.

Formatters are still a work in progress as I'm not sure how
much preprocessing we should add here to fish for vsprintf()
doing type casts to int/float which is not something the command
line can/should support.
2026-02-01 11:11:29 +01:00
Franco Fichtner
70629923bb reporting: render as string instead for #9686
Revert ca06d54676942764b3.  A command line is a string and
don't offer exceptions to escaping.

We may consider replacing %[^s%] with %s in the format
string but there aren't many cases where it matters either.
Should just be part of the documentation we need to offer
soon.
2026-01-31 14:56:34 +01:00
Ad Schellevis
ca06d54676 backend: fix regression in 796a5c725b, when using %d or %f as formatters, we can't push them through escapeshellarg() as it will mangle the data unneeded. closes https://github.com/opnsense/core/issues/9686 2026-01-31 09:21:04 +01:00
Franco Fichtner
311184daa8 firewall: fix 2f60fcb062cff removing anchor rendering
PR: https://forum.opnsense.org/index.php?topic=50520.0
2026-01-30 23:12:19 +01:00
Monviech
0f6cc03c69
Firewall: NAT: Destination NAT: The local-port field does not support range and well-known name (#9668)
* Make validation messages clearer
* Use selector in loop to determine where to replace the data (just the label is changed)
2026-01-30 14:53:54 +01:00
Franco Fichtner
c264c90504 interfaces: check dhcpdv6.enabled for -1 and add legacy config sections
PR: https://forum.opnsense.org/index.php?topic=50580.0
2026-01-30 12:43:48 +01:00
Monviech
ec20be4dd4
Firewall: Rules [new]: FilterBaseController requires Base\UserException (#9669) 2026-01-30 12:12:04 +01:00
Franco Fichtner
7a11458ea2 interfaces: fix migration for no-release option
This was introduced in d87ce014d96f storing the enabled value as "yes"
instead of true.  Threat it the same as the debug option.

PR: https://forum.opnsense.org/index.php?topic=50575.0 (and others)
2026-01-30 10:50:51 +01:00
Franco Fichtner
ffe3b40872 mvc: migration tweaks again for #9666 2026-01-30 10:07:29 +01:00
Franco Fichtner
5cc95f47a6 mvc: minor logging tweak for stdout 2026-01-30 10:00:13 +01:00
Franco Fichtner
be4900b112 pluginctl: use verbose migration mode #9666 2026-01-30 09:42:51 +01:00
Ad Schellevis
816fd574c9 mvc: support verbose logging in run_migrations.php, closes https://github.com/opnsense/core/issues/9666
This might be practical for some other system tools eventually too, since Syslog() is our own wrapper, we can echo output to stdout when requested. By making the callout static, each component can still have its own logger instance (and verbose log to stdout)

usage: /usr/local/opnsense/mvc/script/run_migrations.php -v
2026-01-30 09:28:25 +01:00
Franco Fichtner
9e70ee7508 mvc: use linter to find two wrong file names; closes #9638 2026-01-30 09:02:05 +01:00
Franco Fichtner
cff4c085d3 make: add a linter for PHP class name consistency #9638 2026-01-30 09:02:04 +01:00
Monviech
c827a02ef6
dhcp/kea: Use hostdiscovery service as ndp source in kea_prefix_watcher script (#9648)
* Use hostdiscovery service as ndp source in kea_prefix_watcher script via list_hosts.py, only request IPv6 addresses, fall back to ndp when hostdiscovery is not running
2026-01-30 08:36:16 +01:00
Monviech
5d571dcc89
Firewall: Rules [new]: normalize overload table between uuid and name (#9657)
* Firewall: Rules [new]: The mvc page stores the overload table as UUID, the legacy page as alias name. Turn UUID into alias name and vice versa during upload and download of rules, and then resolve it to a name before setting it in pf configuration.
* Firewall: Rules [new]: view, show translated value in the advanced field tooltip when possible, this will show the alias name instead of the UUID, fix upload bootgrid reload and hint the successful import with the change message
* Firewall: Rules [new]: There were error(s) loading the rules: /tmp/rules.debug:235: 'max-src-conn-rate' maximum rate must be < 4294967
2026-01-30 08:34:48 +01:00
Franco Fichtner
476ad93d6f firewall: fix typo with sprintf(); closes #9664 2026-01-29 21:19:39 +01:00
Ad Schellevis
1ddc63e402 Firewall: Aliases - set password input to autocomplete="new-password", closes https://github.com/opnsense/core/pull/9610
Similar as https://github.com/opnsense/core/pull/5311
2026-01-29 21:09:22 +01:00
Ad Schellevis
60695dd259 Firewall: Rules [new] - on import, validate uuid (either empty or valid), rework 34d7d77426 so other imports can use the same validation ( https://github.com/opnsense/core/issues/9661 ) 2026-01-29 21:09:22 +01:00
Matthias Kaduk
0642e17bc5
Bootgrid: allow multi word tooltips (#9656) 2026-01-29 11:47:53 +01:00
Franco Fichtner
87445129bf LICENSE: sync 2026-01-29 08:36:36 +01:00
Franco Fichtner
1ddc661a49 system: move to old location for better diff 2026-01-29 08:35:26 +01:00
Franco Fichtner
35575f9446 system: use known menu notation and annotate with "[]" for consistency 2026-01-29 08:32:11 +01:00
Franco Fichtner
43de1e0e42 interfaces: generalise the dhcp6c_script using the new IFNAME variable #7647
Now that the new dhcp6c code is in 26.1 we can start using it.

The file was conceptually created inline via d36f0f4f62557 and before was
a single command line script... so add appropriate copyrights from that
time onward.

Many thanks to Martin for pinoeering this back in the day!
2026-01-28 22:27:07 +01:00
Franco Fichtner
1e1a6a37f6 make: pretty up previous, use tools.git wording 2026-01-28 22:27:07 +01:00
Franco Fichtner
8c1a820340 mvc: style 2026-01-28 22:15:39 +01:00
Ad Schellevis
34d7d77426 Firewall: Rules [new] - on import, validate uuid (either empty or valid), closes https://github.com/opnsense/core/issues/9661 2026-01-28 21:22:47 +01:00
Ad Schellevis
f8560f063f mvc: support throwing exceptions in importRecordSet(.., $data_callback, ..) for importCsv() to add validation on the input data.
requirement for: https://github.com/opnsense/core/issues/9661
2026-01-28 21:14:02 +01:00
Ad Schellevis
12aab2a9e0 filter / style - remove excess comma leading to parse errors in our api documentation parser (collect_api_endpoints.py) 2026-01-28 20:40:24 +01:00
Stephan de Wit
f7fac5a6f4 interfaces: host discovery: make sure the full dump includes NDP output if hostwatch is disabled 2026-01-28 10:25:32 +01:00
Franco Fichtner
44dbcd103b interfaces: default missing here causing migration to flip the value #9569
Disables IPv6 on images which isn't what we want.
2026-01-28 09:27:04 +01:00
Ad Schellevis
83f9492087 Services: Kea DHCP: Kea DHCPv6 - add validation "Pool overlaps with an existing one." and fix pd_pools being in the wrong loop. for https://github.com/opnsense/core/issues/9343 2026-01-27 21:12:39 +01:00
Ad Schellevis
fcab636a4c Services: Kea DHCP: Kea DHCPv6 - add validation "Invalid Pool boundaries, offered address is not the first address in the prefix." for https://github.com/opnsense/core/issues/9343 2026-01-27 20:18:20 +01:00
Ad Schellevis
b6a59bb7e5 Services: Kea DHCP: Kea DHCPv6 - add validation "Delegated length must be longer than or equal to prefix length", for https://github.com/opnsense/core/issues/9343" 2026-01-27 20:05:34 +01:00
Franco Fichtner
f456d15d76 backend: buh-bye mwexec() and mwexec_bg() 2026-01-27 19:39:45 +01:00
Franco Fichtner
fce3f7973a system: Persian into development mode 2026-01-27 19:18:36 +01:00
Franco Fichtner
660fa8210b console: fix overwrite of 'dhcp' configuration
Although this is correct from a pure config.xml.sample perspective
it clearly purges further configuration from the file which we better
avoid.
2026-01-27 17:31:59 +01:00
Franco Fichtner
b2a376cece interfaces: fix previous 2026-01-27 13:03:39 +01:00
Franco Fichtner
a35dce38e8 firmware: revoke 25.7 fingerprint 2026-01-27 12:57:28 +01:00
Stephan de Wit
c030ca6507 interfaces: automatic discovery: use descriptive interface names if available 2026-01-27 12:01:59 +01:00
Franco Fichtner
163162a8ac firmware: remove upgrade hint 2026-01-27 11:44:08 +01:00
Franco Fichtner
6d3ca746d1 radvd: get rid of tabs now that the file content is stable
Always bugged me for multiple reasons.  Other files got the same
treatment over the years.  For 26.1.x to avoid noise.
2026-01-27 11:37:55 +01:00
Stephan de Wit
ca9d7a550a dnsmasq: typo 2026-01-27 11:28:57 +01:00
Monviech
ee962f01db
Services: Kea DHCP: Kea DHCPv6 / Reservations: Lease6 view must distinguish between duid and hwaddr (#9651)
Since with just a boolean "is_reserved" key we don't know why it was reserved, the search button cannot distinguish between duid and hwaddr. So we need to add some sort of metadata, in this case "is_reserved" which can contain this information. Now the DHCPv6 Reservation page can distinguish between the two choices of reservation origin, and change the lease lookup button to either search for a duid or hwaddr.
2026-01-27 11:27:48 +01:00
77 changed files with 871 additions and 388 deletions

View File

@ -28,7 +28,7 @@ Copyright (c) 2021 Kyle Evans <kevans@FreeBSD.org>
Copyright (c) 2015 Manuel Faux <mfaux@conf.at>
Copyright (c) 2003-2006 Manuel Kasper <mk@neon1.net>
Copyright (c) 2012 Marcello Coutinho
Copyright (c) 2018 Martin Wasley <mjwasley@gmail.com>
Copyright (c) 2017-2020 Martin Wasley <mjwasley@gmail.com>
Copyright (c) 2022 Maurice Walker <maurice@walker.earth>
Copyright (c) 2010-2015 Michael Bostock
Copyright (c) 2018-2021 Michael Muenz <m.muenz@gmail.com>

View File

@ -205,7 +205,7 @@ CORE_CONFLICTS:= ${CORE_CONFLICTS:S/^/os-/g:O}
mount:
@if [ ! -f ${WRKDIR}/.mount_done ]; then \
echo -n "Enabling core live mount..."; \
echo -n ">>> Enabling core live mount..."; \
sed ${SED_REPLACE} ${.CURDIR}/src/${VERSIONFILE}.in > \
${.CURDIR}/src/${VERSIONFILE}; \
mount_unionfs ${.CURDIR}/src ${LOCALBASE}; \
@ -216,7 +216,7 @@ mount:
umount:
@if [ -f ${WRKDIR}/.mount_done ]; then \
echo -n "Disabling core live mount..."; \
echo -n ">>> Disabling core live mount..."; \
umount -f "<above>:${.CURDIR}/src"; \
rm ${WRKDIR}/.mount_done; \
echo "done"; \
@ -227,7 +227,7 @@ manifest-check:
# check if all annotations are in the version file
.for REPLACEMENT in ${REPLACEMENTS}
@grep -q '\"${REPLACEMENT}\": \"%%${REPLACEMENT}%%\"' ${.CURDIR}/src/${VERSIONFILE}.in || \
(echo "Could not find ${REPLACEMENT} in version file"; exit 1)
(echo ">>> Could not find ${REPLACEMENT} in version file" >&2; exit 1)
.endfor
manifest:
@ -333,11 +333,13 @@ package: lint-plist manifest-check package-check clean-wrksrc
upgrade-check:
@if ! ${PKG} info ${CORE_NAME} > /dev/null; then \
REAL_NAME=$$(pkg which -q /usr/local/opnsense/version/core); \
echo ">>> Cannot find installed package. Use CORE_NAME=$${REAL_NAME%-*} instead." >&2; \
echo ">>> Upgrade cannot continue without installed package" >&2; \
echo ">>> To continue anyway set CORE_NAME=$${REAL_NAME%-*}" >&2; \
exit 1; \
fi
@if [ "$$(${VERSIONBIN} -vH)" = "${CORE_PKGVERSION} ${CORE_HASH}" ]; then \
echo "Installed version already matches ${CORE_PKGVERSION} ${CORE_HASH}" >&2; \
echo ">>> Installed version already matches ${CORE_PKGVERSION} ${CORE_HASH}" >&2; \
echo ">>> To continue anyway set CORE_HASH=<something>" >&2; \
exit 1; \
fi

View File

@ -108,6 +108,9 @@ lint-model:
lint-acl:
@${COREREFDIR}/Scripts/dashboard-acl.sh ${COREREFDIR}
lint-class:
@${COREREFDIR}/Scripts/class-filename.sh ${COREREFDIR}
SCRIPTDIRS!= if [ -d ${.CURDIR}/src/opnsense/scripts ]; then find ${.CURDIR}/src/opnsense/scripts -type d -depth 1; fi
lint-exec:
@ -151,4 +154,4 @@ lint-plist:
@rm ${WRKDIR}/plist.*
.endif
lint: lint-plist lint-desc lint-shell lint-xml lint-model lint-acl lint-exec lint-php
lint: lint-plist lint-desc lint-shell lint-xml lint-model lint-acl lint-class lint-exec lint-php

52
Scripts/class-filename.sh Executable file
View File

@ -0,0 +1,52 @@
#!/bin/sh
# Copyright (c) 2026 Franco Fichtner <franco@opnsense.org>
#
# 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 BY THE AUTHOR AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
RET=0
for FILE in $(find src -name "*.php"); do
CLASS=$(grep ^class ${FILE} | awk '{ print $2 }')
if [ -z "${CLASS}" ]; then
continue
fi
MULTI=
OK=
for _CLASS in ${CLASS}; do
if [ "$(basename ${FILE})" == "${_CLASS}.php" ]; then
OK=${_CLASS}
else
MULTI="${_CLASS} ${MULTI}"
fi
done
if [ -z "${OK}" ]; then
echo "${FILE}: error: does not match class name" ${CLASS}
RET=1
elif [ -n "${MULTI}" ]; then
echo "${FILE}: warning: has additional classes" ${MULTI}
fi
done
exit ${RET}

3
plist
View File

@ -79,7 +79,7 @@
/usr/local/etc/pkg/fingerprints/OPNsense/revoked/pkg.opnsense.org.20240105
/usr/local/etc/pkg/fingerprints/OPNsense/revoked/pkg.opnsense.org.20240611
/usr/local/etc/pkg/fingerprints/OPNsense/revoked/pkg.opnsense.org.20241217
/usr/local/etc/pkg/fingerprints/OPNsense/trusted/pkg.opnsense.org.20250710
/usr/local/etc/pkg/fingerprints/OPNsense/revoked/pkg.opnsense.org.20250710
/usr/local/etc/pkg/fingerprints/OPNsense/trusted/pkg.opnsense.org.20260120
/usr/local/etc/rc
/usr/local/etc/rc.bootup
@ -1242,6 +1242,7 @@
/usr/local/opnsense/scripts/interfaces/carp_global_status.php
/usr/local/opnsense/scripts/interfaces/carp_set_status.php
/usr/local/opnsense/scripts/interfaces/dhclient-script
/usr/local/opnsense/scripts/interfaces/dhcp6c_script.sh
/usr/local/opnsense/scripts/interfaces/gen_duid.php
/usr/local/opnsense/scripts/interfaces/ifctl.sh
/usr/local/opnsense/scripts/interfaces/lib/__init__.py

View File

@ -33,6 +33,7 @@
<disablenatreflection>yes</disablenatreflection>
<usevirtualterminal>1</usevirtualterminal>
<disableconsolemenu/>
<ipv6allow>1</ipv6allow>
<powerd_ac_mode>hadp</powerd_ac_mode>
<powerd_battery_mode>hadp</powerd_battery_mode>
<powerd_normal_mode>hadp</powerd_normal_mode>

View File

@ -170,7 +170,7 @@ function convert_config($verbose = false)
$function = $verbose ? 'pass_safe' : 'mwexecf';
/* chain the new migration into this function call */
$function('/usr/local/sbin/pluginctl -m');
$function('/usr/local/sbin/pluginctl %s', $verbose ? '-m' : '-M');
/* register pluggable interfaces */
$function('/usr/local/sbin/pluginctl -i');

View File

@ -392,7 +392,8 @@ EOD;
}
}
$config['dnsmasq']['enable'] = '1';
$config['dnsmasq']['dhcp'] = ['enable_ra' => '1'];
config_read_array('dnsmasq', 'dhcp');
$config['dnsmasq']['dhcp']['enable_ra'] = '1';
$config['dnsmasq']['dhcp_ranges'][] = [
'@attributes' => ['uuid' => generate_uuid()],
'start_addr' => '192.168.1.100',

View File

@ -187,8 +187,8 @@ function filter_configure_sync($verbose = false, $load_aliases = true)
filter_core_bootstrap($fw);
$cnfint = iterator_to_array($fw->getInterfaceMapping());
plugins_firewall($fw);
// register user rules, returns kill states for schedules
$sched_kill_states = filter_core_rules_user($fw);
// register legacy user rules
filter_core_rules_user($fw);
// manual outbound nat rules
if (
@ -256,6 +256,33 @@ function filter_configure_sync($verbose = false, $load_aliases = true)
foreach ((new OPNsense\Firewall\DNat())->rule->sortedBy(['sequence']) as $key => $rule) {
$fw->registerForwardRule(600, $rule->getNodeContent());
}
/**
* XXX: Apply schedules on all installed rules so we can use both legacy and MVC ones.
* Eventually this needs to be replaced, but as this requires legacy code, we cannot do that easily now
* without increasing impact.
*/
$sched_kill_states = [];
foreach ($fw->iterateFilterRules() as $rule) {
$rrule = $rule->getRawRule();
if (!empty($rrule['sched'])) {
$descr = $rrule['descr'] ?? '';
$descr .= " ({$rrule['sched']})";
$rule->updateDescription($descr);
foreach ($config['schedules']['schedule'] as $sched) {
if ($sched['name'] == $rrule['sched']) {
if (!filter_get_time_based_rule_status($sched)) {
if (!isset($config['system']['schedule_states'])) {
$sched_kill_states[] = $rrule['label'];
}
/* disable rule, suffix label to mark end of schedule */
$rule->disable();
$rule->updateDescription("[FIN] " . $descr);
}
break;
}
}
}
}
openlog("firewall", LOG_DAEMON, LOG_LOCAL4);

View File

@ -658,7 +658,7 @@ function filter_core_rules_system($fw, $defaults)
$parts = explode('-', $dnatrule['destination']['port']);
if (count($parts) == 2) {
$range_end = $parts[1] - $parts[0] + $dnatrule['local-port'];
$dnatrule['local-port'] .= sprint("-%s", $range_end);
$dnatrule['local-port'] .= sprintf('-%s', $range_end);
}
}
$fw->registerFilterRule(500000, $tmprule); /* automatic at the end of our ruleset */
@ -667,14 +667,12 @@ function filter_core_rules_system($fw, $defaults)
}
/**
* register user rules, returns kill states for schedules
* register user rules
*/
function filter_core_rules_user($fw)
{
global $config;
$sched_kill_states = [];
foreach ((new \OPNsense\Firewall\Group())->ifgroupentry->iterateItems() as $node) {
$ifgroups[(string)$node->ifname] = !empty((string)$node->sequence) ? (string)$node->sequence : 0;
}
@ -696,9 +694,6 @@ function filter_core_rules_user($fw)
if (empty($rule['descr'])) {
$rule['descr'] = '';
}
if (!empty($rule['sched'])) {
$rule['descr'] .= " ({$rule['sched']})";
}
if (isset($rule['floating'])) {
$prio = 200000;
@ -707,25 +702,7 @@ function filter_core_rules_user($fw)
} else {
$prio = 400000;
}
/* is a time based rule schedule attached? */
if (!empty($rule['sched']) && !empty($config['schedules'])) {
foreach ($config['schedules']['schedule'] as $sched) {
if ($sched['name'] == $rule['sched']) {
if (!filter_get_time_based_rule_status($sched)) {
if (!isset($config['system']['schedule_states'])) {
$sched_kill_states[] = $rule['label'];
}
/* disable rule, suffix label to mark end of schedule */
$rule['disabled'] = true;
$rule['descr'] = "[FIN] " . $rule['descr'];
}
break;
}
}
}
$fw->registerFilterRule($prio, $rule);
}
}
return $sched_kill_states;
}

View File

@ -1655,18 +1655,6 @@ function _interfaces_wlan_clone($device, $wlcfg)
$wlcfg_mode = $wlcfg['mode'] ?? 'bss';
}
switch ($wlcfg_mode) {
case "hostap":
$mode = 'wlanmode hostap';
break;
case "adhoc":
$mode = 'wlanmode adhoc';
break;
default:
$mode = '';
break;
}
if (does_interface_exist($device)) {
$ifconfig_str = shell_safe('/sbin/ifconfig %s', $device);
if (($wlcfg_mode == 'hostap') && !preg_match('/hostap/si', $ifconfig_str)) {
@ -1690,7 +1678,18 @@ function _interfaces_wlan_clone($device, $wlcfg)
legacy_interface_destroy($device);
if (mwexecf('/sbin/ifconfig wlan create wlandev %s %s bssid name %s', [$baseif, $mode, $device])) {
$wlan_frmt = ['/sbin/ifconfig wlan create wlandev %s'];
$wlan_args = [$baseif];
if (in_array($wlcfg_mode, ['adhoc', 'hostap'])) {
$wlan_frmt[] = 'wlanmode %s';
$wlan_args[] = $wlcfg_mode;
}
$wlan_frmt[] = 'bssid name %s';
$wlan_args[] = $device;
if (mwexecf($wlan_frmt, $wlan_args)) {
log_msg("Failed to clone interface {$baseif} to {$device}", LOG_ERR);
return null;
}
@ -2980,71 +2979,6 @@ function interface_dhcpv6_prepare($interface, $wancfg, $cleanup = false)
@unlink("/var/etc/dhcp6c.{$device}.conf");
}
$dhcp6cscript = <<<EOF
#!/bin/sh
FORCE=
case \${REASON} in
INFOREQ|REBIND|RENEW|REQUEST|SOLICIT)
/usr/bin/logger -t dhcp6c "dhcp6c_script: \${REASON} on {$device} executing"
ARGS=
for NAMESERVER in \${new_domain_name_servers}; do
ARGS="\${ARGS} -a \${NAMESERVER}"
done
/usr/local/sbin/ifctl -i {$device} -6nd \${ARGS}
ARGS=
for DOMAIN in \${new_domain_name}; do
ARGS="\${ARGS} -a \${DOMAIN}"
done
/usr/local/sbin/ifctl -i {$device} -6sd \${ARGS}
if [ \${REASON} = "REQUEST" -o \${REASON} = "SOLICIT" ]; then
/usr/bin/logger -t dhcp6c "dhcp6c_script: \${REASON} on {$device} connected to server"
FORCE=\${REASON}
# can safely clear the prefix during connect
/usr/local/sbin/ifctl -i {$device} -6pd
fi
if [ \${REASON} != "INFOREQ" -a -n "\${PDINFO}" ]; then
ARGS=
for PD in \${PDINFO}; do
ARGS="\${ARGS} -a \${PD}"
done
if /usr/local/sbin/ifctl -i {$device} -6pu \${ARGS}; then
/usr/bin/logger -t dhcp6c "dhcp6c_script: \${REASON} on {$device} prefix now \${PDINFO}"
if [ -z "\${FORCE}" ]; then
FORCE=\${REASON}
fi
fi
fi
/usr/local/sbin/configctl -d interface newipv6 {$device} \${FORCE}
;;
EXIT|RELEASE)
/usr/bin/logger -t dhcp6c "dhcp6c_script: \${REASON} on {$device} executing"
/usr/local/sbin/ifctl -i {$device} -6nd
/usr/local/sbin/ifctl -i {$device} -6sd
/usr/local/sbin/ifctl -i {$device} -6pd
/usr/local/sbin/configctl -d interface newipv6 {$device}
;;
*)
/usr/bin/logger -t dhcp6c "dhcp6c_script: \${REASON} on {$device} ignored"
;;
esac
EOF;
@file_put_contents("/var/etc/dhcp6c_{$interface}_script.sh", $dhcp6cscript);
@chmod("/var/etc/dhcp6c_{$interface}_script.sh", 0755);
$dhcp6cconf = '';
/* merge configs and prepare single instance of dhcp6c for startup */
@ -3071,7 +3005,7 @@ function DHCP6_Config_File_Basic($interface, $wancfg, $wanif, $id = 0)
$dhcp6cconf .= " information-only;\n";
$dhcp6cconf .= " request domain-name-servers;\n";
$dhcp6cconf .= " request domain-name;\n";
$dhcp6cconf .= " script \"/var/etc/dhcp6c_{$interface}_script.sh\";\n";
$dhcp6cconf .= " script \"/usr/local/opnsense/scripts/interfaces/dhcp6c_script.sh\";\n";
$dhcp6cconf .= "};\n";
} elseif ($wancfg['ipaddrv6'] == 'dhcp6') {
$assoc_pd = [];
@ -3136,7 +3070,7 @@ function DHCP6_Config_File_Basic($interface, $wancfg, $wanif, $id = 0)
$dhcp6cconf .= " request domain-name;\n";
}
$dhcp6cconf .= " script \"/var/etc/dhcp6c_{$interface}_script.sh\";\n";
$dhcp6cconf .= " script \"/usr/local/opnsense/scripts/interfaces/dhcp6c_script.sh\";\n";
$dhcp6cconf .= "};\n";
if (!isset($wancfg['dhcp6prefixonly'])) {
@ -3181,7 +3115,7 @@ function DHCP6_Config_File_Advanced($interface, $wancfg, $wanif, $id = 0)
$information_only = " information-only;\n";
}
$script = " script \"/var/etc/dhcp6c_{$interface}_script.sh\";\n";
$script = " script \"/usr/local/opnsense/scripts/interfaces/dhcp6c_script.sh\";\n";
if ($wancfg['adv_dhcp6_interface_statement_script'] != '') {
$script = " script \"{$wancfg['adv_dhcp6_interface_statement_script']}\";\n";
}

View File

@ -157,7 +157,7 @@ function openssh_configure_do($verbose = false, $interface_map = null)
$sshport = isset($sshcfg['port']) ? $sshcfg['port'] : 22;
$sshconf = "# This file was automatically generated by openssh_configure_do()\n";
$sshconf = "# This file was automatically generated by openssh_configure_do()\n";
$sshconf .= "# If you want to include custom options please use this directory:\n";
$sshconf .= "Include /usr/local/etc/ssh/sshd_config.d/*.conf\n";
$sshconf .= "Port {$sshport}\n";

View File

@ -58,36 +58,31 @@ function pf_cron()
{
global $config;
$jobs = array();
$jobs = [];
if (isset($config['filter']['rule'])) {
foreach ($config['filter']['rule'] as $rule) {
if (empty($rule['disabled']) && !empty($rule['sched'])) {
$jobs[]['autocron'] = array('/usr/bin/logger "reload filter for configured schedules" ; /usr/local/etc/rc.filter_configure', '1,16,31,46');
break;
}
}
if ((new OPNsense\Firewall\Filter(true))->hasSchedule()) {
$jobs[]['autocron'] = ['/usr/bin/logger "reload filter for configured schedules" ; /usr/local/etc/rc.filter_configure', '1,16,31,46'];
}
/* bogons fetch always set in default config.xml */
switch ($config['system']['bogons']['interval']) {
case 'daily':
$jobs[]['autocron'] = array('/usr/local/sbin/configctl -d filter schedule bogons', '1', '3', '*', '*', '*');
$jobs[]['autocron'] = ['/usr/local/sbin/configctl -d filter schedule bogons', '1', '3', '*', '*', '*'];
break;
case 'weekly':
$jobs[]['autocron'] = array('/usr/local/sbin/configctl -d filter schedule bogons', '1', '3', '*', '*', '0');
$jobs[]['autocron'] = ['/usr/local/sbin/configctl -d filter schedule bogons', '1', '3', '*', '*', '0'];
break;
case 'monthly':
default:
$jobs[]['autocron'] = array('/usr/local/sbin/configctl -d filter schedule bogons', '1', '3', '1', '*', '*');
$jobs[]['autocron'] = ['/usr/local/sbin/configctl -d filter schedule bogons', '1', '3', '1', '*', '*'];
break;
}
$jobs[]['autocron'] = array(
$jobs[]['autocron'] = [
'/usr/local/bin/flock -n -E 0 -o /tmp/filter_update_tables.lock ' .
'/usr/local/opnsense/scripts/filter/update_tables.py --quick',
'*'
);
];
return $jobs;
}

View File

@ -149,8 +149,16 @@ function radvd_configure_do($verbose = false, $ignorelist = [])
$carp_mode = false;
$src_addr = false;
$ifcfgipv6 = null;
if (!$entry->AdvRASrcAddress->isEmpty()) {
$ifcfgipv6 = $entry->AdvRASrcAddress->getValue();
} elseif (!$entry->Base6Interface->isEmpty()) {
$ifcfgipv6 = get_interface_ipv6($entry->Base6Interface->getValue());
} else {
$ifcfgipv6 = get_interface_ipv6($dhcpv6if);
}
$ifcfgipv6 = !$entry->AdvRASrcAddress->isEmpty() ? $entry->AdvRASrcAddress->getValue() : get_interface_ipv6($dhcpv6if);
if (!is_ipaddrv6($ifcfgipv6)) {
$radvdconf .= "# Skipping addressless interface {$dhcpv6if}\n";
continue;
@ -181,24 +189,24 @@ function radvd_configure_do($verbose = false, $ignorelist = [])
}
$radvdconf .= "# Generated RADVD config for manual assignment on {$dhcpv6if}\n";
$radvdconf .= "interface {$device} {\n";
$radvdconf .= "\tAdvSendAdvert on;\n";
$radvdconf .= "\tMinRtrAdvInterval {$entry->MinRtrAdvInterval->getValue()};\n";
$radvdconf .= "\tMaxRtrAdvInterval {$entry->MaxRtrAdvInterval->getValue()};\n";
$radvdconf .= " AdvSendAdvert on;\n";
$radvdconf .= " MinRtrAdvInterval {$entry->MinRtrAdvInterval->getValue()};\n";
$radvdconf .= " MaxRtrAdvInterval {$entry->MaxRtrAdvInterval->getValue()};\n";
if ($entry->AdvDefaultLifetime->isSet()) {
$radvdconf .= "\tAdvDefaultLifetime {$entry->AdvDefaultLifetime->getValue()};\n";
$radvdconf .= " AdvDefaultLifetime {$entry->AdvDefaultLifetime->getValue()};\n";
}
$radvdconf .= sprintf("\tAdvLinkMTU %s;\n", !empty($mtu) ? $mtu : 0);
$radvdconf .= "\tAdvDefaultPreference {$entry->AdvDefaultPreference->getValue()};\n";
$radvdconf .= sprintf(" AdvLinkMTU %s;\n", !empty($mtu) ? $mtu : 0);
$radvdconf .= " AdvDefaultPreference {$entry->AdvDefaultPreference->getValue()};\n";
switch ($entry->mode->getValue()) {
case 'assist':
case 'managed':
$radvdconf .= "\tAdvManagedFlag on;\n";
$radvdconf .= "\tAdvOtherConfigFlag on;\n";
$radvdconf .= " AdvManagedFlag on;\n";
$radvdconf .= " AdvOtherConfigFlag on;\n";
break;
case 'stateless':
$radvdconf .= "\tAdvManagedFlag off;\n";
$radvdconf .= "\tAdvOtherConfigFlag on;\n";
$radvdconf .= " AdvManagedFlag off;\n";
$radvdconf .= " AdvOtherConfigFlag on;\n";
break;
default:
break;
@ -239,72 +247,72 @@ function radvd_configure_do($verbose = false, $ignorelist = [])
if ($src_addr) {
/* inject configured link-local address into the RA message */
$radvdconf .= "\tAdvRASrcAddress {\n";
$radvdconf .= "\t\t{$ifcfgipv6};\n";
$radvdconf .= "\t};\n";
$radvdconf .= " AdvRASrcAddress {\n";
$radvdconf .= " {$ifcfgipv6};\n";
$radvdconf .= " };\n";
}
if ($carp_mode) {
/* to avoid wrong MAC being stuck during failover */
$radvdconf .= "\tAdvSourceLLAddress off;\n";
$radvdconf .= " AdvSourceLLAddress off;\n";
}
/* to avoid final advertisement with zero router lifetime */
$radvdconf .= "\tRemoveAdvOnExit " . (!$entry->RemoveAdvOnExit->isEmpty() ? $entry->RemoveAdvOnExit->getValue() : ($carp_mode ? 'off' : 'on')) . ";\n";
$radvdconf .= " RemoveAdvOnExit " . (!$entry->RemoveAdvOnExit->isEmpty() ? $entry->RemoveAdvOnExit->getValue() : ($carp_mode ? 'off' : 'on')) . ";\n";
/* VIPs may duplicate readings from system */
$stanzas = array_unique($stanzas);
foreach ($stanzas as $stanza) {
if ($stanza === 'base6') {
$radvdconf .= "\tprefix ::/64 {\n";
$radvdconf .= " prefix ::/64 {\n";
$baseif = get_real_interface($entry->Base6Interface->getValue(), 'inet6');
$radvdconf .= "\t\tBase6Interface {$baseif};\n";
$radvdconf .= " Base6Interface {$baseif};\n";
} else {
$radvdconf .= "\tprefix {$stanza} {\n";
$radvdconf .= " prefix {$stanza} {\n";
}
$radvdconf .= "\t\tDeprecatePrefix " . (!$entry->DeprecatePrefix->isEmpty() ? $entry->DeprecatePrefix->getValue() : ($carp_mode ? 'off' : 'on')) . ";\n";
$radvdconf .= " DeprecatePrefix " . (!$entry->DeprecatePrefix->isEmpty() ? $entry->DeprecatePrefix->getValue() : ($carp_mode ? 'off' : 'on')) . ";\n";
switch ($entry->mode->getValue()) {
case 'assist':
case 'stateless':
case 'unmanaged':
$radvdconf .= "\t\tAdvOnLink on;\n";
$radvdconf .= "\t\tAdvAutonomous on;\n";
$radvdconf .= " AdvOnLink on;\n";
$radvdconf .= " AdvAutonomous on;\n";
break;
case 'managed':
$radvdconf .= "\t\tAdvOnLink on;\n";
$radvdconf .= "\t\tAdvAutonomous off;\n";
$radvdconf .= " AdvOnLink on;\n";
$radvdconf .= " AdvAutonomous off;\n";
break;
case 'router':
$radvdconf .= "\t\tAdvOnLink off;\n";
$radvdconf .= "\t\tAdvAutonomous off;\n";
$radvdconf .= " AdvOnLink off;\n";
$radvdconf .= " AdvAutonomous off;\n";
break;
default:
break;
}
if (!$entry->AdvValidLifetime->isEmpty()) {
$radvdconf .= "\t\tAdvValidLifetime {$entry->AdvValidLifetime->getValue()};\n";
$radvdconf .= " AdvValidLifetime {$entry->AdvValidLifetime->getValue()};\n";
}
if (!$entry->AdvPreferredLifetime->isEmpty()) {
$radvdconf .= "\t\tAdvPreferredLifetime {$entry->AdvPreferredLifetime->getValue()};\n";
$radvdconf .= " AdvPreferredLifetime {$entry->AdvPreferredLifetime->getValue()};\n";
}
$radvdconf .= "\t};\n";
$radvdconf .= " };\n";
}
if (!$entry->routes->isEmpty()) {
foreach ($entry->routes->getValues() as $raroute) {
$radvdconf .= "\troute {$raroute} {\n";
$radvdconf .= "\t\tRemoveRoute " . (!$entry->RemoveRoute->isEmpty() ? $entry->RemoveRoute->getValue() : ($carp_mode ? 'off' : 'on')) . ";\n";
$radvdconf .= " route {$raroute} {\n";
$radvdconf .= " RemoveRoute " . (!$entry->RemoveRoute->isEmpty() ? $entry->RemoveRoute->getValue() : ($carp_mode ? 'off' : 'on')) . ";\n";
if (!$entry->AdvRouteLifetime->isEmpty()) {
$radvdconf .= "\t\tAdvRouteLifetime {$entry->AdvRouteLifetime->getValue()};\n";
$radvdconf .= " AdvRouteLifetime {$entry->AdvRouteLifetime->getValue()};\n";
}
$radvdconf .= "\t};\n";
$radvdconf .= " };\n";
}
}
if (!$entry->nat64prefix->isEmpty()) {
$radvdconf .= "\tnat64prefix {$entry->nat64prefix->getValue()} {\n";
$radvdconf .= "\t};\n";
$radvdconf .= " nat64prefix {$entry->nat64prefix->getValue()} {\n";
$radvdconf .= " };\n";
}
$dnssl = [];
@ -346,19 +354,19 @@ function radvd_configure_do($verbose = false, $ignorelist = [])
}
if (count($rdnss)) {
$radvdconf .= "\tRDNSS " . implode(" ", $rdnss) . " {\n";
$radvdconf .= " RDNSS " . implode(" ", $rdnss) . " {\n";
if (!$entry->AdvRDNSSLifetime->isEmpty()) {
$radvdconf .= "\t\tAdvRDNSSLifetime {$entry->AdvRDNSSLifetime->getValue()};\n";
$radvdconf .= " AdvRDNSSLifetime {$entry->AdvRDNSSLifetime->getValue()};\n";
}
$radvdconf .= "\t};\n";
$radvdconf .= " };\n";
}
if (count($dnssl)) {
$radvdconf .= "\tDNSSL " . implode(" ", $dnssl) . " {\n";
$radvdconf .= " DNSSL " . implode(" ", $dnssl) . " {\n";
if (!$entry->AdvDNSSLLifetime->isEmpty()) {
$radvdconf .= "\t\tAdvDNSSLLifetime {$entry->AdvDNSSLLifetime->getValue()};\n";
$radvdconf .= " AdvDNSSLLifetime {$entry->AdvDNSSLLifetime->getValue()};\n";
}
$radvdconf .= "\t};\n";
$radvdconf .= " };\n";
}
$radvdconf .= "};\n";
@ -432,17 +440,17 @@ function radvd_configure_do($verbose = false, $ignorelist = [])
$radvdconf .= "# Generated RADVD config for {$autotype} assignment from {$trackif} on {$if}\n";
$radvdconf .= "interface {$device} {\n";
$radvdconf .= "\tAdvSendAdvert on;\n";
$radvdconf .= sprintf("\tAdvLinkMTU %s;\n", !empty($mtu) ? $mtu : 0);
$radvdconf .= "\tAdvManagedFlag on;\n";
$radvdconf .= "\tAdvOtherConfigFlag on;\n";
$radvdconf .= " AdvSendAdvert on;\n";
$radvdconf .= sprintf(" AdvLinkMTU %s;\n", !empty($mtu) ? $mtu : 0);
$radvdconf .= " AdvManagedFlag on;\n";
$radvdconf .= " AdvOtherConfigFlag on;\n";
if (!empty($networkv6)) {
$radvdconf .= "\tprefix {$networkv6} {\n";
$radvdconf .= "\t\tDeprecatePrefix on;\n";
$radvdconf .= "\t\tAdvOnLink on;\n";
$radvdconf .= "\t\tAdvAutonomous on;\n";
$radvdconf .= "\t};\n";
$radvdconf .= " prefix {$networkv6} {\n";
$radvdconf .= " DeprecatePrefix on;\n";
$radvdconf .= " AdvOnLink on;\n";
$radvdconf .= " AdvAutonomous on;\n";
$radvdconf .= " };\n";
}
foreach (config_read_array('virtualip', 'vip') as $vip) {
@ -462,18 +470,18 @@ function radvd_configure_do($verbose = false, $ignorelist = [])
continue;
}
$radvdconf .= "\tprefix {$vipnetv6} {\n";
$radvdconf .= "\t\tDeprecatePrefix on;\n";
$radvdconf .= "\t\tAdvOnLink on;\n";
$radvdconf .= "\t\tAdvAutonomous on;\n";
$radvdconf .= "\t};\n";
$radvdconf .= " prefix {$vipnetv6} {\n";
$radvdconf .= " DeprecatePrefix on;\n";
$radvdconf .= " AdvOnLink on;\n";
$radvdconf .= " AdvAutonomous on;\n";
$radvdconf .= " };\n";
}
if (count($dnslist) > 0) {
$radvdconf .= "\tRDNSS " . implode(" ", $dnslist) . " { };\n";
$radvdconf .= " RDNSS " . implode(" ", $dnslist) . " { };\n";
}
if (!empty($config['system']['domain'])) {
$radvdconf .= "\tDNSSL {$config['system']['domain']} { };\n";
$radvdconf .= " DNSSL {$config['system']['domain']} { };\n";
}
$radvdconf .= "};\n";
}

View File

@ -280,7 +280,6 @@ function get_locale_list()
if (!product::getInstance()->development()) {
/* below 30% progress or currently unvetted */
unset($locales['fa_IR']);
unset($locales['vi_VN']);
}

View File

@ -922,30 +922,6 @@ function url_safe($format, $args = [])
return vsprintf($format, $args);
}
function mwexec($command, $mute = false) /* XXX deprecated */
{
$oarr = [];
$retval = 0;
$garbage = exec("{$command} 2>&1", $oarr, $retval); /* XXX to be removed */
unset($garbage);
if ($retval != 0 && $mute !== true) {
$output = implode(' ', $oarr);
log_msg(sprintf("The command '%s'%s returned exit code '%d', the output was '%s'", $command, !empty($mute) ? "($mute) " : '', $retval, $output), LOG_ERR);
unset($output);
}
unset($oarr);
return $retval;
}
function mwexec_bg($command, $mute = false) /* XXX deprecated */
{
mwexec("/usr/sbin/daemon -f {$command}", $mute);
}
/* safe shell command formatter */
function exec_safe($format, $args = [])
{

View File

@ -1,3 +1 @@
# configuration for opnsense-update(8), do not edit
UPGRADE_RELEASE="26.1"

View File

@ -172,7 +172,7 @@ class ApiControllerBase extends ControllerRoot
$this->response->setHeader($parts[0], ltrim($parts[1]));
}
rewind($stream);
$this->response->setContent($stream);
$this->response->setContent($stream, true);
}
/**

View File

@ -31,6 +31,13 @@
Can be helpful to determine the maximum size a transport is able to send.
</help>
</field>
<field>
<id>ping.settings.interval</id>
<label>Interval (seconds)</label>
<type>text</type>
<hint>1</hint>
<help>Specify the number of seconds to wait between sending pings.</help>
</field>
<field>
<id>ping.settings.description</id>
<label>Description</label>

View File

@ -76,22 +76,20 @@ class LeasesController extends ApiControllerBase
$reservedKeys = [];
foreach ((new Dnsmasq())->hosts->iterateItems() as $host) {
if (!empty($host->client_id)) {
$reservedKeys[] = (string)$host->client_id;
if (!$host->client_id->isEmpty()) {
$reservedKeys[] = strtolower($host->client_id->getValue());
}
if (!empty($host->hwaddr)) {
foreach (explode(',', (string)$host->hwaddr) as $hwaddr) {
if (!empty($hwaddr)) {
$reservedKeys[] = $hwaddr;
}
if (!$host->hwaddr->isEmpty()) {
foreach ($host->hwaddr->getValues() as $hwaddr) {
$reservedKeys[] = strtolower($hwaddr);
}
}
}
foreach ($records as &$record) {
$is_ipv6 = Util::isIpv6Address($record['address'] ?? '');
$key = $is_ipv6 ? ($record['client_id'] ?? '') : ($record['hwaddr'] ?? '');
$key = strtolower($is_ipv6 ? ($record['client_id'] ?? '') : ($record['hwaddr'] ?? ''));
$record['is_reserved'] = in_array($key, $reservedKeys, true) ? '1' : '0';
}

View File

@ -114,7 +114,7 @@
<id>dnsmasq.add_mac</id>
<label>Add MAC</label>
<type>dropdown</type>
<help>Add the MAC address of the requestor to DNS queries which are forwarded upstream. The MAC address will only be added if the upstream DNS Server is in the same subnet as the requestor. Since this is not standardized, it should be considered experiemental. This is useful for selective DNS filtering on the upstream DNS server.</help>
<help>Add the MAC address of the requestor to DNS queries which are forwarded upstream. The MAC address will only be added if the upstream DNS Server is in the same subnet as the requestor. Since this is not standardized, it should be considered experimental. This is useful for selective DNS filtering on the upstream DNS server.</help>
<advanced>true</advanced>
</field>
<field>

View File

@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2020 Deciso B.V.
* Copyright (C) 2020-2026 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -25,9 +25,12 @@
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
namespace OPNsense\Firewall\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
use OPNsense\Base\FieldTypes\PortField;
use OPNsense\Base\UserException;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Firewall\Alias;
@ -209,21 +212,37 @@ abstract class FilterBaseController extends ApiMutableModelControllerBase
{
$result = [
'single' => [
'label' => gettext("Single port or range"),
'label' => gettext('Single port or range'),
],
'aliases' => [
'label' => gettext("Aliases"),
'label' => gettext('Aliases'),
'items' => [],
],
// XXX: Well known ports could be gathered from /etc/services but there is a lot of noise
'ports' => [
'label' => gettext("Ports"),
'label' => gettext('Ports'),
'items' => [
"" => gettext("any"),
'' => gettext('any'),
],
],
];
/*
* XXX Eventually it might make more sense to instantate the
* actual protocol fields of the rules in order to get the full
* list of options in one go (modifying the model XML to
* automatically get the correct values).
*
* This works using e.g.
* (new Filter())->rules->rule->getTemplateNode()->source_port
* but loads all rules as a side effect if they exist which we
* want to avoid and raises the question how we're going to deal
* with every field's own setup as we can't simply derive one
* field's accepted values for another.
*/
foreach (PortField::getWellKnown() as $port) {
$result['ports']['items'][$port] = strtoupper($port);
}
foreach ((new Alias())->aliases->alias->iterateItems() as $alias) {
if ($alias->type == 'internal') {
/* currently only used for legacy bindings, align with legacy_list_aliases() usage */
@ -376,9 +395,7 @@ abstract class FilterBaseController extends ApiMutableModelControllerBase
$node = $mdl->getNodeByReference($node_reference . '.' . $uuid);
if ($node === null) {
throw new UserException(
gettext("Rule not found"),
);
throw new UserException(gettext('Rule not found'));
}
$node->log = $log;

View File

@ -30,10 +30,11 @@ namespace OPNsense\Firewall\Api;
use OPNsense\Base\UserException;
use OPNsense\Core\Config;
use OPNsense\Core\Backend;
use OPNsense\Firewall\Alias;
use OPNsense\Firewall\Category;
use OPNsense\Firewall\Filter;
use OPNsense\Firewall\Group;
use OPNsense\Firewall\Util;
use OPNsense\Firewall\Alias;
class FilterController extends FilterBaseController
{
@ -81,18 +82,21 @@ class FilterController extends FilterBaseController
{
$categories = $this->request->get('category');
$show_all = !empty($this->request->get('show_all'));
if (!empty($this->request->get('interface'))) {
$interfaces = explode(',', $this->request->get('interface'));
if ($show_all) {
/* add groups which contain the selected interface when looking at full impact*/
if (!$this->request->has('interface')) {
// ALL rules
$interfaces = null;
} else {
// interface param may be empty
$interfaces = array_filter(explode(',', (string)$this->request->get('interface')), 'strlen');
if ($show_all && !empty($interfaces)) {
/* add groups which contain the selected interface when looking at full impact */
foreach ((new Group())->ifgroupentry->iterateItems() as $groupItem) {
if (array_intersect($interfaces, $groupItem->members->getValues())) {
$interfaces[] = $groupItem->ifname->getValue();
}
}
}
} else {
$interfaces = [];
}
/* filter logic for mvc rules */
@ -100,6 +104,11 @@ class FilterController extends FilterBaseController
$is_cat = empty($categories) || array_intersect(explode(',', $record->categories), $categories);
$rule_interfaces = $record->interface->getValues();
// ALL rules, skip interface logic entirely
if ($interfaces === null) {
return $is_cat;
}
if (!$record->interfacenot->isEmpty()) {
$if_intersects = !array_intersect($interfaces, $rule_interfaces); /* All but interface */
} else {
@ -151,11 +160,12 @@ class FilterController extends FilterBaseController
$is_cat = empty($categories) || array_intersect($r_categories, $categories);
if (!empty($record['interfacenot'])) {
$is_if = !array_intersect(explode(',', $record['interface'] ?? ''), $interfaces);
$is_if = !array_intersect(explode(',', $record['interface'] ?? ''), $interfaces ?? []);
} else {
$is_if = array_intersect(explode(',', $record['interface'] ?? ''), $interfaces);
$is_if = array_intersect(explode(',', $record['interface'] ?? ''), $interfaces ?? []);
}
$is_if = $is_if || empty($record['interface']);
// ALL interfaces or floating always matches
$is_if = $is_if || $interfaces === null || empty($record['interface']);
if ($is_cat && $is_if) {
/* translate/convert legacy fields before returning, similar to mvc handling */
@ -371,24 +381,29 @@ class FilterController extends FilterBaseController
$result = [
'floating' => [
'label' => gettext('Floating'),
'icon' => 'fa fa-layer-group text-primary',
'items' => []
'icon' => 'fa fa-layer-group fa-fw text-primary',
'items' => [],
],
'groups' => [
'label' => gettext('Groups'),
'icon' => 'fa fa-sitemap text-warning',
'items' => []
'icon' => 'fa fa-sitemap fa-fw text-warning',
'items' => [],
],
'interfaces' => [
'label' => gettext('Interfaces'),
'icon' => 'fa fa-ethernet text-info',
'items' => []
]
'icon' => 'fa fa-ethernet fa-fw text-info',
'items' => [],
],
'any' => [
'label' => gettext('Any'),
'icon' => 'fa fa-globe-europe fa-fw',
'items' => [],
],
];
// Count rules per interface
$ruleCounts = [];
foreach ((new \OPNsense\Firewall\Filter())->rules->rule->iterateItems() as $rule) {
foreach ((new Filter())->rules->rule->iterateItems() as $rule) {
$interfaces = $rule->interface->getValues();
if (!$rule->interfacenot->isEmpty() || count($interfaces) !== 1) {
@ -399,6 +414,7 @@ class FilterController extends FilterBaseController
$ruleCounts[$interfaces[0]] = ($ruleCounts[$interfaces[0]] ?? 0) + 1;
}
}
$totalRules = array_sum($ruleCounts);
// Helper to build item with label and count
$makeItem = fn($value, $label, $count, $type) => [
@ -409,23 +425,30 @@ class FilterController extends FilterBaseController
];
// Floating
$result['floating']['items'][] = $makeItem('', gettext('Any'), $ruleCounts['floating'] ?? 0, 'floating');
$result['floating']['items'][] = $makeItem('__floating', gettext('Floating'), $ruleCounts['floating'] ?? 0, 'floating');
// Groups
foreach ((new \OPNsense\Firewall\Group())->ifgroupentry->iterateItems() as $groupItem) {
$name = (string)$groupItem->ifname;
$result['groups']['items'][] = $makeItem($name, $name, $ruleCounts[$name] ?? 0, 'group');
foreach ((new Group())->ifgroupentry->iterateItems() as $groupItem) {
$name = $groupItem->ifname->getValue();
$descr = $groupItem->descr->getValue();
$descr = empty($descr) ? $name : "{$descr} ($name)";
$result['groups']['items'][] = $makeItem($name, $descr, $ruleCounts[$name] ?? 0, 'group');
}
// Interfaces
$groupKeys = array_column($result['groups']['items'], 'value');
foreach (\OPNsense\Core\Config::getInstance()->object()->interfaces->children() as $key => $intf) {
if (!in_array($key, $groupKeys)) {
foreach (Config::getInstance()->object()->interfaces->children() as $key => $intf) {
// XXX: Loopback excluded since no rules should be on there
if (!in_array($key, array_merge($groupKeys, ['lo0']))) {
$descr = !empty($intf->descr) ? (string)$intf->descr : strtoupper($key);
$result['interfaces']['items'][] = $makeItem($key, $descr, $ruleCounts[$key] ?? 0, 'interface');
}
}
// ALL rules
$result['any']['items'][] = $makeItem('__any', gettext('All rules'), $totalRules, 'any');
// Sort items by count and alphabetically
foreach ($result as &$section) {
usort($section['items'], fn($a, $b) =>
@ -441,14 +464,18 @@ class FilterController extends FilterBaseController
if ($this->request->isGet()) {
/* categories have unique names, export names instead of ids so we can easily map them on other targets */
$categories = [];
$aliases = [];
foreach ((new Category())->categories->category->iterateItems() as $key => $category) {
$categories[$key] = $category->name->getValue();
}
foreach ((new Alias())->aliases->alias->iterateItems() as $key => $alias) {
$aliases[$key] = $alias->name->getValue();
}
/* XXX: as shaper1/2 don't have functional keys, we can only export uuid's here*/
$this->exportCsv($this->getModel()->rules->rule->asRecordSet(
false,
['sort_order', 'prio_group'],
function ($node, $record) use ($categories) {
function ($node, $record) use ($categories, $aliases) {
if (!empty($record['categories'])) {
$cats = [];
foreach (explode(',', $record['categories']) as $key) {
@ -458,6 +485,9 @@ class FilterController extends FilterBaseController
}
$record['categories'] = implode(',', $cats);
}
if (!empty($record['overload']) && isset($aliases[$record['overload']])) {
$record['overload'] = $aliases[$record['overload']];
}
return array_merge(['@uuid' => $node->getAttribute('uuid')], $record);
}
));
@ -469,15 +499,18 @@ class FilterController extends FilterBaseController
if ($this->request->isPost() && $this->request->hasPost('payload')) {
/* catgories have unique names, need to map them to uuids */
$categories = [];
$aliases = [];
foreach ((new Category())->categories->category->iterateItems() as $key => $category) {
$categories[$category->name->getValue()] = $key;
}
foreach ((new Alias())->aliases->alias->iterateItems() as $key => $alias) {
$aliases[$alias->name->getValue()] = $key;
}
return $this->importCsv(
'rules.rule',
$this->request->getPost('payload'),
['@uuid'],
function (&$record) use ($categories) {
function (&$record) use ($categories, $aliases) {
if (!empty($record['categories'])) {
/* only map what we know, ignore the rest */
$cats = [];
@ -488,6 +521,9 @@ class FilterController extends FilterBaseController
}
$record['categories'] = implode(',', $cats);
}
if (!empty($record['overload']) && isset($aliases[$record['overload']])) {
$record['overload'] = $aliases[$record['overload']];
}
}
);
} else {

View File

@ -279,6 +279,16 @@
<visible>false</visible>
</grid_view>
</field>
<field>
<id>rule.tcpflags_any</id>
<label>TCP flags any</label>
<type>checkbox</type>
<help>Use this to choose TCP flags that must be cleared for this rule to match.</help>
<advanced>true</advanced>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
<field>
<id>rule.sched</id>
<label>Schedule</label>
@ -286,6 +296,7 @@
<advanced>true</advanced>
<grid_view>
<visible>false</visible>
<formatter>sched</formatter>
</grid_view>
</field>
<!-- XXX: start as advanced, promote to non-advanced feature in a later version -->
@ -652,7 +663,6 @@
<grid_view>
<formatter>statistics</formatter>
<sequence>115</sequence>
<min-width>200</min-width>
</grid_view>
</field>
<field>

View File

@ -30,6 +30,7 @@ namespace OPNsense\Hostdiscovery\Api;
use OPNsense\Base\ApiMutableServiceControllerBase;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
class ServiceController extends ApiMutableServiceControllerBase
{
@ -40,12 +41,21 @@ class ServiceController extends ApiMutableServiceControllerBase
public function searchAction()
{
$if_descr = [];
foreach ((Config::getInstance()->object())->interfaces->children() as $key => $node) {
if (!empty((string)$node->if)) {
$descr = (string)$node->descr;
$if_descr[(string)$node->if] = strlen($descr) ? $descr : strtoupper($key);
}
}
$data = json_decode((new Backend())->configdRun('hostwatch dump_full'), true) ?? [];
$records = [];
foreach ((!empty($data) && !empty($data['rows'])) ? $data['rows'] : [] as $rec) {
$records[] = [
'source' => $data['source'],
'interface_name' => $rec[0],
'interface_name' => $if_descr[$rec[0]] ?? $rec[0],
'ether_address' => $rec[1],
'ip_address' => $rec[2],
'organization_name' => $rec[3],

View File

@ -31,10 +31,10 @@ namespace OPNsense\IPsec\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
/**
* Class ManualSPD
* Class ManualSpd
* @package OPNsense\IPsec\Api
*/
class ManualSPDController extends ApiMutableModelControllerBase
class ManualSpdController extends ApiMutableModelControllerBase
{
protected static $internalModelName = 'swanctl';
protected static $internalModelClass = 'OPNsense\IPsec\Swanctl';

View File

@ -28,7 +28,7 @@
namespace OPNsense\Interfaces;
class VxLanController extends \OPNsense\Base\IndexController
class VxlanController extends \OPNsense\Base\IndexController
{
public function indexAction()
{

View File

@ -60,16 +60,21 @@ abstract class LeasesController extends ApiControllerBase
];
}
// Mark records as reserved based on hwaddr (IPv4) or duid (IPv6) match
// Mark records as reserved based on hwaddr (IPv4) or duid/hwaddr (IPv6) match
$resv4 = [];
$resv6 = [];
foreach ((new KeaDhcpv4())->reservations->reservation->iterateItems() as $reservation) {
$resv4[strtolower((string)$reservation->hw_address)] = true;
$resv4[strtolower($reservation->hw_address->getValue())] = 'hwaddr';
}
foreach ((new KeaDhcpv6())->reservations->reservation->iterateItems() as $reservation) {
$resv6[strtolower((string)$reservation->duid)] = true;
// At least one of these is required in the model
if (!$reservation->duid->isEmpty()) {
$resv6[strtolower($reservation->duid->getValue())] = 'duid';
} elseif (!$reservation->hw_address->isEmpty()) {
$resv6[strtolower($reservation->hw_address->getValue())] = 'hwaddr';
}
}
if (!empty($leases) && isset($leases['records'])) {
@ -87,13 +92,21 @@ abstract class LeasesController extends ApiControllerBase
$mac = strtoupper(substr(str_replace(':', '', $record['hwaddr']), 0, 6));
$record['mac_info'] = isset($mac_db[$mac]) ? $mac_db[$mac] : '';
// Reservation
$record['is_reserved'] = '';
$addr = $record['address'] ?? '';
if (strpos($addr, ':') !== false) {
$duid = strtolower($record['duid'] ?? '');
$record['is_reserved'] = isset($resv6[$duid]) ? '1' : '0';
$mac = strtolower($record['hwaddr'] ?? '');
if (isset($resv6[$duid])) {
$record['is_reserved'] = $resv6[$duid];
} elseif (isset($resv6[$mac])) {
$record['is_reserved'] = $resv6[$mac];
}
} else {
$mac_lower = strtolower($record['hwaddr'] ?? '');
$record['is_reserved'] = isset($resv4[$mac_lower]) ? '1' : '0';
$mac = strtolower($record['hwaddr'] ?? '');
if (isset($resv4[$mac])) {
$record['is_reserved'] = $resv4[$mac];
}
}
}
} else {

View File

@ -41,7 +41,9 @@ class ConfigMaintenance
{
$this->modelmap = $this->loadModels();
$this->legacyPaths = [
'filter.rule' => gettext("Firewall / Rules (legacy)")
'dhcpd' => gettext('Services: ISC DHCPv4 [legacy]'),
'dhcpdv6' => gettext('Services: ISC DHCPv6 [legacy]'),
'filter.rule' => gettext('Firewall: Rules [legacy]'),
];
}

View File

@ -40,6 +40,8 @@ class Shell
*/
public static function exec_safe($format, $args = [])
{
$command = '/usr/bin/true';
if (!is_array($format)) {
$format = [$format];
}
@ -49,10 +51,17 @@ class Shell
}
foreach ($args as $id => $arg) {
/* XXX casts to string, formatter only really supports %% and %s */
$args[$id] = escapeshellarg($arg ?? '');
}
return vsprintf(implode(' ', $format), $args);
try {
$command = vsprintf(implode(' ', $format), $args);
} catch (\Error $e) {
error_log($e);
}
return $command;
}
/**

View File

@ -36,6 +36,7 @@ class Syslog
private $name = null;
private $option = null;
private $facility = null;
private static $echo_stdout = false;
public function __construct($name, $option = null, $facility = null)
{
@ -48,6 +49,9 @@ class Syslog
{
openlog($this->name, $this->option, $this->facility);
syslog($level, $message);
if (self::$echo_stdout) {
echo sprintf("%s\n", trim($message));
}
}
public function alert($message)
@ -90,4 +94,20 @@ class Syslog
{
$this->send(LOG_WARNING, $message);
}
/**
* Enable local (stdout) logging for all syslog instances for this application, for debug purposes
*/
public static function enableLocalEcho()
{
self::$echo_stdout = true;
}
/**
* Disable local (stdout) logging for all syslog instances for this application
*/
public static function disableLocalEcho()
{
self::$echo_stdout = false;
}
}

View File

@ -198,7 +198,7 @@ class FilterRule extends Rule
}
// restructure flags
if (isset($rule['protocol']) && $rule['protocol'] == "tcp") {
if (isset($rule['tcpflags_any'])) {
if (!empty($rule['tcpflags_any'])) {
$rule['flags'] = "any";
} elseif (!empty($rule['tcpflags2'])) {
$rule['flags'] = "";

View File

@ -239,6 +239,8 @@ class Plugin
foreach (explode(',', $types) as $type) {
foreach ($this->anchors as $anchorKey => $anchor) {
if (strpos($anchorKey, "{$type}.{$placement}") === 0) {
$result .= $type == "fw" ? "" : "{$type}-";
$result .= "anchor \"{$anchor['name']}\"";
if ($anchor['quick']) {
$result .= " quick";
}

View File

@ -476,6 +476,16 @@ abstract class Rule
return 'internal2'; // late
}
public function updateDescription($descr)
{
$this->rule['descr'] = $descr;
}
public function disable()
{
$this->rule['disabled'] = 1;
}
/**
* return raw rule
*/

View File

@ -177,6 +177,38 @@ class Util
}
}
/**
* fetch alias by uuid from model
* @param string $uuid
* @return Alias|null
*/
private static function getAliasByUuid($uuid)
{
if (self::$aliasObject == null) {
// Cache the alias object to avoid object creation overhead.
self::$aliasObject = new Alias(true);
self::$aliasObject->flushCache();
}
if (!empty($uuid)) {
return self::$aliasObject->getNodeByReference('aliases.alias.' . $uuid);
}
return null;
}
/**
* resolve alias uuid to alias name
* @param string $uuid
* @return string|null
*/
public static function aliasUuidToName($uuid)
{
$alias = self::getAliasByUuid($uuid);
if ($alias !== null) {
return $alias->name->getValue();
}
return null;
}
/**
* fetch alias by name from model
* @param string $name name

View File

@ -752,7 +752,15 @@ abstract class BaseModel
* e.g. /system/user should place new entries at /system
**/
$pxpath = implode("/", array_slice(explode("/", $xpath), 0, -1));
$toDom = dom_import_simplexml($target_node->xpath($pxpath)[0]);
$target = $target_node->xpath($pxpath);
if (empty($target)) {
/* target doesn't exist yet, create the root node by traversing the tree (starting inside /opnsense) */
$target = $target_node;
foreach (array_slice(explode("/", trim($xpath, "/")), 1, -1) as $p) {
$target = count($target_node->xpath($p)) == 0 ? $target->addChild($p) : $target->xpath($p)[0];
}
}
$toDom = dom_import_simplexml($target[0]);
foreach ($newNodes as $node) {
if ($node !== null) {
$toDom->appendChild($toDom->ownerDocument->importNode($node, true));

View File

@ -331,8 +331,29 @@ class ArrayField extends BaseField
}
foreach ($records as $idx => $record) {
$uuid = $record['@uuid'] ?? '';
if (
!empty($uuid) &&
(
!is_string($uuid) ||
preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid) !== 1
)
) {
$results['validations'][] = [
'sequence' => $idx,
'field' => '@uuid',
'message' => sprintf("Invalid UUID offered (%s)", $uuid)
];
continue;
}
if (is_callable($data_callback)) {
$data_callback($record);
try {
$data_callback($record);
} catch (\Exception $e) {
/* callbacks may trow errors to report validation issues */
$results['validations'][] = ['sequence' => $idx, 'message' => $e->getMessage()];
continue;
}
}
$keydata = [];
foreach ($keyfields as $keyfield) {

View File

@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2015-2020 Deciso B.V.
* Copyright (C) 2015-2026 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -39,7 +39,7 @@ class PortField extends BaseListField
/**
* @var array list of well known services
*/
private static $wellknownservices = [
protected static $wellknownservices = [
'cvsup',
'domain',
'ftp',
@ -107,6 +107,15 @@ class PortField extends BaseListField
*/
private $enableAlias = false;
/**
* get the list of well known services
* @return array service names
*/
public static function getWellKnown()
{
return self::$wellknownservices;
}
/**
* generate validation data (list of port numbers and well know ports)
*/

View File

@ -25,6 +25,10 @@
<MaximumValue>65535</MaximumValue>
</packetsize>
<disable_frag type="BooleanField"/>
<interval type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>120</MaximumValue>
</interval>
<description type="DescriptionField"/>
</settings>
</items>

View File

@ -49,7 +49,7 @@
<EnableWellKnown>Y</EnableWellKnown>
<EnableRanges>Y</EnableRanges>
<EnableAlias>Y</EnableAlias>
<ValidationMessage>Please specify a valid portnumber, name, alias or range.</ValidationMessage>
<ValidationMessage>Please specify a valid port number, well-known name, alias or range.</ValidationMessage>
</port>
<not type="BooleanField"/>
</source>
@ -61,14 +61,14 @@
<EnableWellKnown>Y</EnableWellKnown>
<EnableRanges>Y</EnableRanges>
<EnableAlias>Y</EnableAlias>
<ValidationMessage>Please specify a valid portnumber, name, alias or range.</ValidationMessage>
<ValidationMessage>Please specify a valid port number, well-known name, alias or range.</ValidationMessage>
</port>
<not type="BooleanField"/>
</destination>
<target type="NetworkAliasField"/>
<local-port type="PortField">
<EnableAlias>Y</EnableAlias>
<ValidationMessage>Please specify a valid portnumber, name, alias or range.</ValidationMessage>
<ValidationMessage>Please specify a valid port number or alias.</ValidationMessage>
</local-port>
<!-- -->
<poolopts type="OptionField">

View File

@ -29,6 +29,7 @@
namespace OPNsense\Firewall\FieldTypes;
use OPNsense\Core\Config;
use OPNsense\Firewall\Util;
use OPNsense\Firewall\Group;
use OPNsense\Base\FieldTypes\ArrayField;
use OPNsense\Base\FieldTypes\ContainerField;
@ -88,6 +89,15 @@ class FilterRuleContainerField extends ContainerField
$result['to_port'] = (string)$this->destination_port;
}
}
if (!$this->overload->isEmpty()) {
$alias = Util::aliasUuidToName($this->overload->getValue());
if ($alias !== null) {
$result['overload'] = $alias;
} else {
// fall back to default virusprod table
unset($result['overload']);
}
}
// field mappings and differences
$result['disabled'] = empty((string)$this->enabled);
$result['descr'] = (string)$this->description;

View File

@ -133,7 +133,7 @@ class Filter extends BaseModel
if (!in_array($rule->protocol, ['TCP', 'TCP/UDP'])) {
foreach (
[
'statetimeout', 'max-src-conn', 'tcpflags1', 'tcpflags2',
'statetimeout', 'max-src-conn', 'tcpflags1', 'tcpflags2', 'tcpflags_any',
'max-src-conn-rate', 'max-src-conn-rates', 'overload'
] as $fieldname
) {
@ -230,6 +230,13 @@ class Filter extends BaseModel
));
}
if (!$rule->tcpflags1->isEmpty() && !$rule->tcpflags2->isEmpty() && !$rule->tcpflags_any->isEmpty()) {
$messages->appendMessage(new Message(
gettext("If you select any TCP flags, you cannot also select specific ones."),
$rule->tcpflags_any->__reference
));
}
if ((string)$rule->{'set-prio'} == '' && (string)$rule->{'set-prio-low'} != '') {
$messages->appendMessage(new Message(
gettext("Set priority for low latency and acknowledgements " .
@ -366,4 +373,22 @@ class Filter extends BaseModel
}
return false;
}
public function hasSchedule()
{
foreach ($this->rules->rule->iterateItems() as $rule) {
if (!$rule->sched->isEmpty() && !$rule->enabled->isEmpty()) {
return true;
}
}
$cfg = (\OPNsense\Core\Config::getInstance())->object();
if (isset($cfg->filter->rule)) {
foreach ($cfg->filter->children() as $tag => $rule) {
if ($tag === 'rule' && !empty($rule->sched) && empty((string)$rule->disabled)) {
return true;
}
}
}
return false;
}
}

View File

@ -229,7 +229,7 @@
</max>
<max-src-conn-rate type="IntegerField">
<MinimumValue>1</MinimumValue>
<MaximumValue>2147483647</MaximumValue>
<MaximumValue>4294967</MaximumValue>
</max-src-conn-rate>
<max-src-conn-rates type="IntegerField">
<MinimumValue>1</MinimumValue>
@ -328,6 +328,7 @@
<cwr>cwr</cwr>
</OptionValues>
</tcpflags2>
<tcpflags_any type="BooleanField"/>
<categories type="ModelRelationField">
<Model>
<rulesets>

View File

@ -61,7 +61,7 @@ class SET1_0_0 extends BaseModelMigration
if (isset($config->system->$key)) {
$nodes[$_key] = (string)$config->system->$key;
}
} elseif ($key == 'dhcp6_debug') {
} elseif (in_array($key, ['dhcp6_debug', 'dhcp6_norelease'])) {
$nodes[$key] = isset($config->system->$key) ? '1' : '0';
} elseif (isset($config->system->$key)) {
$nodes[$key] = (string)$config->system->$key;

View File

@ -91,6 +91,31 @@ class KeaDhcpv6 extends BaseModel
}
}
}
// validate changed pd_pools
foreach ($this->pd_pools->pd_pool->iterateItems() as $pool) {
if (!$validateFullModel && !$pool->isFieldChanged()) {
continue;
}
$key = $pool->__reference;
if ($pool->prefix_len->getValue() >= $pool->delegated_len->getValue()) {
$messages->appendMessage(new Message(gettext("Delegated length must be longer than or equal to prefix length"), $key . ".delegated_len"));
}
$subnet = $pool->prefix->getValue() . "/" . $pool->prefix_len->getValue();
$trange = Util::cidrToRange($subnet);
if (!Util::isSubnetStrict($subnet)) {
$messages->appendMessage(new Message(gettext("Invalid Pool boundaries, offered address is not the first address in the prefix."), $key . ".prefix"));
}
foreach ($this->pd_pools->pd_pool->iterateItems() as $tmppool) {
if ($key === $tmppool->__reference) {
continue;
}
$osubnet = $tmppool->prefix->getValue() . "/" . $tmppool->prefix_len->getValue();
$orange = Util::cidrToRange($osubnet);
if (Util::isIPInCIDR($orange[0], $subnet) || Util::isIPInCIDR($trange[0], $osubnet)) {
$messages->appendMessage(new Message(gettext("Pool overlaps with an existing one."), $key . ".prefix"));
}
}
}
return $messages;
}

View File

@ -954,7 +954,7 @@
<td>
<select id="alias.authtype" data-container="body" class="selectpicker" style="margin-bottom: 3px;"></select>
<input type="text" placeholder="{{lang._('Username')}}" class="form-control" size="50" id="alias.username"/>
<input type="password" class="form-control" size="50" id="alias.password"/>
<input type="password" class="form-control" autocomplete="new-password" size="50" id="alias.password"/>
<div class="hidden" data-for="help_for_alias.authtype">
<small>
{{lang._('If the remote server enforces authorization, you can specify the authorization type here.')}}

View File

@ -159,7 +159,7 @@
title="${row.log == '1'
? '{{ lang._("Disable Logging") }}'
: '{{ lang._("Enable Logging") }}'}">
<i class="fa fa-exclamation-circle fa-fw ${row.log == '1' ? 'text-info' : 'text-muted'}"></i>
<i class="fa fa-fw ${row.log == '1' ? 'fa-bell' : 'fa-bell-slash'}"></i>
</button>
<button type="button" class="btn btn-xs btn-default command-edit
@ -493,8 +493,16 @@
ajaxGet('/api/firewall/d_nat/list_port_select_options', [], function (data) {
if (!data || !data.single) return;
// local-port does not support port ranges, so we replace the label for clarity
const singlePortOnly = $.extend(true, {}, data);
singlePortOnly.single.label = "{{ lang._('Single port') }}";
$(".port_selector").each(function () {
$(this).replaceInputWithSelector(data, false);
const opts = $(this).is('#row_rule\\.local-port .port_selector')
? singlePortOnly
: data;
$(this).replaceInputWithSelector(opts, false);
});
});

View File

@ -108,7 +108,8 @@
/* hook import/export buttons */
$("#upload_rules").SimpleFileUploadDlg({
onAction: function(){
$('#{{formGridFilterRule["edit_dialog_id"]}}').bootgrid('reload');
$("#{{formGridFilterRule['table_id']}}").bootgrid('reload');
$("#change_message_base_form").stop(true, false).slideDown(1000).delay(2000).slideUp(2000);
}
});
@ -162,11 +163,15 @@
}
// Add interface selectpicker, or fall back to hash for the first load
let selectedInterface = $('#interface_select').val();
if ((!selectedInterface || selectedInterface.length === 0) && pendingUrlInterface) {
request['interface'] = pendingUrlInterface;
if (selectedInterface == null && pendingUrlInterface != null) {
selectedInterface = pendingUrlInterface;
pendingUrlInterface = null; // consume the hash so it is not used again
} else if (selectedInterface && selectedInterface.length > 0) {
request['interface'] = selectedInterface;
}
if (selectedInterface === '__floating') {
request.interface = '';
} else if (selectedInterface !== null && selectedInterface !== '__any') {
request.interface = selectedInterface;
// '__any' omit parameter for all rules
}
if (inspectEnabled) {
// Send as a comma separated string
@ -267,7 +272,7 @@
title="${row.log == '1'
? '{{ lang._("Disable Logging") }}'
: '{{ lang._("Enable Logging") }}'}">
<i class="fa fa-exclamation-circle fa-fw ${row.log == '1' ? 'text-info' : 'text-muted'}"></i>
<i class="fa fa-fw ${row.log == '1' ? 'fa-bell' : 'fa-bell-slash'}"></i>
</button>
<button type="button" class="btn btn-xs btn-default command-edit
@ -317,9 +322,8 @@
}
if (
row[column.id] !== '' &&
row[column.id] !== 'any' &&
row[column.id] !== 'None'
row[column.id] !== undefined &&
!['', 'any', 'None'].includes(row[column.id])
) {
return row[column.id];
} else {
@ -444,7 +448,7 @@
const usedAdvancedFields = [];
advancedFieldIds.forEach(function (fieldId) {
const value = row[fieldId];
const value = row["%" + fieldId] ?? row[fieldId];
if (value !== undefined) {
const lowerValue = value.toString().toLowerCase().trim();
// Check: if the value is empty OR starts with any default prefix, consider it default
@ -522,8 +526,9 @@
const states = row["states"] ?? "";
const packets = row["packets"] ?? "";
const bytes = row["bytes"] ?? "";
const uuid = row["uuid"] ?? "";
function render(icon, title, value, is_number = false) {
function render(icon, title, value, is_number = false, link = null) {
if (!value || value === "0") {
return "";
}
@ -533,14 +538,18 @@
return `
<span data-toggle="tooltip" title="${title}: ${numValue.toLocaleString()}">
<i class="fa fa-fw ${icon} text-muted"></i> ${formatted}
${link
? `<a href="${link}" target="_blank" rel="noopener noreferrer" id="${uuid}_states">
<i class="fa fa-fw ${icon}"></i> ${formatted}
</a>`
: `<i class="fa fa-fw ${icon}"></i> ${formatted}`}
</span>
`;
}
const parts = [
render("fa-chart-line", "{{ lang._('States') }}", states, true, `/ui/diagnostics/firewall/states#${uuid}`),
render("fa-bullseye", "{{ lang._('Evaluations') }}", evals, true),
render("fa-chart-line", "{{ lang._('States') }}", states, true),
render("fa-box", "{{ lang._('Packets') }}", packets, true),
render("fa-database", "{{ lang._('Bytes') }}", bytes)
].filter(Boolean);
@ -549,17 +558,23 @@
return "";
}
// Split into two vertical rows
const firstGroup = parts.slice(0, 2).join(" ");
const secondGroup = parts.slice(2).join(" ");
return `
<div class="stats-cell">
<div>${firstGroup}</div>
<div>${secondGroup}</div>
${parts.join("")}
</div>
`;
},
sched: function(column, row) {
if (row.isGroup || typeof row[column.id] !== "string" || row[column.id] === "") {
return "";
}
return `
${row[column.id]} &nbsp;
<a href="/firewall_schedule_edit.php?name=${row[column.id]}" data-toggle="tooltip" title="{{ lang._('Edit') }}">
<i class="fa fa-calendar text-muted"></i>
</a>
`;
},
},
},
commands: {
@ -716,7 +731,8 @@
const bgClassMap = {
floating: 'bg-primary',
group: 'bg-warning',
interface: 'bg-info'
interface: 'bg-info',
any: ''
};
const badgeClass = bgClassMap[item.type] || 'bg-info';
@ -727,7 +743,6 @@
<span>
${count > 0 ? `<span class="badge badge-sm ${badgeClass}">${count}</span>` : ''}
${label}
<small class="text-muted ms-2"><em>${subtext}</em></small>
</span>
`.trim()
};
@ -745,6 +760,9 @@
if (allOptions.includes(ifaceFromHash)) {
$('#interface_select').val(ifaceFromHash).selectpicker('refresh');
}
} else {
// Default to ALL interfaces
$('#interface_select').selectpicker('val', '__any');
}
interfaceInitialized = true;
@ -1044,11 +1062,20 @@
.stats-cell {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 4px 10px;
align-items: center;
container-type: inline-size;
}
.stats-cell div {
gap: 6px;
.stats-cell > span {
white-space: nowrap;
}
@container (max-width: 160px) {
.stats-cell > span {
flex: 1 1 50%;
}
}
</style>

View File

@ -145,7 +145,7 @@
title="${row.log == '1'
? '{{ lang._("Disable Logging") }}'
: '{{ lang._("Enable Logging") }}'}">
<i class="fa fa-exclamation-circle fa-fw ${row.log == '1' ? 'text-info' : 'text-muted'}"></i>
<i class="fa fa-fw ${row.log == '1' ? 'fa-bell' : 'fa-bell-slash'}"></i>
</button>
<button type="button" class="btn btn-xs btn-default command-edit

View File

@ -145,7 +145,7 @@
title="${row.log == '1'
? '{{ lang._("Disable Logging") }}'
: '{{ lang._("Enable Logging") }}'}">
<i class="fa fa-exclamation-circle fa-fw ${row.log == '1' ? 'text-info' : 'text-muted'}"></i>
<i class="fa fa-fw ${row.log == '1' ? 'fa-bell' : 'fa-bell-slash'}"></i>
</button>
<button type="button" class="btn btn-xs btn-default command-edit

View File

@ -145,7 +145,7 @@
title="${row.log == '1'
? '{{ lang._("Disable Logging") }}'
: '{{ lang._("Enable Logging") }}'}">
<i class="fa fa-exclamation-circle fa-fw ${row.log == '1' ? 'text-info' : 'text-muted'}"></i>
<i class="fa fa-fw ${row.log == '1' ? 'fa-bell' : 'fa-bell-slash'}"></i>
</button>
<button type="button" class="btn btn-xs btn-default command-edit

View File

@ -175,6 +175,7 @@
if (params.has('hostname')) $('#reservation\\.hostname').val(params.get('hostname'));
if (params.has('ip_address')) $('#reservation\\.ip_address').val(params.get('ip_address'));
if (params.has('duid')) $('#reservation\\.duid').val(params.get('duid'));
if (params.has('hw_address')) $('#reservation\\.hw_address').val(params.get('hw_address'));
history.replaceState(null, null, window.location.pathname + '#reservations');
});
$(this).find('.command-add').trigger('click');

View File

@ -84,6 +84,11 @@
"timestamp": function (column, row) {
return moment.unix(row[column.id]).local().format('YYYY-MM-DD HH:mm:ss');
},
"reservation": function (column, row) {
return row.is_reserved !== ''
? "{{ lang._('static') }}"
: "{{ lang._('dynamic') }}";
},
"commands": function (column, row) {
const baseUrl = `/ui/kea/dhcp/v4#reservations`;
const searchUrl = `${baseUrl}&search=${encodeURIComponent(row.hwaddr || '')}`;
@ -96,7 +101,7 @@
let btn;
if (row.is_reserved === '1') {
if (row.is_reserved !== '') {
btn = $(`
<button type="button" class="btn btn-xs" data-toggle="tooltip"
title="{{ lang._('Find Reservation') }}">
@ -151,6 +156,7 @@
<th data-column-id="valid_lifetime" data-type="integer">{{ lang._('Lifetime') }}</th>
<th data-column-id="expire" data-type="string" data-formatter="timestamp">{{ lang._('Expire') }}</th>
<th data-column-id="hostname" data-type="string" data-formatter="overflowformatter">{{ lang._('Hostname') }}</th>
<th data-column-id="is_reserved" data-type="string" data-formatter="reservation" data-width="6em">{{ lang._('Lease Type') }}</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>

View File

@ -84,19 +84,33 @@
"timestamp": function (column, row) {
return moment.unix(row[column.id]).local().format('YYYY-MM-DD HH:mm:ss');
},
"reservation": function (column, row) {
return row.is_reserved !== ''
? "{{ lang._('static') }}"
: "{{ lang._('dynamic') }}";
},
"commands": function (column, row) {
const baseUrl = `/ui/kea/dhcp/v6#reservations`;
const searchUrl = `${baseUrl}&search=${encodeURIComponent(row.duid || '')}`;
let searchValue = '';
if (row.is_reserved === 'duid') {
searchValue = row.duid || '';
} else if (row.is_reserved === 'hwaddr') {
searchValue = row.hwaddr || '';
}
const searchUrl = `${baseUrl}&search=${encodeURIComponent(searchValue)}`;
const addUrlParams = {
ip_address: row.address || '',
duid: row.duid || '',
hw_address: row.hwaddr || '',
hostname: row.hostname || ''
};
const addUrl = `${baseUrl}?${new URLSearchParams(addUrlParams)}`;
let btn;
if (row.is_reserved === '1') {
if (row.is_reserved !== '') {
btn = $(`
<button type="button" class="btn btn-xs" data-toggle="tooltip"
title="{{ lang._('Find Reservation') }}">
@ -152,6 +166,7 @@
<th data-column-id="valid_lifetime" data-type="integer">{{ lang._('Lifetime') }}</th>
<th data-column-id="expire" data-type="string" data-formatter="timestamp">{{ lang._('Expire') }}</th>
<th data-column-id="hostname" data-type="string" data-formatter="overflowformatter">{{ lang._('Hostname') }}</th>
<th data-column-id="is_reserved" data-type="string" data-formatter="reservation" data-width="6em">{{ lang._('Lease Type') }}</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>

View File

@ -314,6 +314,8 @@
$.extend(jQuery.fn.UIBootgrid.translations, {
add: "{{ lang._('Add') }}",
deleteSelected: "{{ lang._('Delete selected') }}",
enableSelected: "{{ lang._('Enable selected') }}",
disableSelected: "{{ lang._('Disable selected') }}",
edit: "{{ lang._('Edit') }}",
disable: "{{ lang._('Disable') }}",
enable: "{{ lang._('Enable') }}",
@ -327,7 +329,8 @@
refresh: "{{ lang._('Refresh') }}",
infosTotal: "{{ lang._('Showing %s to %s of %s entries') | format('{{ctx.start}}','{{ctx.end}}','{{ctx.totalRows}}') }}",
infos: "{{ lang._('Showing %s to %s') | format('{{ctx.start}}','{{ctx.end}}') }}",
resetGrid: "{{ lang._('Reset grid layout') }}"
resetGrid: "{{ lang._('Reset grid layout') }}",
searchColumns: "{{ lang._('Search columns') }}"
});
</script>

View File

@ -32,7 +32,22 @@ require_once('script/load_phalcon.php');
use OPNsense\Core\Config;
$classprefix = !empty($argv[1]) ? str_replace('/', '\\', $argv[1]) : '';
$classprefix = '';
foreach ($argv as $idx => $arg) {
if ($idx > 0) {
if (strpos($arg, '-') === 0) {
if ($arg == '-v') {
/* verbose mode, force logging to stdout as well */
OPNsense\Core\Syslog::enableLocalEcho();
} elseif ($arg == '-h') {
echo sprintf("usage : %s [-v] [h] <classperefix>\n", $argv[0]);
exit(0);
}
} else {
$classprefix = str_replace('/', '\\', $arg);
}
}
}
$class_info = new \ReflectionClass("OPNsense\\Base\\BaseModel");
$executed_migration = false;
@ -79,6 +94,8 @@ foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($model_dir
}
}
OPNsense\Core\Syslog::disableLocalEcho();
if ($executed_migration) {
// make changes persistent
Config::getInstance()->save();

View File

@ -38,6 +38,8 @@ if output_cmd opnsense-update -u; then
elif output_cmd opnsense-update -K; then
output_reboot keep-log
fi
# kernel is already applied so continue
output_reboot keep-log
fi
output_txt "The upgrade was aborted due to an error."

View File

@ -151,7 +151,7 @@ abstract class Base
return $this;
}
$cmd_frmt = ['/usr/local/bin/rrdtool create %s --step %d'];
$cmd_frmt = ['/usr/local/bin/rrdtool create %s --step %s'];
$cmd_args = [$this->filename, 60];
foreach ($this->datasets as $dataset) {

View File

@ -0,0 +1,96 @@
#!/bin/sh
# Copyright (c) 2017-2020 Martin Wasley <mjwasley@gmail.com>
# Copyright (c) 2017-2026 Franco Fichtner <franco@opnsense.org>
#
# 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.
FORCE=
if [ -z "${REASON}" -o -z "${IFNAME}" ]; then
/usr/bin/logger -t dhcp6c "dhcp6c_script: missing REASON or IFNAME"
fi
case ${REASON} in
INFOREQ|REBIND|RENEW|REQUEST|SOLICIT)
/usr/bin/logger -t dhcp6c "dhcp6c_script: ${REASON} on ${IFNAME} executing"
IFCFILE="/tmp/dhcp6c.${IFNAME}.ifconfig"
ENVFILE="/tmp/dhcp6c.${IFNAME}.env"
if [ "$(pluginctl -g OPNsense.Interfaces.settings.dhcp6_debug)" = "1" ]; then
ifconfig -L > "${IFCFILE}"
env | sort > "${ENVFILE}"
else
rm -f "${IFCFILE}" "${ENVFILE}"
fi
ARGS=
for NAMESERVER in ${new_domain_name_servers}; do
ARGS="${ARGS} -a ${NAMESERVER}"
done
/usr/local/sbin/ifctl -i "${IFNAME}" -6nd ${ARGS}
ARGS=
for DOMAIN in ${new_domain_name}; do
ARGS="${ARGS} -a ${DOMAIN}"
done
/usr/local/sbin/ifctl -i "${IFNAME}" -6sd ${ARGS}
if [ ${REASON} = "REQUEST" -o ${REASON} = "SOLICIT" ]; then
/usr/bin/logger -t dhcp6c "dhcp6c_script: ${REASON} on ${IFNAME} connected to server"
FORCE=${REASON}
# can safely clear the prefix during connect
/usr/local/sbin/ifctl -i "${IFNAME}" -6pd
fi
if [ ${REASON} != "INFOREQ" -a -n "${PDINFO}" ]; then
ARGS=
for PD in ${PDINFO}; do
ARGS="${ARGS} -a ${PD}"
done
if /usr/local/sbin/ifctl -i "${IFNAME}" -6pu ${ARGS}; then
/usr/bin/logger -t dhcp6c "dhcp6c_script: ${REASON} on ${IFNAME} prefix now ${PDINFO}"
if [ -z "${FORCE}" ]; then
FORCE=${REASON}
fi
fi
fi
/usr/local/sbin/configctl -d interface newipv6 "${IFNAME}" ${FORCE}
;;
EXIT|RELEASE)
/usr/bin/logger -t dhcp6c "dhcp6c_script: ${REASON} on ${IFNAME} executing"
/usr/local/sbin/ifctl -i "${IFNAME}" -6nd
/usr/local/sbin/ifctl -i "${IFNAME}" -6sd
/usr/local/sbin/ifctl -i "${IFNAME}" -6pd
/usr/local/sbin/configctl -d interface newipv6 "${IFNAME}"
;;
*)
/usr/bin/logger -t dhcp6c "dhcp6c_script: ${REASON} on ${IFNAME} ignored"
;;
esac

View File

@ -172,6 +172,9 @@ if __name__ == '__main__':
args.append(settings['packetsize'])
if settings.get('disable_frag', '0') == '1':
args.append('-D')
if settings.get('interval', '') != '':
args.append('-i')
args.append(settings['interval'])
args.append(settings.get('hostname', ''))
if os.path.isfile(log_target):
os.remove(log_target)

View File

@ -28,6 +28,7 @@
import argparse
import csv
import ipaddress
import ujson
import os
import time
import subprocess
@ -69,24 +70,27 @@ def yield_log_records(filename, poll_interval=5):
else:
time.sleep(poll_interval)
class NDP:
""" simplistic NDP link local lookup
class Hostwatch:
"""
Use hostwatch service to gather link-local IPv6 addresses.
The endpoint falls back to NDP when hostwatch service is not running.
"""
def __init__(self):
self._def_local_db = {}
def reload(self):
ndpdata = subprocess.run(['/usr/sbin/ndp', '-an'], capture_output=True, text=True).stdout
for idx, line in enumerate(ndpdata.split("\n")):
parts = line.split()
if idx > 0 and len(parts) > 1:
try:
addr = parts[0].split('%')[0]
if ipaddress.ip_address(addr).is_link_local:
self._def_local_db[parts[1]] = parts[0]
except (ValueError, IndexError):
pass
self._def_local_db.clear()
out = subprocess.run(
['/usr/local/opnsense/scripts/interfaces/list_hosts.py', '--proto', 'inet6', '--ndp'],
capture_output=True,
text=True
).stdout
for row in ujson.loads(out).get("rows", []):
# [ifname, mac, ip]
if ipaddress.ip_address(row[2]).is_link_local:
self._def_local_db[row[1]] = row[2]
def get(self, mac):
if mac not in self._def_local_db:
@ -110,7 +114,7 @@ if __name__ == '__main__':
if not os.path.isfile(inputargs.filename):
syslog.syslog(syslog.LOG_ERR, "lease file does not exist: %s" % inputargs.filename)
sys.exit(1)
ndp = NDP()
hostwatch = Hostwatch()
for record in yield_log_records(inputargs.filename):
# IA_PD: type 2, prefix_len <= 64 - the delegated prefix
if record.get('lease_type', 0) == 2 and record.get('prefix_len', 128) <= 64:
@ -118,7 +122,7 @@ if __name__ == '__main__':
if (prefix not in prefixes or prefixes[prefix].get('hwaddr') != record.get('hwaddr')) \
and record.get('expire', 0) > time.time():
prefixes[prefix] = record
ll_addr = ndp.get(record.get('hwaddr'))
ll_addr = hostwatch.get(record.get('hwaddr'))
# lazy drop
subprocess.run(['/sbin/route', 'delete', '-inet6', prefix], capture_output=True)
if record.get('valid_lifetime', 0) > 0:

View File

@ -539,7 +539,8 @@ function console_configure_dhcpd($version = 4)
}
if ($version === 6) {
$config['dnsmasq']['dhcp'] = ['enable_ra' => '1'];
config_read_array('dnsmasq', 'dhcp');
$config['dnsmasq']['dhcp']['enable_ra'] = '1';
}
}

View File

@ -25,7 +25,7 @@ type:script_output
message:get host discovery daemon status
[dump_full]
command:/usr/local/opnsense/scripts/interfaces/list_hosts.py -v
command:/usr/local/opnsense/scripts/interfaces/list_hosts.py -v -n
parameters:
errors:no
type:script_output

View File

@ -30,6 +30,15 @@
flex-wrap: wrap;
}
.bootgrid-footer-commands {
padding-left: 8px;
padding-right: 15px;
}
.dropdown-menu .dropdown-search {
padding: 6px 10px;
}
@media (max-width: 1024px) {
.bootgrid-header .actionBar {
flex-wrap: wrap;

View File

@ -221,11 +221,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
/* move border from footer to last row */
.tabulator .tabulator-footer {
border-top: none;

View File

@ -192,6 +192,10 @@ function setFormData(parent,data) {
targetNode.append($("<optgroup/>").attr('label', group).append(items));
}
}
if (targetNode.parent().hasClass('bootstrap-select')) {
/* if our node is a selectpicker type, refresh after re-populating */
targetNode.selectpicker('refresh');
}
} else if (targetNode.prop("type") === "checkbox") {
// checkbox type
targetNode.prop("checked", node[keypart] != 0);

View File

@ -164,6 +164,8 @@ class UIBootgrid {
deleteSelectedButton: false,
commands: {}, //additional registered commands
virtualDOM: false,
selection: true,
multiSelect: true,
stickySelect: false,
rowSelect: false,
triggerEditFor: null,
@ -248,11 +250,15 @@ class UIBootgrid {
this.compatOptions['selectableRows'] = 1;
if (bootGridOptions?.multiSelect ?? true) {
this.compatOptions['selectableRows'] = true;
} else {
this.options.multiSelect = false;
}
// TODO rowSelect toggle (currently not support by tabulator)
this.options.rowSelect = bootGridOptions?.rowSelect ?? false;
} else {
this.options.selection = false;
this.options.multiSelect = false;
// remove checkbox select column
this.compatOptions['rowHeader'] = null;
}
@ -1018,6 +1024,17 @@ class UIBootgrid {
this.$element.before($(this._getHeader()));
// column selection dropdown search bar
const $menu = $(`#${this.id}-columnselect-items`);
$menu.on("input", ".columnsearch", function () {
var q = $.trim($(this).val()).toLowerCase();
$menu.children("li").not(".columnsearch-li, .divider").each(function () {
var text = $(this).text().trim().toLowerCase();
$(this).toggle(text.indexOf(q) !== -1);
});
});
// search functionality
$(`#${this.id}-search-field`).val(this.searchPhrase).on("keyup", (e) => {
this._search($(`#${this.id}-search-field`).val(), e);
@ -1124,8 +1141,8 @@ class UIBootgrid {
}
const title = typeof command?.title === "function"
? `title=${command?.title()}`
: `title=${command?.title}` ?? '';
? `title="${command?.title()}"`
: `title="${command?.title}"` ?? '';
const $element = $(`
<button type="button" class="btn btn-xs ${key === 'add' ? 'btn-primary' : 'btn-default'} command-${key} bootgrid-tooltip" ${title}>
@ -1133,7 +1150,7 @@ class UIBootgrid {
</button>
`);
if (key === 'add' || key === 'delete-selected') {
if (command?.primary) {
$commandContainer.append($element);
} else {
$element.appendTo($footerSecondary);
@ -1214,7 +1231,12 @@ class UIBootgrid {
<span class="dropdown-text"><span class="icon fa-solid fa-list"></span></span>
<span class="caret"></span>
</button>
<ul id="${this.id}-columnselect-items" class="dropdown-menu pull-right" role="menu"></ul>
<ul id="${this.id}-columnselect-items" class="dropdown-menu pull-right" role="menu">
<li class="dropdown-search columnsearch-li">
<input type="text" class="form-control input-sm columnsearch" placeholder="${this._translate('searchColumns')}"/>
</li>
<li class="divider"></li>
</ul>
</div>
</div>
</div>
@ -1529,6 +1551,8 @@ class UIBootgrid {
* - requires: an array of strings marking which this.crud properties are required
* - sequence: order of commands rendering
* - footer: true|false whether this command should be rendered in the table footer
* - primary: true|false only if footer: true, whether this command should be rendered as part
* of the primary button container (intended for primary CRUD actions)
* - classname: required. icon class added to the span inside the button element
* - filter: a function that returns true or false determining if the command should be rendered.
* the cell object is passed in only if footer: false
@ -1545,6 +1569,7 @@ class UIBootgrid {
classname: 'fa fa-plus fa-fw',
sequence: 100,
footer: true,
primary: true,
title: this._translate('add')
},
"edit": {
@ -1587,12 +1612,37 @@ class UIBootgrid {
}
}
},
"enable-selected": {
method: this.command_toggle_selected.bind(this, true),
requires: ['toggle'],
classname: 'fa fa-fw fa-check-square-o',
sequence: 200,
filter: () => {
return this.options.selection && this.options.multiSelect && !this.options.stickySelect;
},
footer: true,
primary: true,
title: this._translate('enableSelected'),
},
"disable-selected": {
method: this.command_toggle_selected.bind(this, false),
requires: ['toggle'],
classname: 'fa fa-fw fa-square-o',
sequence: 300,
filter: () => {
return this.options.selection && this.options.multiSelect && !this.options.stickySelect;
},
footer: true,
primary: true,
title: this._translate('disableSelected'),
},
"delete-selected": {
method: this.command_delete_selected.bind(this),
requires: ['del'],
classname: 'fa fa-trash-o fa-fw',
sequence: 100,
sequence: 400,
footer: true,
primary: true,
title: this._translate('deleteSelected')
}
};
@ -1648,8 +1698,8 @@ class UIBootgrid {
if (has_option) {
let title = typeof command?.title === "function"
? `title=${command?.title(cell)}`
: `title=${command?.title}` ?? '';
? `title="${command?.title(cell)}"`
: `title="${command?.title}"` ?? '';
html.push(`
<button type="button"
@ -1743,7 +1793,6 @@ class UIBootgrid {
mapDataToFormUI(urlMap, server_params).done((payload) => {
// update selectors
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
// clear validation errors (if any)
clearFormValidation('frm_' + editDlg);
let target = $('#' + editDlg);
@ -2047,6 +2096,22 @@ class UIBootgrid {
});
}
command_toggle_selected(enable, event) {
event.stopPropagation();
const rows = this.table.getSelectedData();
if (rows.length > 0) {
const deferreds = [];
rows.forEach((row) => {
const uuid = row[this.options.datakey];
deferreds.push(ajaxCall(`${this.crud['toggle']}${uuid}/${enable ? "1" : "0"}`, {}, null));
})
$.when.apply(null, deferreds).done(() => {
this._reload(true);
this.showSaveAlert(event);
});
}
}
_debounce(f, delay = 50, ensure = true) {
// debounce to prevent a flood of calls in a short time
let lastCall = Number.NEGATIVE_INFINITY;

View File

@ -985,20 +985,21 @@ $.fn.replaceInputWithSelector = function (data, multiple=false) {
});
$this_input.attr('id', $(this).attr('id'));
$this_input.change(function(){
let selopt = multiple ? $(this).val().split(',') : [$(this).val()];
let selopt = [];
/* skim given options for valid ones in our list, so we can safely set them and identify manual input*/
let givenopts = multiple ? $(this).val().split(',') : [$(this).val()];
$this_select.find('option').each(function(){
if (selopt.includes($(this).val())) {
selopt.splice(selopt.indexOf($(this).val()), 1)
$(this).attr('selected', 'selected');
} else {
$(this).prop('selected', false);
if (givenopts.includes($(this).val())) {
selopt.push($(this).val());
}
});
if (selopt.length == 0) {
$this_input.hide(); /* items not in selector, show input */
} else {
/* none of the options are selected, so we need to feed empty_select_token here and show manual input*/
$this_select.val(multiple ? [empty_select_token]: empty_select_token);
$this_input.show();
$this_select.val(empty_select_token);
} else {
$this_select.val(multiple ? selopt : selopt[0]);
$this_input.hide();
}
$this_select.selectpicker('refresh');
});

View File

@ -225,11 +225,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
.tabulator-tableholder::after {
content: '';
display: block;

View File

@ -225,11 +225,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
.tabulator-tableholder::after {
content: "";
display: block;

View File

@ -224,11 +224,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
.tabulator-tableholder::after {
content: '';
display: block;

View File

@ -239,11 +239,6 @@
padding-right: 20px;
}
.bootgrid-footer-commands {
width: 90px;
padding-left: 8px;
}
.tabulator-tableholder::after {
content: "";
display: block;

View File

@ -148,12 +148,12 @@ function get_all_services($name = '', $id = '')
}
$jflags = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_INVALID_UTF8_IGNORE;
$opts = getopt('46cDdfghIimrqSsvXx', [], $optind);
$opts = getopt('46cDdfghIiMmrqSsvXx', [], $optind);
$args = array_slice($argv, $optind);
$ret = 0;
if (isset($opts['h'])) {
echo "Usage: pluginctl [-4|-6|-c|-D|-d|-f|-g|-h|-I|-i|-m|-r|-q|-S|-s|-v|-X|-x] ...\n\n";
echo "Usage: pluginctl [-4|-6|-c|-D|-d|-f|-g|-h|-I|-i|-M|-m|-r|-q|-S|-s|-v|-X|-x] ...\n\n";
echo "\t-4 IPv4 address mode, return primary address of interface\n";
echo "\t-6 IPv4 address mode, return primary address of interface\n";
echo "\t-c configure mode (default), executes plugin [_configure] hook\n";
@ -164,7 +164,8 @@ if (isset($opts['h'])) {
echo "\t-h show this help text and exit\n";
echo "\t-I information mode, lists registered device statistics\n";
echo "\t-i invoke dynamic interface registration\n";
echo "\t-m invoke model migrations\n";
echo "\t-M invoke model migrations in quiet mode\n";
echo "\t-m invoke model migrations in verbose mode\n";
echo "\t-q quiet switch for configure mode\n";
echo "\t-r run mode (e.g. command)\n";
echo "\t-S service metadata dump\n";
@ -300,8 +301,10 @@ if (isset($opts['h'])) {
$payload = new ArrayObject();
}
echo json_encode($payload, $jflags) . PHP_EOL;
} elseif (isset($opts['m'])) {
} elseif (isset($opts['M'])) {
pass_safe('/usr/local/opnsense/mvc/script/run_migrations.php', [], $ret);
} elseif (isset($opts['m'])) {
pass_safe('/usr/local/opnsense/mvc/script/run_migrations.php -v', [], $ret);
} elseif (isset($opts['v'])) {
pass_safe('/usr/local/opnsense/mvc/script/run_validations.php', [], $ret);
} elseif (empty($args[0])) {

View File

@ -961,7 +961,7 @@ include("head.inc");
<td><a id="help_for_interface" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Interface");?></td>
<td>
<?php if (!empty($pconfig['floating'])): ?>
<select name="interface[]" id="interface" title="Select interfaces..." multiple="multiple" class="selectpicker" data-live-search="true" data-size="5" tabindex="2">
<select name="interface[]" id="interface" title="<?= html_safe(gettext('Select interfaces...')) ?>" multiple="multiple" class="selectpicker" data-live-search="true" data-size="5" tabindex="2">
<?php else: ?>
<select name="interface" id="interface" class="selectpicker" data-live-search="true" data-size="5">
<?php endif ?>

View File

@ -291,7 +291,7 @@ include("head.inc");
<tr>
<td><a id="help_for_interface" href="#" class="showhelp"><i class="fa fa-info-circle"></i></a> <?=gettext("Interface");?></td>
<td>
<select name="interface[]" title="<?=gettext("Select interfaces...");?>" multiple="multiple" class="selectpicker" data-live-search="true" data-size="5" <?=!empty($pconfig['associated-rule-id']) ? "disabled" : "";?>>
<select name="interface[]" title="<?= html_safe(gettext('Select interfaces...')) ?>" multiple="multiple" class="selectpicker" data-live-search="true" data-size="5">
<?php
foreach (legacy_config_get_interfaces(array("enable" => true)) as $iface => $ifaceInfo): ?>
<option value="<?=$iface;?>"

View File

@ -690,10 +690,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
}
}
if (isset($config['dhcpd']) && isset($config['dhcpd'][$if]['enable']) && !preg_match('/^staticv4/', $pconfig['type'])) {
if (isset($config['dhcpd'][$if]['enable']) && !preg_match('/^staticv4/', $pconfig['type'])) {
$input_errors[] = gettext("The DHCP Server is active on this interface and it can be used only with a static IP configuration. Please disable the DHCP Server service on this interface first, then change the interface configuration.");
}
if (isset($config['dhcpdv6']) && isset($config['dhcpdv6'][$if]['enable']) && !preg_match('/^staticv6/', $pconfig['type6']) && !isset($pconfig['dhcpd6track6allowoverride'])) {
if (isset($config['dhcpdv6'][$if]['enable']) && !preg_match('/^staticv6/', $pconfig['type6']) && !isset($pconfig['dhcpd6track6allowoverride']) && $config['dhcpdv6'][$if]['enable'] != '-1') {
$input_errors[] = gettext("The DHCPv6 Server is active on this interface and it can be used only with a static IPv6 configuration. Please disable the DHCPv6 Server service on this interface first, then change the interface configuration.");
}