summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Obser <florian@cvs.openbsd.org>2017-10-11 17:21:45 +0000
committerFlorian Obser <florian@cvs.openbsd.org>2017-10-11 17:21:45 +0000
commitc2f2d8d47222b02535d92dd23123946c9f300717 (patch)
tree97272d42109d24d1ec7f768f32e7e4096eea98dd
parent4823558b926229a8c8e503a67e43c5570e65c71f (diff)
Generate a router advertisement with scapy and check that slaacd
receives it by parsing slacctl show interface. This is a first stab, more things should be checked.
-rw-r--r--regress/sbin/slaacd/IfInfo.py44
-rw-r--r--regress/sbin/slaacd/Makefile9
-rw-r--r--regress/sbin/slaacd/Slaacctl.py235
-rw-r--r--regress/sbin/slaacd/process_ra.py46
4 files changed, 333 insertions, 1 deletions
diff --git a/regress/sbin/slaacd/IfInfo.py b/regress/sbin/slaacd/IfInfo.py
new file mode 100644
index 00000000000..39c0a90bc29
--- /dev/null
+++ b/regress/sbin/slaacd/IfInfo.py
@@ -0,0 +1,44 @@
+# $OpenBSD: IfInfo.py,v 1.1 2017/10/11 17:21:44 florian Exp $
+# Copyright (c) 2017 Florian Obser <florian@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.
+
+import pprint
+import subprocess
+import re
+
+class IfInfo(object):
+ def __init__(self, ifname):
+ self.ifname = ifname
+ self.mac = None
+ self.ll = None
+ self.out = subprocess.check_output(['ifconfig', ifname])
+ self.parse(self.out)
+
+ def __str__(self):
+ return "{0}: mac: {1}, link local: {2}".format(self.ifname,
+ self.mac, self.ll)
+
+ def parse(self, str):
+ lines = str.split("\n")
+ for line in lines:
+ lladdr = re.match("^\s+lladdr (.+)", line)
+ link_local = re.match("^\s+inet6 ([^%]+)", line)
+ if lladdr:
+ self.mac = lladdr.group(1)
+ continue
+ elif link_local:
+ self.ll = link_local.group(1)
+
+ if self.mac and self.ll:
+ return
diff --git a/regress/sbin/slaacd/Makefile b/regress/sbin/slaacd/Makefile
index 0c9149e5de9..2db47fa7275 100644
--- a/regress/sbin/slaacd/Makefile
+++ b/regress/sbin/slaacd/Makefile
@@ -1,4 +1,4 @@
-# $OpenBSD: Makefile,v 1.3 2017/10/11 17:17:03 florian Exp $
+# $OpenBSD: Makefile,v 1.4 2017/10/11 17:21:44 florian Exp $
# The following ports must be installed:
#
@@ -68,6 +68,13 @@ run-regress-send-solicitation: cleanup setup
@echo '\n======== $@ ========'
route -T${RTABLE} exec ${PYTHON}sniff_sol.py ${CTR_SOCK}
+
+TARGETS += parse-ra
+run-regress-parse-ra: cleanup setup
+ @echo '\n======== $@ ========'
+ route -T${RTABLE} exec ${PYTHON}process_ra.py ${PAIR1} ${PAIR2} \
+ ${CTR_SOCK}
+
TARGETS += cleanup
run-regress-cleanup: cleanup
diff --git a/regress/sbin/slaacd/Slaacctl.py b/regress/sbin/slaacd/Slaacctl.py
new file mode 100644
index 00000000000..7d889a783f1
--- /dev/null
+++ b/regress/sbin/slaacd/Slaacctl.py
@@ -0,0 +1,235 @@
+# $OpenBSD: Slaacctl.py,v 1.1 2017/10/11 17:21:44 florian Exp $
+# Copyright (c) 2017 Florian Obser <florian@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.
+
+import pprint
+import subprocess
+import re
+
+class ShowInterface(object):
+ def __init__(self, ifname, sock, debug=0):
+ self.ifname = ifname
+ self.sock = sock
+ self.debug = debug
+ self.index = None
+ self.running = None
+ self.privacy = None
+ self.lladdr = None
+ self.linklocal = None
+ self.RAs = []
+ self.addr_proposals = []
+ self.def_router_proposals = []
+ self.out = subprocess.check_output(['slaacctl', '-s', self.sock,
+ 'sh', 'in', self.ifname])
+ self.parse(self.out)
+
+ def __str__(self):
+ rep = dict()
+ iface = dict()
+ rep[self.ifname] = iface
+ iface['index'] = self.index
+ iface['running'] = self.running
+ iface['privacy'] = self.privacy
+ iface['lladdr'] = self.lladdr
+ iface['linklocal'] = self.linklocal
+ iface['RAs'] = self.RAs
+ iface['addr_proposals'] = self.addr_proposals
+ iface['def_router_proposals'] = self.def_router_proposals
+ return (pprint.pformat(rep, indent=4))
+
+ def parse(self, str):
+ state = 'START'
+ ra = None
+ prefix = None
+ addr_proposal = None
+ def_router_proposal = None
+ lines = str.split("\n")
+ for line in lines:
+ if self.debug == 1:
+ print line
+ if re.match("^\s*$", line):
+ pass
+ elif state == 'START':
+ ifname = re.match("^(\w+):", line).group(1)
+ if ifname != self.ifname:
+ raise ValueError("unexpected interface "
+ + "name: " + ifname)
+ state = 'IFINFO'
+ elif state == 'IFINFO':
+ m = re.match("^\s+index:\s+(\d+)\s+running:"
+ + "\s+(\w+)\s+privacy:\s+(\w+)", line)
+ self.index = m.group(1)
+ self.running = m.group(2)
+ self.privacy = m.group(3)
+ state = 'IFLLADDR'
+ elif state == 'IFLLADDR':
+ self.lladdr = re.match("^\s+lladdr:\s+(.*)",
+ line).group(1)
+ state = 'IFLINKLOCAL'
+ elif state == 'IFLINKLOCAL':
+ self.linklocal = re.match("^\s+inet6:\s+(.*)",
+ line).group(1)
+ state = 'IFDONE'
+ elif state == 'IFDONE':
+ is_ra = re.match("^\s+Router Advertisement "
+ + "from\s+(.*)", line)
+ is_addr_proposal = re.match("^\s+Address "
+ + "proposals", line)
+ if is_ra:
+ ra = dict()
+ ra['prefixes'] = []
+ ra['rdns'] = []
+ ra['search'] = []
+ ra['from'] = is_ra.group(1)
+ self.RAs.append(ra)
+ state = 'RASTART'
+ elif is_addr_proposal:
+ state = 'ADDRESS_PROPOSAL'
+ elif state == 'RASTART':
+ m = re.match("\s+received:\s+(.*);\s+(\d+)s "
+ + "ago", line)
+ ra['received'] = m.group(1)
+ ra['ago'] = m.group(2)
+ state = 'RARECEIVED'
+ elif state == 'RARECEIVED':
+ m = re.match("\s+Cur Hop Limit:\s+(\d+), M: "
+ + "(\d+), O: (\d+), "
+ + "Router Lifetime:\s+(\d+)s", line)
+ ra['cur_hop_limit'] = m.group(1)
+ ra['M'] = m.group(2)
+ ra['O'] = m.group(3)
+ ra['lifetime'] = m.group(4)
+ state = 'RACURHOPLIMIT'
+ elif state == 'RACURHOPLIMIT':
+ ra['preference'] = re.match("^\s+Default "
+ + "Router Preference:\s+(.*)",
+ line).group(1)
+ state = 'RAPREFERENCE'
+ elif state == 'RAPREFERENCE':
+ m = re.match("^\s+Reachable Time:\s+(\d+)ms, "
+ + "Retrans Timer:\s+(\d+)ms", line)
+ ra['reachable_time'] = m.group(1)
+ ra['retrans_timer'] = m.group(2)
+ state = 'RAOPTIONS'
+ elif state == 'RAOPTIONS':
+ is_addr_proposal = re.match("^\s+Address "
+ + "proposals", line)
+ is_rdns = re.match("^\s+rdns: (.*), "
+ + "lifetime:\s+(\d+)", line)
+ is_search = re.match("^\s+search: (.*), "
+ + "lifetime:\s+(\d+)", line)
+ is_prefix = re.match("^\s+prefix:\s+(.*)", line)
+ if is_addr_proposal:
+ state = 'ADDRESS_PROPOSAL'
+ elif is_prefix:
+ prefix = dict()
+ ra['prefixes'].append(prefix)
+ prefix['prefix'] = is_prefix.group(1)
+ state = 'PREFIX'
+ elif is_rdns:
+ rdns = dict()
+ ra['rdns'].append(rdns)
+ rdns['addr'] = is_rdns.group(1)
+ rdns['lifetime'] = is_rdns.group(2)
+ state = 'RAOPTIONS'
+ elif is_search:
+ search = dict()
+ ra['search'].append(search)
+ search['search'] = is_search.group(1)
+ search['lifetime'] = is_search.group(2)
+ state = 'RAOPTIONS'
+ elif state == 'PREFIX':
+ m = re.match("^\s+On-link: (\d+), "
+ + "Autonomous address-configuration: "
+ + "(\d+)", line)
+ prefix['on_link'] = m.group(1)
+ prefix['autonomous'] = m.group(2)
+ state = 'PREFIX_ONLINK'
+ elif state == 'PREFIX_ONLINK':
+ m = re.match("^\s+vltime:\s+(\d+|infinity), "
+ + "pltime:\s+(\d+|infinity)", line)
+ prefix['vltime'] = m.group(1)
+ prefix['pltime'] = m.group(2)
+ state = 'RAOPTIONS'
+ elif state == 'ADDRESS_PROPOSAL':
+ is_id = re.match("^\s+id:\s+(\d+), "
+ + "state:\s+(.+), privacy: (.+)", line)
+ is_defrouter = re.match("\s+Default router "
+ + "proposals", line)
+ if is_id:
+ addr_proposal = dict()
+ self.addr_proposals.append(
+ addr_proposal)
+ addr_proposal['id'] = is_id.group(1)
+ addr_proposal['state'] = is_id.group(2)
+ addr_proposal['privacy'] = \
+ is_id.group(3)
+ state = 'ADDRESS_PROPOSAL_LIFETIME'
+ elif is_defrouter:
+ state = 'DEFAULT_ROUTER'
+ elif state == 'ADDRESS_PROPOSAL_LIFETIME':
+ m = re.match("^\s+vltime:\s+(\d+), "
+ + "pltime:\s+(\d+), "
+ + "timeout:\s+(\d+)s", line)
+ addr_proposal['vltime'] = m.group(1)
+ addr_proposal['pltime'] = m.group(2)
+ addr_proposal['timeout'] = m.group(3)
+ state = 'ADDRESS_PROPOSAL_UPDATED'
+ elif state == 'ADDRESS_PROPOSAL_UPDATED':
+ m = re.match("^\s+updated:\s+(.+);\s+(\d+)s "
+ + "ago", line)
+ addr_proposal['updated'] = m.group(1)
+ addr_proposal['updated_ago'] = m.group(2)
+ state = 'ADDRESS_PROPOSAL_ADDR_PREFIX'
+ elif state == 'ADDRESS_PROPOSAL_ADDR_PREFIX':
+ m = re.match("^\s+(.+), (.+)", line)
+ addr_proposal['addr'] = m.group(1)
+ addr_proposal['prefix'] = m.group(2)
+ state = 'ADDRESS_PROPOSAL'
+ elif state == 'DEFAULT_ROUTER':
+ m = re.match("^\s+id:\s+(\d+), state:\s+(.+)",
+ line)
+ if m:
+ def_router_proposal = dict()
+ self.def_router_proposals.append(
+ def_router_proposal)
+ def_router_proposal['id'] = m.group(1)
+ def_router_proposal['state'] = \
+ m.group(2)
+ state = 'DEFAULT_ROUTER_PROPOSAL'
+ else:
+ state = 'DONE'
+ elif state == 'DEFAULT_ROUTER_PROPOSAL':
+ m = re.match("^\s+router: (.+)", line)
+ def_router_proposal['router'] = m.group(1)
+ state = 'DEFAULT_ROUTER_PROPOSAL_ROUTER'
+ elif state == 'DEFAULT_ROUTER_PROPOSAL_ROUTER':
+ m = re.match("^\s+router lifetime:\s+(\d)",
+ line)
+ def_router_proposal['lifetime'] = m.group(1)
+ state = 'DEFAULT_ROUTER_PROPOSAL_LIFETIME'
+ elif state == 'DEFAULT_ROUTER_PROPOSAL_LIFETIME':
+ m = re.match("^\s+Preference: (.+)", line)
+ def_router_proposal['pref'] = m.group(1)
+ state = 'DEFAULT_ROUTER_PROPOSAL_PREF'
+ elif state == 'DEFAULT_ROUTER_PROPOSAL_PREF':
+ m = re.match("^\s+updated: ([^;]+); (\d+)s ago,"
+ + " timeout:\s+(\d+)", line)
+ def_router_proposal['updated'] = m.group(1)
+ def_router_proposal['ago'] = m.group(2)
+ def_router_proposal['timeout'] = m.group(3)
+ state = 'DEFAULT_ROUTER'
+ elif state == 'DONE':
+ raise ValueError("got additional data: "
+ + "{0}".format(line))
diff --git a/regress/sbin/slaacd/process_ra.py b/regress/sbin/slaacd/process_ra.py
new file mode 100644
index 00000000000..13cd155b06c
--- /dev/null
+++ b/regress/sbin/slaacd/process_ra.py
@@ -0,0 +1,46 @@
+from IfInfo import IfInfo
+from Slaacctl import ShowInterface
+import subprocess
+import sys
+from scapy.all import *
+import unittest
+
+
+rtadv_if = IfInfo(sys.argv[1])
+slaac_if = IfInfo(sys.argv[2])
+sock = sys.argv[3]
+
+eth = Ether(src=rtadv_if.mac)
+ip = IPv6(dst="ff02::1", src=rtadv_if.ll)
+ra = ICMPv6ND_RA(prf='Medium (default)', routerlifetime=1800)
+pref = ICMPv6NDOptPrefixInfo(prefixlen=64, prefix='2001:db8:1::',
+ validlifetime=2592000, preferredlifetime=604800, L=1, A=1)
+mtu = ICMPv6NDOptMTU(mtu=1500)
+rdnss = ICMPv6NDOptRDNSS(lifetime=86400, dns=['2001:db8:53::a',
+ '2001:db8:53::b'])
+dnssl = ICMPv6NDOptDNSSL(lifetime=86400, searchlist=['invalid', 'home.invalid'])
+
+p = eth/ip/ra/pref/mtu/rdnss/dnssl
+
+sendp(p, iface=rtadv_if.ifname, verbose=0)
+
+slaac_show_interface = ShowInterface(slaac_if.ifname, sock, debug=0)
+
+
+class TestRouterAdvertisementParsing(unittest.TestCase):
+ def test_number_ras(self):
+ self.assertEqual(len(slaac_show_interface.RAs), 1)
+
+ def test_number_addr_proposals(self):
+ self.assertEqual(len(slaac_show_interface.addr_proposals), 2)
+
+ def test_number_def_router_proposals(self):
+ self.assertEqual(len(
+ slaac_show_interface.def_router_proposals), 1)
+
+if __name__ == '__main__':
+ suite = unittest.TestLoader().loadTestsFromTestCase(
+ TestRouterAdvertisementParsing)
+ if not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful():
+ print slaac_show_interface
+ sys.exit(1)