def test_authentication_sessions_till_disconnect(self): mac = 'aa:bb:cc:dd:ee:01' session.seen(mac=mac, vlan='eth0.1', ip='1.2.3.1') session.add_authentication_session(mac, source='test') self.assertTrue(session.source_in_authentication_sessions(mac, 'test')) self.assertEqual(session.get_authentication_sessions(mac), [{ 'till_disconnect': True, 'source': 'test' }]) self.assertEqual( session.get_authentication_sessions(mac, source='test'), [{ 'till_disconnect': True, 'source': 'test' }]) self.assertEqual( session.get_authentication_sessions(mac, till_disconnect=False), []) session.end(mac) self.assertFalse(session.source_in_authentication_sessions( mac, 'test')) self.assertEqual(session.get_authentication_sessions(mac), [])
def test_seen_new_mac_new_vlan_new_ip(self, notify_new_MAC_session, notify_new_VLAN_session, notify_new_IP_session): response = session.seen(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.4') self.assertEqual(response, (True, True, True)) self.assertEqual(notify_new_MAC_session.call_count, 0) self.assertEqual(notify_new_VLAN_session.call_count, 0) self.assertEqual(notify_new_IP_session.call_count, 1) self.assertTrue(session.is_online(mac='aa:bb:cc:dd:ee:01')) self.assertTrue( session.is_online(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1')) self.assertTrue( session.is_online(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.4')) response = session.seen(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.4') self.assertEqual(response, (False, False, False)) self.assertEqual(notify_new_MAC_session.call_count, 0) self.assertEqual(notify_new_VLAN_session.call_count, 0) self.assertEqual(notify_new_IP_session.call_count, 1) # Not changed
def test_seen_known_mac(self, notify_new_MAC_session, notify_new_VLAN_session, notify_new_IP_session): session.seen(mac='aa:bb:cc:dd:ee:01') notify_new_MAC_session.reset_mock() response = session.seen(mac='aa:bb:cc:dd:ee:01') self.assertEqual(response, (False, False, False)) self.assertEqual(notify_new_MAC_session.call_count, 0) self.assertEqual(notify_new_VLAN_session.call_count, 0) self.assertEqual(notify_new_IP_session.call_count, 0) self.assertTrue(session.is_online(mac='aa:bb:cc:dd:ee:01'))
def set_port_of_mac(self, mac): ''' Will try to fond port of given ''' ports_with_new_macs = self.get_ports_with_new_macs() if ports_with_new_macs: device_ips = { port.pop('device_ip') for port in ports_with_new_macs } for device_ip in device_ips: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) mac_ports = loop.run_until_complete(self.get_macs_on_device(device_ip)) loop.close() port = mac_ports.get(mac, None) if port in ports_with_new_macs: session.seen(mac, port=port) port['device_ip'] = device_ip self.port_has_no_new_macs(port) break
async def get_assignments(request): ''' Will create new session for Mac and allow it on VLAN and on the net if authorized''' mac = extract_mac(request.get('Calling-Station-Id', None)) port = await find_port(request) session.seen(mac, port=port) auth_type = request.get('ELAN-Auth-Type', None) if auth_type == 'radius-mac': authz = await asyncio.get_event_loop().run_in_executor( None, nac.checkAuthz, mac) elif auth_type == 'radius-dot1x': authentication_provider = request.get('ELAN-Auth-Provider') login = request.get('ELAN-Login', request.get('User-Name')) authz = await asyncio.get_event_loop().run_in_executor( None, functools.partial(nac.newAuthz, mac, no_duplicate_source=True, source='radius-dot1x', till_disconnect=True, authentication_provider=authentication_provider, login=login)) if not authz: # log no assignment rule matched.... event = Event(event_type='device-not-authorized', source=auth_type, level='warning') event.add_data('mac', mac, 'mac') event.add_data('port', port, 'port') if auth_type == 'radius-dot1x': event.add_data('authentication_provider', authentication_provider, 'authentication') event.add_data('login', login) event.notify() raise NotAuthorized return {'ELAN-Vlan-Id': str(authz.assign_vlan)}
def test_get_current_session_ids(self): session.seen(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.4') session.seen(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.5') session.seen(mac='aa:bb:cc:dd:ee:02', vlan='eth0.1') session.seen(mac='aa:bb:cc:dd:ee:02', vlan='eth0.2') session.seen(mac='aa:bb:cc:dd:ee:03') session_ids = session.get_current_session_ids() self.assertEqual( set(session_ids.keys()), { ('aa:bb:cc:dd:ee:01', 'eth0.1', '1.2.3.4'), ('aa:bb:cc:dd:ee:01', 'eth0.1', '1.2.3.5'), ('aa:bb:cc:dd:ee:01', 'eth0.1', None), ('aa:bb:cc:dd:ee:02', 'eth0.1', None), ('aa:bb:cc:dd:ee:02', 'eth0.2', None), ('aa:bb:cc:dd:ee:01', None, None), ('aa:bb:cc:dd:ee:02', None, None), ('aa:bb:cc:dd:ee:03', None, None), })
def test_seen_new_mac(self, notify_new_MAC_session, notify_new_VLAN_session, notify_new_IP_session, notify_MAC_port): response = session.seen(mac='aa:bb:cc:dd:ee:01') self.assertEqual(response, (True, False, False)) self.assertEqual(notify_new_MAC_session.call_count, 1) self.assertEqual(notify_new_VLAN_session.call_count, 0) self.assertEqual(notify_new_IP_session.call_count, 0) self.assertEqual(notify_MAC_port.call_count, 0) self.assertTrue(session.is_online(mac='aa:bb:cc:dd:ee:01'))
def test_end(self, notify_end_IP_session, notify_end_VLAN_session, notify_end_MAC_session): session.seen(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.1') session.seen(mac='aa:bb:cc:dd:ee:02', vlan='eth0.2', ip='1.2.3.2') session.seen(mac='aa:bb:cc:dd:ee:03', vlan='eth0.3', ip='1.2.3.3') session.end(mac='aa:bb:cc:dd:ee:01') self.assertFalse(session.is_online('aa:bb:cc:dd:ee:01')) self.assertFalse(session.is_online('aa:bb:cc:dd:ee:01', vlan='eth0.1')) self.assertFalse( session.is_online('aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.1')) self.assertEqual(notify_end_MAC_session.call_count, 1) session.end(mac='aa:bb:cc:dd:ee:02', vlan='eth0.2') self.assertTrue(session.is_online('aa:bb:cc:dd:ee:02')) self.assertFalse(session.is_online('aa:bb:cc:dd:ee:02', vlan='eth0.2')) self.assertFalse( session.is_online('aa:bb:cc:dd:ee:02', vlan='eth0.2', ip='1.2.3.2')) self.assertEqual(notify_end_VLAN_session.call_count, 1) session.end(mac='aa:bb:cc:dd:ee:03', vlan='eth0.3', ip='1.2.3.3') self.assertTrue(session.is_online('aa:bb:cc:dd:ee:03')) self.assertTrue(session.is_online('aa:bb:cc:dd:ee:03', vlan='eth0.3')) self.assertFalse( session.is_online('aa:bb:cc:dd:ee:03', vlan='eth0.3', ip='1.2.3.3')) self.assertEqual(notify_end_IP_session.call_count, 1)
async def parse_trap_str(self, trap_str, timeout=5): ''' parse the trap and return - trap type - port (ip???/interface) - mac if present ''' # if creds not known, poll switch # If creds , parse trap,-> smart way, trying each one... and saving the matches..., # -> parse trap... (create a thread just to process trap ?) # -> check if swicth/ switch port known and if not poll splitted_trap_str = trap_str.split('|', 3) trap_time = (datetime.datetime.strptime('{} {}'.format(*splitted_trap_str[0:2]), '%Y-%m-%d %H:%M:%S') - datetime.datetime(1970, 1, 1)).total_seconds() # Epoch snmp_connection_str = splitted_trap_str[2] # example of snmp_connection_str: UDP: [10.30.0.2]:56550->[10.30.0.5] -> grab what is enclosed in first brackets device_ip = snmp_connection_str.split(']', 1)[0].split('[', 1)[1] # Grab SNMP read credentials of device read_params = await self.get_read_params(device_ip) if not read_params: return try: trap = await asyncio.wait_for(self._parse_trap_str(device_ip, trap_str, read_params), timeout) except asyncio.TimeoutError: trap = None if trap and trap['trapType'] != 'unknown': ''' Trap types: - up: - trapIfIndex - down - trapIfIndex - mac: - trapOperation: - learnt - removed - unknown -> what do we do ? -> macsuck ? - trapIfIndex - trapMac - trapVlan - secureMacAddrViolation: - trapIfIndex - trapMac - trapVlan - dot11Deauthentication: - trapMac - wirelessIPS - trapMac - roaming - trapSSID - trapIfIndex - trapVlan - trapMac - trapClientUserName - trapConnectionType ''' if 'trapMac' in trap: if trap['trapType'] in ['secureMacAddrViolation', 'wirelessIPS', 'roaming'] or \ (trap['trapType'] == ['mac'] and trap['trapOperation'] == 'learnt'): port = await self.getPortFromIndex(device_ip, trap.get('trapIfIndex', None)) vlan = trap.get('vlan', None) session.seen(trap['trapMac'], vlan=vlan, port=port, time=trap_time) if port is None: DebugEvent(source='snmp-notification')\ .add_data('details', 'Port not found')\ .add_data('trap', trap)\ .add_data('trap_str', trap_str)\ .notify() elif trap['trapType'] == 'dot11Deauthentication'or \ (trap['trapType'] == ['mac'] and trap['trapOperation'] == 'removed'): session.end(mac=trap['trapMac'], time=trap_time) elif trap['trapType'] in ['up', 'down']: port = await self.getPortFromIndex(device_ip, trap['trapIfIndex']) if port: port['device_ip'] = device_ip if trap['trapType'] == 'up': # remember this port could have new ip. self.port_has_new_macs(port) else: self.port_has_no_new_macs(port) # remove macs that are no longer on port mac_ports = await self.get_macs_on_device(device_ip) for mac in session.port_macs(port): if mac not in mac_ports: session.end(mac) # TODO: Mark Port as potentially containing a new mac -> macksuck when new mac? else: event = Event(event_type='runtime-failure', source='snmp-notification', level='warning') event.add_data('ip', device_ip) event.notify()
def test_notify_current_sessions(self): session.seen(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.4') session.seen(mac='aa:bb:cc:dd:ee:01', vlan='eth0.1', ip='1.2.3.5') session.seen(mac='aa:bb:cc:dd:ee:02', vlan='eth0.1') session.seen(mac='aa:bb:cc:dd:ee:02', vlan='eth0.2') session.seen(mac='aa:bb:cc:dd:ee:02') session.seen(mac='aa:bb:cc:dd:ee:03') self.assertEqual(session.notify_current_sessions(), 5)
def test_seen_with_port(self, notify_MAC_port, notify_end_MAC_session): self.assertIsNone(session.mac_port('aa:bb:cc:dd:ee:01')) session.seen(mac='aa:bb:cc:dd:ee:01', port={ 'local_id': 1, 'interface': 'i1' }) self.assertTrue(session.is_online(mac='aa:bb:cc:dd:ee:01')) self.assertEqual(session.mac_port('aa:bb:cc:dd:ee:01'), { 'local_id': 1, 'interface': 'i1' }) self.assertEqual(notify_MAC_port.call_count, 0) self.assertEqual(notify_end_MAC_session.call_count, 0, 'No session end on same port') session.seen(mac='aa:bb:cc:dd:ee:01', port={ 'local_id': 1, 'interface': 'i2' }) self.assertEqual(session.mac_port('aa:bb:cc:dd:ee:01'), { 'local_id': 1, 'interface': 'i2' }) self.assertEqual(notify_MAC_port.call_count, 0) self.assertEqual(notify_end_MAC_session.call_count, 1, 'session ended on change of port') session.seen(mac='aa:bb:cc:dd:ee:01', port={ 'local_id': 1, 'interface': None }) self.assertEqual(session.mac_port('aa:bb:cc:dd:ee:01'), { 'local_id': 1, 'interface': 'i2' }) self.assertEqual(notify_MAC_port.call_count, 0) self.assertEqual(notify_end_MAC_session.call_count, 1, 'Session end on port change') session.seen(mac='aa:bb:cc:dd:ee:01', port={ 'local_id': 2, 'interface': None, 'ssid': 'SSID' }) self.assertEqual(session.mac_port('aa:bb:cc:dd:ee:01'), { 'local_id': 2, 'interface': None, 'ssid': 'SSID' }) self.assertEqual(notify_MAC_port.call_count, 0) self.assertEqual(notify_end_MAC_session.call_count, 2, 'No session end on same port') session.seen(mac='aa:bb:cc:dd:ee:01', port={ 'local_id': 2, 'interface': 'i3' }) self.assertEqual(session.mac_port('aa:bb:cc:dd:ee:01'), { 'local_id': 2, 'interface': 'i3', 'ssid': 'SSID' }) self.assertEqual(notify_MAC_port.call_count, 1) self.assertEqual(notify_end_MAC_session.call_count, 2, 'No session end on same port')
def process_packet(self, packet): try: # device sessions mac = packet.eth.src if session.ignore_MAC(mac): return nic = self.interfaces[int(packet.frame_info.interface_id)] vlan_id = 0 packet_vlan = getattr(packet, 'vlan', None) if packet_vlan: vlan_id = packet_vlan.id vlan = '{nic}.{vlan_id}'.format(nic=nic, vlan_id=vlan_id) epoch = int(float(packet.frame_info.time_epoch)) if packet.highest_layer == 'ARP' \ or \ packet.highest_layer == 'ICMPV6' and str(packet.icmpv6.type) in ('136', '135'): # ('Neighbor Advertisement', 'Neighbor Solicitation') if packet.highest_layer == 'ARP': ip = packet.arp.src_proto_ipv4 else: ip = packet.ipv6.src if session.ignore_IP(ip): mac_added, vlan_added, _ip_added = session.seen(mac, vlan=vlan, time=epoch) else: mac_added, vlan_added, _ip_added = session.seen(mac, vlan=vlan, ip=ip, time=epoch) else: mac_added, vlan_added, _ip_added = session.seen(mac, vlan=vlan, time=epoch) tasks = [] # tasks to be launched in thread if mac_added: tasks.append( functools.partial(DeviceSnmpManager().set_port_of_mac, mac)) if vlan_added and nac.vlan_has_access_control(vlan): tasks.append( functools.partial(self.checkAuthzOnVlan, mac, vlan)) if tasks: task = threading.Thread( target=lambda: [task() for task in tasks]) task.start() source = packet.highest_layer # more meaningful name if source == 'BROWSER': source = 'NetBIOS' elif source == 'BOOTP': source = 'DHCPV4' # Hostname: grab it from netbios or dhcpv4 or dhcpv6 or mdns hostname = None try: hostname = str(packet.nbdgm.source_name) # ends with <??> p = re.compile('<..>$') hostname = p.sub('', hostname) except AttributeError: pass try: hostname = str(packet.bootp.option_hostname) except AttributeError: pass try: hostname = str(packet.dhcpv6.client_fqdn) except AttributeError: pass try: if int(packet.mdns.dns_flags_response) and int( packet.mdns.dns_flags_authoritative): mdns = packet.mdns try: a_list = mdns.dns_a.fields.copy() a_list.reverse() except AttributeError: a_list = [] try: aaaa_list = mdns.dns_aaaa.fields.copy() aaaa_list.reverse() except AttributeError: aaaa_list = [] resp_types = mdns.dns_resp_type.fields.copy() resp_types.reverse() for field in mdns.dns_resp_name.fields: name, *domain = field.showname_value.split('.') if domain == ['local']: resp_type = resp_types.pop().showname_value.split( ' ')[0] target = None if resp_type == 'A': target = a_list.pop().showname_value elif resp_type == 'AAAA': target = aaaa_list.pop().showname_value if target: if session.mac_has_ip_on_vlan( mac, target, vlan): hostname = name break # Only first one found... except AttributeError: pass if hostname: device.seen_hostname(mac, hostname, source) # DHCP fingerprint if source == 'DHCPV4': try: fingerprint = { 'request_list': ','.join( str(option.hex_value) for option in packet.bootp.option_request_list_item.fields), 'vendor': str(getattr(packet.bootp, 'option_vendor_class_id', '')) } except AttributeError: pass else: device.seen_fingerprint(mac, fingerprint, source, hostname) elif source == 'DHCPV6': try: fingerprint = { 'request_list': ','.join( str(option.hex_value) for option in packet.dhcpv6.requested_option_code.fields), 'vendor': str(getattr(packet.dhcpv6, 'vendorclass_data', '')), 'enterprise': str( getattr(packet.dhcpv6, 'vendorclass_enterprise', '')) } except AttributeError: pass else: device.seen_fingerprint(mac, fingerprint, source, hostname) except Exception: ExceptionEvent(source='device-tracker')\ .add_data('packet', str(packet))\ .notify()