Project

General

Profile

Feature #16822 » pfsense-openvpn-dhcp6pd.patch

Patch - Dan Mahoney, 05/02/2026 08:57 PM

View differences:

etc/inc/openvpn.inc 2026-05-02 20:17:54.664708000 +0000 → etc/inc/openvpn.inc 2026-05-02 20:38:26.015835975 +0000
1028 1028
	if ($mode == 'server') {
1029 1029

  
1030 1030
		list($ip, $cidr) = openvpn_gen_tunnel_network($settings['tunnel_network']);
1031
		list($ipv6, $prefix) = openvpn_gen_tunnel_network($settings['tunnel_networkv6']);
1031
		if (!empty($settings['tunnel_networkv6_type']) && $settings['tunnel_networkv6_type'] === 'pd6') {
1032
			$pd6_subnet = openvpn_get_dhcp6pd_subnet(
1033
				$settings['tunnel_track6_interface'] ?? 'wan',
1034
				(int)($settings['tunnel_track6_prefix_id'] ?? 0)
1035
			);
1036
			list($ipv6, $prefix) = $pd6_subnet ? explode('/', $pd6_subnet) : ['', ''];
1037
		} else {
1038
			list($ipv6, $prefix) = openvpn_gen_tunnel_network($settings['tunnel_networkv6']);
1039
		}
1032 1040
		$mask = gen_subnet_mask($cidr);
1033 1041

  
1034 1042
		// configure tls modes
......
2419 2427
	return array($ipv6_1, $ipv6_2);
2420 2428
}
2421 2429

  
2430
/**
2431
 * Given a WAN interface (friendly name, e.g. "wan") and a DHCPv6 PD
2432
 * prefix ID integer, return the corresponding /64 subnet carved from the
2433
 * delegated prefix currently on that interface.
2434
 *
2435
 * pfSense stores the delegation size in interfaces/<wan>/dhcp6-ia-pd-len as
2436
 * the number of subnet bits (e.g. 4 for a /60 delegation), NOT the prefix
2437
 * length itself.  So the actual prefix length is (64 - dhcp6-ia-pd-len).
2438
 *
2439
 * Discovers the delegated base prefix by finding an existing track6 LAN
2440
 * interface that tracks the same upstream WAN, reading its live IPv6 address,
2441
 * and masking it down to the delegation prefix length.  The prefix_id is
2442
 * then OR'd into the lower subnet_bits of the 4th 16-bit group.
2443
 *
2444
 * Returns a string like "2001:db8:1:3::/64", or false on failure (logged).
2445
 */
2446
function openvpn_get_dhcp6pd_subnet($wan_iface, $prefix_id) {
2447
	$subnet_bits = (int)config_get_path("interfaces/{$wan_iface}/dhcp6-ia-pd-len", 0);
2448
	if ($subnet_bits < 1 || $subnet_bits > 16) {
2449
		log_error("openvpn_get_dhcp6pd_subnet: invalid or missing dhcp6-ia-pd-len ({$subnet_bits}) for {$wan_iface}");
2450
		return false;
2451
	}
2452

  
2453
	$pd_len = 64 - $subnet_bits;
2454
	$max_id = (1 << $subnet_bits) - 1;
2455
	if ((int)$prefix_id < 0 || (int)$prefix_id > $max_id) {
2456
		log_error("openvpn_get_dhcp6pd_subnet: prefix_id {$prefix_id} out of range 0-{$max_id} for /{$pd_len} delegation on {$wan_iface}");
2457
		return false;
2458
	}
2459

  
2460
	foreach (config_get_path('interfaces', []) as $if => $ifcfg) {
2461
		if (!isset($ifcfg['ipaddrv6']) || $ifcfg['ipaddrv6'] !== 'track6') {
2462
			continue;
2463
		}
2464
		if (!isset($ifcfg['track6-interface']) || $ifcfg['track6-interface'] !== $wan_iface) {
2465
			continue;
2466
		}
2467
		$if_ipv6 = get_interface_ipv6($if);
2468
		if (!is_ipaddrv6($if_ipv6)) {
2469
			continue;
2470
		}
2471
		// Mask down to the /$pd_len delegation base, zeroing subnet+IID bits.
2472
		$base = gen_subnetv6($if_ipv6, $pd_len);
2473
		if (!is_ipaddrv6($base)) {
2474
			continue;
2475
		}
2476
		// Expand to 8 colon-separated 16-bit groups.
2477
		// The prefix_id occupies the lower $subnet_bits of group index 3
2478
		// (bits 49-64).  gen_subnetv6 has already zeroed those bits, so
2479
		// we simply OR in $prefix_id.
2480
		$expanded  = Net_IPv6::uncompress($base, true);
2481
		$groups    = explode(':', $expanded);
2482
		$groups[3] = sprintf('%04x', hexdec($groups[3]) | (int)$prefix_id);
2483
		$subnet    = Net_IPv6::compress(implode(':', $groups)) . '/64';
2484
		return $subnet;
2485
	}
2486

  
2487
	log_error("openvpn_get_dhcp6pd_subnet: no active track6 interface found on {$wan_iface}");
2488
	return false;
2489
}
2490

  
2491
/**
2492
 * Build a list of WAN interfaces that have DHCPv6 prefix delegation configured.
2493
 * Used to populate the UI dropdown for pd6 tunnel network type selection.
2494
 * Returns array of [ 'friendly_name' => 'Description (/<len> PD)' ].
2495
 */
