From 107f7b14ec4ca2eb266cf100ebae0591f49e94cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Ferr=C3=A3o?= <2031761+viniciusferrao@users.noreply.github.com> Date: Mon, 7 Sep 2026 01:01:34 -0300 Subject: [PATCH] test(xCAT-test): drive detect_dhcpd with tcpdump on PATH Both scripts run for real with PATH holding one directory that carries a tcpdump stand-in, which records the path and arguments it was started with. The plain run stops at the interface step, before any socket or capture. The capture runs inside a private network and mount namespace, where the loopback interface is the only one, its default route keeps the DHCP discover on the host, a tmpfs over /tmp holds the dump file, and /usr/sbin/tcpdump is hidden so the previous guard fails there on every host. Hosts that cannot create the namespace skip that part. --- xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t | 145 +++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t diff --git a/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t new file mode 100644 index 000000000..8122b1d67 --- /dev/null +++ b/xCAT-test/unit/detect_dhcpd_tcpdump_lookup.t @@ -0,0 +1,145 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use Config; +use File::Path qw(make_path); +use File::Spec; +use File::Temp qw(tempdir); +use FindBin; +use Test::More; + +use lib "$FindBin::Bin/../../perl-xCAT"; +use xCAT::CommandUtils; + +# Both detect_dhcpd copies refused to run unless /usr/sbin/tcpdump existed. Debian and Ubuntu +# install tcpdump as /usr/bin/tcpdump, so the rogue-DHCP detector refused to run on every Ubuntu +# management node whether tcpdump was installed or not, and the probe reported its tcpdump check +# as failed. The scripts are driven for real with a tcpdump that PATH alone can reach and that +# records how it was started. Outside the namespace below, a host that also carries +# /usr/sbin/tcpdump lets the previous guard pass as well. + +my $repo = File::Spec->rel2abs( File::Spec->catdir( $FindBin::Bin, '..', '..' ) ); +my $tools = "$repo/xCAT-server/share/xcat/tools/detect_dhcpd"; +my $probe = "$repo/xCAT-probe/subcmds/detect_dhcpd"; +plan skip_all => 'detect_dhcpd not found' unless -f $tools && -f $probe; + +my $perl = $Config{perlpath}; +my $mac = '02:00:5e:00:53:01'; + +# The capture run masks /tmp, so every fixture lives outside it. +my @fixture_dir = ( -d '/var/tmp' && -w '/var/tmp' ) ? ( DIR => '/var/tmp' ) : (); + +# An XCATROOT whose lib/perl and probe/lib/perl are this checkout, so the scripts load the +# libraries they would load on a management node. +my $root = tempdir( @fixture_dir, CLEANUP => 1 ); +make_path("$root/lib/perl", "$root/probe/lib"); +symlink( "$repo/perl-xCAT/xCAT", "$root/lib/perl/xCAT" ) or die "symlink: $!"; +symlink( "$repo/xCAT-probe/lib/perl", "$root/probe/lib/perl" ) or die "symlink: $!"; + +sub write_script { + my ( $path, $body ) = @_; + open( my $fh, '>', $path ) or die "$path: $!"; + print {$fh} "#!/bin/sh\n$body"; + close($fh); + chmod 0755, $path; +} + +# PATH will hold one directory: a tcpdump that records its invocation and the tools the scripts +# pipe through. The plain run gets an ip that answers nothing, so the scripts stop at the +# interface step before they open a socket or fork the capture. +my $marker = File::Spec->catfile( tempdir( @fixture_dir, CLEANUP => 1 ), 'tcpdump.ran' ); +sub fixture_bin { + my (%with) = @_; + my $bindir = tempdir( @fixture_dir, CLEANUP => 1 ); + write_script( "$bindir/tcpdump", qq{printf '%s\\n' "\$0" "\$*" > '$marker'\nexit 0\n} ); + foreach my $tool (qw(awk head grep ps)) { + my $real = xCAT::CommandUtils::find_executable($tool) or next; + symlink( $real, "$bindir/$tool" ) or die "symlink $tool: $!"; + } + if ( $with{real_ip} ) { + symlink( $with{real_ip}, "$bindir/ip" ) or die "symlink ip: $!"; + } else { + write_script( "$bindir/ip", "exit 0\n" ); + } + return $bindir; +} + +sub script_command { + my ( $script, @args ) = @_; + return "$perl -I '$repo/perl-xCAT' -I '$repo/xCAT-probe/lib/perl' '$script' @args"; +} + +sub run_with_path { + my ( $bindir, $script, @args ) = @_; + local $ENV{PATH} = $bindir; + local $ENV{XCATROOT} = $root; + unlink $marker; + return `@{[ script_command( $script, @args ) ]} 2>&1`; +} + +sub recorded_invocation { + open( my $fh, '<', $marker ) or return; + chomp( my @lines = <$fh> ); + close($fh); + return @lines; +} + +my $plain = fixture_bin(); +my $out = run_with_path( $plain, $tools, '-i', 'lo', '-m', $mac, '-t', '1' ); +unlike( $out, qr/install tcpdump/, 'the tool accepts a tcpdump found through PATH' ); +like( $out, qr/IP\/MAC/, '... and gets as far as the interface step' ); +$out = run_with_path( $plain, $probe, '-i', 'lo', '-m', $mac, '-d', '1' ); +unlike( $out, qr/please install 'tcpdump' first/, 'the probe accepts a tcpdump found through PATH' ); +like( $out, qr/IP\/MAC/, '... and gets as far as the interface step' ); + +# The capture itself runs only inside a private network and mount namespace: the loopback +# interface is the only one, its default route keeps the DHCP discover on the host, and a tmpfs +# over /tmp keeps the dump file out of the shared one. The scripts then reach tcpdump as they do +# on a management node. The loopback interface has no Ethernet address, so the MAC is given. +# /usr/sbin/tcpdump is hidden there, so the previous guard fails on every host. +sub isolation { + my %bin = map { $_ => xCAT::CommandUtils::find_executable($_) } qw(unshare mount ip); + return unless $bin{unshare} && $bin{mount} && $bin{ip}; + my $setup = "$bin{mount} -t tmpfs tmpfs /tmp && $bin{ip} link set lo up && $bin{ip} route add default dev lo" + . " && { [ ! -e /usr/sbin/tcpdump ] || $bin{mount} --bind /dev/null /usr/sbin/tcpdump; }"; + foreach my $flags (qw(-mn -rmn)) { + next unless system("$bin{unshare} $flags sh -c '$setup' >/dev/null 2>&1") == 0; + return { unshare => $bin{unshare}, flags => $flags, setup => $setup, ip => $bin{ip} }; + } + return; +} + +sub run_isolated { + my ( $ns, $bindir, $script, @args ) = @_; + unlink $marker; + my $command = "env PATH='$bindir' XCATROOT='$root' " . script_command( $script, @args ); + my $shell = "$ns->{unshare} $ns->{flags} sh -c \"$ns->{setup} && exec $command\" 2>&1"; + my $out = `$shell`; + diag($out) if $?; + return $out; +} + +SKIP: { + skip 'the private /tmp would hide this checkout or its fixtures', 8 + if index( $repo, '/tmp/' ) == 0 || !@fixture_dir; + my $ns = isolation(); + skip 'no private network namespace on this host', 8 unless $ns; + my $isolated = fixture_bin( real_ip => $ns->{ip} ); + + $out = run_isolated( $ns, $isolated, $tools, '-i', 'lo', '-m', $mac, '-t', '1' ); + like( $out, qr/servers reply/, 'the tool runs its capture window in the namespace' ); + ok( -f $marker, '... and starts tcpdump' ); + my ( $ran, $args ) = recorded_invocation(); + is( $ran, "$isolated/tcpdump", '... by the path it resolved' ); + is( $args, "-i lo port 68 -n -vvvvvv", '... with the capture arguments' ); + + $out = run_isolated( $ns, $isolated, $probe, '-i', 'lo', '-m', $mac, '-d', '1' ); + like( $out, qr/servers replied/, 'the probe runs its capture window in the namespace' ); + ok( -f $marker, '... and starts tcpdump' ); + ( $ran, $args ) = recorded_invocation(); + is( $ran, "$isolated/tcpdump", '... by the path it resolved' ); + is( $args, "-i lo port 68 -n -vvvvvv", '... with the capture arguments' ); +} + +done_testing();