diff --git a/autotests/testNetconfig/ssidTKIP.conf b/autotests/testNetconfig/ap-main.conf similarity index 83% rename from autotests/testNetconfig/ssidTKIP.conf rename to autotests/testNetconfig/ap-main.conf index 11ef15f0..0ec7fdfa 100644 --- a/autotests/testNetconfig/ssidTKIP.conf +++ b/autotests/testNetconfig/ap-main.conf @@ -1,6 +1,6 @@ hw_mode=g channel=1 -ssid=ssidTKIP +ssid=ap-main wpa=1 wpa_pairwise=TKIP diff --git a/autotests/testNetconfig/ap-ns1.conf b/autotests/testNetconfig/ap-ns1.conf new file mode 100644 index 00000000..5459173f --- /dev/null +++ b/autotests/testNetconfig/ap-ns1.conf @@ -0,0 +1,7 @@ +hw_mode=g +channel=1 +ssid=ap-ns1 + +wpa=1 +wpa_pairwise=TKIP +wpa_passphrase=secret123 diff --git a/autotests/testNetconfig/connection_test.py b/autotests/testNetconfig/connection_test.py index 5481e624..aebb0a2e 100644 --- a/autotests/testNetconfig/connection_test.py +++ b/autotests/testNetconfig/connection_test.py @@ -13,6 +13,7 @@ import testutil from config import ctx import os import socket +import datetime, time class Test(unittest.TestCase): @@ -27,6 +28,14 @@ class Test(unittest.TestCase): return True + def get_ll_addrs6(ns, ifname): + show_ip = ns.start_process(['ip', 'addr', 'show', ifname]) + show_ip.wait() + for l in show_ip.out.split('\n'): + if 'inet6 fe80::' in l: + return socket.inet_pton(socket.AF_INET6, l.split(None, 1)[1].split('/', 1)[0]) + return None + wd = IWD(True) psk_agent = PSKAgent("secret123") @@ -35,7 +44,7 @@ class Test(unittest.TestCase): devices = wd.list_devices(1) device = devices[0] - ordered_network = device.get_ordered_network('ssidTKIP') + ordered_network = device.get_ordered_network('ap-ns1') self.assertEqual(ordered_network.type, NetworkType.psk) @@ -46,18 +55,21 @@ class Test(unittest.TestCase): condition = 'obj.state == DeviceState.connected' wd.wait_for_object_condition(device, condition) + connect_time = time.time() testutil.test_iface_operstate() - testutil.test_ifaces_connected() testutil.test_ip_address_match(device.name, '192.168.1.10', 17, 24) ctx.non_block_wait(check_addr, 10, device, exception=Exception("IPv6 address was not set")) + # Cannot use test_ifaces_connected() across namespaces (implementation details) + testutil.test_ip_connected(('192.168.1.10', ctx), ('192.168.1.1', self.ns1)) + ifname = str(device.name) - router_ll_addr = [addr for addr, _, _ in testutil.get_addrs6(self.hapd.ifname) if addr[0:2] == b'\xfe\x80'][0] + router_ll_addr = get_ll_addrs6(self.ns1, self.hapd.ifname) # Since we're in an isolated VM with freshly created interfaces we know any routes - # will have been created by IWD and don't have to allow for pre-existing routes + # will have been created by IWD and we don't have to allow for pre-existing routes # in the table. # Flags: 1=RTF_UP, 2=RTF_GATEWAY expected_routes4 = { @@ -94,6 +106,67 @@ class Test(unittest.TestCase): # of our log since we care about the end result here. self.assertEqual(expected_rclog, entries[-3:]) + leases_file = self.parse_lease_file('/tmp/dhcpd.leases', socket.AF_INET) + lease = leases_file['leases'][socket.inet_pton(socket.AF_INET, '192.168.1.10')] + self.assertEqual(lease['state'], 'active') + self.assertTrue(lease['starts'] < connect_time) + self.assertTrue(lease['ends'] > connect_time) + # The T1 is 15 seconds per dhcpd.conf. This is the approximate interval between lease + # renewals we should see from the client (+/- 1 second + some jitter). Wait a little + # less than twice that time (25s) so that we can expect the lease was renewed strictly + # once (not 0 or 2 times) by that time, check that the lease timestamps have changed by + # at least 10s so as to leave a lot of margin. + renew_time = lease['starts'] + 15 + now = time.time() + ctx.non_block_wait(lambda: False, renew_time + 10 - now, exception=False) + + leases_file = self.parse_lease_file('/tmp/dhcpd.leases', socket.AF_INET) + new_lease = leases_file['leases'][socket.inet_pton(socket.AF_INET, '192.168.1.10')] + self.assertEqual(new_lease['state'], 'active') + self.assertTrue(new_lease['starts'] > lease['starts'] + 10) + self.assertTrue(new_lease['starts'] < lease['starts'] + 25) + self.assertTrue(new_lease['ends'] > lease['ends'] + 10) + self.assertTrue(new_lease['ends'] < lease['ends'] + 25) + + # Now wait another T1 seconds but don't let our DHCP client get its REQUEST out this + # time so as to test renew timeouts and resends. The retry interval is 60 seconds + # since (T2 - T1) / 2 is shorter than 60s. It is now about 10s since the last + # renewal or 5s before the next DHCPREQUEST frame that is going to be lost. We'll + # wait T1 seconds, so until about 10s after the failed attempt, we'll check that + # there was no renewal by that time, just in case, and we'll reenable frame delivery. + # We'll then wait another 60s and we should see the lease has been successfully + # renewed some 10 seconds earlier on the 1st DHCPREQUEST retransmission. + # + # We can't use hswim to block the frames from reaching the AP because we'd lose + # beacons and get disconnected. We also can't drop our subnet route or IP address + # because IWD's sendto() call would synchronously error out and the DHCP client + # would just give up. Add a false route to break routing to 192.168.1.1 and delete + # it afterwards. + os.system('ip route add 192.168.1.1/32 dev ' + ifname + ' via 192.168.1.100 preference 0') + + lease = new_lease + renew_time = lease['starts'] + 15 + now = time.time() + ctx.non_block_wait(lambda: False, renew_time + 10 - now, exception=False) + + leases_file = self.parse_lease_file('/tmp/dhcpd.leases', socket.AF_INET) + new_lease = leases_file['leases'][socket.inet_pton(socket.AF_INET, '192.168.1.10')] + self.assertEqual(new_lease['starts'], lease['starts']) + + os.system('ip route del 192.168.1.1/32 dev ' + ifname + ' via 192.168.1.100 preference 0') + + retry_time = lease['starts'] + 75 + now = time.time() + ctx.non_block_wait(lambda: False, retry_time + 10 - now, exception=False) + + leases_file = self.parse_lease_file('/tmp/dhcpd.leases', socket.AF_INET) + new_lease = leases_file['leases'][socket.inet_pton(socket.AF_INET, '192.168.1.10')] + self.assertEqual(new_lease['state'], 'active') + self.assertTrue(new_lease['starts'] > lease['starts'] + 70) + self.assertTrue(new_lease['starts'] < lease['starts'] + 85) + self.assertTrue(new_lease['ends'] > lease['ends'] + 70) + self.assertTrue(new_lease['ends'] < lease['ends'] + 85) + device.disconnect() condition = 'not obj.connected' @@ -116,25 +189,27 @@ class Test(unittest.TestCase): except: pass - hapd = HostapdCLI() - cls.hapd = hapd + cls.ns1 = ctx.get_namespace('ns1') + cls.hapd = HostapdCLI('ap-ns1.conf') # TODO: This could be moved into test-runner itself if other tests ever # require this functionality (p2p, FILS, etc.). Since its simple # enough it can stay here for now. - ctx.start_process(['ip', 'addr','add', '192.168.1.1/255.255.128.0', - 'dev', hapd.ifname,]).wait() - ctx.start_process(['touch', '/tmp/dhcpd.leases']).wait() - cls.dhcpd_pid = ctx.start_process(['dhcpd', '-f', '-cf', '/tmp/dhcpd.conf', - '-lf', '/tmp/dhcpd.leases', - hapd.ifname], cleanup=remove_lease4) + cls.ns1.start_process(['ip', 'addr','add', '192.168.1.1/17', + 'dev', cls.hapd.ifname]).wait() + cls.ns1.start_process(['touch', '/tmp/dhcpd.leases']).wait() + cls.dhcpd_pid = cls.ns1.start_process(['dhcpd', '-f', '-d', '-cf', '/tmp/dhcpd.conf', + '-lf', '/tmp/dhcpd.leases', + cls.hapd.ifname], cleanup=remove_lease4) - ctx.start_process(['ip', 'addr', 'add', '3ffe:501:ffff:100::1/72', - 'dev', hapd.ifname]).wait() - ctx.start_process(['touch', '/tmp/dhcpd6.leases']).wait() - cls.dhcpd6_pid = ctx.start_process(['dhcpd', '-6', '-f', '-cf', '/tmp/dhcpd-v6.conf', - '-lf', '/tmp/dhcpd6.leases', - hapd.ifname], cleanup=remove_lease6) - ctx.start_process(['sysctl', 'net.ipv6.conf.' + hapd.ifname + '.forwarding=1']).wait() + cls.ns1.start_process(['ip', 'addr', 'add', '3ffe:501:ffff:100::1/72', + 'dev', cls.hapd.ifname]).wait() + cls.ns1.start_process(['touch', '/tmp/dhcpd6.leases']).wait() + cls.dhcpd6_pid = cls.ns1.start_process(['dhcpd', '-6', '-f', '-d', + '-cf', '/tmp/dhcpd-v6.conf', + '-lf', '/tmp/dhcpd6.leases', + cls.hapd.ifname], cleanup=remove_lease6) + cls.ns1.start_process(['sysctl', + 'net.ipv6.conf.' + cls.hapd.ifname + '.forwarding=1']).wait() # Send out Router Advertisements telling clients to use DHCPv6. # Note trying to send the RAs from the router's global IPv6 address by adding a # "AdvRASrcAddress { 3ffe:501:ffff:100::1; };" line will fail because the client @@ -142,7 +217,7 @@ class Test(unittest.TestCase): # with a non-link-local gateway address that is present on another interface in the # same namespace. config = open('/tmp/radvd.conf', 'w') - config.write('interface ' + hapd.ifname + ''' { + config.write('interface ' + cls.hapd.ifname + ''' { AdvSendAdvert on; AdvManagedFlag on; prefix 3ffe:501:ffff:100::/72 { AdvAutonomous off; }; @@ -151,7 +226,8 @@ class Test(unittest.TestCase): route 3ffe:501:ffff:500::/66 { AdvRoutePreference high; }; };''') config.close() - cls.radvd_pid = ctx.start_process(['radvd', '-n', '-d5', '-p', '/tmp/radvd.pid', '-C', '/tmp/radvd.conf']) + cls.radvd_pid = cls.ns1.start_process(['radvd', '-n', '-d5', + '-p', '/tmp/radvd.pid', '-C', '/tmp/radvd.conf']) cls.orig_path = os.environ['PATH'] os.environ['PATH'] = '/tmp/test-bin:' + os.environ['PATH'] @@ -160,14 +236,83 @@ class Test(unittest.TestCase): @classmethod def tearDownClass(cls): IWD.clear_storage() - ctx.stop_process(cls.dhcpd_pid) + cls.ns1.stop_process(cls.dhcpd_pid) cls.dhcpd_pid = None - ctx.stop_process(cls.dhcpd6_pid) + cls.ns1.stop_process(cls.dhcpd6_pid) cls.dhcpd6_pid = None - ctx.stop_process(cls.radvd_pid) + cls.ns1.stop_process(cls.radvd_pid) cls.radvd_pid = None os.system('rm -rf /tmp/radvd.conf /tmp/resolvconf.log /tmp/test-bin') os.environ['PATH'] = cls.orig_path + @staticmethod + def parse_lease_file(path, family): + file = open(path, 'r') + lines = file.readlines() + file.close() + + stack = [[]] + statement = [] + token = '' + for line in lines: + whitespace = False + quote = False + for ch in line: + if not quote and ch in ' \t\r\n;{}=#': + if len(token): + statement.append(token) + token = '' + if not quote and ch in ';{}': + if len(statement): + stack[-1].append(statement) + statement = [] + if ch == '"': + quote = not quote + elif quote or ch not in ' \t\r\n;{}#': + token += ch + if ch == '#': + break + elif ch == '{': + stack.append([]) + elif ch == '}': + statements = stack.pop() + stack[-1][-1].append(statements) + if len(token): + statement.append(token) + token = '' + if len(statement): + stack[-1].append(statement) + statements = stack.pop(0) + if len(stack): + raise Exception('Unclosed block(s)') + + contents = {'leases':{}} + for s in statements: + if s[0] == 'lease': + ip = socket.inet_pton(family, s[1]) + lease = {} + for param in s[2]: + if param[0] in ('starts', 'ends', 'tstp', 'tsfp', 'atsfp', 'cltt'): + weekday = param[1] + year, month, day = param[2].split('/') + hour, minute, second = param[3].split(':') + dt = datetime.datetime( + int(year), int(month), int(day), + int(hour), int(minute), int(second), + tzinfo=datetime.timezone.utc) + lease[param[0]] = dt.timestamp() + elif param[0:2] == ['binding', 'state']: + lease['state'] = param[2] + elif param[0:2] == ['hardware', 'ethernet']: + lease['hwaddr'] = bytes([int(v, 16) for v in param[2].split(':')]) + elif param[0] in ('preferred-life', 'max-life'): + lease[param[0]] = int(param[1]) + elif param[0] in ('client-hostname'): + lease[param[0]] = param[1] + contents['leases'][ip] = lease # New entries overwrite older ones + elif s[0] == 'server-duid': + contents[s[0]] = s[1] + return contents + if __name__ == '__main__': unittest.main(exit=True) diff --git a/autotests/testNetconfig/dhcpd.conf b/autotests/testNetconfig/dhcpd.conf index d8a9d24c..0a154637 100644 --- a/autotests/testNetconfig/dhcpd.conf +++ b/autotests/testNetconfig/dhcpd.conf @@ -1,5 +1,15 @@ -default-lease-time 600; # 10 minutes -max-lease-time 7200; # 2 hours +default-lease-time 120; # 2 minutes +min-lease-time 120; # 2 minutes +max-lease-time 120; # 2 minutes +option dhcp-renewal-time 15; # 15 secs for T1 +# We set a relatively low lease lifetime of 2 minutes but our renewal interval +# (T1) is still unproportionally low to speed the test up -- 12% instead of the +# default 50% lifetime value. We need a lifetime in the order of minutes +# because minimum lease renewal retry interval is 60s per spec. However by +# default dhcpd will not renew leases that are newer than 25% their lifetime. +# Set that threshold to 1% so that we can verify that the lease is renewed +# without waiting too long. +dhcp-cache-threshold 1; option broadcast-address 192.168.127.255; option routers 192.168.1.254; diff --git a/autotests/testNetconfig/hw.conf b/autotests/testNetconfig/hw.conf index d5adc9ad..edf656d6 100644 --- a/autotests/testNetconfig/hw.conf +++ b/autotests/testNetconfig/hw.conf @@ -1,9 +1,11 @@ [SETUP] -num_radios=3 +num_radios=4 start_iwd=0 [HOSTAPD] -rad0=ssidTKIP.conf +rad2=ap-main.conf +rad3=ap-ns1.conf [NameSpaces] -ns0=rad2 +ns0=rad0 +ns1=rad3 diff --git a/autotests/testNetconfig/ssidTKIP.psk b/autotests/testNetconfig/static.psk similarity index 100% rename from autotests/testNetconfig/ssidTKIP.psk rename to autotests/testNetconfig/static.psk diff --git a/autotests/testNetconfig/static_test.py b/autotests/testNetconfig/static_test.py index d9f0b9cb..01d694ca 100644 --- a/autotests/testNetconfig/static_test.py +++ b/autotests/testNetconfig/static_test.py @@ -32,7 +32,7 @@ class Test(unittest.TestCase): dev1 = wd.list_devices(1)[0] dev2 = wd_ns0.list_devices(1)[0] - ordered_network = dev1.get_ordered_network('ssidTKIP') + ordered_network = dev1.get_ordered_network('ap-main') self.assertEqual(ordered_network.type, NetworkType.psk) @@ -80,7 +80,7 @@ class Test(unittest.TestCase): # of the log since we care about the end result here. self.assertEqual(expected_rclog, entries[-3:]) - ordered_network = dev2.get_ordered_network('ssidTKIP') + ordered_network = dev2.get_ordered_network('ap-main') condition = 'not obj.connected' wd_ns0.wait_for_object_condition(ordered_network.network_object, condition) @@ -117,7 +117,7 @@ class Test(unittest.TestCase): except: pass - hapd = HostapdCLI() + hapd = HostapdCLI('ap-main.conf') # TODO: This could be moved into test-runner itself if other tests ever # require this functionality (p2p, FILS, etc.). Since it's simple # enough it can stay here for now. @@ -127,7 +127,7 @@ class Test(unittest.TestCase): cls.dhcpd_pid = ctx.start_process(['dhcpd', '-f', '-cf', '/tmp/dhcpd.conf', '-lf', '/tmp/dhcpd.leases', hapd.ifname], cleanup=remove_lease) - IWD.copy_to_storage('ssidTKIP.psk', '/tmp/storage') + IWD.copy_to_storage('static.psk', '/tmp/storage', 'ap-main.psk') cls.orig_path = os.environ['PATH'] os.environ['PATH'] = '/tmp/test-bin:' + os.environ['PATH'] diff --git a/autotests/util/hostapd.py b/autotests/util/hostapd.py index 758427fe..3ae8ff89 100644 --- a/autotests/util/hostapd.py +++ b/autotests/util/hostapd.py @@ -33,7 +33,7 @@ class HostapdCLI(object): _instances = WeakValueDictionary() def __new__(cls, config=None, *args, **kwargs): - hapd = ctx.hostapd[config] + hapd = ctx.get_hapd_instance(config) if not config: config = hapd.config @@ -58,10 +58,10 @@ class HostapdCLI(object): if not ctx.hostapd: raise Exception("No hostapd instances are configured") - if not config and len(ctx.hostapd.instances) > 1: + if not config and sum([len(hapd.instances) for hapd in ctx.hostapd]) > 1: raise Exception('config must be provided if more than one hostapd instance exists') - hapd = ctx.hostapd[config] + hapd = ctx.get_hapd_instance(config) self.interface = hapd.intf self.config = hapd.config