2496
function openvpn_build_dhcp6pd_wan_list() {
2497
	$list = [];
2498
	foreach (config_get_path('interfaces', []) as $if => $ifcfg) {
2499
		if (!isset($ifcfg['ipaddrv6']) || $ifcfg['ipaddrv6'] !== 'dhcp6') {
2500
			continue;
2501
		}
2502
		$subnet_bits = (int)($ifcfg['dhcp6-ia-pd-len'] ?? 0);
2503
		if ($subnet_bits < 1 || $subnet_bits > 16) {
2504
			continue;
2505
		}
2506
		$pd_len = 64 - $subnet_bits;
2507
		$descr  = !empty($ifcfg['descr']) ? $ifcfg['descr'] : strtoupper($if);
2508
		$list[$if] = "{$descr} (/{$pd_len} PD)";
2509
	}
2510
	return $list;
2511
}
2512

  
2513
/**
2514
 * Resync all OpenVPN instances that use DHCPv6 PD tracking on $wan_iface,
2515
 * regardless of which interface the instance is bound to.  Called from
2516
 * rc.newwanipv6 to catch instances bound to "any" or a LAN interface that
2517
 * openvpn_resync_all() would not otherwise restart.
2518
 */
2519
function openvpn_resync_dhcp6pd_wan($wan_iface) {
2520
	foreach (array("server", "client") as $type) {
2521
		foreach (config_get_path("openvpn/openvpn-{$type}", []) as $settings) {
2522
			if (isset($settings['disable'])) {
2523
				continue;
2524
			}
2525
			if (($settings['tunnel_networkv6_type'] ?? '') !== 'pd6') {
2526
				continue;
2527
			}
2528
			if (($settings['tunnel_track6_interface'] ?? '') !== $wan_iface) {
2529
				continue;
2530
			}
2531
			// Skip if already handled by openvpn_resync_all (bound to this WAN).
2532
			if ($settings['interface'] === $wan_iface) {
2533
				continue;
2534
			}
2535
			openvpn_resync($type, $settings);
2536
		}
2537
	}
2538
}
2539

  
2422 2540
function openvpn_clear_route($mode, $settings) {
2423 2541
	if (empty($settings['tunnel_network'])) {
2424 2542
		return;
2425
-- a/usr/local/www/vpn_openvpn_server.php	2026-05-02 20:17:54.655450000 +0000
2543
++ b/usr/local/www/vpn_openvpn_server.php	2026-05-02 20:39:07.864719743 +0000
......
174 174

  
175 175
		$pconfig['tunnel_network'] = $this_server_config['tunnel_network'];
176 176
		$pconfig['tunnel_networkv6'] = $this_server_config['tunnel_networkv6'];
177
		$pconfig['tunnel_networkv6_type']   = $this_server_config['tunnel_networkv6_type']   ?? 'static';
178
		$pconfig['tunnel_track6_interface'] = $this_server_config['tunnel_track6_interface'] ?? '';
179
		$pconfig['tunnel_track6_prefix_id'] = $this_server_config['tunnel_track6_prefix_id'] ?? 0;
177 180

  
178 181
		$pconfig['remote_network'] = $this_server_config['remote_network'];
179 182
		$pconfig['remote_networkv6'] = $this_server_config['remote_networkv6'];
......
449 452
		$input_errors[] = gettext("The submitted IPv4 Tunnel Network is already in use.");
450 453
	}
451 454

  
452
	if (!empty($pconfig['tunnel_networkv6']) && !openvpn_validate_tunnel_network($pconfig['tunnel_networkv6'], 'ipv6')) {
453
		$input_errors[] = gettext("The field 'IPv6 Tunnel Network' must contain a valid IPv6 prefix or an alias with a single IPv6 prefix.");
454
	}
455
	if (($pconfig['tunnel_networkv6_type'] ?? 'static') === 'static') {
456
		if (!empty($pconfig['tunnel_networkv6']) && !openvpn_validate_tunnel_network($pconfig['tunnel_networkv6'], 'ipv6')) {
457
			$input_errors[] = gettext("The field 'IPv6 Tunnel Network' must contain a valid IPv6 prefix or an alias with a single IPv6 prefix.");
458
		}
455 459

  
456
	if (!empty($pconfig['tunnel_networkv6']) &&
457
	    (!isset($this_server_config) ||
458
	    ($this_server_config['tunnel_networkv6'] != $pconfig['tunnel_networkv6'])) &&
459
	    openvpn_is_tunnel_network_in_use($pconfig['tunnel_networkv6'])) {
460
		$input_errors[] = gettext("The submitted IPv6 Tunnel Network is already in use.");
460
		if (!empty($pconfig['tunnel_networkv6']) &&
461
		    (!isset($this_server_config) ||
462
		    ($this_server_config['tunnel_networkv6'] != $pconfig['tunnel_networkv6'])) &&
463
		    openvpn_is_tunnel_network_in_use($pconfig['tunnel_networkv6'])) {
464
			$input_errors[] = gettext("The submitted IPv6 Tunnel Network is already in use.");
465
		}
466
	} else {
467
		// pd6 type: validate WAN interface and prefix_id.
468
		if (empty($pconfig['tunnel_track6_interface'])) {
469
			$input_errors[] = gettext("IPv6 Tunnel Network: a WAN interface must be selected for DHCPv6 PD tracking.");
470
		} else {
471
			$subnet_bits = (int)config_get_path("interfaces/{$pconfig['tunnel_track6_interface']}/dhcp6-ia-pd-len", 0);
472
			if ($subnet_bits < 1 || $subnet_bits > 16) {
473
				$input_errors[] = gettext("The selected interface does not have a valid DHCPv6 prefix delegation configured.");
474
			} else {
475
				$max_id    = (1 << $subnet_bits) - 1;
476
				$pd_len    = 64 - $subnet_bits;
477
				$prefix_id = (int)($pconfig['tunnel_track6_prefix_id'] ?? 0);
478
				if ($prefix_id < 0 || $prefix_id > $max_id) {
479
					$input_errors[] = sprintf(gettext(
480
						"IPv6 Prefix ID must be between 0 and %d (0x%x) for a /%d delegation."
481
					), $max_id, $max_id, $pd_len);
482
				}
483
			}
484
		}
461 485
	}
462 486

  
463 487
	if ($result = openvpn_validate_cidr($pconfig['remote_network'], 'IPv4 Remote Network', true, "ipv4", true)) {
......
763 787
		foreach (array('', 'v6') as $ntype) {
764 788
			$server["tunnel_network{$ntype}"] = openvpn_tunnel_network_fix($pconfig["tunnel_network{$ntype}"]);
765 789
		}
790
		$server['tunnel_networkv6_type']   = $pconfig['tunnel_networkv6_type']   ?? 'static';
791
		$server['tunnel_track6_interface'] = $pconfig['tunnel_track6_interface'] ?? '';
792
		$server['tunnel_track6_prefix_id'] = (int)($pconfig['tunnel_track6_prefix_id'] ?? 0);
766 793
		$server['remote_network'] = $pconfig['remote_network'];
767 794
		$server['remote_networkv6'] = $pconfig['remote_networkv6'];
768 795
		$server['gwredir'] = $pconfig['gwredir'];
......
1294 1321
			'including DCO, Exit Notify, and Inactive.',
1295 1322
			'<br/>');
1296 1323

  
1324
	$section->addInput(new Form_Select(
1325
		'tunnel_networkv6_type',
1326
		'IPv6 Tunnel Network',
1327
		$pconfig['tunnel_networkv6_type'] ?? 'static',
1328
		[
1329
			'static' => gettext('Static (enter prefix below)'),
1330
			'pd6'    => gettext('Track Interface (DHCPv6 PD)'),
1331
		]
1332
	))->setHelp('Select "Static" to enter a fixed IPv6 prefix, or "Track Interface" to ' .
1333
			'automatically derive a /64 from a DHCPv6 delegated prefix on a WAN interface. ' .
1334
			'The tracked prefix is updated automatically when the delegated prefix changes.');
1335

  
1297 1336
	$section->addInput(new Form_Input(
1298 1337
		'tunnel_networkv6',
1299
		'IPv6 Tunnel Network',
1338
		'IPv6 Tunnel Network Prefix',
1300 1339
		'text',
1301 1340
		$pconfig['tunnel_networkv6']
1302
	))->setHelp('This is the IPv6 virtual network or network type alias with a single entry used for private ' .
1303
			'communications between this server and client hosts expressed using CIDR notation ' .
1304
			'(e.g. fe80::/64). The ::1 address in the network will be assigned to the server ' .
1305
			'virtual interface. The remaining addresses will be assigned to connecting clients.');
1341
	))->setHelp('IPv6 virtual network in CIDR notation (e.g. fe80::/64). ' .
1342
			'The ::1 address will be assigned to the server virtual interface. ' .
1343
			'Only used when type is Static.');
