diff options
author | Florian Obser <florian@cvs.openbsd.org> | 2017-10-11 17:21:45 +0000 |
---|---|---|
committer | Florian Obser <florian@cvs.openbsd.org> | 2017-10-11 17:21:45 +0000 |
commit | c2f2d8d47222b02535d92dd23123946c9f300717 (patch) | |
tree | 97272d42109d24d1ec7f768f32e7e4096eea98dd | |
parent | 4823558b926229a8c8e503a67e43c5570e65c71f (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.py | 44 | ||||
-rw-r--r-- | regress/sbin/slaacd/Makefile | 9 | ||||
-rw-r--r-- | regress/sbin/slaacd/Slaacctl.py | 235 | ||||
-rw-r--r-- | regress/sbin/slaacd/process_ra.py | 46 |
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) |