commit 4082270d068338fa01a433a9006a1073fc4d087b Author: Damian Johnson atagar@torproject.org Date: Sat Jan 30 12:56:27 2016 -0800
Allow stem.util.proc.connection() to query by user
Our proc contents provide connection metadata with only two things we can filter by: inode and uid. Thus far we've used inodes to allow lookups by pid (thing we usually want to resolve connections by), but soon I'm gonna have a need for resolution by uid.
This also allows the funciton to provide all system connections if no pid or user is provided. --- stem/util/connection.py | 2 +- stem/util/proc.py | 133 ++++++++++++++++++++++++++---------------------- test/unit/util/proc.py | 47 +++++++++++++---- 3 files changed, 111 insertions(+), 71 deletions(-)
diff --git a/stem/util/connection.py b/stem/util/connection.py index e72bd12..342afde 100644 --- a/stem/util/connection.py +++ b/stem/util/connection.py @@ -202,7 +202,7 @@ def get_connections(resolver, process_pid = None, process_name = None): raise IOError("There's multiple processes named '%s'. %s requires a single pid to provide the connections." % (process_name, resolver))
if resolver == Resolver.PROC: - return [Connection(*conn) for conn in stem.util.proc.connections(process_pid)] + return stem.util.proc.connections(pid = process_pid)
resolver_command = RESOLVER_COMMAND[resolver].format(pid = process_pid)
diff --git a/stem/util/proc.py b/stem/util/proc.py index 7fdd70f..40f9ba8 100644 --- a/stem/util/proc.py +++ b/stem/util/proc.py @@ -50,10 +50,12 @@ future, use them at your own risk.** import base64 import os import platform +import pwd import socket import sys import time
+import stem.util.connection import stem.util.enum import stem.util.str_tools
@@ -325,35 +327,87 @@ def file_descriptors_used(pid): raise IOError('Unable to check number of file descriptors used: %s' % exc)
-def connections(pid): +def connections(pid = None, user = None): """ - Queries connection related information from the proc contents. This provides - similar results to netstat, lsof, sockstat, and other connection resolution - utilities (though the lookup is far quicker). + Queries connections from the proc contents. This matches netstat, lsof, and + friends but is much faster. If no **pid** or **user** are provided this + provides all present connections.
- :param int pid: process id of the process to be queried + :param int pid: pid to provide connections for + :param str user: username to look up connections for
- :returns: A listing of connection tuples of the form **[(local_ipAddr1, - local_port1, foreign_ipAddr1, foreign_port1, protocol, is_ipv6), ...]** - (addresses and protocols are strings and ports are ints) + :returns: **list** of :class:`~stem.util.connection.Connection` instances
:raises: **IOError** if it can't be determined """
+ start_time, conn = time.time(), [] + + if pid: + parameter = 'connections for pid %s' % pid + + try: + pid = int(pid) + + if pid < 0: + raise IOError("Process pids can't be negative: %s" % pid) + except (ValueError, TypeError): + raise IOError('Process pid was non-numeric: %s' % pid) + elif user: + parameter = 'connections for user %s' % user + else: + parameter = 'all connections' + try: - pid = int(pid) + inodes = _inodes_for_sockets(pid) if pid else [] + process_uid = pwd.getpwnam(user).pw_uid if user else None
- if pid < 0: - raise IOError("Process pids can't be negative: %s" % pid) - except (ValueError, TypeError): - raise IOError('Process pid was non-numeric: %s' % pid) + for proc_file_path in ('/proc/net/tcp', '/proc/net/tcp6', '/proc/net/udp', '/proc/net/udp6'): + if proc_file_path.endswith('6') and not os.path.exists(proc_file_path): + continue # ipv6 proc contents are optional
- if pid == 0: - return [] + try: + with open(proc_file_path, 'rb') as proc_file: + proc_file.readline() # skip the first line + + for line in proc_file: + _, l_addr, f_addr, status, _, _, _, uid, _, inode = line.split()[:10] + protocol = proc_file_path[10:].rstrip('6') # 'tcp' or 'udp' + is_ipv6 = proc_file_path.endswith('6') + + if inodes and inode not in inodes: + continue + elif process_uid and int(uid) != process_uid: + continue + elif protocol == 'tcp' and status != b'01': + continue # skip tcp connections that aren't yet established + + local_ip, local_port = _decode_proc_address_encoding(l_addr, is_ipv6) + foreign_ip, foreign_port = _decode_proc_address_encoding(f_addr, is_ipv6) + conn.append(stem.util.connection.Connection(local_ip, local_port, foreign_ip, foreign_port, protocol, is_ipv6)) + except IOError as exc: + raise IOError("unable to read '%s': %s" % (proc_file_path, exc)) + except Exception as exc: + raise IOError("unable to parse '%s': %s" % (proc_file_path, exc)) + + _log_runtime(parameter, '/proc/net/[tcp|udp]', start_time) + return conn + except IOError as exc: + _log_failure(parameter, exc) + raise + + +def _inodes_for_sockets(pid): + """ + Provides inodes in use by a process for its sockets.
- # fetches the inode numbers for socket file descriptors + :param int pid: process id of the process to be queried + + :returns: **list** with inodes for its sockets + + :raises: **IOError** if it can't be determined + """
- start_time, parameter = time.time(), 'process connections' inodes = []
try: @@ -376,50 +430,9 @@ def connections(pid): continue # descriptors may shift while we're in the middle of iterating over them
# most likely couldn't be read due to permissions - exc = IOError('unable to determine file descriptor destination (%s): %s' % (exc, fd_path)) - _log_failure(parameter, exc) - raise exc - - if not inodes: - # unable to fetch any connections for this process - return [] - - # check for the connection information from the /proc/net contents - - conn = [] - - for proc_file_path in ('/proc/net/tcp', '/proc/net/tcp6', '/proc/net/udp', '/proc/net/udp6'): - if not os.path.exists(proc_file_path): - continue - - try: - with open(proc_file_path, 'rb') as proc_file: - proc_file.readline() # skip the first line - - for line in proc_file: - _, l_addr, f_addr, status, _, _, _, _, _, inode = line.split()[:10] - - if inode in inodes: - protocol = proc_file_path[10:].rstrip('6') # 'tcp' or 'udp' - is_ipv6 = proc_file_path.endswith('6') - - if protocol == 'tcp' and status != b'01': - continue # skip tcp connections that aren't yet established - - local_ip, local_port = _decode_proc_address_encoding(l_addr, is_ipv6) - foreign_ip, foreign_port = _decode_proc_address_encoding(f_addr, is_ipv6) - conn.append((local_ip, local_port, foreign_ip, foreign_port, protocol, is_ipv6)) - except IOError as exc: - exc = IOError("unable to read '%s': %s" % (proc_file_path, exc)) - _log_failure(parameter, exc) - raise exc - except Exception as exc: - exc = IOError("unable to parse '%s': %s" % (proc_file_path, exc)) - _log_failure(parameter, exc) - raise exc + raise IOError('unable to determine file descriptor destination (%s): %s' % (exc, fd_path))
- _log_runtime(parameter, '/proc/net/[tcp|udp]', start_time) - return conn + return inodes
def _decode_proc_address_encoding(addr, is_ipv6): diff --git a/test/unit/util/proc.py b/test/unit/util/proc.py index ee52d21..a828981 100644 --- a/test/unit/util/proc.py +++ b/test/unit/util/proc.py @@ -6,6 +6,7 @@ import io import unittest
from stem.util import proc +from stem.util.connection import Connection from test import mocking
try: @@ -225,12 +226,9 @@ class TestProc(unittest.TestCase): '/proc/net/udp': io.BytesIO(udp) }[param]
- # tests the edge case of pid = 0 - self.assertEqual([], proc.connections(0)) - expected_results = [ - ('17.17.17.17', 4369, '34.34.34.34', 8738, 'tcp', False), - ('187.187.187.187', 48059, '204.204.204.204', 52428, 'udp', False), + Connection('17.17.17.17', 4369, '34.34.34.34', 8738, 'tcp', False), + Connection('187.187.187.187', 48059, '204.204.204.204', 52428, 'udp', False), ]
self.assertEqual(expected_results, proc.connections(pid)) @@ -256,19 +254,48 @@ class TestProc(unittest.TestCase): }[param]
path_exists_mock.side_effect = lambda param: { - '/proc/net/tcp': False, '/proc/net/tcp6': True, - '/proc/net/udp': False, '/proc/net/udp6': False }[param]
open_mock.side_effect = lambda param, mode: { + '/proc/net/tcp': io.BytesIO(''), '/proc/net/tcp6': io.BytesIO(TCP6_CONTENT), + '/proc/net/udp': io.BytesIO(''), }[param]
expected_results = [ - ('2a01:4f8:190:514a::2', 443, '2001:638:a000:4140::ffff:189', 40435, 'tcp', True), - ('2a01:4f8:190:514a::2', 443, '2001:858:2:2:aabb:0:563b:1526', 44469, 'tcp', True), + Connection('2a01:4f8:190:514a::2', 443, '2001:638:a000:4140::ffff:189', 40435, 'tcp', True), + Connection('2a01:4f8:190:514a::2', 443, '2001:858:2:2:aabb:0:563b:1526', 44469, 'tcp', True), ]
- self.assertEqual(expected_results, proc.connections(pid)) + self.assertEqual(expected_results, proc.connections(pid = pid)) + + @patch('os.path.exists') + @patch('pwd.getpwnam') + @patch('stem.util.proc.open', create = True) + def test_connections_ipv6_by_user(self, open_mock, getpwnam_mock, path_exists_mock): + """ + Tests the connections function with ipv6 addresses. + """ + + getpwnam_mock('me').pw_uid = 106 + + path_exists_mock.side_effect = lambda param: { + '/proc/net/tcp6': True, + '/proc/net/udp6': False + }[param] + + open_mock.side_effect = lambda param, mode: { + '/proc/net/tcp': io.BytesIO(''), + '/proc/net/tcp6': io.BytesIO(TCP6_CONTENT), + '/proc/net/udp': io.BytesIO(''), + }[param] + + expected_results = [ + Connection('::ffff:5.9.158.75', 5222, '::ffff:78.54.134.33', 38330, 'tcp', True), + Connection('2a01:4f8:190:514a::2', 5269, '2001:6f8:126f:11::26', 50594, 'tcp', True), + Connection('::ffff:5.9.158.75', 5222, '::ffff:78.54.134.33', 38174, 'tcp', True), + ] + + self.assertEqual(expected_results, proc.connections(user = 'me'))
tor-commits@lists.torproject.org