Split codebase into daemon and ui components

This commit is contained in:
adisbladis 2017-06-12 01:44:48 +08:00
parent 371207a71f
commit 0d5561c734
Failed to generate hash of commit
10 changed files with 302 additions and 52 deletions

44
bin/opensnitch-qt Executable file
View 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()

View file

@ -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)

View 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()

View file

@ -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()

View file

@ -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()

View file

@ -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
View 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()

View file

@ -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()

View file

@ -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

View file

@ -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',
])