From 727967ed6dd017811f8e06e3d43204c771a396e2 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Thu, 24 Apr 2025 16:23:32 +0200 Subject: [PATCH] Services: ISC DHCPv6 - show "tracking" interfaces when enabled and offer an explicit disable (#8576) * Services: ISC DHCPv6 - show "tracking" interfaces when enabled an offer an explicit disable option for the service in question so someone could use dnsmasq or kea instead. To avoid large changes, we opt for a minimal set here. In services_dhcpv6.php, we add a separate form and handler in case tracking (without dhcpd6track6allowoverride) is set, which either flushes the unused isc-dhcpv6 server configuration when enabled (default) or writes a small section only including ['enabled' => -1]. For visibility, we show the calculated range as would be set by dhcpd_dhcp6_configure() when tracking is used. The backend code then double checks the services which er explicitly disabled (-1) and skip processing for these (not enabled). In order to make people aware of the fact that an isc-dhcpv6 server could be running, make sure the menu system also reflects reality. Since router advertisements are stored within the same container and will need a toggle as well, keep the value of ramode so we have a way to intervene in a similar way as for dhcpv6. One small side affect of this commit is that it will show "Services: Router Advertisements" for the tracking interface, which we need to implement later. One of the building blocks for: https://github.com/opnsense/core/issues/8528 * Update src/www/services_dhcpv6.php Co-authored-by: Franco Fichtner * Services: Router Advertisements: show "tracking" interfaces when enabled an offer an explicit disable option for the service in question so someone could use dnsmasq instead. More or less the same construction as added for dhcpv6, using the ramode field to switch between types (disabled or assisted). While here, also bugfix fieldname in services_dhcpv6.php also for https://github.com/opnsense/core/issues/8528 --------- Co-authored-by: Franco Fichtner --- src/etc/inc/plugins.inc.d/dhcpd.inc | 21 ++- src/etc/inc/plugins.inc.d/radvd.inc | 12 +- .../models/OPNsense/Base/Menu/MenuSystem.php | 2 +- src/www/services_dhcpv6.php | 129 ++++++++++++++++-- src/www/services_router_advertisements.php | 116 ++++++++++++++-- 5 files changed, 251 insertions(+), 29 deletions(-) diff --git a/src/etc/inc/plugins.inc.d/dhcpd.inc b/src/etc/inc/plugins.inc.d/dhcpd.inc index 991c04934..0d2e6fee6 100644 --- a/src/etc/inc/plugins.inc.d/dhcpd.inc +++ b/src/etc/inc/plugins.inc.d/dhcpd.inc @@ -47,16 +47,23 @@ function dhcpd_run() function dhcpd_dhcpv6_enabled() { global $config; + $explicit_off = []; /* handle manually configured DHCP6 server settings first */ foreach (config_read_array('dhcpdv6') as $dhcpv6if => $dhcpv6ifconf) { if (isset($config['interfaces'][$dhcpv6if]['enable']) && isset($dhcpv6ifconf['enable'])) { - return true; + if ($dhcpv6ifconf['enable'] == '-1') { + $explicit_off[] = $dhcpv6if; + } else { + return true; + } } } - /* handle DHCP-PD prefixes and 6RD dynamic interfaces */ - foreach (legacy_config_get_interfaces(array('virtual' => false)) as $ifcfg) { + foreach (legacy_config_get_interfaces(array('virtual' => false)) as $ifnm => $ifcfg) { + if (in_array($ifnm, $explicit_off)) { + continue; + } if (isset($ifcfg['enable']) && !empty($ifcfg['track6-interface']) && !isset($ifcfg['dhcpd6track6allowoverride'])) { return true; } @@ -864,8 +871,12 @@ function dhcpd_dhcp6_configure($verbose = false, $blacklist = []) if (!isset($config['interfaces'][$ifname]['dhcpd6track6allowoverride'])) { /* mock a real server */ - $dhcpdv6cfg[$ifname] = array(); - $dhcpdv6cfg[$ifname]['enable'] = true; + if (!empty($dhcpdv6cfg[$ifname]) && $dhcpdv6cfg[$ifname]['enable'] == '-1') { + /* tracking, but dhcpv6 disabled */ + $dhcpdv6cfg[$ifname] = []; + } else { + $dhcpdv6cfg[$ifname] = ['enable' => true]; + } /* fixed range */ $ifcfgipv6arr[7] = '1000'; diff --git a/src/etc/inc/plugins.inc.d/radvd.inc b/src/etc/inc/plugins.inc.d/radvd.inc index 2de8f8b5a..f241a915d 100644 --- a/src/etc/inc/plugins.inc.d/radvd.inc +++ b/src/etc/inc/plugins.inc.d/radvd.inc @@ -40,16 +40,21 @@ function radvd_configure() function radvd_enabled() { global $config; + $explicit_off = []; /* handle manually configured DHCP6 server settings first */ foreach (config_read_array('dhcpdv6') as $dhcpv6if => $dhcpv6ifconf) { if (isset($config['interfaces'][$dhcpv6if]['enable']) && isset($dhcpv6ifconf['ramode']) && $dhcpv6ifconf['ramode'] != 'disabled') { return true; + } elseif (isset($dhcpv6ifconf['ramode']) && $dhcpv6ifconf['ramode'] == 'disabled') { + $explicit_off[] = $dhcpv6if; } } - /* handle DHCP-PD prefixes and 6RD dynamic interfaces */ - foreach (legacy_config_get_interfaces(array('virtual' => false)) as $ifcfg) { + foreach (legacy_config_get_interfaces(array('virtual' => false)) as $ifnm => $ifcfg) { + if (in_array($ifnm, $explicit_off)) { + continue; + } if (isset($ifcfg['enable']) && !empty($ifcfg['track6-interface']) && !isset($ifcfg['dhcpd6track6allowoverride'])) { return true; } @@ -349,6 +354,9 @@ function radvd_configure_do($verbose = false, $blacklist = []) } elseif (isset($blacklist[$if])) { $radvdconf .= "# Skipping blacklisted interface {$if}\n"; continue; + } elseif (!empty($config['dhcpdv6'][$if]) && !empty($config['dhcpdv6'][$if]['ramode']) && $config['dhcpdv6'][$if]['ramode'] == 'disabled') { + $radvdconf .= "# Skipping explicit disabled interface {$if}\n"; + continue; } $trackif = $config['interfaces'][$if]['track6-interface']; diff --git a/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php index 32ba718fc..7973e95c2 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php +++ b/src/opnsense/mvc/app/models/OPNsense/Base/Menu/MenuSystem.php @@ -265,7 +265,7 @@ class MenuSystem ) { $iftargets['dhcp4'][$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); } - if (!empty(filter_var($node->ipaddrv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) || !empty($node->dhcpd6track6allowoverride)) { + if (!empty(filter_var($node->ipaddrv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) || !empty($node->{'track6-interface'})) { $iftargets['dhcp6'][$key] = !empty($node->descr) ? (string)$node->descr : strtoupper($key); } } diff --git a/src/www/services_dhcpv6.php b/src/www/services_dhcpv6.php index c25a988fd..3ed71f2dd 100644 --- a/src/www/services_dhcpv6.php +++ b/src/www/services_dhcpv6.php @@ -1,7 +1,7 @@ * Copyright (C) 2010 Seth Mos * All rights reserved. @@ -34,6 +34,104 @@ require_once("system.inc"); require_once("interfaces.inc"); require_once("plugins.inc.d/dhcpd.inc"); + +function show_track6_form($if) +{ + global $config; + $service_hook = 'dhcpd6'; + include("head.inc"); + include("fbegin.inc"); + $enable_label = gettext("Enable"); + $range_label = gettext("Range"); + $enable_descr = sprintf(gettext("Enable DHCPv6 server on " . "%s " ."interface"),!empty($config['interfaces'][$if]['descr']) ? htmlspecialchars($config['interfaces'][$if]['descr']) : strtoupper($if)); + $enable_input = 'checked="checked"'; + $save_btn_text = html_safe(gettext('Save')); + /* calculated "fixed" range */ + list ($ifcfgipv6) = interfaces_primary_address6($if, legacy_interfaces_details()); + $range = ['from' => '?', 'to' => '?']; + if (is_ipaddrv6($ifcfgipv6)) { + $ifcfgipv6 = Net_IPv6::getNetmask($ifcfgipv6, 64); + $ifcfgipv6arr = explode(':', $ifcfgipv6); + $ifcfgipv6arr[7] = '1000'; + $range['from'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); + $ifcfgipv6arr[7] = '2000'; + $range['to'] = Net_IPv6::compress(implode(':', $ifcfgipv6arr)); + } + + if (!empty($config['dhcpdv6']) && !empty($config['dhcpdv6'][$if]) && isset($config['dhcpdv6'][$if]['enable']) && $config['dhcpdv6'][$if]['enable'] == '-1') { + /* disabled */ + $enable_input = ''; + } + + $range_tr = << + $range_label + {$range['from']} - {$range['to']} + + EOD; + + + echo << +
+
+
+
+
+
+ + + + + + + + + $range_tr + + + + + +
+
+
$enable_label + + $enable_descr +
  + + +
+
+
+
+
+
+
+ + EOD; + include("foot.inc"); +} + +function process_track6_form($if) +{ + $dhcpdv6cfg = &config_read_array('dhcpdv6'); + $this_server = []; + if (isset($dhcpdv6cfg[$if]) && isset($dhcpdv6cfg[$if]['ramode'])) { + /* keep ramode for router advertisements so we can use this field to disable the service when in tracking mode */ + $this_server['ramode'] = $dhcpdv6cfg[$if]['ramode']; + } + if (empty($_POST['enable'])) { + $this_server['enable'] = '-1'; + } + $dhcpdv6cfg[$if] = $this_server; + write_config(); + reconfigure_dhcpd(); + filter_configure(); + header(url_safe('Location: /services_dhcpv6.php?if=%s', array($if))); +} + + function reconfigure_dhcpd() { system_resolver_configure(); @@ -41,17 +139,15 @@ function reconfigure_dhcpd() clear_subsystem_dirty('staticmapsv6'); } +$if = null; +$act = null; if ($_SERVER['REQUEST_METHOD'] === 'GET') { // handle identifiers and action if (!empty($_GET['if']) && !empty($config['interfaces'][$_GET['if']]) && isset($config['interfaces'][$_GET['if']]['enable']) && (is_ipaddr($config['interfaces'][$_GET['if']]['ipaddrv6']) || - !empty($config['interfaces'][$_GET['if']]['dhcpd6track6allowoverride']))) { + !empty($config['interfaces'][$_GET['if']]['track6-interface']))) { $if = $_GET['if']; - } else { - /* if no interface is provided this invoke is invalid */ - header(url_safe('Location: /index.php')); - exit; } } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { // handle identifiers and actions @@ -60,11 +156,28 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { } if (!empty($_POST['act'])) { $act = $_POST['act']; - } else { - $act = null; } } +/** + * XXX: In case of tracking, show different form and only handle on/off options. + * this code injection is intended to keep changes as minimal as possible and avoid regressions on existing isc-dhcp6 installs, + * while showing current state for tracking interfaces. + **/ +if ($if === null) { + /* if no interface is provided this invoke is invalid */ + header(url_safe('Location: /index.php')); + exit; +} elseif (!empty($config['interfaces'][$if]['track6-interface']) && !isset($config['interfaces'][$if]['dhcpd6track6allowoverride'])) { + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + show_track6_form($if); + } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + process_track6_form($if); + } + exit; +} +/* default form processing */ + $ifcfgip = $config['interfaces'][$if]['ipaddrv6']; $ifcfgsn = $config['interfaces'][$if]['subnetv6']; diff --git a/src/www/services_router_advertisements.php b/src/www/services_router_advertisements.php index 4ab69c0c5..7c951823d 100644 --- a/src/www/services_router_advertisements.php +++ b/src/www/services_router_advertisements.php @@ -2,7 +2,7 @@ /* * Copyright (C) 2016-2022 Franco Fichtner - * Copyright (C) 2014-2016 Deciso B.V. + * Copyright (C) 2014-2025 Deciso B.V. * Copyright (C) 2003-2004 Manuel Kasper * Copyright (C) 2010 Seth Mos * All rights reserved. @@ -37,6 +37,108 @@ function val_int_in_range($value, $min, $max) { return (((string)(int)$value) == $value) && $value >= $min && $value <= $max; } +function show_track6_form($if) +{ + global $config; + $service_hook = 'radvd'; + include("head.inc"); + include("fbegin.inc"); + + $ra_label = gettext('Router Advertisements'); + $save_btn_text = html_safe(gettext('Save')); + + if (!empty($config['dhcpdv6']) && !empty($config['dhcpdv6'][$if]) && isset($config['dhcpdv6'][$if]['ramode']) && $config['dhcpdv6'][$if]['ramode'] == 'disabled') { + /* disabled */ + $options = "\n"; + $options .= ""; + } else { + $options = "\n"; + $options .= ""; + } + + echo << +
+
+
+
+
+
+ + + + + + + + + + + + + +
+
+
$ra_label + +
  + + +
+
+
+
+
+
+
+ + EOD; + + include("foot.inc"); +} + +function process_track6_form($if) +{ + $dhcpdv6cfg = &config_read_array('dhcpdv6'); + $this_server = []; + if (isset($dhcpdv6cfg[$if]) && isset($dhcpdv6cfg[$if]['enable'])) { + /* keep enable for dhcpv6 so we can use this field to disable the service when in tracking mode */ + $this_server['enable'] = $dhcpdv6cfg[$if]['enable']; + } + if (!empty($_POST['ramode'])) { + $this_server['ramode'] = 'disabled'; + } + $dhcpdv6cfg[$if] = $this_server; + write_config(); + radvd_configure_do(); + header(url_safe('Location: /services_router_advertisements.php?if=%s', array($if))); +} + +$if = null; +if (!empty($_REQUEST['if']) && !empty($config['interfaces'][$_REQUEST['if']])) { + $if = $_REQUEST['if']; +} else { + /* if no interface is provided this invoke is invalid */ + header(url_safe('Location: /index.php')); + exit; +} + +/** + * XXX: In case of tracking, show different form and only handle on/off options. + * this code injection is intended to keep changes as minimal as possible and avoid regressions on existing isc-dhcp6 installs, + * while showing current state for tracking interfaces. + **/ +if (!empty($config['interfaces'][$if]) && !empty($config['interfaces'][$if]['track6-interface']) && !isset($config['interfaces'][$if]['dhcpd6track6allowoverride'])) { + if ($_SERVER['REQUEST_METHOD'] === 'GET') { + show_track6_form($if); + } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { + process_track6_form($if); + } + exit; +} +/* default form processing */ + + $advanced_options = [ 'AdvDefaultLifetime', 'AdvValidLifetime', @@ -50,14 +152,6 @@ $advanced_options = [ ]; if ($_SERVER['REQUEST_METHOD'] === 'GET') { - if (!empty($_GET['if']) && !empty($config['interfaces'][$_GET['if']])) { - $if = $_GET['if']; - } else { - /* if no interface is provided this invoke is invalid */ - header(url_safe('Location: /index.php')); - exit; - } - $pconfig = array(); $config_copy_fieldsnames = array('ramode', 'rapriority', 'rainterface', 'ramininterval', 'ramaxinterval', 'radomainsearchlist'); $config_copy_fieldsnames = array_merge($advanced_options, $config_copy_fieldsnames); @@ -93,10 +187,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { $pconfig['raroutes'] = array(); } } elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (!empty($_POST['if']) && !empty($config['interfaces'][$_POST['if']])) { - $if = $_POST['if']; - } - /* input validation */ $input_errors = array(); $pconfig = $_POST;