mirror of
https://github.com/evilsocket/opensnitch.git
synced 2025-03-04 00:24:40 +01:00
Split codebase into daemon and ui components
This commit is contained in:
parent
371207a71f
commit
0d5561c734
10 changed files with 302 additions and 52 deletions
44
bin/opensnitch-qt
Executable file
44
bin/opensnitch-qt
Executable file
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
# This file is part of OpenSnitch.
|
||||
#
|
||||
# Copyright(c) 2017 Simone Margaritelli
|
||||
# evilsocket@gmail.com
|
||||
# http://www.evilsocket.net
|
||||
#
|
||||
# This file may be licensed under the terms of of the
|
||||
# GNU General Public License Version 2 (the ``GPL'').
|
||||
#
|
||||
# Software distributed under the License is distributed
|
||||
# on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
|
||||
# express or implied. See the GPL for the specific language
|
||||
# governing rights and limitations.
|
||||
#
|
||||
# You should have received a copy of the GPL along with this
|
||||
# program. If not, go to http://www.gnu.org/licenses/gpl.html
|
||||
# or write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
from argparse import ArgumentParser
|
||||
from opensnitch.ui import QtApp
|
||||
import logging
|
||||
import signal
|
||||
|
||||
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("--debug", dest="debug",
|
||||
action="store_true", default=False,
|
||||
help="Enable debug logs")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(
|
||||
format='[%(asctime)s] (%(levelname)s) %(message)s',
|
||||
level=logging.INFO if args.debug is False else logging.DEBUG)
|
||||
|
||||
# Handle ctrl-c
|
||||
# note that this will cause any finally blocks and similar cleanups
|
||||
# to not get executed
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
app = QtApp()
|
||||
app.run()
|
|
@ -73,6 +73,9 @@ if __name__ == '__main__':
|
|||
if not os.geteuid() == 0:
|
||||
sys.exit('OpenSnitch must be run as root.')
|
||||
|
||||
if 'DBUS_SESSION_BUS_ADDRESS' not in os.environ:
|
||||
raise RuntimeError('DBUS_SESSION_BUS_ADDRESS not set')
|
||||
|
||||
# set_keepcaps allows us to keep caps across setuid call
|
||||
prctl.set_keepcaps(True)
|
||||
prctl.set_caps(*REQUIRED_CAPS)
|
||||
|
@ -97,3 +100,6 @@ if __name__ == '__main__':
|
|||
finally:
|
||||
logging.info("Quitting ...")
|
||||
snitch.stop()
|
||||
|
||||
# Temporary hack to handle Ctrl-C
|
||||
os.kill(os.getpid(), 15)
|
89
opensnitch/dbus_service.py
Normal file
89
opensnitch/dbus_service.py
Normal file
|
@ -0,0 +1,89 @@
|
|||
from gi.repository import GLib
|
||||
|
||||
import dbus.mainloop.glib
|
||||
import dbus.mainloop
|
||||
import dbus.service
|
||||
import dbus
|
||||
|
||||
import logging
|
||||
|
||||
from opensnitch.rule import RuleSaveOption, RuleVerdict
|
||||
|
||||
|
||||
BUS_NAME = 'io.opensnitch.service'
|
||||
OBJECT_PATH = '/'
|
||||
|
||||
|
||||
class OpensnitchService(dbus.service.Object):
|
||||
|
||||
def __init__(self, handlers, rules):
|
||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
|
||||
bus = dbus.SessionBus()
|
||||
bus_name = dbus.service.BusName(BUS_NAME, bus=bus)
|
||||
|
||||
self.handlers = handlers
|
||||
self._rules = rules
|
||||
|
||||
super().__init__(bus_name, OBJECT_PATH)
|
||||
|
||||
@dbus.service.signal(BUS_NAME, signature='usqssuss')
|
||||
def prompt(self, connection_id,
|
||||
hostname,
|
||||
dst_port,
|
||||
dst_addr,
|
||||
proto,
|
||||
app_pid,
|
||||
app_path,
|
||||
app_cmdline):
|
||||
# Signal is emitted by calling decorated function
|
||||
logging.debug('Prompting connection id %s', connection_id)
|
||||
|
||||
@dbus.service.method(BUS_NAME, in_signature='iiib', out_signature='b')
|
||||
def connection_set_result(self, connection_id, save_option,
|
||||
verdict, apply_to_all):
|
||||
save_option = int(save_option)
|
||||
verdict = int(verdict)
|
||||
apply_to_all = bool(apply_to_all)
|
||||
|
||||
try:
|
||||
handler = self.handlers[int(connection_id)]
|
||||
except KeyError:
|
||||
return
|
||||
else:
|
||||
try:
|
||||
handler.future.set_result((save_option,
|
||||
verdict,
|
||||
apply_to_all))
|
||||
except Exception as e:
|
||||
logging.debug('Could not set result %s', e)
|
||||
|
||||
if RuleSaveOption(save_option) != RuleSaveOption.ONCE:
|
||||
self._rules.add_rule(handler.conn, RuleVerdict(verdict),
|
||||
apply_to_all, save_option)
|
||||
|
||||
@dbus.service.method(BUS_NAME,
|
||||
in_signature='i', out_signature='b')
|
||||
def connection_recheck_verdict(self, connection_id):
|
||||
try:
|
||||
handler = self.handlers[int(connection_id)]
|
||||
except KeyError:
|
||||
# If connection is not found or verdict is set connection is
|
||||
# considered to be handled
|
||||
return True
|
||||
else:
|
||||
conn = handler.conn
|
||||
|
||||
verd = self._rules.get_verdict(conn)
|
||||
if verd is None:
|
||||
return False
|
||||
|
||||
handler.future.set_result((RuleSaveOption.ONCE,
|
||||
verd,
|
||||
False)) # Apply to all
|
||||
|
||||
return True
|
||||
|
||||
def run(self):
|
||||
loop = GLib.MainLoop()
|
||||
loop.run()
|
|
@ -1,3 +1,21 @@
|
|||
# This file is part of OpenSnitch.
|
||||
#
|
||||
# Copyright(c) 2017 Adam Hose
|
||||
# adis@blad.is
|
||||
# http://www.evilsocket.net
|
||||
#
|
||||
# This file may be licensed under the terms of of the
|
||||
# GNU General Public License Version 2 (the ``GPL'').
|
||||
#
|
||||
# Software distributed under the License is distributed
|
||||
# on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
|
||||
# express or implied. See the GPL for the specific language
|
||||
# governing rights and limitations.
|
||||
#
|
||||
# You should have received a copy of the GPL along with this
|
||||
# program. If not, go to http://www.gnu.org/licenses/gpl.html
|
||||
# or write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
import iptc
|
||||
|
||||
|
||||
|
@ -27,7 +45,10 @@ class IPTCRules:
|
|||
|
||||
def remove(self):
|
||||
for c, r in self.chains.items():
|
||||
c.delete_rule(r)
|
||||
try:
|
||||
c.delete_rule(r)
|
||||
except iptc.ip4tc.IPTCError:
|
||||
pass
|
||||
|
||||
self.commit()
|
||||
|
||||
|
|
|
@ -24,12 +24,12 @@ import threading
|
|||
import logging
|
||||
import weakref
|
||||
|
||||
from opensnitch.ui import QtApp
|
||||
from opensnitch.connection import Connection
|
||||
from opensnitch.dns import DNSCollector
|
||||
from opensnitch.rule import RuleVerdict, Rules
|
||||
from opensnitch.procmon import ProcMon
|
||||
from opensnitch.iptables import IPTCRules
|
||||
from opensnitch.dbus_service import OpensnitchService
|
||||
|
||||
|
||||
MARK_PACKET_DROP = 101285
|
||||
|
@ -56,11 +56,17 @@ class NetfilterQueueWrapper(threading.Thread):
|
|||
super().__init__()
|
||||
self.snitch = snitch
|
||||
|
||||
self.connection_futures = weakref.WeakValueDictionary()
|
||||
self.handlers = weakref.WeakValueDictionary()
|
||||
self.latest_packet_id = 0
|
||||
|
||||
self._q = None
|
||||
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
if self._q is not None:
|
||||
self._q.unbind()
|
||||
|
||||
def pkt_callback(self, pkt):
|
||||
try:
|
||||
data = pkt.get_payload()
|
||||
|
@ -84,8 +90,16 @@ class NetfilterQueueWrapper(threading.Thread):
|
|||
verd = self.snitch.rules.get_verdict(conn)
|
||||
if verd is None:
|
||||
handler = PacketHandler(conn, pkt, self.snitch.rules)
|
||||
self.connection_futures[conn.id] = handler.future
|
||||
self.snitch.qt_app.prompt_user(conn)
|
||||
self.handlers[conn.id] = handler
|
||||
self.snitch.dbus_service.prompt(
|
||||
conn.id,
|
||||
conn.hostname,
|
||||
conn.dst_port,
|
||||
conn.dst_addr,
|
||||
conn.proto,
|
||||
conn.app.pid or 0,
|
||||
conn.app.path or '',
|
||||
conn.app.cmdline or '')
|
||||
|
||||
elif RuleVerdict(verd) == RuleVerdict.DROP:
|
||||
drop_packet(pkt, conn)
|
||||
|
@ -101,9 +115,8 @@ class NetfilterQueueWrapper(threading.Thread):
|
|||
logging.exception(e)
|
||||
|
||||
def run(self):
|
||||
q = None
|
||||
try:
|
||||
q = NetfilterQueue()
|
||||
self._q = q = NetfilterQueue()
|
||||
q.bind(0, self.pkt_callback, 1024 * 2)
|
||||
q.run()
|
||||
|
||||
|
@ -153,7 +166,9 @@ class Snitch:
|
|||
self.q = NetfilterQueueWrapper(self)
|
||||
self.procmon = ProcMon()
|
||||
self.iptcrules = None
|
||||
self.qt_app = QtApp(self.q.connection_futures, self.rules)
|
||||
|
||||
self.dbus_service = OpensnitchService(
|
||||
self.q.handlers, self.rules)
|
||||
|
||||
def start(self):
|
||||
if ProcMon.is_ftrace_available():
|
||||
|
@ -161,10 +176,11 @@ class Snitch:
|
|||
self.procmon.start()
|
||||
|
||||
self.iptcrules = IPTCRules()
|
||||
self.qt_app.run()
|
||||
self.dbus_service.run()
|
||||
|
||||
def stop(self):
|
||||
self.procmon.disable()
|
||||
self.q.stop()
|
||||
|
||||
if self.iptcrules is not None:
|
||||
self.iptcrules.remove()
|
||||
|
||||
self.procmon.disable()
|
||||
|
|
|
@ -23,6 +23,7 @@ import os
|
|||
|
||||
from .desktop_parser import LinuxDesktopParser
|
||||
from .dialog import Dialog
|
||||
from .dbus import DBusHandler
|
||||
|
||||
|
||||
# TODO: Implement tray icon and menu.
|
||||
|
@ -33,16 +34,13 @@ DIALOG_UI_PATH = "%s/dialog.ui" % RESOURCES_PATH
|
|||
|
||||
|
||||
class QtApp:
|
||||
def __init__(self, connection_futures, rules):
|
||||
def __init__(self):
|
||||
self.desktop_parser = LinuxDesktopParser()
|
||||
self.app = QtWidgets.QApplication([])
|
||||
self.connection_queue = queue.Queue()
|
||||
self.rules = rules
|
||||
self.dialog = Dialog(self, connection_futures, self.desktop_parser)
|
||||
self.dialog = Dialog(self, self.desktop_parser)
|
||||
|
||||
self.dbus_handler = DBusHandler(self.app, self)
|
||||
|
||||
def run(self):
|
||||
self.app.exec()
|
||||
|
||||
def prompt_user(self, connection):
|
||||
self.connection_queue.put(connection)
|
||||
self.dialog.add_connection_signal.emit()
|
||||
|
|
70
opensnitch/ui/dbus.py
Normal file
70
opensnitch/ui/dbus.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
# This file is part of OpenSnitch.
|
||||
#
|
||||
# Copyright(c) 2017 Adam Hose
|
||||
# adis@blad.is
|
||||
# http://www.evilsocket.net
|
||||
#
|
||||
# This file may be licensed under the terms of of the
|
||||
# GNU General Public License Version 2 (the ``GPL'').
|
||||
#
|
||||
# Software distributed under the License is distributed
|
||||
# on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
|
||||
# express or implied. See the GPL for the specific language
|
||||
# governing rights and limitations.
|
||||
#
|
||||
# You should have received a copy of the GPL along with this
|
||||
# program. If not, go to http://www.gnu.org/licenses/gpl.html
|
||||
# or write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
from collections import namedtuple
|
||||
from PyQt5 import QtCore, QtDBus
|
||||
import logging
|
||||
|
||||
|
||||
UIConnection = namedtuple('UIConnection', (
|
||||
'id',
|
||||
'hostname',
|
||||
'dst_port',
|
||||
'dst_addr',
|
||||
'proto',
|
||||
'app_pid',
|
||||
'app_path',
|
||||
'app_cmdline',
|
||||
))
|
||||
|
||||
|
||||
class DBusHandler(QtCore.QObject):
|
||||
|
||||
def __init__(self, app, parent):
|
||||
self.parent = parent
|
||||
self.app = app
|
||||
super().__init__(app)
|
||||
|
||||
self.__dbus = QtDBus.QDBusConnection.sessionBus()
|
||||
self.__dbus.registerObject('/', self)
|
||||
self.interface = QtDBus.QDBusInterface(
|
||||
'io.opensnitch.service',
|
||||
'/',
|
||||
'io.opensnitch.service',
|
||||
self.__dbus)
|
||||
|
||||
if not self.interface.isValid():
|
||||
raise RuntimeError('Could not connect to dbus')
|
||||
logging.info('Connected to dbus service')
|
||||
|
||||
sig_connect = self.__dbus.connect(
|
||||
'io.opensnitch.service',
|
||||
'/',
|
||||
'io.opensnitch.service',
|
||||
'prompt',
|
||||
self.prompt_user)
|
||||
if not sig_connect:
|
||||
raise RuntimeError('Could not connect dbus signal')
|
||||
logging.info('Connected dbus signal')
|
||||
|
||||
@QtCore.pyqtSlot(QtDBus.QDBusMessage)
|
||||
def prompt_user(self, msg):
|
||||
args = msg.arguments()
|
||||
connection = UIConnection(*args)
|
||||
self.parent.connection_queue.put(connection)
|
||||
self.parent.dialog.add_connection_signal.emit()
|
|
@ -18,7 +18,9 @@
|
|||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
from opensnitch.rule import RuleVerdict, RuleSaveOption
|
||||
from PyQt5 import QtCore, QtGui, uic, QtWidgets
|
||||
from PyQt5 import QtDBus
|
||||
import threading
|
||||
import logging
|
||||
import queue
|
||||
import sys
|
||||
import os
|
||||
|
@ -40,7 +42,7 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
|
||||
add_connection_signal = QtCore.pyqtSignal()
|
||||
|
||||
def __init__(self, app, connection_futures, desktop_parser, parent=None):
|
||||
def __init__(self, app, desktop_parser, parent=None):
|
||||
self.connection = None
|
||||
QtWidgets.QDialog.__init__(self, parent,
|
||||
QtCore.Qt.WindowStaysOnTopHint)
|
||||
|
@ -49,10 +51,10 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
self.start_listeners()
|
||||
|
||||
self.connection_queue = app.connection_queue
|
||||
self.connection_futures = connection_futures
|
||||
self.rules = app.rules
|
||||
self.add_connection_signal.connect(self.handle_connection)
|
||||
|
||||
self.app = app
|
||||
|
||||
self.desktop_parser = desktop_parser
|
||||
|
||||
self.rule_lock = threading.Lock()
|
||||
|
@ -70,27 +72,31 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
return
|
||||
|
||||
# Check if process is still alive, if not we dont need to handle
|
||||
if connection.app and connection.app.pid:
|
||||
if connection.app_pid:
|
||||
try:
|
||||
os.kill(connection.app.pid, 0)
|
||||
os.kill(connection.app_pid, 0)
|
||||
except ProcessLookupError:
|
||||
return
|
||||
|
||||
# Re-check in case permanent rule was added since connection was queued
|
||||
verd = False
|
||||
with self.rule_lock:
|
||||
verd = self.rules.get_verdict(connection)
|
||||
if verd is not None:
|
||||
self.set_conn_result(connection, RuleSaveOption.ONCE,
|
||||
verd, False)
|
||||
msg = self.app.dbus_handler.interface.call(
|
||||
'connection_recheck_verdict', connection.id)
|
||||
reply = QtDBus.QDBusReply(msg)
|
||||
if reply.isValid() and reply.value():
|
||||
verd = True
|
||||
elif not reply.isValid():
|
||||
logging.error(msg.arguments()[0])
|
||||
|
||||
# Lock needs to be released before callback can be triggered
|
||||
if verd is not None:
|
||||
if verd:
|
||||
return self.add_connection_signal.emit()
|
||||
|
||||
self.connection = connection
|
||||
if connection.app.path is not None:
|
||||
if connection.app_path is not None:
|
||||
app_name, app_icon = self.desktop_parser.get_info_by_path(
|
||||
connection.app.path)
|
||||
connection.app_path)
|
||||
else:
|
||||
app_name = 'Unknown'
|
||||
app_icon = None
|
||||
|
@ -110,7 +116,7 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
|
||||
message = self.MESSAGE_TEMPLATE % (
|
||||
helpers.get_app_name_and_cmdline(self.connection),
|
||||
getattr(self.connection.app, 'pid', 'Unknown'),
|
||||
self.connection.app_pid or 'Unknown',
|
||||
self.connection.hostname,
|
||||
self.connection.proto.upper(),
|
||||
self.connection.dst_port,
|
||||
|
@ -174,13 +180,19 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
def _block_action(self):
|
||||
self._action(RuleVerdict.DROP, True)
|
||||
|
||||
def set_conn_result(self, connection, option, verdict, apply_to_all):
|
||||
try:
|
||||
fut = self.connection_futures[connection.id]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
fut.set_result((option, verdict, apply_to_all))
|
||||
def set_conn_result(self, connection_id, save_option,
|
||||
verdict, apply_to_all):
|
||||
msg = self.app.dbus_handler.interface.call(
|
||||
'connection_set_result',
|
||||
connection_id,
|
||||
RuleSaveOption(save_option).value,
|
||||
RuleVerdict(verdict).value,
|
||||
apply_to_all)
|
||||
reply = QtDBus.QDBusReply(msg)
|
||||
if not reply.isValid():
|
||||
logging.info(
|
||||
'Could not apply result to connection "%s"', connection_id)
|
||||
logging.error(msg.arguments()[0])
|
||||
|
||||
def _action(self, verdict, apply_to_all=False):
|
||||
with self.rule_lock:
|
||||
|
@ -193,16 +205,9 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
elif s_option == "Forever":
|
||||
option = RuleSaveOption.FOREVER
|
||||
|
||||
self.set_conn_result(self.connection, option,
|
||||
self.set_conn_result(self.connection.id, option,
|
||||
verdict, apply_to_all)
|
||||
|
||||
# We need to freeze UI thread while storing rule, otherwise another
|
||||
# connection that would have been affected by the rule will pop up
|
||||
# TODO: Figure out how to do this nicely when separating UI
|
||||
if option != RuleSaveOption.ONCE:
|
||||
self.rules.add_rule(self.connection, verdict,
|
||||
apply_to_all, option)
|
||||
|
||||
# Check if we have any unhandled connections on the queue
|
||||
self.connection = None # Indicate next connection can be handled
|
||||
self.hide()
|
||||
|
|
|
@ -32,14 +32,14 @@ def get_app_name_and_cmdline(conn):
|
|||
if conn.proto == 'icmp':
|
||||
return 'Unknown'
|
||||
|
||||
if conn.app.cmdline is not None:
|
||||
if conn.app_cmdline is not None:
|
||||
# TODO: Figure out why we get mixed types here
|
||||
cmdline = conn.app.cmdline if isinstance(conn.app.cmdline, str) else conn.app.cmdline.decode() # noqa
|
||||
path = conn.app.path if isinstance(conn.app.path, str) else conn.app.path.decode() # noqa
|
||||
cmdline = conn.app_cmdline if isinstance(conn.app_cmdline, str) else conn.app_cmdline.decode() # noqa
|
||||
path = conn.app_path if isinstance(conn.app_path, str) else conn.app.path_decode() # noqa
|
||||
|
||||
if cmdline.startswith(conn.app.path):
|
||||
if cmdline.startswith(conn.app_path):
|
||||
return cmdline
|
||||
else:
|
||||
return "%s %s" % (path, cmdline)
|
||||
else:
|
||||
return conn.app.path
|
||||
return conn.app_path
|
||||
|
|
3
setup.py
3
setup.py
|
@ -40,7 +40,7 @@ setup(name='opensnitch',
|
|||
author_email='evilsocket@gmail.com',
|
||||
url='http://www.github.com/evilsocket/opensnitch',
|
||||
packages=find_packages(),
|
||||
scripts=['bin/opensnitch'],
|
||||
scripts=['bin/opensnitchd', 'bin/opensnitch-qt'],
|
||||
package_data={'': ['*.ui']},
|
||||
license='GPL',
|
||||
zip_safe=False,
|
||||
|
@ -52,4 +52,5 @@ setup(name='opensnitch',
|
|||
'pyinotify',
|
||||
'python-iptables',
|
||||
'python-prctl',
|
||||
'python-gobject',
|
||||
])
|
||||
|
|
Loading…
Add table
Reference in a new issue