Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send identity request on port up #116

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 85 additions & 6 deletions chewie/chewie.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -9,19 +13,27 @@
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):
"""unpacks a 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_PORT_UP_IDENTITY_REQUEST_WAIT_PERIOD = 20
DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL = 60

def __init__(self, interface_name, logger=None,
auth_handler=None, failure_handler=None, logoff_handler=None,
Expand All @@ -46,7 +58,10 @@ 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.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()
Expand Down Expand Up @@ -128,17 +143,74 @@ def port_down(self, port_id):
# 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):
"""
should be called by faucet when port has come up
Args:
port_id (str): id of port.
"""
self.logger.warning("port %s up", port_id)
self.set_port_status(port_id, True)
# TODO send preemptive identity request.

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.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,
FullEAPStateMachine.DISABLED, FullEAPStateMachine.NO_STATE,
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):
"""
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)))
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] = {}

Expand Down Expand Up @@ -193,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)

Expand Down Expand Up @@ -241,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.

Expand All @@ -264,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
8 changes: 8 additions & 0 deletions chewie/eap_state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions chewie/timer_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')
38 changes: 37 additions & 1 deletion test/test_chewie.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down