def _handle_request(self, client_request): request_id, server_mtype, record = (client_request.mac, client_request.xID), DHCP.NOT_SET, None Log.debug(f'REQ | TYPE={client_request.mtype}, ID={request_id}') if (client_request.mtype == DHCP.RELEASE): self._release(client_request.ciaddr, client_request.mac) elif (client_request.mtype == DHCP.DISCOVER): server_mtype, record = self._discover(request_id, client_request) elif (client_request.mtype == DHCP.REQUEST): server_mtype, record = self._request(request_id, client_request) # TODO: logging purposes only. probably isnt needed. the below condition are protected by # the initiated value being "DHCP.NOT_SET" so we dont need to cover for them. else: Log.warning(f'Unknown request type from {client_request.mac}') # this is filtering out response types like dhcp nak | modifying lease before # sending to ensure a power failure will have persistent record data. if (server_mtype not in [DHCP.NOT_SET, DHCP.DROP, DHCP.NAK]): self.leases.modify( # pylint: disable=no-member client_request.handout_ip, record ) # only types specified in list require a response. if (server_mtype in [DHCP.OFFER, DHCP.ACK, DHCP.NAK]): client_request.generate_server_response(server_mtype) self.send_to_client(client_request, server_mtype)
def _handle_request(self, client_request): request_id, response_mtype = (client_request.mac, client_request.xID), None Log.debug(f'REQ | TYPE={client_request.mtype}, ID={request_id}') if (client_request.mtype == DHCP.RELEASE): self._release(client_request.ip, client_request.mac) elif (client_request.mtype == DHCP.DISCOVER): response_mtype, record = self._discover(request_id, client_request) elif (client_request.mtype == DHCP.REQUEST): response_mtype, record = self._request(request_id, client_request) if (response_mtype): client_request.generate_server_response(response_mtype) # this is filtering out response types like dhcp nak if (record): self.leases.modify( # pylint: disable=no-member client_request.handout_ip, record) self.send_to_client(client_request) else: Log.warning(f'Unknown request type from {client_request.mac}')
def _handle_request(self, client_request): request_id, request_mtype = (client_request.mac, client_request.xID), None Log.debug(f'REQ | TYPE={client_request.mtype}, ID={request_id}') if (client_request.mtype == DHCP.RELEASE): self._release(client_request.ciaddr, client_request.mac) elif (client_request.mtype == DHCP.DISCOVER): request_mtype, record = self._discover(request_id, client_request) elif (client_request.mtype == DHCP.REQUEST): request_mtype, record = self._request(request_id, client_request) # TODO: i believe this is why the fatal exception was being raised. the log system # was not working so i cant prove it yet. adding a return just in case, which should be # there anyways as this condition is null and should not be responded to anyways. else: Log.warning(f'Unknown request type from {client_request.mac}') return if (request_mtype): client_request.generate_server_response(request_mtype) # this is filtering out response types like dhcp nak if (record): self.leases.modify( # pylint: disable=no-member client_request.handout_ip, record) self.send_to_client(client_request, request_mtype)
def Start(self): self.LoadInterfaces() self.Log = LogHandler(self) self.Syslog = SyslogHandler(self) self.Automate = Automate(self) self.DNSCache = DNSCache(self) self.TLSRelay = TLSRelay(self) self.UDPRelay = UDPRelay(self) self.DNSRelay = DNSRelay(self) ListFile = ListFiles() ListFile.CombineDomains() ListFile.CombineKeywords() self.LoadKeywords() self.LoadTLDs() self.LoadSignatures() Sniffer = DNSSniffer(self) # setting from_proxy arg to True to have the sniffer sleep for 5 seconds while settings can initialize threading.Thread(target=Sniffer.Start, args=(True,)).start() threading.Thread(target=self.DNSRelay.Start).start() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) asyncio.run(self.RecurringTasks())
def get_current_bytes(): try: return {iface[0][1][:-1]:[iface[3][1], iface[5][1]] for iface in [[y.split() for y in x.split('\\')] for x in check_output('ip -s -o link', shell=True).decode().splitlines() \ if 'wan' in x or 'lan' in x]} except Exception as E: Log = LogHandler(module=LOG_MOD) Log.message(f'Interface bandwidth module error | {E}')
def update_startup_script(self): UpdateScript = US() try: UpdateScript.run() except Exception: Log.error('DNX update script failed to run on startup.') else: Log.notice('DNX update script ran on startup.') os.remove(_update_script)
def _release(self, ip_address, mac_address): dhcp = ServerResponse(server=self) # if mac/ lease mac match, the lease will be removed from the table if dhcp.release(ip_address, mac_address): self.leases.modify(ip_address) # pylint: disable=no-member else: Log.warning(f'Client {mac_address} attempted invalid release.')
def send_to_client(client_request): if (client_request.bcast): Log.debug(f'Sent BROADCAST to 255.255.255.255:68') client_request.sock.sendto(client_request.send_data, (f'{BROADCAST}', 68)) else: Log.debug(f'Sent UNICAST to {client_request.handout_ip}:68') client_request.sock.sendto(client_request.send_data, (f'{client_request.handout_ip}', 68))
def __init__(self): self.Log = LogHandler(LOG_MOD) # add configuration check for icmp checks prior to handing out ip. self.icmp_check = True self.Leases = DHCPLeases(self) self.ongoing = {} self.handout_lock = threading.Lock() self.log_queue_lock = threading.Lock()
def send_to_client(client_request, request_mtype): if (request_mtype is DHCP.RENEWING): client_request.sock.sendto(client_request.send_data, (f'{client_request.ciaddr}', 68)) Log.debug(f'Sent unicast to {client_request.ciaddr}:68') # NOTE: sending broadcast because f**k. else: client_request.sock.sendto(client_request.send_data, (f'{BROADCAST}', 68)) Log.debug(f'Sent broadcast for {client_request.handout_ip} to 255.255.255.255:68')
def _get_available_ip(self): local_net = list(self._intf_net)[self._r_start:self._r_end] for _ in range(len(local_net)): ip_address = _fast_choice(local_net) if not self.is_available(ip_address): continue if (self._check_icmp_reach): # TODO: figure out how we will get notified if icmp_reachable(ip_address): continue return ip_address else: Log.critical('IP handout error. No available IPs in range.') # TODO: comeback
def start(self): self.get_interface_settings() self.Log = LogHandler(process=self) self.Automate = Automate(self) self.SyslogUDP = UDPMessage(self) self.SyslogTCP = TCPMessage(self) self.automate_threads() self.initialize() self._ready_interface_service()
def Start(self): self.LoadInterfaces() self.Automate = Automate(self) self.Log = LogHandler(self) self.Syslog = SyslogHandler(self) Sniffer = IPSSniffer(self) # True boolean notifies thread that it was the initial start and to minimize sleep time threading.Thread(target=Sniffer.Start, args=(True,)).start() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) asyncio.run(self.Main())
def _get_available_ip(self): handout_range = self._handout_range for _ in range(len(handout_range)): ip_address = _fast_choice(handout_range) if not self._is_available(ip_address): continue if (self._check_icmp_reach ): # TODO: figure out how we will get notified if icmp_reachable(ip_address): continue return ip_address else: Log.critical('IP handout error. No available IPs in range.' ) # TODO: comeback
def timeout_status(): with ConfigurationManager('license') as dnx: dnx_license = dnx.load_configuration()['license'] if not dnx_license['activated']: return timestamp = dnx_license['timestamp'] if (fast_time() - timestamp >= ONE_DAY and dnx_license['validated']): dnx_license['validated'] = False dnx.write_configuration(dnx_license) Log.warning( 'DNX license has been invalidated because it has not contacted the license server in 24 hours.' )
def handle_system_action(page_settings): action = page_settings['action'] response = request.form.get(f'system_{action}', None) if (response == 'YES'): page_settings.pop('control', None) page_settings.pop('user_role', None) page_settings.update({'confirmed': True, 'login_btn': True}) Log.warning(f'DNX Firewall {action} initiated.') system_action_method = getattr(System, action) threading.Thread(target=system_action_method).start() elif (response == 'NO'): return redirect(url_for('dashboard')) return render_template('dnx_device.html', **page_settings)
def user_login(cls, form, login_ip): '''function to authenticate user to the dnx web frontend. pass in flask form and source ip. return will be a boolean representing whether user is authenticated/authorized or not.''' self = cls() threading.Thread(target=self._login_timer).start() authorized, username, user_role = self._user_login(form, login_ip) if (authorized): Log.simple_write( LOG_NAME, 'notice', f'User {username} successfully logged in from {login_ip}.') else: Log.simple_write( LOG_NAME, 'warning', f'Failed login attempt for user {username} from {login_ip}.') while not self._time_expired: time.sleep(.202) return authorized, username, user_role
file_path='dnx_frontend') as session_tracker: stored_tracker = session_tracker.load_configuration() if (action is CFG.ADD): stored_tracker['active_users'][username] = { 'user_role': user_role, 'remote_addr': remote_addr, 'logged_in': time.time( ), # NOTE: can probably make this human readable format here. 'last_seen': None } elif (action is CFG.DEL): stored_tracker['active_users'].pop(username, None) session_tracker.write_configuration(stored_tracker) @app.before_request def user_timeout(): session.permanent = True app.permanent_session_lifetime = timedelta(minutes=30) session.modified = True ## SETUP LOGGING CLASS Log.run(name=LOG_NAME) if __name__ == '__main__': app.run(debug=True)
def __init__(self): self._Log = Log() self._time_expired = threading.Event()
class Authentication: def __init__(self): self._Log = Log() self._time_expired = threading.Event() @staticmethod ## see if this is safe. if use returns something outside of dictionary, error will occur. def get_user_role(username): local_accounts = load_configuration('logins')['users'] try: return local_accounts[username]['role'] except KeyError: return None @classmethod def user_login(cls, form, login_ip): '''function to authenticate user to the dnx web frontend. pass in flask form and source ip. return will be a boolean representing whether user is authenticated/authorized or not.''' self = cls() threading.Thread(target=self._login_timer).start() authorized, username, user_role = self._user_login(form, login_ip) if (authorized): self._Log.simple_write( LOG_NAME, 'notice', f'User {username} successfully logged in from {login_ip}.') else: self._Log.simple_write( LOG_NAME, 'warning', f'Failed login attempt for user {username} from {login_ip}.') while not self._time_expired: time.sleep(.202) return authorized, username, user_role def _user_login(self, form, login_ip): password = form.get('password', None) username = form.get('username', '').lower() if (not username or not password): return False, None, None hexpass = self.hash_password(username, password) if not self._user_authorized(username, hexpass): return False, username, None # checking for web ui authorization (admins/users only. cli accounts will fail.) user_role = self.get_user_role(username) if user_role not in ['admin', 'user']: return False, username, None return True, username, user_role def hash_password(self, username, password): salt_one = len(username) salt_two = len(password) if (salt_two > salt_one): salt = salt_two / salt_one else: salt = salt_one / salt_two index = int(float(salt_one / 2)) part_one = username[:index] part_two = username[index:] salt = f'{part_one}{salt}{part_two}' password = f'{password}{salt}'.encode('utf-8') hash_object = hashlib.sha256(password).hexdigest() hash_part = f'{hash_object}'[:salt_one * 2] hash_total = f'{hash_part}{hash_object}'.encode('utf-8') hash_total = hashlib.sha256(hash_total) return hash_total.hexdigest() def _user_authorized(self, username, hexpass): local_accounts = load_configuration('logins')['users'] try: password = local_accounts[username]['password'] except KeyError: return False else: # returning True on password match else False return password == hexpass def _login_timer(self): time.sleep(.6) self._time_expired.set()
class DHCPServer: def __init__(self): self.Log = LogHandler(LOG_MOD) # add configuration check for icmp checks prior to handing out ip. self.icmp_check = True self.Leases = DHCPLeases(self) self.ongoing = {} self.handout_lock = threading.Lock() self.log_queue_lock = threading.Lock() def Start(self): self.LoadInterfaces() # -- Creating Lease Dictionary -- # self.Leases.BuildRange() self.Leases.LoadLeases() # -- Building Server Options Dictionary -- # self.SetServerOptions() # -- Creating socket && starting main server thread -- # self.CreateSocket() threading.Thread(target=self.Server).start() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) asyncio.run(self.Main()) async def Main(self): ## -- Starting Server and Timers in Asyncio Gather -- ## await asyncio.gather(self.Leases.LeaseTimer(), self.Leases.ReservationTimer(), self.Leases.WritetoFile(), self.Log.QueueHandler(self.log_queue_lock)) def CreateSocket(self): self.s = socket(AF_INET, SOCK_DGRAM) self.s.setsockopt(SOL_SOCKET, SO_REUSEADDR,1) self.s.setsockopt(SOL_SOCKET, SO_BROADCAST,1) self.s.setsockopt(SOL_SOCKET, SO_BINDTODEVICE, self.bind_int) self.s.bind(('0.0.0.0', 67)) print('[+] Listening on Port 67') def Server(self): while True: try: Parse = DHCPParser(self.dhcp_server_options) rdata = self.s.recv(1024) ## removed recvfrom, dont need since ip of client is identified in packet print(rdata) response_info, options = Parse.Data(rdata) options = Parse.Options(options) threading.Thread(target=self.Response, args=(response_info, options)).start() except Exception as E: print(E) def Response(self, response_info, options): print('Response Thread') mtype, xID, ciaddr, mac_address = response_info if (mac_address in self.ongoing and self.ongoing[mac_address] != xID): options[53] = [1, DHCP_NAK] elif mtype == DHCP_DISCOVER: options[53] = [1, DHCP_OFFER] elif mtype == DHCP_REQUEST: options[53] = [1, DHCP_ACK] elif mtype == DHCP_RELEASE: self.Leases.Release(ciaddr, mac_address) if (mtype != DHCP_RELEASE): self.SendResponse(response_info, options) def SendResponse(self, response_info, options): print('Send Thread') mtype, xID, ciaddr, mac_address = response_info mtype = options[53][1] ## -- Set ongiong request flag, NAK duplicates -- ## if (mtype != DHCP_NAK): self.ongoing[mac_address] = xID threading.Thread(target=self.OngoingTimer, args=(mac_address,)).start() ## locking handout method call to ensure checking/setting leases is thread safe with self.handout_lock: handout_ip = self.Leases.Handout(mac_address) response_info = xID, mac_address, ciaddr, handout_ip, options Response = DHCPResponse(response_info) sdata = Response.Assemble() if (mtype in {DHCP_OFFER, DHCP_ACK, DHCP_NAK} and handout_ip): if (ciaddr == '0.0.0.0'): print(f'Sent BROADCAST TYPE: {mtype} to 255.255.255.255:68') self.s.sendto(sdata, ('255.255.255.255', 68)) else: print(f'Sent UNICAST TYPE: {mtype} to {ciaddr}:68') self.s.sendto(sdata, (ciaddr, 68)) ## -- Remove ongiong request flag, NAK duplicates -- ## if (mtype == DHCP_ACK and handout_ip): self.ongoing.pop(mac_address, None) def SetServerOptions(self): print('[+] DHCP: Setting server options.') insideip, netmask, broadcast, mtu = self.InterfaceInfo() dhcp_server_options = {} dhcp_server_options[1] = [4, inet_aton(netmask)] # OPT 1 | Subnet Mask dhcp_server_options[3] = [4, inet_aton(insideip)] # OPT 3 | Router dhcp_server_options[6] = [4, inet_aton(insideip)] # OPT 6 | DNS Server dhcp_server_options[26] = [2, mtu] # OPT 26 | MTU dhcp_server_options[28] = [4, inet_aton(broadcast)] # OPT 28 | Broadcast dhcp_server_options[51] = [4, 86400] # OPT 51 | Lease Time dhcp_server_options[54] = [4, inet_aton(insideip)] # OPT 54 | Server Identity dhcp_server_options[58] = [4, 43200] # OPT 58 | Renew Time dhcp_server_options[59] = [4, 74025] # OPT 59 | Rebind Time self.dhcp_server_options = dhcp_server_options def OngoingTimer(self, mac): time.sleep(6) self.ongoing.pop(mac, None) def InterfaceInfo(self): Interface = Int() insideip = Interface.IP(self.lan_int) netmask = Interface.Netmask(self.lan_int) broadcast = Interface.Broadcast(self.lan_int) mtu = Interface.MTU(self.lan_int) return(insideip, netmask, broadcast, mtu) def LoadInterfaces(self): with open(f'{HOME_DIR}/data/config.json', 'r') as settings: setting = json.load(settings) self.lan_int = setting['settings']['interface']['inside'] self.bind_int = f'{self.lan_int}\0'.encode('utf-8')
class DNSProxy: ''' Main Class for DNS Proxy. This class directly controls the logic regarding the signatures, whether something should be blocked or allowed, managing signature updates from user front end configurations. This class also serves as a bridge between the DNS Proxy Sniffer and DNS Relay give them a single point to flag traffic and identify traffic that should not be relayed/blocked via the class variable "flagged_traffic" dictionary. If the Proxy sniffer detects traffic that should be blocked it inputs the connection info into the dictionary for the DNS Relay to refer to before relaying the traffic. If the query information matches a dictionary item the DNS Relay will not forward the traffic to the configured public resolvers. ''' def __init__(self): self.ip_whitelist = {} self.dns_whitelist = {} self.dns_blacklist = {} self.dns_sigs = {} self.dns_records = {} self.dns_servers = {} self.allowed_request = {} self.flagged_request = {} self.shared_decision_lock = threading.Lock() self.log_queue_lock = threading.Lock() self.logging_level = 0 self.syslog_enabled = False ''' Start Method to Initialize All proxy configurations, including cleaning the database tables to the configures length. Starting a child thread for DNS Relay and DNS Proxy Sniffer, to handle requests, and doing an AsyncIO gather on proxy timer methods in main thread for rule updates ''' def Start(self): self.LoadInterfaces() self.Log = LogHandler(self) self.Syslog = SyslogHandler(self) self.Automate = Automate(self) self.DNSCache = DNSCache(self) self.TLSRelay = TLSRelay(self) self.UDPRelay = UDPRelay(self) self.DNSRelay = DNSRelay(self) ListFile = ListFiles() ListFile.CombineDomains() ListFile.CombineKeywords() self.LoadKeywords() self.LoadTLDs() self.LoadSignatures() Sniffer = DNSSniffer(self) # setting from_proxy arg to True to have the sniffer sleep for 5 seconds while settings can initialize threading.Thread(target=Sniffer.Start, args=(True,)).start() threading.Thread(target=self.DNSRelay.Start).start() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) asyncio.run(self.RecurringTasks()) def SignatureCheck(self, packet): timestamp = time.time() dns_record = False whitelisted_query = False redirect = False log_connection = False category = None request_info = {'src_ip': packet.src_ip, 'src_port': packet.src_port, 'request': {1: packet.request, 2: packet.request2}} if (packet.request in self.dns_records): dns_record = True # Whitelist check of FQDN then overall domain ## elif (packet.request in self.dns_whitelist or packet.request2 in self.dns_whitelist or packet.src_ip in self.ip_whitelist): whitelisted_query = True ## will prevent all other checks from being processed if a local dns record is found for domain (for performance) if (dns_record): self.ApplyDecision(request_info, decision=FLAGGED) ## P1. Standard Category blocking of FQDN || if whitelisted, will check to ensure its not a malicious category ## before allowing it to continue elif (packet.request in self.dns_sigs): redirect, reason, category = self.StandardBlock(request_info, whitelisted_query) ## P2. Standard Category blocking of overall domain || micro.com if whitelisted, will check to ensure its not ## a malicious category before allowing it to continue elif (packet.request2 in self.dns_sigs): redirect, reason, category = self.StandardBlock(request_info, whitelisted_query, position=2) ## P1/P2 Blacklist block of FQDN if not whitelisted ## elif (not whitelisted_query) and (packet.request in self.dns_blacklist or packet.request2 in self.dns_blacklist): print(f'Blacklist Block: {packet.request}') redirect = True reason = 'blacklist' category = 'time based' ## TLD (top level domain) block ## elif (packet.request_tld in self.tlds): print(f'TLD Block: {packet.request}') redirect = True category = packet.request_tld reason = 'tld filter' ## Keyword Search within domain || block if match ## else: for keyword, cat in self.keywords.items(): if (keyword in packet.request): redirect = True reason = 'keyword' category = cat break ## Redirect to firewall if traffic match/blocked ## if (redirect): self.ApplyDecision(request_info, decision=FLAGGED) DNSResponse(packet, self.lan_int, self.lan_ip).Response() ##Redirect to IP Address in local DNS Record (user configurable) elif (dns_record): record_ip = self.dns_records.get(packet.request) DNSResponse(packet, self.lan_int, record_ip).Response() else: self.ApplyDecision(request_info, decision=ALLOWED) ## Log to Infected Clients DB Table if matching malicious type categories if (category in {'malicious', 'cryptominer'} and self.logging_level >= ALERT): table ='infectedclients' if (category in {'malicious'}): reason = 'malware' elif (category in {'cryptominer'}): reason = 'cryptominer' logging_options = {'infected_client': packet.src_mac, 'src_ip': packet.src_ip, 'detected_host': packet.request, 'reason': reason} self.TrafficLogging(table, timestamp, logging_options) # logs redirected/blocked requests if (redirect and self.logging_level >= NOTICE): action = 'blocked' log_connection = True # logs all requests, regardless of action of proxy if not already logged elif (not redirect and self.logging_level >= INFORMATIONAL): category = 'N/A' reason = 'logging' action = 'allowed' log_connection = True if (log_connection): table = 'dnsproxy' logging_options = {'src_ip': packet.src_ip, 'request': packet.request, 'category': category , 'reason': reason, 'action': action} self.TrafficLogging(table, timestamp, logging_options) def StandardBlock(self, request_info, whitelisted_query, position=1): redirect = False # print(f'Standard Block: {request}') reason = 'category' category = self.dns_sigs[request_info['request'][position]] if (not whitelisted_query or category in {'malicious', 'cryptominer'}): redirect = True return redirect, reason, category def ApplyDecision(self, request_info, decision): info = SName(**request_info) request_tracker = getattr(self, f'{decision}_request') try: request_tracker[info.src_ip].update({info.src_port: info.request[1]}) except KeyError: request_tracker[info.src_ip] = {info.src_port: info.request[1]} # else: # self.Log.AddtoQueue(f'Client Source port overlap detected: {src_ip}:{src_port}') def TrafficLogging(self, table, timestamp, logging_options): ProxyDB = DBConnector(table) ProxyDB.Connect() if (table in {'dnsproxy'}): ProxyDB.StandardInput(timestamp, logging_options) if (self.syslog_enabled): self.AlertSyslog(logging_options) elif (table in {'infectedclients'}): ProxyDB.InfectedInput(timestamp, logging_options) ProxyDB.Disconnect() def AlertSyslog(self, logging_options): opt = SName(**logging_options) if (opt.category in {'malicious', 'cryptominer'}): msg_level = ALERT else: if (opt.action == 'blocked'): msg_level = NOTICE elif (opt.action == 'allowed'): msg_level = INFORMATIONAL message = f'src.ip={opt.src_ip}; request={opt.request}; category={opt.category}; ' message += f'filter={opt.reason}; action={opt.action}' self.Syslog.Message(EVENT, msg_level, message) def LoadSignatures(self): with open(f'{HOME_DIR}/data/whitelist.json', 'r') as whitelists: whitelist = json.load(whitelists) wl_exceptions = whitelist['whitelists']['exceptions'] with open(f'{HOME_DIR}/data/blacklist.json', 'r') as blacklists: blacklist = json.load(blacklists) bl_exceptions = blacklist['blacklists']['exceptions'] with open(f'{HOME_DIR}/dnx_domainlists/blocked.domains', 'r') as blocked: while True: line = blocked.readline().strip().split() if (not line): break if (line != '\n'): domain = line[0] category = line[1] if (domain not in wl_exceptions): self.dns_sigs[domain] = category for domain in bl_exceptions: self.dns_sigs[domain] = 'blacklist' def LoadTLDs(self): self.tlds = set() with open(f'{HOME_DIR}/data/dns_proxy.json', 'r') as tlds: tld = json.load(tlds) all_tlds = tld['dns_proxy']['tlds'] for entry, info in all_tlds.items(): tld_enabled = info['enabled'] if (tld_enabled): self.tlds.add(entry) ## consider making a combine keywords file. this would be in line with ip and domain categories def LoadKeywords(self): self.keywords = {} with open(f'{HOME_DIR}/dnx_domainlists/blocked.keywords', 'r') as blocked: while True: line = blocked.readline().strip().split() if (not line): break if (line != '\n'): keyword = line[0] cat = line[1] self.keywords[keyword] = cat def LoadInterfaces(self): with open(f'{HOME_DIR}/data/config.json', 'r') as settings: setting = json.load(settings) self.lan_int = setting['settings']['interface']['inside'] self.wan_int = setting['settings']['interface']['outside'] Int = Interface() self.lan_ip = Int.IP(self.lan_int) self.wan_ip = Int.IP(self.wan_int) # AsyncIO method called to gather automated/ continuous methods | this is python 3.7 version of async async def RecurringTasks(self): await asyncio.gather(self.Automate.Settings(), self.Automate.Reachability(), self.Automate.DNSRecords(), self.Automate.UserDefinedLists(), self.Automate.ClearCache(), self.Syslog.Settings(SYSLOG_MOD), self.Log.Settings(LOG_MOD), self.Log.QueueHandler(self.log_queue_lock))
class IPSProxy: def __init__(self): self.udp_scan_tracker = {} self.tcp_scan_tracker = {} self.udp_active_scan = {} self.tcp_active_scan = {} self.fw_rules = {} self.ip_whitelist = {} self.tcp_ddos_tracker = {} self.udp_ddos_tracker = {} self.icmp_ddos_tracker = {} self.active_ddos = False self.block_length = 0 self.fw_rule_creation_lock = threading.Lock() self.scan_tracker_lock = threading.Lock() self.ddos_counter_lock = threading.Lock() self.protocol_conversion = {TCP: 'tcp', UDP: 'udp', ICMP: 'icmp'} self.ddos_prevention = False self.portscan_prevention = False self.portscan_reject = False self.syslog_enabled = False self.icmp_allow = False self.logging_level = 0 def Start(self): self.LoadInterfaces() self.Automate = Automate(self) self.Log = LogHandler(self) self.Syslog = SyslogHandler(self) Sniffer = IPSSniffer(self) # True boolean notifies thread that it was the initial start and to minimize sleep time threading.Thread(target=Sniffer.Start, args=(True,)).start() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) asyncio.run(self.Main()) def SignatureCheck(self, packet, connection_type): timestamp = time.time() add_to_tracker = False str_proto = self.protocol_conversion[packet.protocol] if (packet.protocol != ICMP): open_ports = getattr(self, f'open_{str_proto}_ports') ## if source ip is in the whitelist, it will not be added to the DDOS tracker or be check as a potential port scanner if (packet.src_ip in self.ip_whitelist or packet.dst_ip in self.ip_whitelist): pass elif (packet.protocol == ICMP and self.icmp_allow): add_to_tracker = True elif (connection_type == INITIAL and packet.dst_port in open_ports): add_to_tracker = True ## if an above condition is met, a counter will be added to corresponsing tracker to determine PPS in another thread if (add_to_tracker): ddos_tracker = getattr(self, f'{str_proto}_ddos_tracker') # lock to ensure packet counts remain accurate between all threads with self.ddos_counter_lock: tracked = ddos_tracker.get(packet.src_ip, None) if (not tracked): ddos_tracker.update({packet.src_ip: {'count': 1, 'timestamp': time.time()}}) else: count = tracked['count'] + 1 ddos_tracker[packet.src_ip].update({'count': count}) # will prevent packet from being inspected if it is either icmp (no ports to scan) or if an ddos is currently active to # give more system resources to dealing with the ddos if (not self.active_ddos): self.PortScan(packet, connection_type, timestamp) def PortScan(self, packet, connection_type, timestamp): attack_type = PORTSCAN connection_log = False scan_detected = False active_block = False already_blocked = False block_status = None if (packet.src_ip != self.wan_ip): direction = INBOUND tracked_ip = packet.src_ip tracked_port = packet.src_port local_port = packet.dst_port elif (packet.src_ip == self.wan_ip): direction = OUTBOUND tracked_ip = packet.dst_ip tracked_port = packet.dst_port local_port = packet.src_port ## Main Detection Logic proto = self.protocol_conversion[packet.protocol] active_scan = getattr(self, f'{proto}_active_scan') scan_tracker = getattr(self, f'{proto}_scan_tracker') if (direction == INBOUND and tracked_ip in active_scan): active_scan[tracked_ip] = timestamp scan_detected = True with self.scan_tracker_lock: if (local_port not in scan_tracker[tracked_ip]['target']): scan_tracker[tracked_ip]['target'].update({local_port: False}) # will match if packet is a tcp syn and the source ip is not the wan ip (iniated from external ip) elif (connection_type == INITIAL): # if first time the source ip is seen, it will add ip to dictionary try: count = scan_tracker[tracked_ip]['source'].get(tracked_port, 0) + 1 except KeyError: scan_tracker[tracked_ip] = {'source': {}, 'target': {}} count = 1 scan_tracker[tracked_ip]['source'].update({tracked_port: count}) scan_tracker[tracked_ip]['target'].update({local_port: False}) connections = scan_tracker.get(tracked_ip)['target'] if (count >= 2 or len(connections) >= 3) or (packet.protocol == UDP and not packet.udp_payload): active_scan[tracked_ip] = timestamp active_block = True scan_detected = True # print(f'INCREMENTED | {tracked_ip}: {tracked_port} > {count} | {local_port}') # will match if wan ip is responding to a tcp stream being initiated elif (connection_type == RESPONSE): try: open_ports = getattr(self, f'open_{proto}_ports') if (direction == OUTBOUND and local_port in open_ports): print(f'{proto.upper()} PORT RESPONSE: {timestamp}') scan_tracker[tracked_ip]['target'].update({local_port: True}) except KeyError: # maybe log? seeing stream without seeing the initial connection being established pass except Exception: traceback.print_exc() ## Proxy decision logic if (self.portscan_prevention): ## applying lock on firewall rule dictionary lookup due to the small chance 2 threads check before either # can add the key to the dictionary to prevent duplicate rules and timeout method calls initial_block = False with self.fw_rule_creation_lock: if (active_block and tracked_ip not in self.fw_rules): self.fw_rules.update({tracked_ip: timestamp}) initial_block = True # will mark the scan as dropped on the initial block condition (this will prevent multiple logs being sent) if (initial_block): Popen(f'sudo iptables -t mangle -A IPS -s {tracked_ip} -j DROP && \ sudo iptables -t mangle -A IPS -d {tracked_ip} -j DROP', shell=True) print(f'RULE INSERTED: {tracked_ip} > {tracked_port} | {time.time()}') ## will create a timeout thread to ensure firewall is removed if no persistence is configured or to remove # the ip from the scan tracker as well as the active scan dictionary threading.Thread(target=self.ConnectionTimeout, args=(tracked_ip, packet.protocol)).start() block_status = self.ResponseTracker(tracked_ip, packet.protocol) elif (active_block): already_blocked = True # if portscan is detected and user configured to reject, corresponding messages will be sent as a response to the scan if (self.portscan_reject and scan_detected): if (packet.protocol == TCP): TCPPacket = ScanResponse(self.wan_int, packet, protocol=TCP) TCPPacket.Response() elif (packet.protocol == UDP): ICMPPacket = ScanResponse(self.wan_int, packet, protocol=UDP) ICMPPacket.Response() # logging logic if (active_block and block_status == MISSED and self.logging_level >= WARNING): action = 'missed' connection_log = True print(f'MISSED BLOCK: {tracked_ip}') elif (active_block and block_status == BLOCKED and self.logging_level >= NOTICE): action = 'blocked' connection_log = True print(f'ACTIVE BLOCK: {tracked_ip}') ## add a not already blocked to ensure this doesnt get logged alot elif (scan_detected and not already_blocked and self.logging_level >= INFORMATIONAL): action = 'logged' connection_log = True if (connection_log): logging_options = {'ip': tracked_ip, 'protocol': packet.protocol, 'attack_type': attack_type, 'action': action} self.Logging(timestamp, logging_options) ## after not seeing a tracked ip for 3 seconds, they will be removed from all trackers and the iptable rule # will be removed if the user has not configured a persistant blocking time def ConnectionTimeout(self, tracked_ip, protocol): proto = self.protocol_conversion[protocol] active_scan = getattr(self, f'{proto}_active_scan') scan_tracker = getattr(self, f'{proto}_scan_tracker') while True: now = time.time() last_scan = active_scan.get(tracked_ip, None) if (last_scan and now - last_scan >= 3): scan_tracker.pop(tracked_ip, None) active_scan.pop(tracked_ip, None) print(f'TIMED OUT SCANNER {tracked_ip}') if (self.block_length == 0): Popen(f'sudo iptables -t mangle -D IPS -s {tracked_ip} -j DROP && \ sudo iptables -t mangle -D IPS -d {tracked_ip} -j DROP', shell=True) print(f'REMOVED FW RULE FOR {tracked_ip}') self.fw_rules.pop(tracked_ip, None) break time.sleep(1.6) ## this function will wait for 1 second after seeing a local ip respond to a tcp syn. if the tracked ip # does not send a subsequent ack within 1 second, they are inserted into the active scan dictionary def ResponseTracker(self, tracked_ip, protocol): blocked_status = True missed_ports = set() time.sleep(2) protocol = self.protocol_conversion[protocol] open_ports = getattr(self, f'open_{protocol}_ports') scan_tracker = getattr(self, f'{protocol}_scan_tracker') for port in open_ports: response = scan_tracker[tracked_ip]['target'].get(port, None) if (response): missed_ports.add(port) if (missed_ports): blocked_status = False return blocked_status def Logging(self, timestamp, logging_options): ProxyDB = DBConnector(table='ips') ProxyDB.Connect() ProxyDB.IPSInput(timestamp, logging_options) ProxyDB.Disconnect() if (self.syslog_enabled): self.AlertSyslog(logging_options) def AlertSyslog(self, logging_options): opt = SName(**logging_options) if (opt.attack_type == DDOS): msg_level = ALERT elif (opt.attack_type == PORTSCAN): if (opt.action == 'logged'): msg_level = INFORMATIONAL elif (opt.action == 'blocked'): msg_level = NOTICE elif (opt.action == 'missed'): msg_level = WARNING message = f'src.ip={opt.ip}; protocol={opt.protocol}; attack_type={opt.attack_type}; action={opt.action}' self.Syslog.Message(EVENT, msg_level, message) def LoadInterfaces(self): with open(f'{HOME_DIR}/data/config.json', 'r') as settings: self.setting = json.load(settings) self.lan_int = self.setting['settings']['interface']['inside'] self.wan_int = self.setting['settings']['interface']['outside'] Int = Interface() self.wan_ip = Int.IP(self.wan_int) self.broadcast = Int.Broadcast(self.wan_int) # AsyncIO method called to gather automated/ continuous methods | this is python 3.7 version of async async def Main(self): await asyncio.gather(self.Automate.DDOSCalculation(), self.Automate.ClearIPTables(), self.Automate.IPSSettings(), self.Automate.IPWhitelist(), self.Log.Settings(LOG_MOD), self.Syslog.Settings(SYSLOG_MOD))
# return False @staticmethod # will send response to client over socket depending on host details it will decide unicast or broadcast def send_to_client(client_request): if (client_request.bcast): Log.debug(f'Sent BROADCAST to 255.255.255.255:68') client_request.sock.sendto(client_request.send_data, (f'{BROADCAST}', 68)) else: Log.debug(f'Sent UNICAST to {client_request.handout_ip}:68') client_request.sock.sendto(client_request.send_data, (f'{client_request.handout_ip}', 68)) @property def listener_sock(self): l_sock = socket(AF_INET, SOCK_DGRAM) l_sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) l_sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) l_sock.setsockopt(SOL_SOCKET, SO_BINDTODEVICE, f'{self._intf}\0'.encode('utf-8')) l_sock.bind((str(INADDR_ANY), PROTO.DHCP_SVR)) return l_sock if __name__ == '__main__': Log.run(name=LOG_NAME, verbose=VERBOSE, root=ROOT) DHCPServer.run(Log, threaded=False)
class IPProxy: def __init__(self): self.fw_rules = {} self.ip_signatures = {} self.open_tcp_ports = set() self.open_udp_ports = set() self.inbound_session_tracker = {} self.outbound_session_tracker = {} self.tcp_session_tracker = {} self.fw_rule_creation_lock = threading.Lock() self.block_settings = { 'malware': 'mal_block', 'compromised': 'mal_block', 'entry': 'tor_block', 'exit': 'tor_block' } self.chain_settings = { 'malware': 'MALICIOUS', 'compromised': 'MALICIOUS', 'entry': 'TOR', 'exit': 'TOR' } # var initialization to give proxy time to load correct settings self.tor_entry_block = False self.tor_exit_block = False self.malware_block = False self.compromised_block = False self.syslog_enabled = False self.logging_level = 0 def Start(self): self.LoadInterfaces() self.Log = LogHandler(self) self.Syslog = SyslogHandler(self) self.Automate = Automate(self) ListFile = ListFiles() ListFile.CombineIPs() self.Timer = TM() self.LoadSignatures() Sniffer = IPSniffer(self) # True boolean notifies thread that it was the initial start and to minimize sleep time threading.Thread(target=Sniffer.Start, args=(True, )).start() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) asyncio.run(self.Main()) def SignatureCheck(self, packet): timestamp = round(time.time()) signature_match = False active_block = False already_blocked = False log_connection = False direction = None category = None ## OUTBOUND RULE TCP if (packet.dst_ip in self.ip_signatures): direction = OUTBOUND signature_match = True tracked_ip = packet.dst_ip local_ip = packet.src_ip if (packet.procotol == UDP): main_st = self.outbound_session_tracker other_st = self.inbound_session_tracker ## INBOUND RULE TCP elif (packet.dst_port in self.open_tcp_ports and packet.src_ip in self.ip_signatures): direction = INBOUND signature_match = True tracked_ip = packet.src_ip local_ip = packet.dst_ip if (packet.protocol == UDP): main_st = self.inbound_session_tracker other_st = self.outbound_session_tracker if (packet.protocol == TCP and signature_match): ## if new connection, will add to fw_rules dictionary to prevent duplicate iptable rules, then will add to ## outbound session tracker and set active block to true to notify code to put ip table rule in tcp_st = self.tcp_session_tracker category = self.ip_signatures.get(tracked_ip) if (packet.tcp_syn and not packet.tcp_ack): ## applying lock on dict lookup/ add to protect state between condition and insert with self.fw_rule_creation_lock: if (tracked_ip not in self.fw_rules): self.fw_rules.update( {tracked_ip: [timestamp, category]}) tcp_st[tracked_ip] = {local_ip: 0} active_block = True ## will increment packet count of already active connections on responses/acks as a metric of what made it through elif (packet.tcp_ack and not packet.tcp_syn): count = tcp_st[tracked_ip].get(local_ip, 0) + 1 tcp_st[tracked_ip].update({local_ip: count}) print(f'INCREMENTED {tracked_ip}: {count}') # if a new connection is seen, but already blocked packet counter will reset to ensure numbers do not inflate # can probably remove after more recent changes since we do not see new connections so soon anymore elif (packet.tcp_syn and not packet.tcp_ack): tcp_st[tracked_ip] = {local_ip: 0} ## implementing the block via ip table if user configured to do so based on category and direction block_enabled = getattr(self, f'{category}_block') block_direction = getattr(self, self.block_settings[category]) if (block_enabled and active_block) and (block_direction == direction or block_direction == BOTH): self.StandardBlock(tracked_ip, local_ip, direction, category) # session tracker will check packet counts to add confidence metric, this blocks for 1.5 seconds confidence = self.SessionTracker(tracked_ip, local_ip, direction) print(f'TRACKED IP: {tracked_ip} | CONFIDENCE: {confidence}') # this will prevent the informational log option to log unrelated or already blocked connections elif (block_enabled and not active_block): already_blocked = True ## will match if packet is udp protocol and either source or destination ip matches a proxy signature elif (packet.protocol == UDP and signature_match): # will match if direction is not already tracked category = self.ip_signatures.get(tracked_ip) if (tracked_ip not in other_st and tracked_ip not in main_st): ## applying lock on dict lookup/ add to protect state between condition and insert with self.fw_rule_creation_lock: if (tracked_ip not in self.fw_rules): self.fw_rules.update( {tracked_ip: [timestamp, category]}) main_st[tracked_ip] = {local_ip: 0} active_block = True ## will match if connection is being tracked and increment packet count for confidence metric elif (tracked_ip not in other_st and tracked_ip in main_st): count = tcp_st[tracked_ip].get(local_ip, 0) + 1 main_st[tracked_ip].update({local_ip: count}) print(f'INCREMENTED {packet.dst_ip}: {count}') ## implementing the block via ip table if user configured to do so based on category and direction block_enabled = getattr(self, f'{category}_block') block_direction = getattr(self, self.block_settings[category]) if (block_enabled and active_block) and (block_direction == direction or block_direction == BOTH): self.StandardBlock(tracked_ip, local_ip, direction, category) # session tracker will check packet counts to add confidence metric, this blocks for 1.5 seconds confidence = self.SessionTracker(tracked_ip, local_ip, direction) # this will prevent the informational log option to log unrelated or already blocked connections elif (block_enabled and not active_block): already_blocked = True ## Log to Infected Hosts DB Table if matching malicious type categories ## if (category in {'malware'} and direction == OUTBOUND and self.logging_level >= ALERT): reason = 'malware' table = 'infectedclients' logging_options = { 'infected_client': packet.src_mac, 'src_ip': packet.src_ip, 'detected_host': packet.dst_ip, 'reason': reason } self.TrafficLogging(table, timestamp, logging_options) # logs blocked requests that let more than 7 packets through if (active_block and confidence == MEDIUM and self.logging_level >= WARNING): action = 'blocked' log_connection = True # logs redirected/blocked requests that blocked within 7 packets elif (active_block and confidence in {HIGH, VERY_HIGH} and self.logging_level >= NOTICE): action = 'blocked' log_connection = True # logs all interesting requests if not configured to block and log level is informational elif (signature_match) and (not already_blocked and self.logging_level >= INFORMATIONAL): action = 'logged' confidence = 'n/a' log_connection = True if (log_connection): table = 'ipproxy' logging_options = { 'src_ip': packet.src_ip, 'dst_ip': packet.dst_ip, 'category': category, 'direction': direction, 'action': action, 'confidence': confidence } self.TrafficLogging(table, timestamp, logging_options) def StandardBlock(self, blocked_ip, local_ip, direction, category): chain = self.chain_settings[category] Popen(f'sudo iptables -t mangle -A {chain} -s {blocked_ip} -j DROP && \ sudo iptables -t mangle -A {chain} -d {blocked_ip} -j DROP', shell=True) # applying a wait to give response enough time to come back, if # if response is not seen within time, assumes packet was dropped def SessionTracker(self, blocked_ip, local_ip, direction): time.sleep(1.5) count = self.tcp_session_tracker[blocked_ip].get(local_ip) if (count <= 3): confidence = VERY_HIGH elif (3 < count <= 7): confidence = HIGH else: confidence = MEDIUM self.tcp_session_tracker[blocked_ip].update({local_ip: 0}) return confidence def TrafficLogging(self, table, timestamp, logging_options): ProxyDB = DBConnector(table) ProxyDB.Connect() if (table in {'ipproxy'}): ProxyDB.IPInput(timestamp, logging_options) if (self.syslog_enabled): self.AlertSyslog(logging_options) elif (table in {'infectedclients'}): ProxyDB.InfectedInput(timestamp, logging_options) ProxyDB.Disconnect() def AlertSyslog(self, logging_options): opt = SName(logging_options) if (opt.category in {'malware'}): msg_level = ALERT else: if (opt.confidence == MEDIUM): msg_level = WARNING elif (opt.confidence in {HIGH, VERY_HIGH}): msg_level = NOTICE elif (opt.action == 'logged'): msg_level = INFORMATIONAL message = f'src.ip={opt.src_ip}; dst.ip={opt.dst_ip}; category={opt.category}; ' message += f'direction={opt.direction}; action={opt.action}; confidence={opt.confidence}' self.Syslog.Message(EVENT, msg_level, message) # Loading lists of interesting traffic into sets def LoadSignatures(self): with open(f'{HOME_DIR}/dnx_iplists/blocked.ips', 'r') as blocked: while True: line = blocked.readline().strip().split() if (not line): break if (line != '\n'): host = line[0] category = line[1] self.ip_signatures[host] = category def LoadInterfaces(self): with open(f'{HOME_DIR}/data/config.json', 'r') as settings: setting = json.load(settings) self.wan_int = setting['settings']['interface']['outside'] self.lan_int = setting['settings']['interface']['inside'] # AsyncIO method called to gather automated/ continuous methods | this is python 3.7 version of async async def Main(self): await asyncio.gather(self.Timer.Settings(), self.Timer.Start(), self.Automate.Blocking(), self.Automate.ClearIPTables(), self.Automate.OpenPorts(), self.Log.Settings(LOG_MOD), self.Syslog.Settings(SYSLOG_MOD))
def __init__(self): self.Log = LogHandler(module=LOG_MOD)
class SyslogService: def __init__(self): self.tcp_fallback = False self.udp_fallback = False self.tls_enabled = False self.syslog_protocol = None self.syslog_queue = deque() self.syslog_servers = {} def start(self): self.get_interface_settings() self.Log = LogHandler(process=self) self.Automate = Automate(self) self.SyslogUDP = UDPMessage(self) self.SyslogTCP = TCPMessage(self) self.automate_threads() self.initialize() self._ready_interface_service() def initialize(self): interface.wait_for_interface(self.lan_int) self.lan_ip = interface.wait_for_ip(self.lan_int) while True: if (self.syslog_servers): break time.sleep(FIVE_SEC) threading.Thread(target=self.process_message_queue).start() # Checking the syslog message queue for entries. if entries it will connection to the configured server over the # configured protocol/ports, then send the sockets to the protocol classes to actually send the messages def process_message_queue(self): while True: tcp_connections = None if (not self.syslog_queue): # waiting 5 second before checking queue again for idle perf time.sleep(FIVE_SEC) continue if (self.syslog_protocol == PROTO.TCP): if (self.tls_enabled): tcp_connections = self.SyslogTCP.tls_connect() # if all tls connections failed and tcp fallback is enabled, will attempt to connect to same servers over standard tcp port if (not tcp_connections and self.tcp_fallback): self.SyslogTCP.tcp_connect() else: self.SyslogTCP.tcp_connect() if (tcp_connections): self.SyslogTCP.send_queue(tcp_connections) if (self.syslog_protocol == PROTO.UDP) or (self.udp_fallback and not tcp_connections): udp_socket = self.SyslogUDP.create_udp_socket() if (udp_socket): self.SyslogUDP.send_queue(udp_socket) def get_interface_settings(self): interface_settings = load_configuration('config.json') self.lan_int = interface_settings['settings']['interface']['inside'] # local socket receiving messages to be sent over syslog from all processes firewall wide. once a message is # received it will add it to the queue to be handled by a separate method. def _main(self): while True: try: syslog_message = self.service_sock.recv(2048) if (syslog_message): self.syslog_queue.append(syslog_message) except OSError: #NOTE: should report this to front end if service socket error. break self._ready_interface_service() def _ready_interface_service(self): while True: error = self._create_service_socket() if (not error): break time.sleep(ONE_SEC) self._main() # using loopback so shouldnt have problems, but just taking precautions. def _create_service_socket(self): self.service_sock = socket(AF_INET, SOCK_DGRAM) self.service_sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) try: self.service_sock.bind((LOCALHOST, SYSLOG_SOCKET)) except OSError: # failed to create socket. interface may be down. return True def automate_threads(self): self.Log.start() threading.Thread(target=self.Automate.get_settings).start()