[libvirt] [RFC] libvirt-TCK scripts to verify spoofing prevention

Gerhard Stenzel gstenzel at linux.vnet.ibm.com
Fri Apr 23 14:25:49 UTC 2010


Daniel, thanks for the comments .. most of them are integrated, but I
have a problem with changing MAC addresses, because the domain xml is
regenerated every time, so I cannot rely on the interface name being
"eth0" as fedora during boot creates a new ethX for each new MAC
address .. any good advice?

On Thu, 2010-04-15 at 15:27 +0100, Daniel P. Berrange wrote: 
> On Thu, Apr 15, 2010 at 02:35:41PM +0200, Gerhard Stenzel wrote:
> > The following patch mainly adds a set of test case to verify that
> > several spoofing attacks are prevented by the nwfilter subsystem.
> > 
> > In order to have a well defined test machine, the patch also includes
> > test scripts to network install a virtual disk from scratch, to boot the
> > virtual test machine prior to running the actual test scripts and to
> > shut it down afterwards.
> > 
> > While I have tried to remove as much dependency on my local setup as
> > possible there is still some left, so I am currently more interested in
> > feedback about the general approach, not necessarily actual inclusion
> > into the libvirt-TCK git.
> 
> Your actual test cases look good, so I'll just put comments about
> the setup/teardown stuff inline.


ok .. sounds good

> 
> > 
> > For example, I am currently trying to find a suitable location for the
> > kickstart file, and also a suitable place for the common_functions.pl.
> 
> The 'lib' directory contains modules which provide common functions &
> code for the test scripts. In this case I'd suggest creating a file
> 
>   lib/Sys/Virt/TCK/NetworkHelpers.pm  (use Sys::Virt::TCK::NetworkHelpers)

ok .. done

> 
> > Index: libvirt-tck/scripts/network/README
> > ===================================================================
> > --- /dev/null
> > +++ libvirt-tck/scripts/network/README
> > @@ -0,0 +1,14 @@
> > +
> > +Test cases:
> > +
> > +000-install-image.t		creates and install a 2GB fedora virtual disk via
> > kickstart file from the network
> > +001-boot-image.t		defines and boots a VM which uses the fedora virtual
> > disk
> > +100-ping-still-working.t	verifies the VM is pingable
> > +210-no-mac-spoofing.t		verifies mac spoofing is prevented
> > +220-no-ip-spoofing.t		verifies ip spoofing is prevented
> > +230-no-mac-broadcast.t		verifies mac broadcasting is prevented
> > +240-no-arp-spoofing.t		verifies arp spoofing is prevented
> > +999-shutdown-image.t		shuts the VM down
> 
> One thing about the TCK test cases is that each one should be 
> self-contained, doing all setup & teardown it requires, not
> reliant on any of the other tests cases or ordering of tests.
> 
> So instead of having the 000-install-image.t & 0001-boot-image.t
> scripts that do setup, you'd want to create some library code
> that can be used to install + boot the guest, and just call that
> from each test case. 

I am currently trying this .. however, what I am struggling with is that
the MAC address is different for every boot.

> 
> > Index: libvirt-tck/scripts/network/000-install-image.t
> > ===================================================================
> > --- /dev/null
> > +++ libvirt-tck/scripts/network/000-install-image.t
> > @@ -0,0 +1,181 @@
> > +# -*- perl -*-
> > +#
> > +# Copyright (C) 2010 IBM Corp.
> > +#
> > +# This program is free software; You can redistribute it and/or modify
> > +# it under the GNU General Public License as published by the Free
> > +# Software Foundation; either version 2, or (at your option) any
> > +# later version
> > +#
> > +# The file "LICENSE" distributed along with this file provides full
> > +# details of the terms and conditions
> > +#
> > +
> > +=pod
> > +
> > +=head1 NAME
> > +
> > +network/000-install-image.t - install network test image
> > +
> > +=head1 DESCRIPTION
> > +
> > +The test case creates and install a 2GB fedora virtual 
> > +disk via kickstart file from the network.
> > +
> > +=cut
> > +
> > +use strict;
> > +use warnings;
> > +
> > +use Test::More tests => 1;
> > +
> > +use Sys::Virt::TCK;
> > +
> > +my $tck = Sys::Virt::TCK->new();
> > +my $conn = eval { $tck->setup(); };
> > +BAIL_OUT "failed to setup test harness: $@" if $@;
> > +END { $tck->cleanup if $tck; }
> > +
> > +# variables which may need to be adapted
> > +my $domain_name ="f12nwtest";
> > +my $disk_name = "/var/lib/libvirt/images/${domain_name}.img";
> > +my $disk_size = "2147483648";
> 
> You want to avoid hardcoding any disk paths in the test cases.
> 
> There's a convenient helper function if you want to create a
> scratch disk for a guest
> 
>    my $path = $tck->create_sparse_disk("nwfilter", "root.img", 2147483648);