1344

  
1345
	$section->addInput(new Form_Select(
1346
		'tunnel_track6_interface',
1347
		'DHCPv6 PD WAN Interface',
1348
		$pconfig['tunnel_track6_interface'] ?? '',
1349
		openvpn_build_dhcp6pd_wan_list()
1350
	))->setHelp('WAN interface with DHCPv6 prefix delegation. Only interfaces with a ' .
1351
			'configured delegation prefix length are shown.');
1352

  
1353
	$section->addInput(new Form_Input(
1354
		'tunnel_track6_prefix_id',
1355
		'DHCPv6 PD Prefix ID',
1356
		'number',
1357
		$pconfig['tunnel_track6_prefix_id'] ?? 0
1358
	))->setHelp('Numeric subnet ID within the delegated prefix (0 = first /64, 1 = second, etc.). ' .
1359
			'For a /60 delegation, valid values are 0-15. For /56, 0-255. For /48, 0-65535. ' .
1360
			'Prefix ID 0 is typically already used by LAN.');
1306 1361

  
1307 1362
	$section->addInput(new Form_Checkbox(
1308 1363
		'serverbridge_dhcp',
......
2381 2436
		}
2382 2437
	}
2383 2438

  
2439
	function tunnel_networkv6_type_change() {
2440
		var pd6 = ($('#tunnel_networkv6_type').val() === 'pd6');
2441
		hideInput('tunnel_networkv6',        pd6);
2442
		hideInput('tunnel_track6_interface', !pd6);
2443
		hideInput('tunnel_track6_prefix_id', !pd6);
2444
	}
