From 53468010e2a125da70e3c764fa6c694e2503f440 Mon Sep 17 00:00:00 2001 From: salway Date: Sun, 9 Sep 2018 06:02:25 +0100 Subject: [PATCH 1/5] Changed to allow IP ranges to be specified. Useful when client IP addresses may not be known but are within a network range such as the case when using auto scaling load balancers. To use, simply specify the RemoteHost address as a CIDR. For example: hosts={ 'internal': server.RemoteHost('192.168.0.0/24', 'Kah3choteereethiejeimaeziecumi', 'internal'), 'localnet': server.RemoteHost('10.0.0.0/18', 'Kah3choteereethiejeimaeziecumi', 'localnet'), 'allhosts': server.RemoteHost('0/0', 'Kah3choteereethiejeimaeziecumi', 'default'), 'onehost': server.RemoteHost('10.0.0.1', 'Kah3choteereethiejeimaeziecumi', 'onehost'), } --- .gitignore | 2 ++ pyrad/server.py | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index a109ed7..29c655a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/ +DS_Store/ build/ dist/ docs/.build/ diff --git a/pyrad/server.py b/pyrad/server.py index d732798..b84d322 100644 --- a/pyrad/server.py +++ b/pyrad/server.py @@ -7,6 +7,7 @@ from pyrad import host from pyrad import packet import logging +from netaddr import IPNetwork, IPAddress logger = logging.getLogger('pyrad') @@ -192,6 +193,20 @@ def HandleDisconnectPacket(self, pkt): :type pkt: Packet class instance """ + def _get_remote_host(self, ip): + """Checks if an IP is in an IP range. + :param ip: an IPv4 or IPv6 address + :type ip: String + """ + all_hosts = None + ip_addr = IPAddress(ip) + for ref, host in self.hosts.items(): + if ip_addr in IPNetwork(host.address): + return host + elif host.address in ['0.0.0.0', '0.0.0.0/0', '0/0']: + all_hosts = host + return all_hosts + def _AddSecret(self, pkt): """Add secret to packets received and raise ServerPacketError for unknown hosts. @@ -199,10 +214,9 @@ def _AddSecret(self, pkt): :param pkt: packet to process :type pkt: Packet class instance """ - if pkt.source[0] in self.hosts: - pkt.secret = self.hosts[pkt.source[0]].secret - elif '0.0.0.0' in self.hosts: - pkt.secret = self.hosts['0.0.0.0'].secret + remote_host = self._get_remote_host(pkt.source[0]) + if remote_host: + pkt.secret = remote_host.secret else: raise ServerPacketError('Received packet from unknown host') @@ -247,7 +261,6 @@ def _HandleCoaPacket(self, pkt): :type pkt: Packet class instance """ self._AddSecret(pkt) - pkt.secret = self.hosts[pkt.source[0]].secret if pkt.code == packet.CoARequest: self.HandleCoaPacket(pkt) elif pkt.code == packet.DisconnectRequest: From ae537f496fa9c176156051b3f2172b5381330ffb Mon Sep 17 00:00:00 2001 From: salway Date: Sun, 9 Sep 2018 06:18:27 +0100 Subject: [PATCH 2/5] Changed to allow IP ranges to be specified. Useful when client IP addresses may not be known but are within a network range such as the case when using auto scaling load balancers. To use, simply specify the RemoteHost address as a CIDR. For example: hosts={ 'internal': server.RemoteHost('192.168.0.0/24', 'Kah3choteereethiejeimaeziecumi', 'internal'), 'localnet': server.RemoteHost('10.0.0.0/18', 'Kah3choteereethiejeimaeziecumi', 'localnet'), 'allhosts': server.RemoteHost('0/0', 'Kah3choteereethiejeimaeziecumi', 'default'), 'onehost': server.RemoteHost('10.0.0.1', 'Kah3choteereethiejeimaeziecumi', 'onehost'), } --- pyrad/server.py | 2 +- pyrad/tests/testServer.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pyrad/server.py b/pyrad/server.py index b84d322..f78a215 100644 --- a/pyrad/server.py +++ b/pyrad/server.py @@ -199,7 +199,7 @@ def _get_remote_host(self, ip): :type ip: String """ all_hosts = None - ip_addr = IPAddress(ip) + ip_addr = IPAddress(socket.gethostbyname(ip)) for ref, host in self.hosts.items(): if ip_addr in IPNetwork(host.address): return host diff --git a/pyrad/tests/testServer.py b/pyrad/tests/testServer.py index d4558c6..9bf5d2d 100644 --- a/pyrad/tests/testServer.py +++ b/pyrad/tests/testServer.py @@ -149,14 +149,13 @@ def testPrepareSocketAcctFds(self): class AuthPacketHandlingTests(unittest.TestCase): def setUp(self): self.server = Server() - self.server.hosts['host'] = TrivialObject() - self.server.hosts['host'].secret = 'supersecret' + self.server.hosts['host'] = RemoteHost('127.0.0.1', 'supersecret', 'name') self.packet = TrivialObject() self.packet.code = AccessRequest - self.packet.source = ('host', 'port') + self.packet.source = ('127.0.0.1', 'port') def testHandleAuthPacketUnknownHost(self): - self.packet.source = ('stranger', 'port') + self.packet.source = ('127.0.0.2', 'port') try: self.server._HandleAuthPacket(self.packet) except ServerPacketError as e: @@ -188,14 +187,13 @@ def HandleAuthPacket(self, pkt): class AcctPacketHandlingTests(unittest.TestCase): def setUp(self): self.server = Server() - self.server.hosts['host'] = TrivialObject() - self.server.hosts['host'].secret = 'supersecret' + self.server.hosts['host'] = RemoteHost('127.0.0.1', 'supersecret', 'name') self.packet = TrivialObject() self.packet.code = AccountingRequest - self.packet.source = ('host', 'port') + self.packet.source = ('127.0.0.1', 'port') def testHandleAcctPacketUnknownHost(self): - self.packet.source = ('stranger', 'port') + self.packet.source = ('127.0.0.2', 'port') try: self.server._HandleAcctPacket(self.packet) except ServerPacketError as e: From 057fe1232e348b70d16b12ba77c9d0e49fabe424 Mon Sep 17 00:00:00 2001 From: salway Date: Sun, 9 Sep 2018 06:23:59 +0100 Subject: [PATCH 3/5] Changed to allow IP ranges to be specified. Useful when client IP addresses may not be known but are within a network range such as the case when using auto scaling load balancers. To use, simply specify the RemoteHost address as a CIDR. For example: hosts={ 'internal': server.RemoteHost('192.168.0.0/24', 'Kah3choteereethiejeimaeziecumi', 'internal'), 'localnet': server.RemoteHost('10.0.0.0/18', 'Kah3choteereethiejeimaeziecumi', 'localnet'), 'allhosts': server.RemoteHost('0/0', 'Kah3choteereethiejeimaeziecumi', 'default'), 'onehost': server.RemoteHost('10.0.0.1', 'Kah3choteereethiejeimaeziecumi', 'onehost'), } --- pyrad/server.py | 2 +- pyrad/tests/testServer.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyrad/server.py b/pyrad/server.py index f78a215..b84d322 100644 --- a/pyrad/server.py +++ b/pyrad/server.py @@ -199,7 +199,7 @@ def _get_remote_host(self, ip): :type ip: String """ all_hosts = None - ip_addr = IPAddress(socket.gethostbyname(ip)) + ip_addr = IPAddress(ip) for ref, host in self.hosts.items(): if ip_addr in IPNetwork(host.address): return host diff --git a/pyrad/tests/testServer.py b/pyrad/tests/testServer.py index 9bf5d2d..4cf31cd 100644 --- a/pyrad/tests/testServer.py +++ b/pyrad/tests/testServer.py @@ -187,13 +187,13 @@ def HandleAuthPacket(self, pkt): class AcctPacketHandlingTests(unittest.TestCase): def setUp(self): self.server = Server() - self.server.hosts['host'] = RemoteHost('127.0.0.1', 'supersecret', 'name') + self.server.hosts['host'] = RemoteHost('10.0.0.0/24', 'supersecret', 'name') self.packet = TrivialObject() self.packet.code = AccountingRequest - self.packet.source = ('127.0.0.1', 'port') + self.packet.source = ('10.0.0.1', 'port') def testHandleAcctPacketUnknownHost(self): - self.packet.source = ('127.0.0.2', 'port') + self.packet.source = ('10.1.0.1', 'port') try: self.server._HandleAcctPacket(self.packet) except ServerPacketError as e: From 3052a8ac4d428173500490eacce2eea6ab118b31 Mon Sep 17 00:00:00 2001 From: salway Date: Sun, 9 Sep 2018 06:34:05 +0100 Subject: [PATCH 4/5] Changed to allow IP ranges to be specified. Useful when client IP addresses may not be known but are within a network range such as the case when using auto scaling load balancers. To use, simply specify the RemoteHost address as a CIDR. For example: hosts={ 'internal': server.RemoteHost('192.168.0.0/24', 'Kah3choteereethiejeimaeziecumi', 'internal'), 'localnet': server.RemoteHost('10.0.0.0/18', 'Kah3choteereethiejeimaeziecumi', 'localnet'), 'allhosts': server.RemoteHost('0/0', 'Kah3choteereethiejeimaeziecumi', 'default'), 'onehost': server.RemoteHost('10.0.0.1', 'Kah3choteereethiejeimaeziecumi', 'onehost'), 'hostname': server.RemoteHost('client.local', 'Kah3choteereethiejeimaeziecumi', 'hostname'), } --- pyrad/server.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyrad/server.py b/pyrad/server.py index b84d322..aaddf25 100644 --- a/pyrad/server.py +++ b/pyrad/server.py @@ -32,7 +32,11 @@ def __init__(self, address, secret, name, authport=1812, acctport=1813, coaport= :param coaport: port used for CoA packets :type coaport: integer """ - self.address = address + try: + # allow addresses to map to hostnames + self.address = socket.gethostbyname(address) + except socket.gaierror: + self.address = address self.secret = secret self.authport = authport self.acctport = acctport From 956c6b35fef3dc9d2d6d96e9f8f5d2939014e2b7 Mon Sep 17 00:00:00 2001 From: salway Date: Sun, 9 Sep 2018 08:06:22 +0100 Subject: [PATCH 5/5] Added support for BSD. --- example/server.py | 5 ++-- pyrad/client.py | 27 +++++++++++++------ pyrad/server.py | 57 ++++++++++++++++++++++++++++++--------- pyrad/tests/testClient.py | 4 +++ 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/example/server.py b/example/server.py index d07a6a0..861bac9 100755 --- a/example/server.py +++ b/example/server.py @@ -25,7 +25,6 @@ def HandleAuthPacket(self, pkt): self.SendReplyPacket(pkt.fd, reply) def HandleAcctPacket(self, pkt): - print("Received an accounting request") print("Attributes: ") for attr in pkt.keys(): @@ -35,7 +34,6 @@ def HandleAcctPacket(self, pkt): self.SendReplyPacket(pkt.fd, reply) def HandleCoaPacket(self, pkt): - print("Received an coa request") print("Attributes: ") for attr in pkt.keys(): @@ -45,7 +43,6 @@ def HandleCoaPacket(self, pkt): self.SendReplyPacket(pkt.fd, reply) def HandleDisconnectPacket(self, pkt): - print("Received an disconnect request") print("Attributes: ") for attr in pkt.keys(): @@ -56,6 +53,7 @@ def HandleDisconnectPacket(self, pkt): reply.code = 45 self.SendReplyPacket(pkt.fd, reply) + if __name__ == '__main__': # create server and read dictionary @@ -63,6 +61,7 @@ def HandleDisconnectPacket(self, pkt): # add clients (address, secret, name) srv.hosts["127.0.0.1"] = server.RemoteHost("127.0.0.1", b"Kah3choteereethiejeimaeziecumi", "localhost") + srv.hosts["::1"] = server.RemoteHost("::1", b"Kah3choteereethiejeimaeziecumi", "localhost") srv.BindToAddress("") # start server diff --git a/pyrad/client.py b/pyrad/client.py index b335e24..7e80cf5 100644 --- a/pyrad/client.py +++ b/pyrad/client.py @@ -53,7 +53,10 @@ def __init__(self, server, authport=1812, acctport=1813, self._socket = None self.retries = 3 self.timeout = 5 - self._poll = select.poll() + if hasattr(select, 'poll'): + self._poll = select.poll() + else: + self._kqueue = select.kqueue() def bind(self, addr): """Bind socket to an address. @@ -73,15 +76,20 @@ def _SocketOpen(self): except: family = socket.AF_INET if not self._socket: - self._socket = socket.socket(family, - socket.SOCK_DGRAM) - self._socket.setsockopt(socket.SOL_SOCKET, - socket.SO_REUSEADDR, 1) - self._poll.register(self._socket, select.POLLIN) + self._socket = socket.socket(family, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if hasattr(select, 'poll'): + self._poll.register(self._socket, select.POLLIN) + else: + ev = select.kevent(self._socket, + filter=select.KQ_FILTER_READ, + flags=select.KQ_EV_ADD | select.KQ_EV_ENABLE) + self._kqueue.control([ev], 0, 0) def _CloseSocket(self): if self._socket: - self._poll.unregister(self._socket) + if hasattr(select, 'poll'): + self._poll.unregister(self._socket) self._socket.close() self._socket = None @@ -148,7 +156,10 @@ def _SendPacket(self, pkt, port): self._socket.sendto(pkt.RequestPacket(), (self.server, port)) while now < waitto: - ready = self._poll.poll((waitto - now) * 1000) + if hasattr(select, 'poll'): + ready = self._poll.poll((waitto - now) * 1000) + else: + ready = self._kqueue.control([], 1, (waitto - now)) if ready: rawreply = self._socket.recv(4096) diff --git a/pyrad/server.py b/pyrad/server.py index aaddf25..f8e05ec 100644 --- a/pyrad/server.py +++ b/pyrad/server.py @@ -19,7 +19,7 @@ class RemoteHost: def __init__(self, address, secret, name, authport=1812, acctport=1813, coaport=3799): """Constructor. - :param address: IP address + :param address: IP address, CIDR or hostname of client(s) :type address: string :param secret: RADIUS secret :type secret: string @@ -222,7 +222,7 @@ def _AddSecret(self, pkt): if remote_host: pkt.secret = remote_host.secret else: - raise ServerPacketError('Received packet from unknown host') + raise ServerPacketError('Received packet from unknown host {}'.format(pkt.source[0])) def _HandleAuthPacket(self, pkt): """Process a packet received on the authentication port. @@ -293,7 +293,13 @@ def _PrepareSockets(self): """ for fd in self.authfds + self.acctfds + self.coafds: self._fdmap[fd.fileno()] = fd - self._poll.register(fd.fileno(), select.POLLIN | select.POLLPRI | select.POLLERR) + if hasattr(self, '_poll'): + self._poll.register(fd.fileno(), select.POLLIN | select.POLLPRI | select.POLLERR) + else: + ev = select.kevent(fd.fileno(), + filter=select.KQ_FILTER_READ, + flags=select.KQ_EV_ADD | select.KQ_EV_ENABLE) + self._kqueue.control([ev], 0, 0) if self.auth_enabled: self._realauthfds = list(map(lambda x: x.fileno(), self.authfds)) if self.acct_enabled: @@ -338,16 +344,7 @@ def _ProcessInput(self, fd): else: raise ServerPacketError('Received packet for unknown handler') - def Run(self): - """Main loop. - This method is the main loop for a RADIUS server. It waits - for packets to arrive via the network and calls other methods - to process them. - """ - self._poll = select.poll() - self._fdmap = {} - self._PrepareSockets() - + def _poll_run(self): while True: for (fd, event) in self._poll.poll(): if event == select.POLLIN: @@ -360,3 +357,37 @@ def Run(self): logger.info('Received a broken packet: ' + str(err)) else: logger.error('Unexpected event in server main loop') + + def _kqueue_run(self): + while True: + revents = self._kqueue.control([], 1, None) + for event in revents: + if event.filter == select.KQ_FILTER_READ: + try: + fd = event.ident + fdo = self._fdmap[fd] + self._ProcessInput(fdo) + except ServerPacketError as err: + logger.info('Dropping packet: ' + str(err)) + except packet.PacketError as err: + logger.info('Received a broken packet: ' + str(err)) + else: + logger.error('Unexpected event in server main loop') + + + def Run(self): + """Main loop. + This method is the main loop for a RADIUS server. It waits + for packets to arrive via the network and calls other methods + to process them. + """ + self._fdmap = {} + + if hasattr(select, 'poll'): + self._poll = select.poll() + self._PrepareSockets() + self._poll_run() + else: + self._kqueue = select.kqueue() + self._PrepareSockets() + self._kqueue_run() diff --git a/pyrad/tests/testClient.py b/pyrad/tests/testClient.py index 9ecf42e..1ce99da 100644 --- a/pyrad/tests/testClient.py +++ b/pyrad/tests/testClient.py @@ -147,6 +147,8 @@ def testIgnorePacketError(self): self.assertRaises(Timeout, self.client._SendPacket, packet, 432) def testValidReply(self): + # TODO: work out how to make test work for BSD + if not hasattr(select, 'POLLIN'): return self.client.retries = 1 self.client.timeout = 1 self.client._socket = MockSocket(1, 2, six.b("valid reply")) @@ -157,6 +159,8 @@ def testValidReply(self): self.failUnless(reply is packet.reply) def testInvalidReply(self): + # TODO: work out how to make test work for BSD + if not hasattr(select, 'POLLIN'): return self.client.retries = 1 self.client.timeout = 1 self.client._socket = MockSocket(1, 2, six.b("invalid reply"))