Add simple OpenFlow tests for switchd.
authorreyk <reyk@openbsd.org>
Tue, 19 Jul 2016 17:04:19 +0000 (17:04 +0000)
committerreyk <reyk@openbsd.org>
Tue, 19 Jul 2016 17:04:19 +0000 (17:04 +0000)
regress/usr.sbin/switchd/Makefile [new file with mode: 0644]
regress/usr.sbin/switchd/OFP.pm [new file with mode: 0644]
regress/usr.sbin/switchd/args-packet-jumbo.pm [new file with mode: 0644]
regress/usr.sbin/switchd/run.pl [new file with mode: 0644]

diff --git a/regress/usr.sbin/switchd/Makefile b/regress/usr.sbin/switchd/Makefile
new file mode 100644 (file)
index 0000000..8319910
--- /dev/null
@@ -0,0 +1,79 @@
+# $OpenBSD: Makefile,v 1.1 2016/07/19 17:04:19 reyk Exp $
+
+# The following ports must be installed for the regression tests:
+# p5-Net-Pcap          Perl interface for libpcap
+# p5-NetPacket         Perl interface for packet encoding/decoding
+# p5-Crypt-Random      To fill payloads with weak random data
+#
+# Check wether all required perl packages are installed.  If some
+# are missing print a warning and skip the tests, but do not fail.
+
+PERL_REQUIRE !=        perl -Mstrict -Mwarnings -e ' \
+    eval { require NetPacket::Ethernet } or print $@; \
+    eval { require Net::Pcap } or print $@; \
+    eval { require Crypt::Random } or print $@; \
+'
+.if ! empty (PERL_REQUIRE)
+regress:
+       @echo "${PERL_REQUIRE}"
+       @echo install these perl packages for additional tests
+.endif
+
+# Automatically generate regress targets from test cases in directory.
+
+ARGS !=                        cd ${.CURDIR} && ls args-*.pm
+TARGETS ?=             ${ARGS}
+REGRESS_TARGETS =      ${TARGETS:S/^/run-regress-/}
+CLEANFILES +=          *.log  ktrace.out stamp-*
+CLEANFILES +=          *.h *.ph
+
+SRC_PATH =             ${.CURDIR}/../../../usr.sbin/switchd
+SYS_PATH =             ${.CURDIR}/../../../sys
+OFP_HEADERS =          ofp.h ofp10.h
+OFP_PERLHEADERS =      ${OFP_HEADERS:S/.h/.ph/}
+
+# Set variables so that make runs with and without obj directory.
+# Only do that if necessary to keep visible output short.
+
+.if ${.CURDIR} == ${.OBJDIR}
+PERLINC =
+PERLPATH =
+.else
+PERLINC =      -I${.CURDIR}
+PERLPATH =     ${.CURDIR}/
+.endif
+
+# The arg tests take a perl hash with arguments controlling the
+# test parameters.  Generally they consist of switch, switchd.
+
+.for a in ${ARGS}
+run-regress-$a: $a
+       @echo '\n======== $@ ========'
+       time SUDO=${SUDO} KTRACE=${KTRACE} SWITCHD=${SWITCHD} perl ${PERLINC} ${PERLPATH}run.pl ${PERLPATH}$a
+.endfor
+
+${OFP_HEADERS}:
+       @-mkdir -p ${.OBJDIR}/net
+       # XXX headers can be in two different locations
+       @-test ${SRC_PATH}/$@ && cp ${SRC_PATH}/$@ ${.OBJDIR}
+       @-test ${SYS_PATH}/$@ && cp ${SYS_PATH}/$@ ${.OBJDIR}/net
+
+.SUFFIXES: .h .ph
+.h.ph:
+       @h2ph -d ${.OBJDIR} $<
+
+${REGRESS_TARGETS:M*}: ${OFP_PERLHEADERS}
+
+# make perl syntax check for all args files
+
+.PHONY: syntax
+
+syntax: stamp-syntax
+
+stamp-syntax: ${ARGS}
+.for a in ${ARGS}
+       @perl -c ${PERLPATH}$a
+.endfor
+       @date >$@
+
+.include <bsd.regress.mk>
diff --git a/regress/usr.sbin/switchd/OFP.pm b/regress/usr.sbin/switchd/OFP.pm
new file mode 100644 (file)
index 0000000..1b07105
--- /dev/null
@@ -0,0 +1,247 @@
+#
+# NetPacket::OFP - Decode and encode OpenFlow packets. 
+#
+
+package NetPacket::OFP;
+
+#
+# Copyright (c) 2016 Reyk Floeter <reyk@openbsd.org>.
+#
+# This package is free software and is provided "as is" without express 
+# or implied warranty.  It may be used, redistributed and/or modified 
+# under the terms of the Perl Artistic License (see
+# http://www.perl.com/perl/misc/Artistic.html)
+#
+
+use strict;
+use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+use NetPacket;
+
+my $myclass;
+BEGIN {
+       $myclass = __PACKAGE__;
+       $VERSION = "0.01";
+}
+sub Version () { "$myclass v$VERSION" }
+
+BEGIN {
+       @ISA = qw(Exporter NetPacket);
+
+       @EXPORT = qw();
+
+       @EXPORT_OK = qw(ofp_strip);
+
+       %EXPORT_TAGS = (
+               ALL         => [@EXPORT, @EXPORT_OK],
+               strip       => [qw(ofp_strip)],
+       );
+}
+
+#
+# Decode the packet
+#
+
+sub decode {
+       my $class = shift;
+       my($pkt, $parent, @rest) = @_;
+       my $self = {};
+
+       # Class fields
+       $self->{_parent} = $parent;
+       $self->{_frame} = $pkt;
+
+       $self->{version} = 1;
+       $self->{type} = 0;
+       $self->{length} = 0;
+       $self->{xid} = 0;
+       $self->{data} = '';
+
+       # Decode OpenFlow packet
+       if (defined($pkt)) {
+               ($self->{version}, $self->{type}, $self->{length},
+                   $self->{xid}, $self->{data}) = unpack("CCnNa*", $pkt);
+       }
+
+       # Return a blessed object
+       bless($self, $class);
+
+       return ($self);
+}
+
+#
+# Strip header from packet and return the data contained in it
+#
+
+undef &udp_strip;
+*ofp_strip = \&strip;
+
+sub strip {
+       my ($pkt, @rest) = @_;
+       my $ofp_obj = decode($pkt);
+       return ($ofp_obj->data);
+}   
+
+#
+# Encode a packet
+#
+
+sub encode {
+       my $class = shift;
+       my $self = shift;
+       my $pkt = '';
+
+       $self->{length} = 8;
+
+       $pkt = pack("CCnN", $self->{version}, $self->{type},
+           $self->{length}, $self->{xid});
+
+       if ($self->{version} == 1) {
+               # PACKET_IN
+               if ($self->{type} == 10) {
+                       $self->{length} += length($self->{data});
+                       $pkt = pack("CCnNNnnCCa*",
+                           $self->{version}, $self->{type},
+                           $self->{length}, $self->{xid}, $self->{buffer_id},
+                           $self->{length} - 8,
+                           $self->{port}, 0, 0, $self->{data});
+               }
+       }
+
+       return ($pkt); 
+}
+
+#
+# Module initialisation
+#
+
+1;
+
+# autoloaded methods go after the END token (&& pod) below
+
+__END__
+
+=head1 NAME
+
+C<NetPacket::OFP> - Assemble and disassemble OpenFlow packets.
+
+=head1 SYNOPSIS
+
+  use NetPacket::OFP;
+
+  $ofp_obj = NetPacket::OFP->decode($raw_pkt);
+  $ofp_pkt = NetPacket::OFP->encode($ofp_obj);
+  $ofp_data = NetPacket::OFP::strip($raw_pkt);
+
+=head1 DESCRIPTION
+
+C<NetPacket::OFP> provides a set of routines for assembling and
+disassembling packets using OpenFlow.
+
+=head2 Methods
+
+=over
+
+=item C<NetPacket::OFP-E<gt>decode([RAW PACKET])>
+
+Decode the raw packet data given and return an object containing
+instance data.  This method will quite happily decode garbage input.
+It is the responsibility of the programmer to ensure valid packet data
+is passed to this method.
+
+=item C<NetPacket::OFP-E<gt>encode($ofp_obj)>
+
+Return a OFP packet encoded with the instance data specified.
+
+=back
+
+=head2 Functions
+
+=over
+
+=item C<NetPacket::OFP::strip([RAW PACKET])>
+
+Return the encapsulated data (or payload) contained in the OpenFlow
+packet.  This data is suitable to be used as input for other
+C<NetPacket::*> modules.
+
+This function is equivalent to creating an object using the
+C<decode()> constructor and returning the C<data> field of that
+object.
+
+=back
+
+=head2 Instance data
+
+The instance data for the C<NetPacket::OFP> object consists of
+the following fields.
+
+=over
+
+=item version
+
+The OpenFlow version.
+
+=item type
+
+The message type.
+
+=item length
+
+The total message length.
+
+=item xid
+
+The transaction Id.
+
+=item data
+
+The encapsulated data (payload) for this packet.
+
+=back
+
+=head2 Exports
+
+=over
+
+=item default
+
+none
+
+=item exportable
+
+ofp_strip
+
+=item tags
+
+The following tags group together related exportable items.
+
+=over
+
+=item C<:strip>
+
+Import the strip function C<ofp_strip>.
+
+=item C<:ALL>
+
+All the above exportable items.
+
+=back
+
+=back
+
+=head1 COPYRIGHT
+
+  Copyright (c) 2016 Reyk Floeter <reyk@openbsd.org>
+
+  This package is free software and is provided "as is" without express 
+  or implied warranty.  It may be used, redistributed and/or modified 
+  under the terms of the Perl Artistic License (see
+  http://www.perl.com/perl/misc/Artistic.html)
+
+=head1 AUTHOR
+
+Reyk Floeter E<lt>reyk@openbsd.orgE<gt>
+
+=cut
+
+# any real autoloaded methods go after this line
diff --git a/regress/usr.sbin/switchd/args-packet-jumbo.pm b/regress/usr.sbin/switchd/args-packet-jumbo.pm
new file mode 100644 (file)
index 0000000..90febe7
--- /dev/null
@@ -0,0 +1,110 @@
+# $OpenBSD: args-packet-jumbo.pm,v 1.1 2016/07/19 17:04:19 reyk Exp $
+
+# Copyright (c) 2016 Reyk Floeter <reyk@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package args_packet_jumbo;
+
+use strict;
+use warnings;
+use base qw(Exporter);
+our @EXPORT = qw(init next);
+
+my $topology = {
+       buffers => {},
+       hosts => {
+               "a00000000001" => {
+                       "port" => 1
+               },
+               "a00000000002" => {
+                       "port" => 2
+               },
+               "a00000000003" => {
+                       "port" => 3
+               }
+       },
+       packets => [
+               {
+                       "src_mac" => "a00000000001",
+                       "dest_mac" => "a00000000002",
+                       "src_ip" => "10.0.0.1",
+                       "src_port" => 12345,
+                       "dest_ip" => "10.0.0.2",
+                       "dest_port" => 80,
+                       "length" => 1000,
+                       "count" => 3,
+                       "ofp_response" => main::OFP_T_PACKET_OUT()
+               },
+               {
+                       "src_mac" => "a00000000002",
+                       "dest_mac" => "a00000000001",
+                       "src_ip" => "10.0.0.2",
+                       "src_port" => 80,
+                       "dest_ip" => "10.0.0.1",
+                       "dest_port" => 12345,
+                       "length" => 1000,
+                       "count" => 3,
+                       "ofp_response" => main::OFP_T_FLOW_MOD()
+
+               },
+               {
+                       "src_mac" => "a00000000001",
+                       "dest_mac" => "ffffffffffff",
+                       "src_ip" => "10.0.0.1",
+                       "dest_ip" => "10.255.255.255",
+                       "length" => 5000,
+                       "count" => 3,
+                       "ofp_response" => main::OFP_T_PACKET_OUT()
+               }
+       ]
+};
+
+sub init {
+       my $class = shift;
+       my $sock = shift;
+       my $self = { "count" => 0,
+           "sock" => $sock, "version" => main::OFP_V_1_0() };
+
+       bless($self, $class);
+       main::ofp_hello($self);
+
+       for (my $i = 0; $i < @{$topology->{packets}}; $i++) {
+               my $packet = $topology->{packets}[$i];
+               my $src = $topology->{hosts}->{$packet->{src_mac}};
+
+               $self->{port} = $src->{port} if ($src);
+
+               for (my $j = 0; $j < $packet->{count}; $j++) {
+                       my $ofp;
+                       $self->{count}++;
+                       $ofp = main::packet_send($self, $packet);
+
+                       if (defined($packet->{ofp_response})) {
+                               if ($ofp->{type} != $packet->{ofp_response}) {
+                                       main::fatal($class,
+                                           "invalid ofp response type ".
+                                           $ofp->{type});
+                               }
+                       }
+               }
+       }
+
+       return ($self);
+}
+
+sub next {
+       # Not used
+}
+
+1;
diff --git a/regress/usr.sbin/switchd/run.pl b/regress/usr.sbin/switchd/run.pl
new file mode 100644 (file)
index 0000000..24f7b8d
--- /dev/null
@@ -0,0 +1,246 @@
+#!/usr/bin/perl -w
+# $OpenBSD: run.pl,v 1.1 2016/07/19 17:04:19 reyk Exp $
+
+# Copyright (c) 2016 Reyk Floeter <reyk@openbsd.org>
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+use strict;
+use warnings;
+
+BEGIN {
+       require OFP;
+       require 'ofp.ph';
+       require 'ofp10.ph';
+       require IO::Socket::INET;
+}
+
+use File::Basename;
+use Net::Pcap;
+use NetPacket::Ethernet;
+use NetPacket::IP;
+use NetPacket::UDP;
+use Data::Dumper;
+use Crypt::Random;
+
+sub fatal {
+       my $class = shift;
+       my $err = shift;
+       print STDERR "*** ".$class.": ".$err."\n";
+       die($err);
+}
+
+sub ofp_debug {
+       my $dir = shift;
+       my $ofp = shift;
+
+       printf("OFP ".$dir." version %d type %d length %d xid %d\n",
+           $ofp->{version},
+           $ofp->{type},
+           $ofp->{length},
+           $ofp->{xid});
+
+}
+
+sub ofp_hello {
+       my $class;
+       my $self = shift;
+       my $hello = NetPacket::OFP->decode() or fatal($class, "new packet");
+       my $resp = ();
+       my $pkt;
+       my $resppkt;
+
+       $hello->{version} = $self->{version};
+       $hello->{type} = OFP_T_HELLO();
+       $hello->{xid} = $self->{xid}++;
+
+       $pkt = NetPacket::OFP->encode($hello);
+
+       ofp_debug(">", $hello);
+
+       # XXX timeout
+       $self->{sock}->send($pkt);
+       $self->{sock}->recv($resppkt, 1024);
+
+       $resp = NetPacket::OFP->decode($resppkt) or
+           fatal($class, "recv'ed packet");
+
+       ofp_debug("<", $resp);
+
+       return ($resp);
+}
+
+sub ofp_packet_in {
+       my $class;
+       my $self = shift;
+       my $data = shift;
+       my $pktin = NetPacket::OFP->decode() or fatal($class, "new packet");
+       my $resp = ();
+       my $pkt;
+       my $resppkt;
+
+       $pktin->{version} = $self->{version};
+       $pktin->{type} = OFP_T_PACKET_IN();
+       $pktin->{xid} = $self->{xid}++;
+       $pktin->{data} = $data;
+       $pktin->{length} += length($data);
+       $pktin->{buffer_id} = $self->{count} || 1;
+       $pktin->{port} = $self->{port} || OFP_PORT_NORMAL();
+
+       $pkt = NetPacket::OFP->encode($pktin);
+
+       ofp_debug(">", $pktin);
+
+       # XXX timeout
+       $self->{sock}->send($pkt);
+       $self->{sock}->recv($resppkt, 1024);
+
+       $resp = NetPacket::OFP->decode($resppkt) or
+           fatal($class, "recv'ed packet");
+
+       ofp_debug("<", $resp);
+
+       return ($resp);
+}
+
+sub packet_send {
+       my $class;
+       my $self = shift;
+       my $packet = shift;
+       my $eth;
+       my $ip;
+       my $udp;
+       my $data;
+       my $pkt;
+       my $src;
+
+       # Payload
+       $data = Crypt::Random::makerandom_octet(Length => $packet->{length});
+
+       # IP header
+       $ip = NetPacket::IP->decode();
+       $ip->{src_ip} = $packet->{src_ip} || "127.0.0.1";
+       $ip->{dest_ip} = $packet->{dest_ip} || "127.0.0.1";
+       $ip->{ver} = NetPacket::IP::IP_VERSION_IPv4;
+       $ip->{hlen} = 5;
+       $ip->{tos} = 0;
+       $ip->{id} = Crypt::Random::makerandom(Size => 16);
+       $ip->{ttl} = 0x5a;
+       $ip->{flags} = 0; #XXX NetPacket::IP::IP_FLAG_DONTFRAG;
+       $ip->{foffset} = 0;
+       $ip->{proto} = NetPacket::IP::IP_PROTO_UDP;
+       $ip->{options} = '';
+
+       # UDP header
+       $udp = NetPacket::UDP->decode();
+       $udp->{src_port} = $packet->{src_port} || 9000;
+       $udp->{dest_port} = $packet->{dest_port} || 9000;
+       $udp->{data} = $data;
+
+       $ip->{data} = $udp->encode($ip);
+       $pkt = $ip->encode() or fatal($class, "ip");
+
+       # Create Ethernet header
+       $self->{data} = pack('H12H12na*' ,
+           $packet->{dest_mac},
+           $packet->{src_mac},
+           NetPacket::Ethernet::ETH_TYPE_IP,
+           $pkt);
+
+       return (main::ofp_packet_in($self, $self->{data}));
+       
+}
+
+sub packet_decode {
+       my $pkt = shift;
+       my $hdr = shift;
+       my $eh = NetPacket::Ethernet->decode($pkt);
+
+       printf("%s %s %04x %d",
+           join(':', unpack '(A2)*', $eh->{src_mac}),
+           join(':', unpack '(A2)*', $eh->{dest_mac}),
+           $eh->{type}, length($pkt));
+       if (length($pkt) < $hdr->{len}) {
+               printf("/%d", $hdr->{len})
+       }
+       printf("\n");
+
+       return ($eh);
+}
+
+sub process {
+       my $sock = shift;
+       my $path = shift;
+       my $pcap_t;
+       my $err;
+       my $pkt;
+       my %hdr;
+       my ($filename, $dirs, $suffix) = fileparse($path, ".pm");
+       (my $func = $filename) =~ s/-/_/g;
+       my $state;
+
+       print "- $filename\n";
+
+       require $path or fatal("main", $path);
+
+       eval {
+               $state = $func->init($sock);
+       };
+
+       return if not $state->{pcap};
+
+       $pcap_t = Net::Pcap::open_offline($dirs."".$state->{pcap}, \$err)
+           or fatal("main", $err);
+
+       while ($pkt = Net::Pcap::next($pcap_t, \%hdr)) {
+
+               $state->{data} = $pkt;
+               $state->{eh} = packet_decode($pkt, \%hdr);
+
+               eval {
+                       $func->next($state);
+               };
+       }
+
+       Net::Pcap::close($pcap_t);
+}
+
+if (@ARGV < 1) {
+    print "\nUsage: run.pl test.pl\n";
+    exit;
+}
+
+# Flush after every write
+$| = 1;
+
+my $test = $ARGV[0];
+my @test_files = ();
+for (@ARGV) {
+       push(@test_files, glob($_));
+}
+
+# Open connection to the controller
+my $sock = new IO::Socket::INET(
+       PeerHost => '127.0.0.1',
+       PeerPort => '6633',
+       Proto => 'tcp',
+) or fatal("main", "ERROR in Socket Creation : $!\n");
+
+# Run all requested tests
+for my $test_file (@test_files) {
+       process($sock, $test_file);
+}
+
+$sock->close();
+
+1;