create_sparse_disk does not work for me, anaconda does not recognize
installable disks .. anyway I got rid of the hardcoded names.

> 
> > +my $kickstart_file ="http://192.168.122.1/ks.cfg";
> > +my $cmdline = "ip=dhcp  gateway=192.168.122.1 ks=${kickstart_file}";
> 
> In the 'conf/default.cfg' file we list the kernel+initrd images for
> various distros. We could likely also have an optional kickstart
> file listed there, so we move this configuration info out of the test
> cases.

sounds good .. but not done yet

> 
> > +# see if the domain already exits
> > +my $already_defined = 0;
> > +diag "searching if ${domain_name} is already defined";
> > +my $nnames = $conn->num_of_defined_domains();
> > +my @names = $conn->list_defined_domain_names($nnames);
> > +foreach (@names){
> > +    if (/${domain_name}/) {
> > +	print "$_ already exists, no need to redefine\n";
> > +	$already_defined = 1;
> > +    }
> > +}
> > +diag $already_defined;
> 
> All guests created by the TCK test cases should have a name prefix
> of 'tck' to avoid clashing with existing guests. Each test case 
> should destroy + undefine all guests it creates. The TCK setup
> code will validate that there are no guests having a name starting
> with a 'tck' prefix & optionally destroy them. So each of your
> test cases should assume the guest doesn't already exist + clean
> it up before exiting.

ok .. working on that one

> 
> NB, the disk files are not deleted between runs, so you will 
> still avoid the overhead of installation.
> 
> 
> I'm talking with the libguestfs folks to see if we can re-use
> their guest appliance + daemon so we can avoid the actual install
> + ssh stuff at some point in the future. 
> 
> > +# check for installation disk and build it if not exists
> > +my $already_installed = 0;
> > +my $pool = $conn->get_storage_pool_by_name("default");
> > +my $nnames = $pool->num_of_storage_volumes();
> > +my @volNames = $pool->list_storage_vol_names($nnames);
> > +foreach (@volNames){
> > +    if (/${domain_name}/) {
> > +	print "$_ already exists, no need to install\n";
> > +	$already_installed = 1;
> > +    }
> > +}
> > +
> > +my $volumexml = "<volume>".
> > +"  <name>${domain_name}.img</name>".
> > +"  <key>${disk_name}</key>".
> > +"  <source>".
> > +"  </source>".
> > +"  <capacity>${disk_size}</capacity>".
> > +"  <allocation>4096</allocation>".
> > +"  <target>".
> > +"    <path>${disk_name}</path>".
> > +"    <format type='raw'/>".
> > +"    <permissions>".
> > +"      <mode>0644</mode>".
> > +"      <owner>0</owner>".
> > +"      <group>0</group>".
> > +"    </permissions>".
> > +"  </target>".
> > +"</volume>";
> 
> 
> If using the create_sparse_disk helper I mentioned, you can avoid this
> chunk of code. Just do a check to see if $path exists or not.

ok . solved differently with generic_volume()

