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:
adisbladis 2017-05-22 02:48:40 +08:00
parent eb3f96c303
commit f8dc4c60b3
Failed to generate hash of commit
8 changed files with 167 additions and 169 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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