2445

  
2384 2446
	function ping_method_change() {
2385 2447
		pvalue = $('#ping_method').val();
2386 2448

  
......
2526 2588
		allow_compression_change();
2527 2589
	});
2528 2590

  
2591
	$('#tunnel_networkv6_type').change(function () {
2592
		tunnel_networkv6_type_change();
2593
	});
2594

  
2529 2595
	function updateCipher(mem) {
2530 2596
		var found = false;
2531 2597
		var ciphers_all = <?= json_encode($openvpn_all_data_ciphers) ?>;
......
2589 2655
	ocspcheck_change();
2590 2656
	allow_compression_change();
2591 2657
	duplicate_cn_change();
2658
	tunnel_networkv6_type_change();
2592 2659
});
2593 2660
//]]>
2594 2661
</script>
2595
-- a/etc/rc.newwanipv6	2026-05-02 20:17:54.710124000 +0000
2662
++ b/etc/rc.newwanipv6	2026-05-02 20:39:13.363539090 +0000
......
263 263
/* start OpenVPN server & clients */
264 264
if (substr($interface_real, 0, 4) != "ovpn") {
265 265
	openvpn_resync_all($interface, 'inet6');
266
	/* Also resync any OpenVPN instances using DHCPv6 PD tracking on this WAN
267
	 * that are NOT bound directly to it (e.g. bound to "any" or a LAN iface). */
268
	openvpn_resync_dhcp6pd_wan($interface);
266 269
}
267 270

  
268 271
/* reconfigure GRE/GIF tunnels */
(1-1/2)