From 119f43cd5948868ee09e5dea62f25454cdbccb72 Mon Sep 17 00:00:00 2001 From: Cort Buffington Date: Tue, 13 Feb 2018 18:59:44 -0600 Subject: [PATCH] ACL Features Added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes hb_bridge_all a more capable program with multiple ingress and egress ACLs for SID and TGID filtering. It’s not as simple as “blacklist” and “whitelist” since MULTIPLE endpoints exist, unlike an MMDVM that has only one connection. --- .gitignore | 1 + acl.py | 76 +++++++++++++++++++++++ hb_bridge_all.py | 113 +++++++++++++++++++++++++++++++--- hb_bridge_all_rules_SAMPLE.py | 40 ++++++++++++ 4 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 acl.py create mode 100644 hb_bridge_all_rules_SAMPLE.py diff --git a/.gitignore b/.gitignore index 6dbef5c..0617424 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ Icon hblink.cfg hb_routing_rules.py hb_confbridge_rules.py +hb_bridge_all_rules.py *.config *.json *.pickle diff --git a/acl.py b/acl.py new file mode 100644 index 0000000..f392f1b --- /dev/null +++ b/acl.py @@ -0,0 +1,76 @@ +from dmr_utils.utils import int_id + +# Lowest possible Subscirber and/or talkgroup IDs allowed by ETSI standard +ID_MIN = 1 +ID_MAX = 16776415 + + +# Checks the supplied ID against the ID given, and the ACL list, and the action +# Returns True if the ID should be allowed, False if it should not be +def acl_check(_id, _acl): + id = int_id(_id) + for entry in _acl[1]: + if entry[0] <= id <= entry[1]: + return _acl[0] + return not _acl[0] + + +def acl_build(_acl): + if not _acl: + return(True, set((ID_MIN, ID_MAX))) + + acl = set() + sections = _acl.split(':') + + if sections[0] == 'PERMIT': + action = True + else: + action = False + + for entry in sections[1].split(','): + if entry == 'ALL': + acl.add((ID_MIN, ID_MAX)) + break + + elif '-' in entry: + start,end = entry.split('-') + start,end = int(start), int(end) + if (ID_MIN <= start <= ID_MAX) or (ID_MIN <= end <= ID_MAX): + acl.add((start, end)) + else: + pass #logger message here + else: + id = int(entry) + if (ID_MIN <= id <= ID_MAX) or (ID_MIN <= id <= ID_MAX): + acl.add((id, id)) + else: + pass #logger message here + + return (action, acl) + + +if __name__ == '__main__': + from time import time + from pprint import pprint + + ACL = { + 'SUB': { + 'K0USY': 'PERMIT:1-5,3120101,3120124' + }, + 'TGID': { + 'GLOBAL': 'DENY:ALL', + 'K0USY': 'PERMIT:1-5,3120,31201' + } + } + + for acl in ACL: + if 'GLOBAL' not in ACL[acl]: + ACL[acl].update({'GLOBAL':'PERMIT:ALL'}) + for acltype in ACL[acl]: + ACL[acl][acltype] = acl_build(ACL[acl][acltype]) + + pprint(ACL) + print + + print(acl_check('\x00\x00\x01', ACL['TGID']['GLOBAL'])) + print(acl_check('\x00\x00\x01', ACL['TGID']['K0USY'])) \ No newline at end of file diff --git a/hb_bridge_all.py b/hb_bridge_all.py index 9d98c35..19573bd 100755 --- a/hb_bridge_all.py +++ b/hb_bridge_all.py @@ -37,6 +37,7 @@ import sys from bitarray import bitarray from time import time from importlib import import_module +from types import ModuleType # Twisted is pretty important, so I keep it separate from twisted.internet.protocol import DatagramProtocol @@ -47,6 +48,7 @@ from twisted.internet import task from hblink import HBSYSTEM, systems, int_id, hblink_handler from dmr_utils.utils import hex_str_3, int_id, get_alias from dmr_utils import decode, bptc, const +from acl import acl_check, acl_build import hb_config import hb_log import hb_const @@ -63,6 +65,19 @@ __status__ = 'pre-alpha' # Module gobal varaibles +# Import rules -- at this point, just ACLs +def import_rules(_rules): + try: + rules_file = import_module(_rules) + logger.info('Rules file found and bridges imported') + return rules_file + except ImportError: + logger.info('Rules file not found. Initializing defaults') + rules_file = ModuleType('rules_file') + rules_file.ACL = {'SID':{}, 'TGID':{}} + return rules_file + + class bridgeallSYSTEM(HBSYSTEM): def __init__(self, _name, _config, _logger): @@ -125,14 +140,79 @@ class bridgeallSYSTEM(HBSYSTEM): if _call_type == 'group': + # Check for GLOBAL Subscriber ID ACL Match + if acl_check(_rf_src, ACL['SID']['GLOBAL']) == False: + if (_stream_id != self.STATUS[_slot]['RX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY INGRESS GLOBAL ACL*** SID: %s HBP, Peer %s', self._system, int_id(_rf_src), int_id(_radio_id)) + self.STATUS[_slot]['RX_STREAM_ID'] = _stream_id + return + # Check for SYSTEM Subscriber ID ACL Match + if acl_check(_rf_src, ACL['SID'][self._system]) == False: + if (_stream_id != self.STATUS[_slot]['RX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY INGRESS SYSTEM ACL*** SID: %s HBP, Peer %s', self._system, int_id(_rf_src), int_id(_radio_id)) + self.STATUS[_slot]['RX_STREAM_ID'] = _stream_id + return + + # Check for GLOBAL Talkgroup ID ACL Match + if acl_check(_dst_id, ACL['TGID']['GLOBAL']) == False: + if (_stream_id != self.STATUS[_slot]['RX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY INGRESS GLOBAL ACL*** TGID: %s HBP, Peer %s', self._system, int_id(_dst_id), int_id(_radio_id)) + self.STATUS[_slot]['RX_STREAM_ID'] = _stream_id + return + # Check for SYSTEM Talkgroup ID ID ACL Match + if acl_check(_dst_id, ACL['TGID'][self._system]) == False: + if (_stream_id != self.STATUS[_slot]['RX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY INGRESS SYSTEM ACL*** TGID: %s HBP, Peer %s', self._system, int_id(_dst_id), int_id(_radio_id)) + self.STATUS[_slot]['RX_STREAM_ID'] = _stream_id + return + # Is this is a new call stream? if (_stream_id != self.STATUS[_slot]['RX_STREAM_ID']): self.STATUS['RX_START'] = pkt_time self._logger.info('(%s) *CALL START* STREAM ID: %s SUB: %s (%s) REPEATER: %s (%s) TGID %s (%s), TS %s', \ self._system, int_id(_stream_id), get_alias(_rf_src, subscriber_ids), int_id(_rf_src), get_alias(_radio_id, peer_ids), int_id(_radio_id), get_alias(_dst_id, talkgroup_ids), int_id(_dst_id), _slot) + + # Mark status variables for use later + self.STATUS[_slot]['RX_RFS'] = _rf_src + self.STATUS[_slot]['RX_TYPE'] = _dtype_vseq + self.STATUS[_slot]['RX_TGID'] = _dst_id + self.STATUS[_slot]['RX_TIME'] = pkt_time + self.STATUS[_slot]['RX_STREAM_ID'] = _stream_id + + for _target in self._CONFIG['SYSTEMS']: if _target != self._system: + + _target_status = systems[_target].STATUS + _target_system = self._CONFIG['SYSTEMS'][_target] + + # Check for GLOBAL Subscriber ID ACL Match + if acl_check(_rf_src, ACL['SID']['GLOBAL']) == False: + if (_stream_id != _target_status[_slot]['TX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY EGRESS GLOBAL ACL*** SID: %s HBP, Peer %s', _target, int_id(_rf_src), int_id(_radio_id)) + _target_status[_slot]['TX_STREAM_ID'] = _stream_id + return + # Check for SYSTEM Subscriber ID ACL Match + if acl_check(_rf_src, ACL['SID'][_target]) == False: + if (_stream_id != _target_status[_slot]['TX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY EGRESS SYSTEM ACL*** SID: %s HBP, Peer %s', _target, int_id(_rf_src), int_id(_radio_id)) + _target_status[_slot]['TX_STREAM_ID'] = _stream_id + return + + # Check for GLOBAL Talkgroup ID ACL Match + if acl_check(_dst_id, ACL['TGID']['GLOBAL']) == False: + if (_stream_id != _target_status[_slot]['TX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY EGRESS GLOBAL ACL*** TGID: %s HBP, Peer %s', _target, int_id(_dst_id), int_id(_radio_id)) + _target_status[_slot]['TX_STREAM_ID'] = _stream_id + return + # Check for SYSTEM Talkgroup ID ID ACL Match + if acl_check(_dst_id, ACL['TGID'][_target]) == False: + if (_stream_id != _target_status[_slot]['TX_STREAM_ID']): + self._logger.warning('(%s) Group Voice Call ***REJECTED BY EGRESS SYSTEM ACL*** TGID: %s HBP, Peer %s', _target, int_id(_dst_id), int_id(_radio_id)) + _target_status[_slot]['TX_STREAM_ID'] = _stream_id + return + systems[_target].send_system(_data) #self._logger.debug('(%s) Packet routed to system: %s', self._system, _target) @@ -142,13 +222,7 @@ class bridgeallSYSTEM(HBSYSTEM): call_duration = pkt_time - self.STATUS['RX_START'] self._logger.info('(%s) *CALL END* STREAM ID: %s SUB: %s (%s) REPEATER: %s (%s) TGID %s (%s), TS %s, Duration: %s', \ self._system, int_id(_stream_id), get_alias(_rf_src, subscriber_ids), int_id(_rf_src), get_alias(_radio_id, peer_ids), int_id(_radio_id), get_alias(_dst_id, talkgroup_ids), int_id(_dst_id), _slot, call_duration) - - # Mark status variables for use later - self.STATUS[_slot]['RX_RFS'] = _rf_src - self.STATUS[_slot]['RX_TYPE'] = _dtype_vseq - self.STATUS[_slot]['RX_TGID'] = _dst_id - self.STATUS[_slot]['RX_TIME'] = pkt_time - self.STATUS[_slot]['RX_STREAM_ID'] = _stream_id + #************************************************ @@ -156,7 +230,6 @@ class bridgeallSYSTEM(HBSYSTEM): #************************************************ if __name__ == '__main__': - import argparse import sys import os @@ -219,6 +292,30 @@ if __name__ == '__main__': if talkgroup_ids: logger.info('ID ALIAS MAPPER: talkgroup_ids dictionary is available') + # Import rules file + rules_file = import_rules('hb_bridge_all_rules') + + # Create ACLs + ACL = rules_file.ACL + + for acl_type in ACL: + if acl_type != 'SID' and acl_type != 'TGID': + sys.exit(('TERMINATE: SID or TGID stanzas not in ACL!!! Exiting to save you grief later')) + + if 'GLOBAL' not in ACL[acl_type]: + ACL[acl_type].update({'GLOBAL':'PERMIT:ALL'}) + + for system_acl in ACL[acl_type]: + if system_acl not in CONFIG['SYSTEMS'] and system_acl != 'GLOBAL': + sys.exit(('TERMINATE: {} ACL configured for system {} that does not exist!!! Exiting to save you grief later'.format(acl_type, system_acl))) + ACL[acl_type][system_acl] = acl_build(ACL[acl_type][system_acl]) + + for system in CONFIG['SYSTEMS']: + for acl_type in ACL: + if system not in ACL[acl_type]: + logger.warning('No SID ACL for system %s - initializing \'PERMIT:ALL\'', system) + ACL[acl_type].update({system: acl_build('PERMIT:ALL')}) + # HBlink instance creation logger.info('HBlink \'hb_bridge_all.py\' (c) 2016 N0MJS & the K0USY Group - SYSTEM STARTING...') diff --git a/hb_bridge_all_rules_SAMPLE.py b/hb_bridge_all_rules_SAMPLE.py new file mode 100644 index 0000000..b1d97e6 --- /dev/null +++ b/hb_bridge_all_rules_SAMPLE.py @@ -0,0 +1,40 @@ +# ACL Entries +# +# The 'action' May be PERMIT|DENY +# Each entry may be a single radio id, a hypenated range (e.g. 1-2999), or the string 'ALL'. +# if "ALL" is used, you may not include any other ranges or individual IDs. +# Format: +# ACL = 'action:id|start-end|,id|start-end,....' +# +# Sections exist for both TGIDs and Subscriber IDs. +# Sections exist for glboal actions, and per-system actions. +# ***FIRST MATCH EXITS*** + +# SID - Subscriber ID section. +# TGID - Talkgroup ID section. +# +# "GLOBAL" affects ALL systems +# "SYSTEM NAME" affects the system in quetion +# ACLs are applied both ingress AND egress +# If you omit GLOBAL or SYSTEM level ACLs, they will be initilzied +# automatically as "PERMIT:ALL" +# +# EXAMPLE: +# ACL = { +# 'SID': { +# 'K0USY': 'PERMIT:1-5,3120101,3120124' +# }, +# 'TGID': { +# 'GLOBAL': 'PERMIT:ALL', +# 'K0USY': 'DENY:1-5,3120,31201' +# } +# } + +ACL = { + 'SID': { + 'GLOBAL': 'PERMIT:ALL' + }, + 'TGID': { + 'GLOBAL': 'PERMIT:ALL' + } +} \ No newline at end of file