diff --git a/xCAT-probe/subcmds/discovery b/xCAT-probe/subcmds/discovery index ebb3d8e5e..b8a82df6c 100755 --- a/xCAT-probe/subcmds/discovery +++ b/xCAT-probe/subcmds/discovery @@ -866,7 +866,7 @@ sub dhcp_dynamic_range_check { } foreach my $subnet4 (@{ $config_json->{Dhcp4}{subnet4} || [] }) { next unless $subnet4->{subnet} && $subnet4->{subnet} =~ m{^([^/]+)/(\d+)$}; - my $mask = prefix_to_netmask($2); + my $mask = xCAT::NetworkUtils::formatNetmask($2, 1, 0); my @ranges; foreach my $pool (@{ $subnet4->{pools} || [] }) { next unless $pool->{pool}; @@ -1036,17 +1036,6 @@ sub compare_ip_value { return 0; } -sub prefix_to_netmask { - my $prefix = shift; - - my $mask_num = 0; - for my $idx (0 .. $prefix - 1) { - $mask_num |= (1 << (31 - $idx)); - } - - return join('.', map { ($mask_num >> (8 * (3 - $_))) & 0xff } 0 .. 3); -} - #------------------------------------------ =head3 diff --git a/xCAT-server/lib/xcat/plugins/dhcp.pm b/xCAT-server/lib/xcat/plugins/dhcp.pm index fa0c1bb1c..da94251ac 100644 --- a/xCAT-server/lib/xcat/plugins/dhcp.pm +++ b/xCAT-server/lib/xcat/plugins/dhcp.pm @@ -2902,7 +2902,7 @@ sub local_ipv4_routes next; } next unless $line =~ /^(\d+(?:\.\d+){3})\/(\d+)\b.*\bdev\s+(\S+)/; - push @routes, [ $1, $3, kea_prefix_to_mask($2), '' ]; + push @routes, [ $1, $3, xCAT::NetworkUtils::formatNetmask( $2, 1, 0 ), '' ]; } } else { my $netstat = kea_command_path('netstat'); @@ -2981,13 +2981,8 @@ sub kea_subnet4_intent if ($gateway && $gateway eq '') { $gateway = xCAT::NetworkUtils->ip_forwarding_enabled() ? $myip : ''; } - if ($gateway) { - my $maskn = unpack("N", inet_aton($mask)); - my $netn = unpack("N", inet_aton($net)); - my $gaten = unpack("N", inet_aton($gateway)); - if (($gaten & $maskn) != ($maskn & $netn)) { - return { error => "Specified gateway $gateway is not valid for $net/$mask, must be on same network" }; - } + if ( $gateway && !xCAT::NetworkUtils::isInSameSubnet( $gateway, $net, $mask, 0 ) ) { + return { error => "Specified gateway $gateway is not valid for $net/$mask, must be on same network" }; } my @option_data; @@ -3004,7 +2999,7 @@ sub kea_subnet4_intent my $domainstring = join(', ', map { $_ eq $domain ? $_ : $_ } grep { $_ } @alldomains); push @option_data, { name => 'domain-search', data => $domainstring } if $domainstring; - my $prefix = kea_mask_to_prefix($mask); + my $prefix = xCAT::NetworkUtils::formatNetmask( $mask, 0, 1 ); my $dynamicrange = $ent ? $ent->{dynamicrange} : undef; if ( $dynamicrange && $ent->{dhcpserver} && xCAT::NetworkUtils->thishostisnot( $ent->{dhcpserver} ) ) { $dynamicrange = undef; @@ -3588,28 +3583,6 @@ sub kea_control_agent_live_enabled return $backend->host_cmds_hook_path() ? 1 : 0; } -sub kea_mask_to_prefix -{ - my ($mask) = @_; - - my $maskn = unpack("N", inet_aton($mask)); - my $bits = 0; - for my $idx (0 .. 31) { - $bits++ if $maskn & (1 << (31 - $idx)); - } - - return $bits; -} - -sub kea_prefix_to_mask -{ - my ($prefix) = @_; - - return '0.0.0.0' unless defined($prefix) && $prefix =~ /^\d+$/ && $prefix > 0 && $prefix <= 32; - my $maskn = (0xffffffff << (32 - $prefix)) & 0xffffffff; - return inet_ntoa(pack("N", $maskn)); -} - sub kea_skip_ipv4_network { my ($net) = @_; diff --git a/xCAT-test/unit/dhcp_kea_plugin_intent.t b/xCAT-test/unit/dhcp_kea_plugin_intent.t index e4f624552..84b68b397 100644 --- a/xCAT-test/unit/dhcp_kea_plugin_intent.t +++ b/xCAT-test/unit/dhcp_kea_plugin_intent.t @@ -7,6 +7,7 @@ use FindBin; use lib "$FindBin::Bin/../../perl-xCAT"; use File::Temp qw(tempdir); +use Socket (); use Test::More; BEGIN { @@ -39,6 +40,34 @@ BEGIN { sub thishostisnot { return 0; } sub ip_forwarding_enabled { return 0; } sub nodeonmynet { return 1; } + sub formatNetmask { + my ( $mask, $orig_type, $new_type ) = @_; + my $mask_number; + + if ( $orig_type == 0 ) { + $mask_number = unpack( 'N', Socket::inet_aton($mask) ); + } elsif ( $orig_type == 1 ) { + $mask_number = ( 2**$mask - 1 ) << ( 32 - $mask ); + } else { + return; + } + + return Socket::inet_ntoa( pack( 'N', $mask_number ) ) if $new_type == 0; + if ( $new_type == 1 ) { + my $binary_mask = unpack( 'B32', pack( 'N', $mask_number ) ); + return $binary_mask =~ tr/1/1/; + } + return; + } + sub isInSameSubnet { + my ( $ip1, $ip2, $mask, $mask_type ) = @_; + return unless $mask_type == 0; + + my $mask_number = unpack( 'N', Socket::inet_aton($mask) ); + my $ip1_number = unpack( 'N', Socket::inet_aton($ip1) ); + my $ip2_number = unpack( 'N', Socket::inet_aton($ip2) ); + return ( $ip1_number & $mask_number ) == ( $ip2_number & $mask_number ); + } $INC{'xCAT/NetworkUtils.pm'} = __FILE__; package xCAT::ServiceNodeUtils; @@ -156,6 +185,40 @@ ok(!xCAT_plugin::dhcp::dhcpd_sysconfig_uses_interface_key('opensuse-tumbleweed') ); } +{ + my $tmpdir = tempdir(CLEANUP => 1); + my $fake_ip = "$tmpdir/ip"; + open(my $ip_fh, '>', $fake_ip) or die "Cannot write fake ip command: $!"; + print {$ip_fh} "#!/bin/sh\n"; + print {$ip_fh} "cat <<'EOF'\n"; + print {$ip_fh} "0.0.0.0/0 dev eth0 proto kernel scope link\n"; + print {$ip_fh} "128.0.0.0/1 dev eth1 proto kernel scope link\n"; + print {$ip_fh} "192.0.2.0/24 dev eth24 proto kernel scope link\n"; + print {$ip_fh} "198.51.100.7/32 dev eth32 proto kernel scope link\n"; + print {$ip_fh} "203.0.113.0/not-a-prefix dev invalid proto kernel scope link\n"; + print {$ip_fh} "EOF\n"; + close($ip_fh); + chmod 0755, $fake_ip; + + no warnings 'redefine'; + local *xCAT_plugin::dhcp::kea_command_path = sub { + my ($command) = @_; + return $fake_ip if $command eq 'ip'; + return; + }; + + is_deeply( + [ xCAT_plugin::dhcp::local_ipv4_routes() ], + [ + [ '0.0.0.0', 'eth0', '0.0.0.0', '' ], + [ '128.0.0.0', 'eth1', '128.0.0.0', '' ], + [ '192.0.2.0', 'eth24', '255.255.255.0', '' ], + [ '198.51.100.7', 'eth32', '255.255.255.255', '' ], + ], + 'local IPv4 route detection converts boundary prefixes and ignores malformed prefixes' + ); +} + { no warnings 'redefine'; local *xCAT_plugin::dhcp::kea_ipv4_routes = sub { @@ -179,6 +242,64 @@ ok(!xCAT_plugin::dhcp::dhcpd_sysconfig_uses_interface_key('opensuse-tumbleweed') is( $intent->{subnets}[0]{subnet}, '10.0.0.0/24', 'rendered subnet comes from local route' ); } +{ + no warnings 'redefine'; + local *xCAT::NetworkUtils::thishostisnot = sub { return 0; }; + + my @prefix_cases = ( + [ '0.0.0.0', '0.0.0.0', 0 ], + [ '128.0.0.0', '128.0.0.0', 1 ], + [ '192.0.2.0', '255.255.255.0', 24 ], + [ '198.51.100.7', '255.255.255.255', 32 ], + ); + + foreach my $case (@prefix_cases) { + my ( $net, $mask, $prefix ) = @$case; + my $nettab = DHCPKeaIntentNetTable->new( + { + %network_entry, + net => $net, + mask => $mask, + dynamicrange => undef, + gateway => undef, + } + ); + my $subnet = xCAT_plugin::dhcp::kea_subnet4_intent( $nettab, $net, $mask, 'eth0', 0, 1, 80 ); + is( $subnet->{subnet}, "$net/$prefix", "$mask renders as prefix $prefix" ); + } +} + +{ + no warnings 'redefine'; + local *xCAT::NetworkUtils::thishostisnot = sub { return 0; }; + + my $same_subnet_table = DHCPKeaIntentNetTable->new( + { + %network_entry, + gateway => '10.0.0.254', + } + ); + my $same_subnet = xCAT_plugin::dhcp::kea_subnet4_intent( + $same_subnet_table, '10.0.0.0', '255.255.255.0', 'eth0', 0, 1, 80 + ); + ok( !$same_subnet->{error}, 'gateway in the subnet remains valid' ); + + my $different_subnet_table = DHCPKeaIntentNetTable->new( + { + %network_entry, + gateway => '192.0.2.1', + } + ); + my $different_subnet = xCAT_plugin::dhcp::kea_subnet4_intent( + $different_subnet_table, '10.0.0.0', '255.255.255.0', 'eth0', 0, 1, 80 + ); + is( + $different_subnet->{error}, + 'Specified gateway 192.0.2.1 is not valid for 10.0.0.0/255.255.255.0, must be on same network', + 'gateway outside the subnet keeps the existing error' + ); +} + { no warnings 'redefine'; local *xCAT_plugin::dhcp::kea_ipv4_routes = sub { diff --git a/xCAT-test/unit/networkutils_netmask.t b/xCAT-test/unit/networkutils_netmask.t new file mode 100644 index 000000000..68efe78ec --- /dev/null +++ b/xCAT-test/unit/networkutils_netmask.t @@ -0,0 +1,57 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use FindBin; +use lib "$FindBin::Bin/../../perl-xCAT"; +use Test::More; + +require xCAT::NetworkUtils; + +my @prefix_cases = ( + [ 0, '0.0.0.0' ], + [ 1, '128.0.0.0' ], + [ 24, '255.255.255.0' ], + [ 32, '255.255.255.255' ], +); + +foreach my $case (@prefix_cases) { + my ( $prefix, $mask ) = @$case; + is( + xCAT::NetworkUtils::formatNetmask( $prefix, 1, 0 ), + $mask, + "prefix $prefix converts to $mask" + ); + is( + xCAT::NetworkUtils::formatNetmask( $mask, 0, 1 ), + $prefix, + "$mask converts to prefix $prefix" + ); +} + +is( + xCAT::NetworkUtils::formatNetmask( undef, 1, 0 ), + undef, + 'undefined netmask input fails cleanly' +); +is( + xCAT::NetworkUtils::formatNetmask( 24, 3, 0 ), + undef, + 'unsupported input format fails cleanly' +); +is( + xCAT::NetworkUtils::formatNetmask( 24, 1, 3 ), + undef, + 'unsupported output format fails cleanly' +); + +ok( + xCAT::NetworkUtils::isInSameSubnet( '10.0.0.254', '10.0.0.0', '255.255.255.0', 0 ), + 'gateway inside an IPv4 subnet matches' +); +ok( + !xCAT::NetworkUtils::isInSameSubnet( '192.0.2.1', '10.0.0.0', '255.255.255.0', 0 ), + 'gateway outside an IPv4 subnet does not match' +); + +done_testing();