From 4a73aff1b186cf3260e86133421e977e96fb7705 Mon Sep 17 00:00:00 2001 From: reyk Date: Tue, 19 Jul 2016 17:04:19 +0000 Subject: [PATCH] Add simple OpenFlow tests for switchd. --- regress/usr.sbin/switchd/Makefile | 79 ++++++ regress/usr.sbin/switchd/OFP.pm | 247 ++++++++++++++++++ regress/usr.sbin/switchd/args-packet-jumbo.pm | 110 ++++++++ regress/usr.sbin/switchd/run.pl | 246 +++++++++++++++++ 4 files changed, 682 insertions(+) create mode 100644 regress/usr.sbin/switchd/Makefile create mode 100644 regress/usr.sbin/switchd/OFP.pm create mode 100644 regress/usr.sbin/switchd/args-packet-jumbo.pm create mode 100644 regress/usr.sbin/switchd/run.pl diff --git a/regress/usr.sbin/switchd/Makefile b/regress/usr.sbin/switchd/Makefile new file mode 100644 index 00000000000..8319910e3e2 --- /dev/null +++ b/regress/usr.sbin/switchd/Makefile @@ -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 diff --git a/regress/usr.sbin/switchd/OFP.pm b/regress/usr.sbin/switchd/OFP.pm new file mode 100644 index 00000000000..1b07105aa4f --- /dev/null +++ b/regress/usr.sbin/switchd/OFP.pm @@ -0,0 +1,247 @@ +# +# NetPacket::OFP - Decode and encode OpenFlow packets. +# + +package NetPacket::OFP; + +# +# Copyright (c) 2016 Reyk Floeter . +# +# 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 - 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 provides a set of routines for assembling and +disassembling packets using OpenFlow. + +=head2 Methods + +=over + +=item Cdecode([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 Cencode($ofp_obj)> + +Return a OFP packet encoded with the instance data specified. + +=back + +=head2 Functions + +=over + +=item C + +Return the encapsulated data (or payload) contained in the OpenFlow +packet. This data is suitable to be used as input for other +C modules. + +This function is equivalent to creating an object using the +C constructor and returning the C field of that +object. + +=back + +=head2 Instance data + +The instance data for the C 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. + +=item C<:ALL> + +All the above exportable items. + +=back + +=back + +=head1 COPYRIGHT + + Copyright (c) 2016 Reyk Floeter + + 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 Ereyk@openbsd.orgE + +=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 index 00000000000..90febe7792d --- /dev/null +++ b/regress/usr.sbin/switchd/args-packet-jumbo.pm @@ -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 +# +# 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 index 00000000000..24f7b8d9c2f --- /dev/null +++ b/regress/usr.sbin/switchd/run.pl @@ -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 +# +# 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; -- 2.20.1