> 
> > +# prepare image
> > +if ($already_installed == 0) {
> > +    diag "Creating ${disk_name}";
> > +    diag $volumexml;
> > +    my $vol = $pool->create_volume($volumexml)
> > +#    system("qemu-img create ${disk_name} ${disk_size}");
> > +}
> > +
> > +my $topxml = "  <name>${domain_name}</name>".
> > +"  <memory>524288</memory>".
> > +"  <currentMemory>524288</currentMemory>".
> > +"  <vcpu>1</vcpu>";
> > +
> > +my $osxml = "  <os>".
> > +"    <type arch='x86_64' machine='fedora-13'>hvm</type>".
> > +"    <kernel>/var/cache/libvirt-tck/os-i686-hvm/vmlinuz</kernel>".
> > +"    <initrd>/var/cache/libvirt-tck/os-i686-hvm/initrd</initrd>".
> > +"    <cmdline>${cmdline}</cmdline>".
> > +"    <boot dev='hd'/>".
> > +"  </os>";
> > +
> > +my $bottomxml = "  <features>".
> > +"    <acpi/>".
> > +"    <apic/>".
> > +"  </features>".
> > +"  <clock offset='utc'/>".
> > +"  <on_poweroff>destroy</on_poweroff>".
> > +"  <on_reboot>restart</on_reboot>".
> > +"  <on_crash>restart</on_crash>".
> > +"  <devices>".
> > +"    <emulator>/usr/bin/qemu-kvm</emulator>".
> > +"    <disk type='file' device='disk'>".
> > +"      <driver name='qemu' type='raw'/>".
> > +"      <source file='${disk_name}'/>".
> > +"      <target dev='hda' bus='ide'/>".
> > +"    </disk>".
> > +"    <controller type='ide' index='0'>".
> > +"    </controller>".
> > +"    <interface type='network'>".
> > +"      <source network='default'/>".
> > +"      <target dev='vnet0'/>".
> > +"      <model type='virtio'/>".
> > +"    </interface>".
> > +"    <serial type='pty'>".
> > +"      <target port='0'/>".
> > +"    </serial>".
> > +"    <console type='pty'>".
> > +"      <target port='0'/>".
> > +"    </console>".
> > +"    <graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'
> > keymap='de'/>".
> > +"    <video>".
> > +"      <model type='cirrus' vram='9216' heads='1'/>".
> > +"    </video>".
> > +"  </devices>";
> > +
> > +my $xml = "<domain type='kvm'>" .
> > +$topxml.
> > +$osxml.
> > +$bottomxml.
> > +"</domain>";
> 
> There are a couple of helpers for building XML in a portable manner.
> In the Sys::Virt::TCK class there's a method 'generic_domain' which
> will find you a bare minimum VM matching the best kernel / arch
> found in the TCK config file. It returns an instance of the the
> class Sys::Virt::TCK::DomainBuilder which lets you then add 
> extra XML to the guest.
> 
> eg, so you could do
> 
>     my $guest = $tck->generic_domain();
> 
>     $guest->boot_cmdline($cmdline);
> 
> 
> The 'boot_cmdline' method doesn't exist yet, but feel frree to extend
> the shared helper modules as needed. Its much nicer to use them than
> to build up XML manually.

ok .. done 

