diff --git a/main.py b/main.py index 3688c414..a7eaa28a 100755 --- a/main.py +++ b/main.py @@ -1,14 +1,20 @@ #!/usr/bin/python +import os +import logging -from opensnitch.packetqueue import PacketQueue +logging.basicConfig(format='[%(asctime)s] (%(levelname)s) %(message)s',level=logging.DEBUG) +logging.getLogger("scapy.runtime").setLevel(logging.ERROR) -q = PacketQueue() +from opensnitch.snitch import Snitch + +snitch = Snitch() try: - q.start() + logging.info( "OpenSnitch running with pid %d." % os.getpid() ) + snitch.start() except KeyboardInterrupt, e: pass -print "\n\nStopping ..." +logging.info( "Quitting ..." ) -q.stop() +snitch.stop() diff --git a/opensnitch/connection.py b/opensnitch/connection.py index 9970b855..02b43b86 100644 --- a/opensnitch/connection.py +++ b/opensnitch/connection.py @@ -5,10 +5,11 @@ from socket import inet_ntoa class Connection: def __init__( self, payload ): - self.data = payload.get_data() + self.data = payload self.pkt = ip.IP( self.data ) self.src_addr = inet_ntoa( self.pkt.src ) self.dst_addr = inet_ntoa( self.pkt.dst ) + self.hostname = None self.src_port = None self.dst_port = None self.proto = None diff --git a/opensnitch/dns.py b/opensnitch/dns.py new file mode 100644 index 00000000..4d22323b --- /dev/null +++ b/opensnitch/dns.py @@ -0,0 +1,32 @@ +import logging +from threading import Lock +from scapy.all import * + +class DNSCollector: + def __init__(self): + self.lock = Lock() + self.hosts = { '127.0.0.1': 'localhost' } + + def is_dns_response(self, packet): + if DNSRR in packet and packet.qd is not None and packet.an is not None: + return True + else: + return False + + def add_response( self, packet ): + with self.lock: + hostname = packet.qd.qname + address = packet.an.rdata + if hostname.endswith('.'): + hostname = hostname[:-1] + + logging.debug( "DNS[%s] = %s" % ( address, hostname ) ) + self.hosts[address] = hostname + + def get_hostname( self, address ): + with self.lock: + if address in self.hosts: + return self.hosts[address] + else: + logging.debug( "No hostname found for address %s" % address ) + return address diff --git a/opensnitch/packetqueue.py b/opensnitch/packetqueue.py deleted file mode 100644 index 21eaf569..00000000 --- a/opensnitch/packetqueue.py +++ /dev/null @@ -1,59 +0,0 @@ -import os -import nfqueue -from socket import AF_INET, AF_INET6, inet_ntoa -from threading import Lock - -from opensnitch.ui import UI -from opensnitch.connection import Connection -from opensnitch.rule import Rules - -class PacketQueue: - lock = Lock() - rules = Rules() - fw_rules = ( "OUTPUT -t mangle -m conntrack --ctstate NEW -j NFQUEUE --queue-num 0 --queue-bypass", - "INPUT --protocol udp --sport 53 -j NFQUEUE --queue-num 0 --queue-bypass" ) - - @staticmethod - def get_verdict( c ): - verdict = PacketQueue.rules.get_verdict(c) - - if verdict is None: - with PacketQueue.lock: - ( verdict, apply_for_all ) = UI.prompt_user( c.app.name, - c.app_path, - None, - c.dst_addr, - c.dst_port, - c.proto ) - PacketQueue.rules.add_rule( c, verdict, apply_for_all ) - - return verdict - - @staticmethod - def pkt_callback(pkt): - conn = Connection(pkt) - verd = nfqueue.NF_ACCEPT - - if conn.proto is not None: - verd = PacketQueue.get_verdict( conn ) - - pkt.set_verdict(verd) - return 1 - - # TODO: Support IPv6! - def __init__( self ): - self.q = nfqueue.queue() - self.q.set_callback( PacketQueue.pkt_callback ) - self.q.fast_open(0, AF_INET) - self.q.set_queue_maxlen(2*1024) - - def start(self): - for r in PacketQueue.fw_rules: - os.system( "iptables -I %s" % r ) - self.q.try_run() - - def stop(self): - for r in PacketQueue.fw_rules: - os.system( "iptables -D %s" % r ) - self.q.unbind(AF_INET) - self.q.close() diff --git a/opensnitch/snitch.py b/opensnitch/snitch.py new file mode 100644 index 00000000..ec9a6b0b --- /dev/null +++ b/opensnitch/snitch.py @@ -0,0 +1,71 @@ +import os +import logging +import nfqueue +from socket import AF_INET, AF_INET6, inet_ntoa +from threading import Lock +from scapy.all import * + +from opensnitch.ui import UI +from opensnitch.connection import Connection +from opensnitch.dns import DNSCollector +from opensnitch.rule import Rules + +class Snitch: + IPTABLES_RULES = ( "OUTPUT -t mangle -m conntrack --ctstate NEW -j NFQUEUE --queue-num 0 --queue-bypass", ) + + # TODO: Support IPv6! + def __init__( self ): + self.lock = Lock() + self.rules = Rules() + self.dns = DNSCollector() + self.q = nfqueue.queue() + + self.q.set_callback( self.pkt_callback ) + self.q.fast_open(0, AF_INET) + self.q.set_queue_maxlen(2*1024) + + def get_verdict(self,c): + verdict = self.rules.get_verdict(c) + + if verdict is None: + with self.lock: + c.hostname = self.dns.get_hostname(c.dst_addr) + ( verdict, apply_for_all ) = UI.prompt_user(c) + self.rules.add_rule( c, verdict, apply_for_all ) + + return verdict + + def pkt_callback(self,pkt): + verd = nfqueue.NF_ACCEPT + + try: + data = pkt.get_data() + packet = IP(data) + + if self.dns.is_dns_response(packet): + self.dns.add_response(packet) + + else: + conn = Connection(data) + if conn.proto is not None: + verd = self.get_verdict( conn ) + except Exception as e: + print e + + pkt.set_verdict(verd) + return 1 + + def start(self): + for r in Snitch.IPTABLES_RULES: + logging.debug( "Applying iptables rule '%s'" % r ) + os.system( "iptables -I %s" % r ) + + self.q.try_run() + + def stop(self): + for r in Snitch.IPTABLES_RULES: + logging.debug( "Deleting iptables rule '%s'" % r ) + os.system( "iptables -D %s" % r ) + + self.q.unbind(AF_INET) + self.q.close() diff --git a/opensnitch/ui.py b/opensnitch/ui.py index 00573574..3b7108dd 100644 --- a/opensnitch/ui.py +++ b/opensnitch/ui.py @@ -3,14 +3,14 @@ import nfqueue class UI: @staticmethod - def prompt_user( app_name, app_path, app_icon, d_addr, d_port, proto ): + def prompt_user( c ): title = 'OpenSnitch' msg = "%s (%s) wants to connect to %s on %s port %s" % ( \ - app_name, - app_path, - d_addr, - proto.upper(), - d_port ) + c.app.name, + c.app_path, + c.hostname, + c.proto.upper(), + c.dst_port ) choices = [ 'Allow Once', 'Allow Forever', 'Allow All',