From 907fed32ef066d76e1f667d8d3501f14b3139006 Mon Sep 17 00:00:00 2001 From: Michael Baird Date: Thu, 21 Feb 2019 16:01:35 +1300 Subject: [PATCH 1/3] send preemptive identity request on port up --- chewie/chewie.py | 56 +++++++++++++++++++++++++++++++++++++++++---- test/test_chewie.py | 38 +++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 5 deletions(-) diff --git a/chewie/chewie.py b/chewie/chewie.py index 8427c36d..2a18f72c 100644 --- a/chewie/chewie.py +++ b/chewie/chewie.py @@ -1,6 +1,10 @@ """Entry point for 802.1X speaker. """ +import random +from chewie.eap import Eap + +from chewie.mac_address import MacAddress from eventlet import sleep, GreenPool from eventlet.queue import Queue @@ -9,9 +13,9 @@ from chewie.radius_socket import RadiusSocket from chewie.eap_state_machine import FullEAPStateMachine from chewie.radius_lifecycle import RadiusLifecycle -from chewie.message_parser import MessageParser, MessagePacker +from chewie.message_parser import MessageParser, MessagePacker, IdentityMessage from chewie.event import EventMessageReceived, EventPortStatusChange -from chewie.utils import get_logger, MessageParseError +from chewie.utils import get_logger, MessageParseError, EapQueueMessage def unpack_byte_string(byte_string): @@ -19,9 +23,16 @@ def unpack_byte_string(byte_string): return "".join("%02x" % x for x in byte_string) +def get_random_id(): + return random.randint(0, 200) + + class Chewie: """Facilitates EAP supplicant and RADIUS server communication""" RADIUS_UDP_PORT = 1812 + PAE_GROUP_ADDRESS = MacAddress.from_string("01:80:C2:00:00:03") + + DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL = 60 def __init__(self, interface_name, logger=None, auth_handler=None, failure_handler=None, logoff_handler=None, @@ -46,7 +57,8 @@ def __init__(self, interface_name, logger=None, if chewie_id: self.chewie_id = chewie_id - self.state_machines = {} # mac: state_machine + self.state_machines = {} # port_id_str: { mac : state_machine} + self.port_to_eapol_id = {} # port_id: last ID used in preemptive identity request. self.eap_output_messages = Queue() self.radius_output_messages = Queue() @@ -127,6 +139,7 @@ def port_down(self, port_id): # all chewie needs to do is change its internal state. # faucet will remove the acls by itself. self.set_port_status(port_id, False) + self.port_to_eapol_id.pop(port_id, None) def port_up(self, port_id): """ @@ -135,7 +148,42 @@ def port_up(self, port_id): port_id (str): id of port. """ self.set_port_status(port_id, True) - # TODO send preemptive identity request. + + self.send_preemptive_identity_request(port_id) + self.timer_scheduler.call_later(self.DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL, + self.send_preemptive_identity_request_if_no_active_on_port, + port_id) + + def send_preemptive_identity_request_if_no_active_on_port(self, port_id): + """ + If there is no active (in progress, or in state success(2)) supplicant send out the + preemptive identity request message. + Args: + port_id (str): + """ + state_machines = self.state_machines.get(port_id, []) + for sm in state_machines.values(): + if sm.state not in [FullEAPStateMachine.LOGOFF, FullEAPStateMachine.LOGOFF2, + FullEAPStateMachine.DISABLED, FullEAPStateMachine.NO_STATE, + FullEAPStateMachine.FAILURE, FullEAPStateMachine.FAILURE2, + FullEAPStateMachine.TIMEOUT_FAILURE, + FullEAPStateMachine.TIMEOUT_FAILURE2]: + break + else: + self.send_preemptive_identity_request(port_id) + + def send_preemptive_identity_request(self, port_id): + """ + Message (EAP Identity Request) that notifies supplicant that port is using 802.1X + Args: + port_id (str): + + """ + _id = get_random_id() + data = IdentityMessage(self.PAE_GROUP_ADDRESS, _id, Eap.REQUEST, "") + self.port_to_eapol_id[port_id] = _id + self.eap_output_messages.put_nowait( + EapQueueMessage(data, self.PAE_GROUP_ADDRESS, MacAddress.from_string(port_id))) def set_port_status(self, port_id, status): port_id_str = str(port_id) diff --git a/test/test_chewie.py b/test/test_chewie.py index 7bcccfd9..423ae3b6 100644 --- a/test/test_chewie.py +++ b/test/test_chewie.py @@ -24,6 +24,7 @@ def patch_things(func): """decorator to mock patch socket operations and random number generators""" + @patch('chewie.chewie.get_random_id', get_random_id_helper) @patch('chewie.chewie.EapSocket', FakeEapSocket) @patch('chewie.chewie.RadiusSocket', FakeRadiusSocket) @patch('chewie.chewie.RadiusLifecycle.generate_request_authenticator', urandom_helper) @@ -41,15 +42,30 @@ def wrapper_setup_gen(self): global SUPPLICANT_REPLY_GENERATOR # pylint: disable=global-statement global RADIUS_REPLY_GENERATOR # pylint: disable=global-statement global URANDOM_GENERATOR # pylint: disable=global-statement + global GET_RANDOM_ID_GENERATOR SUPPLICANT_REPLY_GENERATOR = supplicant_replies_gen(_supplicant_replies) RADIUS_REPLY_GENERATOR = radius_replies_gen(_radius_replies) URANDOM_GENERATOR = urandom() + GET_RANDOM_ID_GENERATOR = fake_get_random_id() func(self) return wrapper_setup_gen return decorator_setup_gen +def fake_get_random_id(): + for i in [103, 0x99]: + yield i + + +GET_RANDOM_ID_GENERATOR = None + + +def get_random_id_helper(): # pylint: disable=unused-argument + """helper for urandom_generator""" + return next(GET_RANDOM_ID_GENERATOR) + + def supplicant_replies_gen(replies): """generator for packets supplicant sends""" for reply in replies: @@ -285,17 +301,37 @@ def test_success_dot1x(self): '00:00:00:00:00:01').state, FullEAPStateMachine.SUCCESS2) + @patch_things def test_port_status_changes(self): """test port status api""" # TODO what can actually be checked here? # the state machine tests already check the statemachine # could check that the preemptive identity request packet is sent. (once implemented) # for now just check api works under python version. - + global TO_SUPPLICANT + pool = eventlet.GreenPool() + pool.spawn(self.chewie.run) + eventlet.sleep(1) self.chewie.port_down("00:00:00:00:00:01") self.chewie.port_up("00:00:00:00:00:01") + # check preemptive sent directly after port up + out_packet = TO_SUPPLICANT.get() + self.assertEqual(out_packet, + bytes.fromhex('0180C2000003000000000001888e010000050167000501')) + + self.assertTrue(TO_SUPPLICANT.empty()) + + while not self.fake_scheduler.jobs: + eventlet.sleep(0.1) + self.fake_scheduler.run_jobs() + eventlet.sleep(0.1) + # check preemptive sent after + out_packet = TO_SUPPLICANT.get_nowait() + self.assertEqual(out_packet, + bytes.fromhex('0180C2000003000000000001888e010000050199000501')) + self.chewie.port_down("00:00:00:00:00:01") @patch_things From adf429ba0929db3a2a32f3de79129f981498af59 Mon Sep 17 00:00:00 2001 From: Michael Baird Date: Mon, 25 Feb 2019 10:41:28 +1300 Subject: [PATCH 2/3] id request --- chewie/chewie.py | 30 +++++++++++++++++++++++++++--- chewie/timer_scheduler.py | 2 ++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/chewie/chewie.py b/chewie/chewie.py index 2a18f72c..615c38ad 100644 --- a/chewie/chewie.py +++ b/chewie/chewie.py @@ -59,6 +59,8 @@ def __init__(self, interface_name, logger=None, self.state_machines = {} # port_id_str: { mac : state_machine} self.port_to_eapol_id = {} # port_id: last ID used in preemptive identity request. + self.port_status = {} # port_id: status (true=up, false=down) + self.port_to_identity_job = {} # port_id: timerJob self.eap_output_messages = Queue() self.radius_output_messages = Queue() @@ -139,6 +141,10 @@ def port_down(self, port_id): # all chewie needs to do is change its internal state. # faucet will remove the acls by itself. self.set_port_status(port_id, False) + + job = self.port_to_identity_job.get(port_id, None) + if job: + job.cancel() self.port_to_eapol_id.pop(port_id, None) def port_up(self, port_id): @@ -147,12 +153,14 @@ def port_up(self, port_id): Args: port_id (str): id of port. """ + self.logger.warning("port %s up", port_id) self.set_port_status(port_id, True) self.send_preemptive_identity_request(port_id) - self.timer_scheduler.call_later(self.DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL, - self.send_preemptive_identity_request_if_no_active_on_port, - port_id) + self.port_to_identity_job[port_id] = self.timer_scheduler.call_later( + self.DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL, + self.send_preemptive_identity_request_if_no_active_on_port, + port_id) def send_preemptive_identity_request_if_no_active_on_port(self, port_id): """ @@ -161,6 +169,16 @@ def send_preemptive_identity_request_if_no_active_on_port(self, port_id): Args: port_id (str): """ + self.logger.warning("thinking about executing timer preemptive on port %s", port_id) + # schedule next request. + self.port_to_identity_job[port_id] = self.timer_scheduler.call_later( + self.DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL, + self.send_preemptive_identity_request_if_no_active_on_port, + port_id) + if not self.port_status.get(port_id, False): + self.logger.warning('cant send output on port %s is down', port_id) + return + state_machines = self.state_machines.get(port_id, []) for sm in state_machines.values(): if sm.state not in [FullEAPStateMachine.LOGOFF, FullEAPStateMachine.LOGOFF2, @@ -168,8 +186,10 @@ def send_preemptive_identity_request_if_no_active_on_port(self, port_id): FullEAPStateMachine.FAILURE, FullEAPStateMachine.FAILURE2, FullEAPStateMachine.TIMEOUT_FAILURE, FullEAPStateMachine.TIMEOUT_FAILURE2]: + self.logger.warning('port is active not sending on port %s', port_id) break else: + self.logger.warning("executing timer premptive on port %s", port_id) self.send_preemptive_identity_request(port_id) def send_preemptive_identity_request(self, port_id): @@ -184,9 +204,13 @@ def send_preemptive_identity_request(self, port_id): self.port_to_eapol_id[port_id] = _id self.eap_output_messages.put_nowait( EapQueueMessage(data, self.PAE_GROUP_ADDRESS, MacAddress.from_string(port_id))) + self.logger.warning("sending premptive on port %s", port_id) def set_port_status(self, port_id, status): port_id_str = str(port_id) + + self.port_status[port_id] = status + if port_id_str not in self.state_machines: self.state_machines[port_id_str] = {} diff --git a/chewie/timer_scheduler.py b/chewie/timer_scheduler.py index 3150444e..2ec97ef5 100644 --- a/chewie/timer_scheduler.py +++ b/chewie/timer_scheduler.py @@ -61,6 +61,7 @@ def call_later(self, timeout, func, *args): """ if not args: args = [] + self.logger.warning("submitted job %s %s", func.__name__, args) expiry_time = time.time() + timeout job = TimerJob(expiry_time, func, args) @@ -85,3 +86,4 @@ def run(self): self.sleep(1) except Exception as e: self.logger.exception(e) + self.logger.warning('timer_scheduler finished quuee') From 72a1cc0454acbdd99df77564b6a0491c4d7cc30a Mon Sep 17 00:00:00 2001 From: Michael Baird Date: Thu, 14 Mar 2019 16:19:44 +1300 Subject: [PATCH 3/3] send (delayed by 20 seconds) identity request on port up. --- chewie/chewie.py | 15 +++++++++++---- chewie/eap_state_machine.py | 8 ++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/chewie/chewie.py b/chewie/chewie.py index 615c38ad..96747e26 100644 --- a/chewie/chewie.py +++ b/chewie/chewie.py @@ -32,6 +32,7 @@ class Chewie: RADIUS_UDP_PORT = 1812 PAE_GROUP_ADDRESS = MacAddress.from_string("01:80:C2:00:00:03") + DEFAULT_PORT_UP_IDENTITY_REQUEST_WAIT_PERIOD = 20 DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL = 60 def __init__(self, interface_name, logger=None, @@ -156,9 +157,8 @@ def port_up(self, port_id): self.logger.warning("port %s up", port_id) self.set_port_status(port_id, True) - self.send_preemptive_identity_request(port_id) self.port_to_identity_job[port_id] = self.timer_scheduler.call_later( - self.DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL, + self.DEFAULT_PORT_UP_IDENTITY_REQUEST_WAIT_PERIOD, self.send_preemptive_identity_request_if_no_active_on_port, port_id) @@ -265,7 +265,8 @@ def receive_eap_messages(self): def send_eap_to_state_machine(self, eap, dst_mac): """sends an eap message to the state machine""" self.logger.info("eap EAP(): %s", eap) - state_machine = self.get_state_machine(eap.src_mac, dst_mac) + message_id = getattr(eap, 'message_id', -1) + state_machine = self.get_state_machine(eap.src_mac, dst_mac, message_id) event = EventMessageReceived(eap, dst_mac) state_machine.event(event) @@ -313,9 +314,10 @@ def get_state_machine_from_radius_packet_id(self, packet_id): """ return self.get_state_machine(**self.radius_lifecycle.packet_id_to_mac[packet_id]) - def get_state_machine(self, src_mac, port_id): + def get_state_machine(self, src_mac, port_id, message_id=-1): """Gets or creates if it does not already exist an FullEAPStateMachine for the src_mac. Args: + message_id (int): eap message id, -1 means none found. src_mac (MacAddress): who's to get. port_id (MacAddress): ID of the port where the src_mac is. @@ -336,4 +338,9 @@ def get_state_machine(self, src_mac, port_id): # TODO what if port is not actually enabled, but then how did they auth? state_machine.port_enabled = True self.state_machines[port_id_str][src_mac_str] = state_machine + else: + if message_id != -1 and message_id == self.port_to_eapol_id.get(port_id_str, -2): + self.logger.info('eap packet is response to chewie initiated authentication') + state_machine.eap_restart = True + state_machine.override_current_id = message_id return state_machine diff --git a/chewie/eap_state_machine.py b/chewie/eap_state_machine.py index 7e13df33..d336f5a9 100644 --- a/chewie/eap_state_machine.py +++ b/chewie/eap_state_machine.py @@ -295,6 +295,8 @@ class FullEAPStateMachine: last_req_data = None # EAP packet method_timeout = None # integer logoff = None # bool + # Non RFC 4137 + override_current_id = None # Lower Later to Stand-Alone Authenticator eap_resp = None # bool @@ -517,6 +519,9 @@ def success_state(self): def initialize_state(self): """Initializes variables when the state machine is activated""" self.current_id = None + if self.override_current_id: + self.current_id = self.override_current_id + self.override_current_id = None self.eap_success = False self.eap_fail = False self.eap_timeout = False @@ -781,6 +786,9 @@ def handle_success(self): self.logger.info('Yay authentication successful %s %s', self.src_mac, self.aaa_identity.identity) self.auth_handler(self.src_mac, str(self.port_id_mac)) + + self.aaa_eap_resp_data = None + # new authentication so cancel the old session timeout event if self.session_timeout_job: self.session_timeout_job.cancel()