diff --git a/plist b/plist index c2b76b18ba..b50e4b1507 100644 --- a/plist +++ b/plist @@ -1083,6 +1083,7 @@ /usr/local/opnsense/mvc/tests/app/library/OPNsense/Firewall/FilterRuleTest/testDirection.conf /usr/local/opnsense/mvc/tests/app/library/OPNsense/Firewall/FilterRuleTest/testIcmp.conf /usr/local/opnsense/mvc/tests/app/library/OPNsense/Firewall/FilterRuleTest/testProtocol.conf +/usr/local/opnsense/mvc/tests/app/library/OPNsense/Interface/IdassocTest.php /usr/local/opnsense/mvc/tests/app/models/OPNsense/ACL/AclConfig/config.xml /usr/local/opnsense/mvc/tests/app/models/OPNsense/ACL/AclTest.php /usr/local/opnsense/mvc/tests/app/models/OPNsense/Base/BaseModel/Migrations/M0_0_1.php diff --git a/src/opnsense/mvc/app/library/OPNsense/Interface/Idassoc.php b/src/opnsense/mvc/app/library/OPNsense/Interface/Idassoc.php index 482112b8e1..30756aae47 100644 --- a/src/opnsense/mvc/app/library/OPNsense/Interface/Idassoc.php +++ b/src/opnsense/mvc/app/library/OPNsense/Interface/Idassoc.php @@ -45,7 +45,7 @@ class Idassoc extends Autoconf $bytes = array_values(unpack('C*', inet_pton($address))); $source_prefix_len = (int)$source_prefix_len; - $prefix_id = hexdec((string)$prefix_id); + $prefix_id = (int)$prefix_id; $id_bits = 64 - $source_prefix_len; for ($i = 0; $i < $id_bits; $i++) { @@ -69,7 +69,7 @@ class Idassoc extends Autoconf private static function calculateUsablePrefixLength($source_prefix_len, $track6_prefix_id, $track6_prefix_range = ''): int { $source_prefix_len = (int)$source_prefix_len; - $track6_prefix_id = hexdec((string)$track6_prefix_id); + $track6_prefix_id = (int)$track6_prefix_id; $track6_prefix_range = (string)$track6_prefix_range !== '' ? (int)$track6_prefix_range : 1; $associated_size = 1 << (64 - $source_prefix_len); diff --git a/src/opnsense/mvc/tests/app/library/OPNsense/Interface/IdassocTest.php b/src/opnsense/mvc/tests/app/library/OPNsense/Interface/IdassocTest.php new file mode 100644 index 0000000000..3de20e42ac --- /dev/null +++ b/src/opnsense/mvc/tests/app/library/OPNsense/Interface/IdassocTest.php @@ -0,0 +1,154 @@ +.track6-interface + * Format: IPv6 CIDR prefix, for example "2001:db8:1234:ab00::/56" + * + * $track6_interface_prefix_len: + * Prefix length extracted from $track6_interface_prefix. + * Format: decimal integer, for example 56 + * + * $track6_prefix_id: + * Configuration value: + * interfaces..track6-prefix-id + * Format: decimal string, for example "0", "10", "16", "255" + * The prefix ID is stored as decimal string in the config, even though the GUI overlays it as hex. + * https://github.com/opnsense/core/blob/fb514217bae6525f36834ebd7279a02ad8626f0f/src/www/interfaces.php#L327 + * + * $track6_prefix_range: + * Configuration value: + * interfaces..track6_prefix_range + * Format: decimal string, interpreted as number of /64 slots, + * for example "1", "2", "4", "16" + * + * $expected: + * Calculated output value consumed by services: + * prefix_on_link or prefix_allocated + * Format: IPv6 CIDR prefix, for example "2001:db8:1234:ab10::/64" + */ +class IdassocTest extends TestCase +{ + private function callPrivateStatic(string $method, array $args = []) + { + $reflection = new \ReflectionClass(Idassoc::class); + $method = $reflection->getMethod($method); + $method->setAccessible(true); + + return $method->invokeArgs(null, $args); + } + + private function assertCalculatedPrefix( + string $track6_interface_prefix, + string $track6_prefix_id, + string $expected + ) { + $this->assertEquals( + $expected, + $this->callPrivateStatic('calculatePrefix', [$track6_interface_prefix, $track6_prefix_id]), + sprintf( + 'track6_interface_prefix=%s track6_prefix_id=%s', + $track6_interface_prefix, + $track6_prefix_id + ) + ); + } + + private function assertUsablePrefixLength( + int $track6_interface_prefix_len, + string $track6_prefix_id, + string $track6_prefix_range, + int $expected + ) { + $this->assertEquals( + $expected, + $this->callPrivateStatic( + 'calculateUsablePrefixLength', + [$track6_interface_prefix_len, $track6_prefix_id, $track6_prefix_range] + ), + sprintf( + 'track6_interface_prefix_len=%s track6_prefix_id=%s track6_prefix_range=%s', + $track6_interface_prefix_len, + $track6_prefix_id, + $track6_prefix_range + ) + ); + } + + public function testPrefixId() + { + $track6_interface_prefix = '2001:db8:1234:ab00::/56'; + + $this->assertCalculatedPrefix($track6_interface_prefix, '0', '2001:db8:1234:ab00::/64'); + $this->assertCalculatedPrefix($track6_interface_prefix, '1', '2001:db8:1234:ab01::/64'); + $this->assertCalculatedPrefix($track6_interface_prefix, '10', '2001:db8:1234:ab0a::/64'); + $this->assertCalculatedPrefix($track6_interface_prefix, '15', '2001:db8:1234:ab0f::/64'); + + // Configuration stores decimal 16; GUI may display this as hexadecimal "10". + $this->assertCalculatedPrefix($track6_interface_prefix, '16', '2001:db8:1234:ab10::/64'); + + $this->assertCalculatedPrefix($track6_interface_prefix, '255', '2001:db8:1234:abff::/64'); + } + + public function testDelegationSize() + { + // /52 leaves 12 bits for the subnet ID until /64. + $this->assertCalculatedPrefix('2001:db8:1234:a000::/52', '16', '2001:db8:1234:a010::/64'); + + // /48 leaves 16 bits for the subnet ID until /64. + $this->assertCalculatedPrefix('2001:db8:1234::/48', '16', '2001:db8:1234:10::/64'); + $this->assertCalculatedPrefix('2001:db8:1234::/48', '48879', '2001:db8:1234:beef::/64'); + } + + public function testPrefixLength() + { + // Empty range or 1 means a single /64 slot. + $this->assertUsablePrefixLength(56, '0', '', 64); + $this->assertUsablePrefixLength(56, '0', '1', 64); + + // Larger ranges can be aggregated when starting at an aligned prefix ID. + $this->assertUsablePrefixLength(56, '0', '2', 63); + $this->assertUsablePrefixLength(56, '0', '4', 62); + $this->assertUsablePrefixLength(56, '0', '16', 60); + + // Configuration stores decimal 16; GUI displays this as hexadecimal 10. + $this->assertUsablePrefixLength(56, '16', '16', 60); + + // Decimal 10 is hexadecimal 0x0a and only aligned to 2 slots. + $this->assertUsablePrefixLength(56, '10', '16', 63); + } +}