> 
> > +
> > +diag $xml;
> > +diag "Defining an inactive domain config";
> > +my $dom;
> > +
> > +# no need to start if already installed
> > +if (($already_installed == 0) && ($already_defined == 0)) {
> > +    ok_domain(sub { $dom = $conn->define_domain($xml) }, "defined
> > persistent domain config");
> > +    $xml = $dom->get_xml_description;
> > +    diag $xml;
> > +    diag "Starting inactive domain config";
> > +    $dom->create;
> > +
> > +    # wait for completion of installation
> > +    diag "wait for installation to finish .. ";
> > +    while($dom->is_active()) {
> > +	sleep(10);
> > +	diag ".. to view progress connect to virtual machine ${domain_name} ..
> > ";
> > +    }
> > +    sleep(10);
> > +    diag " .. done";
> > +    # cleanup
> > +    $dom->undefine;
> > +} else {
> > +    ok_domain { $dom = $conn->get_domain_by_name($domain_name) } "the
> > existing domain object";
> > +}
> > +
> > +
> > +
> > +
> > +
> > Index: libvirt-tck/scripts/network/001-boot-image.t
> > ===================================================================
> > --- /dev/null
> > +++ libvirt-tck/scripts/network/001-boot-image.t
> > @@ -0,0 +1,133 @@
> > +# -*- perl -*-
> > +#
> > +# Copyright (C) 2010 IBM Corp.
> > +#
> > +# This program is free software; You can redistribute it and/or modify
> > +# it under the GNU General Public License as published by the Free
> > +# Software Foundation; either version 2, or (at your option) any
> > +# later version
> > +#
> > +# The file "LICENSE" distributed along with this file provides full
> > +# details of the terms and conditions
> > +#
> > +
> > +=pod
> > +
> > +=head1 NAME
> > +
> > +network/001-boot-image.t - boot installed test image
> > +
> > +=head1 DESCRIPTION
> > +
> > +The test case defines and boots a VM which uses the 
> > +fedora virtual disk create ny 000-install-image
> > +
> > +=cut
> > +
> > +use strict;
> > +use warnings;
> > +
> > +use Test::More tests => 2;
> > +
> > +use Sys::Virt::TCK;
> > +use XML::LibXML;
> > +
> > +require 'scripts/network/common_functions.pl';
> > +
> > +my $tck = Sys::Virt::TCK->new();
> > +my $conn = eval { $tck->setup(); };
> > +BAIL_OUT "failed to setup test harness: $@" if $@;
> > +END { $tck->cleanup if $tck; }
> > +
> > +my $already_defined = 0;
> > +my $domain_name ="f12nwtest";
> > +
> > +# see if the domain already exits
> > +diag "searching if ${domain_name} is already defined";
> > +my $nnames = $conn->num_of_defined_domains();
> > +my @names = $conn->list_defined_domain_names($nnames);
> > +foreach (@names){
> > +    if (/${domain_name}/) {
> > +	print "$_ already exists, no need to redefine\n";
> > +	$already_defined = 1;
> > +    }
> > +}
> > +diag $already_defined;
> > +
> > +my $dom;
> > +my $xml;
> > +
> > +if ($already_defined == 1) {
> > +    ok_domain { $dom = $conn->get_domain_by_name($domain_name) } "the
> > existing domain object";
> > +} else {
> > +    my $topxml = "  <name>${domain_name}</name>".
> > +	"  <memory>524288</memory>".
> > +	"  <currentMemory>524288</currentMemory>".
> > +	"  <vcpu>1</vcpu>";
> > +    
> > +    my $osxml = "  <os>".
> > +	"    <type arch='x86_64' machine='fedora-13'>hvm</type>".
> > +	"    <boot dev='hd'/>".
> > +	"  </os>";
> > +    
> > +    my $bottomxml = "  <features>".
> > +	"    <acpi/>".
> > +	"    <apic/>".
> > +	"  </features>".
> > +	"  <clock offset='utc'/>".
> > +	"  <on_poweroff>destroy</on_poweroff>".
> > +	"  <on_reboot>restart</on_reboot>".
> > +	"  <on_crash>restart</on_crash>".
> > +	"  <devices>".
> > +	"    <emulator>/usr/bin/qemu-kvm</emulator>".
> > +	"    <disk type='file' device='disk'>".
> > +	"      <driver name='qemu' type='raw'/>".
> > +	"      <source file='/var/lib/libvirt/images/${domain_name}.img'/>".
> > +	"      <target dev='hda' bus='ide'/>".
> > +	"    </disk>".
> > +	"    <controller type='ide' index='0'>".
> > +	"    </controller>".
> > +	"    <interface type='network'>".
> > +	"      <source network='default'/>".
> > +	"      <filterref filter='no-spoofing'/>".
> > +	"      <target dev='vnet0'/>".
> > +	"      <model type='virtio'/>".
> > +	"    </interface>".
> > +	"    <serial type='pty'>".
> > +	"      <target port='0'/>".
> > +	"    </serial>".
> > +	"    <console type='pty'>".
> > +	"      <target port='0'/>".
> > +	"    </console>".
> > +	"    <graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1'
> > keymap='de'/>".
> > +	"    <video>".
> > +	"      <model type='cirrus' vram='9216' heads='1'/>".
> > +	"    </video>".
> > +	"  </devices>";
> > +    
> > +    $xml = "<domain type='kvm'>" .
> > +	$topxml.
> > +	$osxml.
> > +	$bottomxml.
> > +	"</domain>";
> 
> Again, I think you'd be better using the '$tck->generic_domain' method,
> and adding support to Sys::Virt::TCK::DomainBuilder for adding the
> '<filterref filter='no-spoofing'/>' bit of the XML.
> 

ok .. done 
> 
> 
> > Index: libvirt-tck/scripts/network/100-ping-still-working.t
> > ===================================================================
> > --- /dev/null
> > +++ libvirt-tck/scripts/network/100-ping-still-working.t
> > @@ -0,0 +1,71 @@
> > +# -*- perl -*-
> > +#
> > +# Copyright (C) 2010 IBM Corp.
> > +#
> > +# This program is free software; You can redistribute it and/or modify
> > +# it under the GNU General Public License as published by the Free
> > +# Software Foundation; either version 2, or (at your option) any
> > +# later version
> > +#
> > +# The file "LICENSE" distributed along with this file provides full
> > +# details of the terms and conditions
> > +#
> > +
> > +=pod
> > +
> > +=head1 NAME
> > +
> > +network/100-ping-still-working.t - verify machines can be pinged from
> > host
> > +
> > +=head1 DESCRIPTION
> > +
> > +The test case validates that it is possible to ping a guest machine
> > from
> > +the host.
> > +
> > +=cut
> > +
> > +use strict;
> > +use warnings;
> > +
> > +use Test::More tests => 4;
> > +
> > +use Sys::Virt::TCK;
> > +use Test::Exception;
> > +use Net::SSH::Perl;
> > +use XML::LibXML;
> > +
> > +require 'scripts/network/common_functions.pl';
> > +
> > +my $tck = Sys::Virt::TCK->new();
> > +my $conn = eval { $tck->setup(); };
> > +BAIL_OUT "failed to setup test harness: $@" if $@;
> > +END {
> > +    $tck->cleanup if $tck;
> > +}
> > +
> > +# create first domain and start it
> > +diag "Trying domain lookup by name";
> > +my $dom1;
> > +my $domain_name ="f12nwtest";
> 
> This is where you'd want to start the guest domain
> 
> > +
> > +ok_domain { $dom1 = $conn->get_domain_by_name($domain_name) } "the
> > running domain object";
> > +my $xml = $dom1->get_xml_description;
> > +diag $xml;
> > +ok($dom1->get_id() > 0, "running domain has an ID > 0");
> > +my $mac1 = get_macaddress($xml);
> > +diag $mac1;
> > +my $guestip1 = get_ip_from_leases($mac1);
> > +diag "ip is $guestip1";
> > +
> > +# check ebtables entry
> > +my $ebtable1 = `/sbin/ebtables -L;/sbin/ebtables -t nat -L`;
> > +diag $ebtable1;
> > +# fixme to include mac adress
> > +ok($ebtable1 =~ "vnet0", "check ebtables entry");
> > +
> > +# ping guest1
> > +my $ping1 = `ping -c 10 $guestip1`;
> > +diag $ping1;
> > +ok($ping1 =~ "10 received", "ping $guestip1 test");
> 
> And run 'destroy' here. If you use a transient guest when
> initially booting ($conn->create_domain($xml))  then you'll
> not have to worry about the undefine step.
> 
> 
> > +
> > +sub get_macaddress {
> > +    my $xmldesc = shift;
> > +
> > +    my $mac;
> > +    my $parser = XML::LibXML->new();
> > +
> > +    my $doc = $parser->parse_string($xmldesc);
> > +
> > +    my $rootel = $doc -> getDocumentElement();
> > +
> > +    my @devices = $rootel->getChildrenByTagName("devices");
> > +    foreach my $device(@devices) {
> > +	my @interfaces = $device->getChildrenByTagName("interface");
> > +	foreach my $interface(@interfaces) {
> > +	    my @targets = $interface->getChildrenByTagName("mac");
> > +	    foreach my $target(@targets) {
> > +		$mac = $target->getAttribute("address");
> > +	    }
> > +	}
> > +    }
> > +    utf8::decode($mac);
> > +    return $mac;
> > +}
> 
> You can simplify this using XPath - the $tck object provides a
> convenient method for performing an XPath query on a $dom object.
> eg that whole method can just be 
> 
>   my $resultt = xpath($dom, "/domain/devices/interface/mac/\@address")
>   my @macaddrs = map { $_->getNodeValue} $result->get_nodelist;
> 
ok .. but not done yet

> 
> Daniel
-- 
Best regards, 

Gerhard Stenzel, 
-----------------------------------------------------------------------------------------------------------------------------------
IBM Deutschland Research & Development GmbH
Vorsitzender des Aufsichtsrats: Martin Jetter
Geschäftsführung: Dirk Wittkopp
Sitz der Gesellschaft: Böblingen
Registergericht: Amtsgericht Stuttgart, HRB 243294




More information about the libvir-list mailing list