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_PORT_UP_IDENTITY_REQUEST_WAIT_PERIOD = 20 DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL = 60 # pylint: disable=too-many-arguments def __init__(self, interface_name, logger=None, auth_handler=None, failure_handler=None, logoff_handler=None, radius_server_ip=None, radius_server_port=None, radius_server_secret=None, chewie_id=None): self.interface_name = interface_name self.log_name = Chewie.__name__ if logger: self.log_name = logger.name + "." + Chewie.__name__ self.logger = get_logger(self.log_name) self.auth_handler = auth_handler self.failure_handler = failure_handler self.logoff_handler = logoff_handler self.radius_server_ip = radius_server_ip self.radius_secret = radius_server_secret self.radius_server_port = self.RADIUS_UDP_PORT if radius_server_port: self.radius_server_port = radius_server_port self.radius_listen_ip = "0.0.0.0" self.radius_listen_port = 0 self.chewie_id = "44-44-44-44-44-44:" # used by the RADIUS Attribute # 'Called-Station' in Access-Request if chewie_id: self.chewie_id = chewie_id self.state_machines = {} # port_id_str: { mac : state_machine} self.port_to_eapol_id = { } # port_id: last ID used in preemptive identity request. # TODO for port_to_eapol_id - may want to set ID to null (-1...) if sent from the # state machine. 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() self.radius_lifecycle = RadiusLifecycle(self.radius_secret, self.chewie_id, self.logger) self.timer_scheduler = timer_scheduler.TimerScheduler(self.logger) self.eap_socket = None self.mab_socket = None self.pool = None self.eventlets = None self.radius_socket = None self.interface_index = None self.eventlets = [] def run(self): """setup chewie and start socket eventlet threads""" self.logger.info("Starting") self.setup_eap_socket() self.setup_mab_socket() self.setup_radius_socket() self.start_threads_and_wait() def running(self): # pylint: disable=no-self-use """Used to nicely exit the event loops""" return True def shutdown(self): """kill eventlets and quit""" for eventlet in self.eventlets: eventlet.kill() def start_threads_and_wait(self): """Start the thread and wait until they complete (hopefully never)""" self.pool = GreenPool() self.eventlets.append(self.pool.spawn(self.send_eap_messages)) self.eventlets.append(self.pool.spawn(self.receive_eap_messages)) self.eventlets.append(self.pool.spawn(self.receive_mab_messages)) self.eventlets.append(self.pool.spawn(self.send_radius_messages)) self.eventlets.append(self.pool.spawn(self.receive_radius_messages)) self.eventlets.append(self.pool.spawn(self.timer_scheduler.run)) self.pool.waitall() def auth_success(self, src_mac, port_id, period, *args, **kwargs): # pylint: disable=unused-variable """authentication shim between faucet and chewie Args: src_mac (MacAddress): the mac of the successful supplicant port_id (MacAddress): the 'mac' identifier of what switch port the success is on period (int): time (seconds) until the session times out. """ if self.auth_handler: self.auth_handler(src_mac, port_id, *args, **kwargs) self.port_to_identity_job[port_id] = self.timer_scheduler.call_later( period, self.reauth_port, src_mac, port_id) def auth_failure(self, src_mac, port_id): """failure shim between faucet and chewie Args: src_mac (MacAddress): the mac of the failed supplicant port_id (MacAddress): the 'mac' identifier of what switch port the failure is on""" if self.failure_handler: self.failure_handler(src_mac, port_id) def auth_logoff(self, src_mac, port_id): """logoff shim between faucet and chewie Args: src_mac (MacAddress): the mac of the logoff supplicant port_id (MacAddress): the 'mac' identifier of what switch port the logoff is on""" if self.logoff_handler: self.logoff_handler(src_mac, port_id) def port_down(self, port_id): """ should be called by faucet when port has gone down. Args: port_id (str): id of port. """ # 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 port_id in self.state_machines: del self.state_machines[port_id] if job: job.cancel() self.port_to_eapol_id.pop(port_id, None) def port_up(self, port_id): """ should be called by faucet when port has come up Args: port_id (str): id of port. """ self.logger.info("port %s up", port_id) self.set_port_status(port_id, True) self.port_to_identity_job[port_id] = self.timer_scheduler.call_later( self.DEFAULT_PORT_UP_IDENTITY_REQUEST_WAIT_PERIOD, 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): """ self.logger.debug( "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.debug('cant send output on port %s is down', port_id) return state_machines = self.state_machines.get(port_id, {}) # pylint: disable=invalid-name for sm in state_machines.values(): if sm.is_in_progress() or sm.is_success(): self.logger.debug('port is active not sending on port %s', port_id) break else: self.logger.debug("executing timer premptive on port %s", port_id) self.send_preemptive_identity_request(port_id) def send_preemptive_identity_request(self, port_id, state_machine=None): """ Message (EAP Identity Request) that notifies supplicant that port is using 802.1X Args: port_id (str): """ _id = get_random_id() # ID of preemptive reauth attempt must be different to ID of initial authentication. if state_machine is not None and hasattr(state_machine, 'current_id'): while _id == state_machine.current_id: _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))) self.logger.info("sending premptive on port %s with ID %s", port_id, _id) def reauth_port(self, src_mac, port_id): """ Send an Identity Request to src_mac, on port_id. prompting the supplicant to re authenticate. Args: src_mac (MacAddress): port_id (str): """ state_machine = self.state_machines.get(port_id, {}).get(str(src_mac), None) if state_machine and state_machine.is_success(): self.logger.info('reauthenticating src_mac: %s on port: %s', src_mac, port_id) self.send_preemptive_identity_request(port_id, state_machine) elif state_machine is None: self.logger.debug( 'not reauthing. state machine on port: %s, mac: %s is none', port_id, src_mac) else: self.logger.debug( "not reauthing, authentication is not in success(2) (state: %s)'", state_machine.state) def set_port_status(self, port_id, status): """ Send status of a port at port_id Args: 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] = {} for _, state_machine in self.state_machines[port_id_str].items(): event = EventPortStatusChange(status) state_machine.event(event) def setup_eap_socket(self): """Setup EAP socket""" log_prefix = "%s.EapSocket" % self.logger.name self.eap_socket = EapSocket(self.interface_name, log_prefix) self.eap_socket.setup() def setup_mab_socket(self): """Setup Mab socket""" log_prefix = "%s.MabSocket" % self.logger.name self.mab_socket = MabSocket(self.interface_name, log_prefix) self.mab_socket.setup() def setup_radius_socket(self): """Setup Radius socket""" log_prefix = "%s.RadiusSocket" % self.logger.name self.radius_socket = RadiusSocket(self.radius_listen_ip, self.radius_listen_port, self.radius_server_ip, self.radius_server_port, log_prefix) self.radius_socket.setup() self.logger.info("Radius Listening on %s:%d", self.radius_listen_ip, self.radius_listen_port) def send_eap_messages(self): """Send EAP messages to Supplicant forever.""" while self.running(): sleep(0) eap_queue_message = self.eap_output_messages.get() self.logger.info("Sending message %s from %s to %s", eap_queue_message.message, str(eap_queue_message.port_mac), str(eap_queue_message.src_mac)) self.eap_socket.send( MessagePacker.ethernet_pack(eap_queue_message.message, eap_queue_message.port_mac, eap_queue_message.src_mac)) def send_eth_to_state_machine(self, packed_message): """Send an ethernet frame to MAB State Machine""" ethernet_packet = EthernetPacket.parse(packed_message) port_id = ethernet_packet.dst_mac src_mac = ethernet_packet.src_mac self.logger.info("Sending MAC to MAB State Machine: %s", src_mac) message_id = -2 state_machine = self.get_state_machine(src_mac, port_id, message_id) event = EventMessageReceived(ethernet_packet, port_id) state_machine.event(event) # NOTE: Should probably throttle packets in once one is received def receive_eap_messages(self): """receive eap messages from supplicant forever.""" while self.running(): sleep(0) self.logger.info("waiting for eap.") packed_message = self.eap_socket.receive() self.logger.info("Received packed_message: %s", str(packed_message)) try: eap, dst_mac = MessageParser.ethernet_parse(packed_message) except MessageParseError as exception: self.logger.warning( "MessageParser.ethernet_parse threw exception.\n" " packed_message: '%s'.\n" " exception: '%s'.", packed_message, exception) continue self.logger.info("Received eap message: %s", str(eap)) self.send_eap_to_state_machine(eap, dst_mac) def receive_mab_messages(self): """Receive DHCP request for MAB.""" while self.running(): sleep(0) self.logger.info("waiting for MAB activity.") packed_message = self.mab_socket.receive() self.logger.info( "Received DHCP packet for MAB. packed_message: %s", str(packed_message)) self.send_eth_to_state_machine(packed_message) 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) message_id = getattr(eap, 'message_id', -1) state_machine = self.get_state_machine(eap.src_mac, dst_mac, message_id) # Check for response to preemptive_eap preemptive_eap_message_id = self.port_to_eapol_id.get(str(dst_mac), -2) if message_id != -1 and message_id == preemptive_eap_message_id: self.logger.debug( 'eap packet is response to chewie initiated authentication') event = EventPreemptiveEAPResponseMessageReceived( eap, dst_mac, preemptive_eap_message_id) else: event = EventMessageReceived(eap, dst_mac) state_machine.event(event) def send_radius_messages(self): """send RADIUS messages to RADIUS Server forever.""" while self.running(): sleep(0) radius_output_bits = self.radius_output_messages.get() packed_message = self.radius_lifecycle.process_outbound( radius_output_bits) self.radius_socket.send(packed_message) self.logger.info("sent radius message.") def receive_radius_messages(self): """receive RADIUS messages from RADIUS server forever.""" while self.running(): sleep(0) self.logger.info("waiting for radius.") packed_message = self.radius_socket.receive() try: radius = MessageParser.radius_parse(packed_message, self.radius_secret, self.radius_lifecycle) except MessageParseError as exception: self.logger.warning( "MessageParser.radius_parse threw exception.\n" " packed_message: '%s'.\n" " exception: '%s'.", packed_message, exception) continue self.logger.info("Received RADIUS message: %s", str(radius)) self.send_radius_to_state_machine(radius) def send_radius_to_state_machine(self, radius): """sends a radius message to the state machine""" event = self.radius_lifecycle.build_event_radius_message_received( radius) state_machine = self.get_state_machine_from_radius_packet_id( radius.packet_id) state_machine.event(event) def get_state_machine_from_radius_packet_id(self, packet_id): """Gets a FullEAPStateMachine from the RADIUS message packet_id Args: packet_id (int): id of the received RADIUS message Returns: FullEAPStateMachine """ return self.get_state_machine( **self.radius_lifecycle.packet_id_to_mac[packet_id]) # TODO change message_id functionality 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. Returns: FullEAPStateMachine """ port_id_str = str(port_id) src_mac_str = str(src_mac) port_state_machines = self.state_machines.get(port_id_str, None) if port_state_machines is None: self.state_machines[port_id_str] = {} self.logger.info("Port based state machines are as follows: %s", self.state_machines[port_id_str]) state_machine = self.state_machines[port_id_str].get(src_mac_str, None) if not state_machine and message_id == -2: # Do MAB self.logger.info("Creating MAB State Machine") log_prefix = "%s.SM - port: %s, client: %s" % ( self.logger.name, port_id_str, src_mac) state_machine = MacAuthenticationBypassStateMachine( self.radius_output_messages, src_mac, self.timer_scheduler, self.auth_success, self.auth_failure, log_prefix) self.state_machines[port_id_str][src_mac_str] = state_machine return state_machine if not state_machine: self.logger.info("Creating EAP FULL State Machine") log_prefix = "%s.SM - port: %s, client: %s" % ( self.logger.name, port_id_str, src_mac) state_machine = FullEAPStateMachine(self.eap_output_messages, self.radius_output_messages, src_mac, self.timer_scheduler, self.auth_success, self.auth_failure, self.auth_logoff, log_prefix) self.state_machines[port_id_str][src_mac_str] = state_machine self.logger.debug( "created new state machine for '%s' on port '%s'", src_mac_str, port_id_str) return state_machine
class SubmitMonitor: def __init__(self, logger: Logger, interval: float = 10): self._logger = logger self._q = Queue() self._ok_submits = 0 self._bad_submits = 0 self._connections = 0 self._interval = interval self._was_ok = self._ok_submits self._was_bad = self._bad_submits self._was_conn = self._connections self._labels = ['attacker_id', 'victim_id', 'task_id', 'submit_ok'] def add(self, ar: models.AttackResult) -> None: self._q.put_nowait(ar) if ar.submit_ok: self.inc_ok() else: self.inc_bad() def inc_ok(self) -> None: self._ok_submits += 1 def inc_bad(self) -> None: self._bad_submits += 1 def inc_conns(self) -> None: self._connections += 1 def _process_statistics(self) -> None: new_ok, new_bad = self._ok_submits, self._bad_submits new_conn = self._connections self._logger.info(f"OK: {new_ok - self._was_ok:>6}, " f"BAD: {new_bad - self._was_bad:>6}, " f"CONN: {new_conn - self._was_conn:>6}, " f"TOTOK: {new_ok:>6}, TOTBAD: {new_bad:>6}, " f"TOTCONN: {new_conn:>6}") self._was_ok = new_ok self._was_bad = new_bad self._was_conn = new_conn def _process_attacks_queue(self) -> None: conn = Connection(config.get_broker_url()) with conn.channel() as channel: producer = Producer(channel) by_label = defaultdict(list) while not self._q.empty(): try: ar: models.AttackResult = self._q.get_nowait() except Empty: continue else: by_label[ar.get_label_key()].append(ar) for ar_list in by_label.values(): if not ar_list: continue monitor_message = { 'type': 'flag_submit', 'data': ar_list[0].get_label_values(), 'value': len(ar_list), } producer.publish( monitor_message, exchange='', routing_key='forcad-monitoring', ) def __call__(self) -> None: while True: try: self._process_statistics() self._process_attacks_queue() except Exception as e: self._logger.error("Error in monitoring: %s", str(e)) eventlet.sleep(self._interval)