mirror of
https://github.com/evilsocket/opensnitch.git
synced 2025-03-04 08:34:40 +01:00
Refactor connection.py to use namedtuples instead of objects
Move utility functions to get app path and cmdline into proc.py get_pid_by_connection now only gets pid by connection data Move Application into connection.py, this didn't really do much anyway and had some overlap with connection Move more things into ui subpackage that should not belong in daemon As an added bonus this also gives a nice little decrease in memory usage
This commit is contained in:
parent
eb3f96c303
commit
f8dc4c60b3
8 changed files with 167 additions and 169 deletions
|
@ -1,43 +0,0 @@
|
|||
# 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.
|
||||
import logging
|
||||
|
||||
|
||||
class Application:
|
||||
def __init__(self, procmon, pid, path):
|
||||
self.pid = pid
|
||||
self.path = path
|
||||
|
||||
try:
|
||||
|
||||
self.cmdline = None
|
||||
|
||||
if self.pid is not None:
|
||||
if procmon.running:
|
||||
self.cmdline = procmon.get_cmdline(pid)
|
||||
if self.cmdline is None:
|
||||
logging.debug(
|
||||
"Could not find pid %s command line with ProcMon", pid) # noqa
|
||||
|
||||
if self.cmdline is None:
|
||||
with open("/proc/%s/cmdline" % pid) as cmd_fd:
|
||||
self.cmdline = cmd_fd.read().replace('\0', ' ').strip()
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
|
@ -16,84 +16,70 @@
|
|||
# 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 opensnitch.proc import get_pid_by_connection
|
||||
from opensnitch.app import Application
|
||||
from collections import namedtuple
|
||||
from socket import inet_ntoa
|
||||
from opensnitch import proc
|
||||
from dpkt import ip
|
||||
from socket import inet_ntoa, getservbyport
|
||||
|
||||
|
||||
class Connection:
|
||||
def __init__(self, packet_id, procmon, payload):
|
||||
self.id = packet_id
|
||||
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
|
||||
self.app = None
|
||||
Application = namedtuple('Application', ('pid', 'path', 'cmdline'))
|
||||
_Connection = namedtuple('Connection', (
|
||||
'id',
|
||||
'data',
|
||||
'pkt',
|
||||
'src_addr',
|
||||
'dst_addr',
|
||||
'hostname',
|
||||
'src_port',
|
||||
'dst_port',
|
||||
'proto',
|
||||
'app'))
|
||||
|
||||
if self.pkt.p == ip.IP_PROTO_TCP:
|
||||
self.proto = 'tcp'
|
||||
self.src_port = self.pkt.tcp.sport
|
||||
self.dst_port = self.pkt.tcp.dport
|
||||
elif self.pkt.p == ip.IP_PROTO_UDP:
|
||||
self.proto = 'udp'
|
||||
self.src_port = self.pkt.udp.sport
|
||||
self.dst_port = self.pkt.udp.dport
|
||||
elif self.pkt.p == ip.IP_PROTO_ICMP:
|
||||
self.proto = 'icmp'
|
||||
self.src_port = None
|
||||
self.dst_port = None
|
||||
|
||||
if self.proto == 'icmp':
|
||||
self.pid = None
|
||||
self.app = None
|
||||
self.app_path = None
|
||||
self.service = None
|
||||
def Connection(procmon, dns, packet_id, payload):
|
||||
data = payload
|
||||
pkt = ip.IP(data)
|
||||
src_addr = inet_ntoa(pkt.src)
|
||||
dst_addr = inet_ntoa(pkt.dst)
|
||||
hostname = dns.get_hostname(dst_addr)
|
||||
src_port = None
|
||||
dst_port = None
|
||||
proto = None
|
||||
app = None
|
||||
|
||||
elif None not in (self.proto, self.src_addr, self.dst_addr):
|
||||
try:
|
||||
self.service = getservbyport(int(self.dst_port), self.proto)
|
||||
except:
|
||||
self.service = None
|
||||
if pkt.p == ip.IP_PROTO_TCP:
|
||||
proto = 'tcp'
|
||||
src_port = pkt.tcp.sport
|
||||
dst_port = pkt.tcp.dport
|
||||
elif pkt.p == ip.IP_PROTO_UDP:
|
||||
proto = 'udp'
|
||||
src_port = pkt.udp.sport
|
||||
dst_port = pkt.udp.dport
|
||||
elif pkt.p == ip.IP_PROTO_ICMP:
|
||||
proto = 'icmp'
|
||||
src_port = None
|
||||
dst_port = None
|
||||
|
||||
self.pid, self.app_path = get_pid_by_connection(procmon,
|
||||
self.src_addr,
|
||||
self.src_port,
|
||||
self.dst_addr,
|
||||
self.dst_port,
|
||||
self.proto)
|
||||
self.app = Application(procmon, self.pid, self.app_path)
|
||||
self.app_path = self.app.path
|
||||
if proto == 'icmp':
|
||||
app = Application(None, None, None)
|
||||
|
||||
def get_app_name(self):
|
||||
if self.app_path == 'Unknown':
|
||||
return self.app_path
|
||||
elif None not in (proto, src_addr, dst_addr):
|
||||
pid = proc.get_pid_by_connection(src_addr,
|
||||
src_port,
|
||||
dst_addr,
|
||||
dst_port,
|
||||
proto)
|
||||
app = Application(
|
||||
pid, *proc._get_app_path_and_cmdline(procmon, pid))
|
||||
|
||||
elif self.app_path == self.app.name:
|
||||
return self.app_path
|
||||
|
||||
else:
|
||||
return "'%s' ( %s )" % ( self.app.name, self.app_path )
|
||||
|
||||
def get_app_name_and_cmdline(self):
|
||||
if self.proto == 'icmp':
|
||||
return 'Unknown'
|
||||
|
||||
if self.app.cmdline is not None:
|
||||
# TODO: Figure out why we get mixed types here
|
||||
cmdline = self.app.cmdline if isinstance(self.app.cmdline, str) else self.app.cmdline.decode()
|
||||
path = self.app.path if isinstance(self.app.path, str) else self.app.path.decode()
|
||||
|
||||
if cmdline.startswith(self.app.path):
|
||||
return cmdline
|
||||
else:
|
||||
return "%s %s" % (path, cmdline)
|
||||
else:
|
||||
return path
|
||||
|
||||
def __repr__(self):
|
||||
return "[%s] %s (%s) -> %s:%s" % ( self.pid, self.app_path, self.proto, self.dst_addr, self.dst_port )
|
||||
return _Connection(
|
||||
packet_id,
|
||||
data,
|
||||
pkt,
|
||||
src_addr,
|
||||
dst_addr,
|
||||
hostname,
|
||||
src_port,
|
||||
dst_port,
|
||||
proto,
|
||||
app)
|
||||
|
|
|
@ -21,30 +21,15 @@ import psutil
|
|||
import os
|
||||
|
||||
|
||||
def get_pid_by_connection(procmon, src_addr, src_p, dst_addr,
|
||||
dst_p, proto='tcp'):
|
||||
def get_pid_by_connection(src_addr, src_p, dst_addr, dst_p, proto='tcp'):
|
||||
pids = (connection.pid for connection in psutil.net_connections(kind=proto)
|
||||
if connection.laddr == (src_addr, int(src_p)) and
|
||||
connection.raddr == (dst_addr, int(dst_p)))
|
||||
|
||||
# We always take the first element as we assume it contains only one
|
||||
# It should not be possible to keep two connections which are the same.
|
||||
for pid in pids:
|
||||
try:
|
||||
appname = None
|
||||
if procmon.running:
|
||||
appname = procmon.get_app_name(pid)
|
||||
|
||||
if appname is None:
|
||||
appname = os.readlink("/proc/%s/exe" % pid)
|
||||
if procmon.running:
|
||||
logging.debug("Could not find pid %s with ProcMon, falling back to /proc/%s/exe -> %s", pid, pid, appname) # noqa
|
||||
else:
|
||||
logging.debug("ProcMon(%s) = %s", pid, appname)
|
||||
|
||||
return (pid, appname)
|
||||
except OSError:
|
||||
return (None, "Unknown")
|
||||
for p in pids:
|
||||
return p
|
||||
|
||||
logging.warning("Could not find process for %s connection %s:%s -> %s:%s",
|
||||
proto,
|
||||
|
@ -53,4 +38,34 @@ def get_pid_by_connection(procmon, src_addr, src_p, dst_addr,
|
|||
dst_addr,
|
||||
dst_p)
|
||||
|
||||
return (None, "Unknown")
|
||||
return None
|
||||
|
||||
|
||||
def _get_app_path_and_cmdline(procmon, pid):
|
||||
path, args = None, None
|
||||
if pid is None:
|
||||
return (path, args)
|
||||
|
||||
pmr = procmon.get_app(pid)
|
||||
if pmr:
|
||||
path = pmr.get('filename')
|
||||
args = pmr.get('args')
|
||||
|
||||
if not path:
|
||||
logging.debug("Could not find pid %s with ProcMon, falling back to /proc/%s/exe -> %s", pid, pid) # noqa
|
||||
try:
|
||||
path = os.readlink("/proc/{}/exe".format(pid))
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
if not args:
|
||||
logging.debug(
|
||||
"Could not find pid %s command line with ProcMon", pid) # noqa
|
||||
|
||||
try:
|
||||
with open("/proc/{}/cmdline".format(pid)) as cmd_fd:
|
||||
cmd_fd.read().replace('\0', ' ').strip()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
|
||||
return (path, args)
|
||||
|
|
|
@ -80,30 +80,19 @@ class ProcMon(threading.Thread):
|
|||
|
||||
return False
|
||||
|
||||
def get_app_name( self, pid ):
|
||||
if pid is not None:
|
||||
pid = int(pid)
|
||||
with self.lock:
|
||||
if pid in self.pids and 'filename' in self.pids[pid]:
|
||||
return self.pids[pid]['filename']
|
||||
|
||||
return None
|
||||
|
||||
def get_cmdline( self, pid ):
|
||||
pid = int(pid)
|
||||
with self.lock:
|
||||
if pid in self.pids and 'args' in self.pids[pid]:
|
||||
return self.pids[pid]['args']
|
||||
|
||||
return None
|
||||
def get_app(self, pid):
|
||||
try:
|
||||
return self.pids[pid]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def _dump( self, pid, e ):
|
||||
logging.debug( "(pid=%d) %s %s" % ( pid, e['filename'], e['args'] if 'args' in e else '' ) )
|
||||
|
||||
def _on_exec( self, pid, filename ):
|
||||
def _on_exec(self, pid, filename):
|
||||
with self.lock:
|
||||
self.pids[pid]['filename'] = filename
|
||||
self._dump( pid, self.pids[pid] )
|
||||
self._dump(pid, self.pids[pid])
|
||||
|
||||
def _on_args( self, pid, args ):
|
||||
with self.lock:
|
||||
|
|
|
@ -44,7 +44,7 @@ class RuleSaveOption(Enum):
|
|||
|
||||
|
||||
def matches(rule, conn):
|
||||
if rule.app_path != conn.app_path:
|
||||
if rule.app_path != conn.app.path:
|
||||
return False
|
||||
|
||||
elif rule.address is not None and rule.address != conn.dst_addr:
|
||||
|
@ -72,7 +72,7 @@ class Rules:
|
|||
|
||||
def get_verdict(self, connection):
|
||||
with self.mutex:
|
||||
for r in self.rules.get(connection.app_path, []):
|
||||
for r in self.rules.get(connection.app.path, []):
|
||||
if matches(r, connection):
|
||||
return r.verdict
|
||||
|
||||
|
@ -101,11 +101,11 @@ class Rules:
|
|||
|
||||
if apply_to_all is True:
|
||||
self._remove_rules_for_path(
|
||||
connection.app_path,
|
||||
connection.app.path,
|
||||
(RuleSaveOption(save_option) == RuleSaveOption.FOREVER))
|
||||
|
||||
r = Rule(
|
||||
connection.app_path,
|
||||
connection.app.path,
|
||||
verdict,
|
||||
connection.dst_addr if not apply_to_all else None,
|
||||
connection.dst_port if not apply_to_all else None,
|
||||
|
|
|
@ -44,8 +44,8 @@ IPTABLES_RULES = (
|
|||
|
||||
|
||||
def drop_packet(pkt, conn):
|
||||
logging.info(
|
||||
"Dropping %s from %s" % (conn, conn.get_app_name()))
|
||||
logging.info('Dropping %s from "%s %s"',
|
||||
conn, conn.app.path, conn.app.cmdline)
|
||||
pkt.set_mark(MARK_PACKET_DROP)
|
||||
pkt.drop()
|
||||
|
||||
|
@ -130,19 +130,19 @@ class Snitch:
|
|||
return
|
||||
|
||||
self.latest_packet_id += 1
|
||||
conn = Connection(self.latest_packet_id, self.procmon, data)
|
||||
conn = Connection(self.procmon, self.dns,
|
||||
self.latest_packet_id, data)
|
||||
if conn.proto is None:
|
||||
logging.debug("Could not detect protocol for packet.")
|
||||
return
|
||||
|
||||
elif conn.pid is None and conn.proto != 'icmp':
|
||||
elif conn.app.pid is None and conn.proto != 'icmp':
|
||||
logging.debug("Could not detect process for connection.")
|
||||
return
|
||||
|
||||
# Get verdict, if verdict cannot be found prompt user in thread
|
||||
verd = self.rules.get_verdict(conn)
|
||||
if verd is None:
|
||||
conn.hostname = self.dns.get_hostname(conn.dst_addr)
|
||||
handler = PacketHandler(conn, pkt, self.rules)
|
||||
self.connection_futures[conn.id] = handler.future
|
||||
self.qt_app.prompt_user(conn)
|
||||
|
|
|
@ -23,6 +23,8 @@ import queue
|
|||
import sys
|
||||
import os
|
||||
|
||||
from opensnitch.ui import helpers
|
||||
|
||||
|
||||
# TODO: Implement tray icon and menu.
|
||||
# TODO: Implement rules editor.
|
||||
|
@ -79,8 +81,12 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
return self.add_connection_signal.emit()
|
||||
|
||||
self.connection = connection
|
||||
app_name, app_icon = self.desktop_parser.get_info_by_path(
|
||||
connection.app.path)
|
||||
if connection.app.path is not None:
|
||||
app_name, app_icon = self.desktop_parser.get_info_by_path(
|
||||
connection.app.path)
|
||||
else:
|
||||
app_name = 'Unknown'
|
||||
app_icon = None
|
||||
|
||||
self.setup_labels(app_name)
|
||||
self.setup_icon(app_icon)
|
||||
|
@ -96,12 +102,12 @@ class Dialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
|
|||
self.app_name_label.setText(app_name or 'Unknown')
|
||||
|
||||
message = self.MESSAGE_TEMPLATE % (
|
||||
self.connection.get_app_name_and_cmdline(),
|
||||
getattr(self.connection.app, 'pid', 'Unknown'),
|
||||
self.connection.hostname,
|
||||
self.connection.proto.upper(),
|
||||
self.connection.dst_port,
|
||||
" (%s)" % self.connection.service or '')
|
||||
helpers.get_app_name_and_cmdline(self.connection),
|
||||
getattr(self.connection.app, 'pid', 'Unknown'),
|
||||
self.connection.hostname,
|
||||
self.connection.proto.upper(),
|
||||
self.connection.dst_port,
|
||||
helpers.get_service(self.connection))
|
||||
self.message_label.setText(message)
|
||||
|
||||
def init_widgets(self):
|
||||
|
|
45
opensnitch/ui/helpers.py
Normal file
45
opensnitch/ui/helpers.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
# 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 socket import getservbyport
|
||||
|
||||
|
||||
def get_service(conn):
|
||||
dst_port = conn.dst_port
|
||||
proto = conn.proto
|
||||
try:
|
||||
return ' ({}) '.format(getservbyport(int(dst_port), proto))
|
||||
except:
|
||||
return ''
|
||||
|
||||
|
||||
def get_app_name_and_cmdline(conn):
|
||||
if conn.proto == 'icmp':
|
||||
return 'Unknown'
|
||||
|
||||
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
|
||||
|
||||
if cmdline.startswith(conn.app.path):
|
||||
return cmdline
|
||||
else:
|
||||
return "%s %s" % (path, cmdline)
|
||||
else:
|
||||
return conn.app.path
|
Loading…
Add table
Reference in a new issue