commit 1082349b59eb6ecd89eca1a1f2aa2ab1df007315 Author: Damian Johnson atagar@torproject.org Date: Sun Sep 22 23:38:17 2013 -0700
Connection resolution support
Adding long overdue support for process connection resolution...
https://trac.torproject.org/7910
This is a highly popular capability of arm, and stem's counterpart for it is quite a bit cleaner (with tests!). That said, this still doesn't get around tor's annoying DisableDebuggerAttachment feature which screws up proc permissions. That's something I'll need to figure out before our next arm or stem release... --- docs/change_log.rst | 1 + stem/util/connection.py | 195 ++++++++++++++++++++++++++++++++++-- stem/util/proc.py | 18 +++- test/integ/util/__init__.py | 2 +- test/integ/util/connection.py | 33 +++++++ test/settings.cfg | 1 + test/unit/util/connection.py | 217 ++++++++++++++++++++++++++++++++++++++++- 7 files changed, 455 insertions(+), 12 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index 13b256d..505261a 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -58,6 +58,7 @@ The following are only available within stem's `git repository
* **Utilities**
+ * Connection resolution via the :func:`~stem.util.connection.get_connections` function (:trac:`7910`) * :func:`~stem.util.system.set_process_name` inserted spaces between characters (:trac:`8631`) * :func:`~stem.util.system.get_pid_by_name` can now pull for all processes with a given name * :func:`~stem.util.system.call` ignored the subprocess' exit status diff --git a/stem/util/connection.py b/stem/util/connection.py index e0b08df..4605a57 100644 --- a/stem/util/connection.py +++ b/stem/util/connection.py @@ -2,22 +2,40 @@ # See LICENSE for licensing information
""" -Connection and networking based utility functions. This will likely be expanded -later to have all of `arm's functions -https://gitweb.torproject.org/arm.git/blob/HEAD:/src/util/connections.py`_, -but for now just moving the parts we need. +Connection and networking based utility functions.
::
+ get_connections - quieries the connections belonging to a given process + get_system_resolvers - provides connection resolution methods that are likely to be available + is_valid_ipv4_address - checks if a string is a valid IPv4 address is_valid_ipv6_address - checks if a string is a valid IPv6 address is_valid_port - checks if something is a valid representation for a port is_private_address - checks if an IPv4 address belongs to a private range or not + expand_ipv6_address - provides an IPv6 address with its collapsed portions expanded get_mask_ipv4 - provides the mask representation for a given number of bits get_mask_ipv6 - provides the IPv6 mask representation for a given number of bits + +.. data:: Resolver (enum) + + Method for resolving a process' connections. + + ================= =========== + Resolver Description + ================= =========== + **PROC** /proc contents + **NETSTAT** netstat command + **SS** ss command + **LSOF** lsof command + **SOCKSTAT** sockstat command under *nix + **BSD_SOCKSTAT** sockstat command under FreeBSD + **BSD_PROCSTAT** procstat command under FreeBSD + ================= =========== """
+import collections import hashlib import hmac import os @@ -25,8 +43,16 @@ import platform import re
import stem.util.proc +import stem.util.system
-from stem.util import enum +from stem.util import enum, log + +# Connection resolution is risky to log about since it's highly likely to +# contain sensitive information. That said, it's also difficult to get right in +# a platform independent fashion. To opt into the logging requried to +# troubleshoot connection resolution set the following... + +LOG_CONNECTION_RESOLUTION = False
Resolver = enum.Enum( ('PROC', 'proc'), @@ -38,11 +64,162 @@ Resolver = enum.Enum( ('BSD_PROCSTAT', 'procstat (bsd)') )
+Connection = collections.namedtuple('Connection', [ + 'local_address', + 'local_port', + 'remote_address', + 'remote_port', + 'protocol', +]) + FULL_IPv4_MASK = "255.255.255.255" FULL_IPv6_MASK = "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF"
CRYPTOVARIABLE_EQUALITY_COMPARISON_NONCE = os.urandom(32)
+RESOLVER_COMMAND = { + Resolver.PROC: '', + + # -n = prevents dns lookups, -p = include process + Resolver.NETSTAT: 'netstat -np', + + # -n = numeric ports, -p = include process, -t = tcp sockets, -u = udp sockets + Resolver.SS: 'ss -nptu', + + # -n = prevent dns lookups, -P = show port numbers (not names), -i = ip only, -w = no warnings + # (lsof provides a '-p <pid>' but oddly in practice it seems to be ~11-28% slower) + Resolver.LSOF: 'lsof -wnPi', + + Resolver.SOCKSTAT: 'sockstat', + + # -4 = IPv4, -c = connected sockets + Resolver.BSD_SOCKSTAT: 'sockstat -4c', + + # -f <pid> = process pid + Resolver.BSD_PROCSTAT: 'procstat -f {pid}', +} + +RESOLVER_FILTER = { + Resolver.PROC: '', + + # tcp 0 586 192.168.0.1:44284 38.229.79.2:443 ESTABLISHED 15843/tor + Resolver.NETSTAT: '^{protocol}\s+.*\s+{local_address}:{local_port}\s+{remote_address}:{remote_port}\s+ESTABLISHED\s+{pid}/{name}\s*$', + + # tcp ESTAB 0 0 192.168.0.20:44415 38.229.79.2:443 users:(("tor",15843,9)) + Resolver.SS: '^{protocol}\s+ESTAB\s+.*\s+{local_address}:{local_port}\s+{remote_address}:{remote_port}\s+users:(("{name}",{pid},[0-9]+))$', + + # tor 3873 atagar 45u IPv4 40994 0t0 TCP 10.243.55.20:45724->194.154.227.109:9001 (ESTABLISHED) + Resolver.LSOF: '^{name}\s+{pid}\s+.*\s+{protocol}\s+{local_address}:{local_port}->{remote_address}:{remote_port} (ESTABLISHED)$', + + # atagar tor 15843 tcp4 192.168.0.20:44092 68.169.35.102:443 ESTABLISHED + Resolver.SOCKSTAT: '^\S+\s+{name}\s+{pid}\s+{protocol}4\s+{local_address}:{local_port}\s+{remote_address}:{remote_port}\s+ESTABLISHED$', + + # _tor tor 4397 12 tcp4 172.27.72.202:54011 127.0.0.1:9001 + Resolver.BSD_SOCKSTAT: '^\S+\s+{name}\s+{pid}\s+\S+\s+{protocol}4\s+{local_address}:{local_port}\s+{remote_address}:{remote_port}$', + + # 3561 tor 4 s - rw---n-- 2 0 TCP 10.0.0.2:9050 10.0.0.1:22370 + Resolver.BSD_PROCSTAT: '^\s*{pid}\s+{name}\s+.*\s+{protocol}\s+{local_address}:{local_port}\s+{remote_address}:{remote_port}$', +} + + +def get_connections(resolver, process_pid = None, process_name = None): + """ + Retrieves a list of the current connections for a given process. The provides + a list of Connection instances, which have four attributes... + + * local_address (str) + * local_port (int) + * remote_address (str) + * remote_port (int) + * protocol (str, generally either 'tcp' or 'udp') + + :param Resolver resolver: method of connection resolution to use + :param int process_pid: pid of the process to retrieve + :param str process_name: name of the process to retrieve + + :raises: + * **ValueError** if using **Resolver.PROC** or **Resolver.BSD_PROCSTAT** + and the process_pid wasn't provided + + * **IOError** if no connections are available or resolution fails + (generally they're indistinguishable). The common causes are the + command being unavailable or permissions. + """ + + def _log(msg): + if LOG_CONNECTION_RESOLUTION: + log.debug(msg) + + _log("=" * 80) + _log("Querying connections for resolver: %s, pid: %s, name: %s" % (resolver, process_pid, process_name)) + + if isinstance(process_pid, str): + try: + process_pid = int(process_pid) + except ValueError: + raise ValueError("Process pid was non-numeric: %s" % process_pid) + + if process_pid is None and resolver in (Resolver.PROC, Resolver.BSD_PROCSTAT): + raise ValueError("%s resolution requires a pid" % resolver) + + if resolver == Resolver.PROC: + return [Connection(*conn) for conn in stem.util.proc.get_connections(process_pid)] + + resolver_command = RESOLVER_COMMAND[resolver].format(pid = process_pid) + + try: + results = stem.util.system.call(resolver_command) + except OSError as exc: + raise IOError("Unable to query '%s': %s" % (resolver_command, exc)) + + resolver_regex_str = RESOLVER_FILTER[resolver].format( + protocol = '(?P<protocol>\S+)', + local_address = '(?P<local_address>[0-9.]+)', + local_port = '(?P<local_port>[0-9]+)', + remote_address = '(?P<remote_address>[0-9.]+)', + remote_port = '(?P<remote_port>[0-9]+)', + pid = process_pid if process_pid else '[0-9]*', + name = process_name if process_name else '\S*', + ) + + _log("Resolver regex: %s" % resolver_regex_str) + _log("Resolver results:\n%s" % '\n'.join(results)) + + connections = [] + resolver_regex = re.compile(resolver_regex_str) + + for line in results: + match = resolver_regex.match(line) + + if match: + attr = match.groupdict() + local_addr = attr['local_address'] + local_port = int(attr['local_port']) + remote_addr = attr['remote_address'] + remote_port = int(attr['remote_port']) + protocol = attr['protocol'].lower() + + if remote_addr == '0.0.0.0': + continue # procstat response for unestablished connections + + if not (is_valid_ipv4_address(local_addr) and is_valid_ipv4_address(remote_addr)): + _log("Invalid address (%s or %s): %s" % (local_addr, remote_addr, line)) + elif not (is_valid_port(local_port) and is_valid_port(remote_port)): + _log("Invalid port (%s or %s): %s" % (local_port, remote_port, line)) + elif protocol not in ('tcp', 'udp'): + _log("Unrecognized protocol (%s): %s" % (protocol, line)) + + conn = Connection(local_addr, local_port, remote_addr, remote_port, protocol) + connections.append(conn) + _log(str(conn)) + + _log("%i connections found" % len(connections)) + + if not connections: + raise IOError("No results found using: %s" % resolver_command) + + return connections +
def get_system_resolvers(system = None): """ @@ -62,8 +239,14 @@ def get_system_resolvers(system = None): elif system in ('Darwin', 'OpenBSD'): resolvers = [Resolver.LSOF] elif system == 'FreeBSD': + # Netstat is available, but lacks a '-p' equivilant so we can't associate + # the results to processes. The platform also has a ss command, but it + # belongs to a spreadsheet application. + resolvers = [Resolver.BSD_SOCKSTAT, Resolver.BSD_PROCSTAT, Resolver.LSOF] else: + # Sockstat isn't available by default on ubuntu. + resolvers = [Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS]
# proc resolution, by far, outperforms the others so defaults to this is able @@ -193,7 +376,7 @@ def is_private_address(address): if address.startswith("172."): second_octet = int(address.split('.')[1])
- if second_octet >= 16 and second_octet <= 31: + if second_octet >= 16 and second_octet <= 31: return True
return False diff --git a/stem/util/proc.py b/stem/util/proc.py index b7efdef..401c319 100644 --- a/stem/util/proc.py +++ b/stem/util/proc.py @@ -309,23 +309,32 @@ def get_connections(pid): :param int pid: process id of the process to be queried
:returns: A listing of connection tuples of the form **[(local_ipAddr1, - local_port1, foreign_ipAddr1, foreign_port1), ...]** (IP addresses are - strings and ports are ints) + local_port1, foreign_ipAddr1, foreign_port1, protocol), ...]** (addresses + and protocols are strings and ports are ints)
:raises: **IOError** if it can't be determined """
+ if isinstance(pid, str): + try: + pid = int(pid) + except ValueError: + raise IOError("Process pid was non-numeric: %s" % pid) + if pid == 0: return []
# fetches the inode numbers for socket file descriptors + start_time, parameter = time.time(), "process connections" inodes = [] + for fd in os.listdir("/proc/%s/fd" % pid): fd_path = "/proc/%s/fd/%s" % (pid, fd)
try: # File descriptor link, such as 'socket:[30899]' + fd_name = os.readlink(fd_path)
if fd_name.startswith('socket:['): @@ -341,7 +350,9 @@ def get_connections(pid): return []
# check for the connection information from the /proc/net contents + conn = [] + for proc_file_path in ("/proc/net/tcp", "/proc/net/udp"): try: proc_file = open(proc_file_path) @@ -357,7 +368,8 @@ def get_connections(pid):
local_ip, local_port = _decode_proc_address_encoding(l_addr) foreign_ip, foreign_port = _decode_proc_address_encoding(f_addr) - conn.append((local_ip, local_port, foreign_ip, foreign_port)) + protocol = proc_file_path[10:] + conn.append((local_ip, local_port, foreign_ip, foreign_port, protocol))
proc_file.close() except IOError as exc: diff --git a/test/integ/util/__init__.py b/test/integ/util/__init__.py index eac6be5..2371a4e 100644 --- a/test/integ/util/__init__.py +++ b/test/integ/util/__init__.py @@ -2,4 +2,4 @@ Integration tests for stem.util.* contents. """
-__all__ = ["conf", "proc", "system"] +__all__ = ["conf", "connection", "proc", "system"] diff --git a/test/integ/util/connection.py b/test/integ/util/connection.py new file mode 100644 index 0000000..70ad7f9 --- /dev/null +++ b/test/integ/util/connection.py @@ -0,0 +1,33 @@ +""" +Integration tests for stem.util.connection functions against the tor process +that we're running. +""" + +import unittest + +import test.runner + +from stem.util.connection import get_connections, get_system_resolvers + + +class TestConnection(unittest.TestCase): + def test_get_connections(self): + runner = test.runner.get_runner() + + if not test.runner.Torrc.PORT in runner.get_options(): + test.runner.skip(self, "(no control port)") + return + elif not test.runner.get_runner().is_ptraceable(): + test.runner.skip(self, "(DisableDebuggerAttachment is set)") + return + + for resolver in get_system_resolvers(): + with runner.get_tor_socket(): + tor_pid = test.runner.get_runner().get_pid() + connections = get_connections(resolver, process_pid = tor_pid) + + for conn in connections: + if conn.local_address == '127.0.0.1' and conn.local_port == test.runner.CONTROL_PORT: + return + + self.fail("Unable to find localhost connection with %s:\n%s" % (resolver, '\n'.join(connections))) diff --git a/test/settings.cfg b/test/settings.cfg index b97c57f..9838012 100644 --- a/test/settings.cfg +++ b/test/settings.cfg @@ -185,6 +185,7 @@ test.unit_tests
test.integ_tests |test.integ.util.conf.TestConf +|test.integ.util.connection.TestConnection |test.integ.util.proc.TestProc |test.integ.util.system.TestSystem |test.integ.descriptor.reader.TestDescriptorReader diff --git a/test/unit/util/connection.py b/test/unit/util/connection.py index 40a2e02..03d83c1 100644 --- a/test/unit/util/connection.py +++ b/test/unit/util/connection.py @@ -9,7 +9,83 @@ from mock import patch
import stem.util.connection
-from stem.util.connection import Resolver +from stem.util.connection import Resolver, Connection + +NETSTAT_OUTPUT = """\ +Active Internet connections (w/o servers) +Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name +tcp 0 0 192.168.0.1:5939 73.94.23.87:443 ESTABLISHED 20586/firefox +tcp 0 0 192.168.0.1:4325 73.94.23.55:443 ESTABLISHED 20586/firefox +tcp 1 0 192.168.0.1:4378 29.208.141.42:443 CLOSE_WAIT 20586/firefox +tcp 0 0 127.0.0.1:22 127.0.0.1:56673 ESTABLISHED - +tcp 0 586 192.168.0.1:44284 38.229.79.2:443 ESTABLISHED 15843/tor +tcp 0 0 192.168.0.1:37909 16.111.19.278:6697 ESTABLISHED - +Active UNIX domain sockets (w/o servers) +Proto RefCnt Flags Type State I-Node PID/Program name Path +unix 14 [ ] DGRAM 8433 - /dev/log +unix 3 [ ] STREAM CONNECTED 34164277 15843/tor +unix 3 [ ] STREAM CONNECTED 34164276 15843/tor +unix 3 [ ] STREAM CONNECTED 7951 - +""" + +SS_OUTPUT = """\ +Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port +tcp CLOSE-WAIT 1 0 192.168.0.1:43780 53.203.145.45:443 users:(("firefox",20586,118)) +tcp ESTAB 55274 0 192.168.0.1:46136 196.153.236.35:80 users:(("firefox",20586,93)) +tcp ESTAB 0 0 192.168.0.1:44092 23.112.135.72:443 users:(("tor",15843,10)) +tcp ESTAB 0 0 127.0.0.1:22 127.0.0.1:56673 +tcp ESTAB 0 0 192.168.0.1:44415 38.229.79.2:443 users:(("tor",15843,9)) +""" + +LSOF_OUTPUT = """\ +COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME +ubuntu-ge 2164 atagar 11u IPv4 13593 0t0 TCP 192.168.0.1:55395->21.89.91.78:80 (CLOSE_WAIT) +tor 15843 atagar 6u IPv4 34164278 0t0 TCP 127.0.0.1:9050 (LISTEN) +tor 15843 atagar 7u IPv4 34164279 0t0 TCP 127.0.0.1:9051 (LISTEN) +tor 15843 atagar 9u IPv4 34188132 0t0 TCP 192.168.0.1:44415->38.229.79.2:443 (ESTABLISHED) +tor 15843 atagar 10u IPv4 34165291 0t0 TCP 192.168.0.1:44092->68.169.35.102:443 (ESTABLISHED) +python 16422 atagar 3u IPv4 34203773 0t0 UDP 127.0.0.1:39624->127.0.0.1:53 +firefox 20586 atagar 66u IPv4 5765353 0t0 TCP 192.168.0.1:47486->62.135.16.134:443 (ESTABLISHED) +firefox 20586 atagar 71u IPv4 13094989 0t0 TCP 192.168.0.1:43762->182.3.10.42:443 (CLOSE_WAIT) +""" + +SOCKSTAT_OUTPUT = """\ +USER PROCESS PID PROTO SOURCE ADDRESS FOREIGN ADDRESS STATE +atagar ubuntu-geoip-pr 2164 tcp4 192.168.0.1:55395 141.18.34.33:80 CLOSE_WAIT +atagar tor 15843 tcp4 127.0.0.1:9050 *:* LISTEN +atagar tor 15843 tcp4 127.0.0.1:9051 *:* LISTEN +atagar tor 15843 tcp4 192.168.0.1:44415 38.229.79.2:443 ESTABLISHED +atagar tor 15843 tcp4 192.168.0.1:44092 68.169.35.102:443 ESTABLISHED +atagar firefox 20586 tcp4 192.168.0.1:47486 213.24.100.160:443 ESTABLISHED +atagar firefox 20586 tcp4 192.168.0.1:43762 32.188.221.72:443 CLOSE_WAIT +""" + +# I don't have actual sockstat and procstat output for FreeBSD. Rather, these +# are snippets of output from email threads. + +BSD_SOCKSTAT_OUTPUT = """\ +_tor tor 4397 7 tcp4 172.27.72.202:9050 *:* +_tor tor 4397 8 udp4 172.27.72.202:53 *:* +_tor tor 4397 9 tcp4 172.27.72.202:9051 *:* +_tor tor 4397 12 tcp4 172.27.72.202:54011 38.229.79.2:9001 +_tor tor 4397 15 tcp4 172.27.72.202:59374 68.169.35.102:9001 +_tor tor 4397 19 tcp4 172.27.72.202:59673 213.24.100.160:9001 +_tor tor 4397 20 tcp4 172.27.72.202:51946 32.188.221.72:443 +_tor tor 4397 22 tcp4 172.27.72.202:60344 21.89.91.78:9001 +""" + +BSD_PROCSTAT_OUTPUT = """\ + PID COMM FD T V FLAGS REF OFFSET PRO NAME + 3561 tor 4 s - rw---n-- 2 0 TCP 10.0.0.2:9050 10.0.0.1:22370 + 3561 tor 5 s - rw---n-- 2 0 TCP 10.0.0.2:9050 0.0.0.0:0 + 3561 tor 6 s - rw---n-- 2 0 TCP 10.0.0.2:9040 0.0.0.0:0 + 3561 tor 7 s - rw---n-- 2 0 UDP 10.0.0.2:53 0.0.0.0:0 + 3561 tor 8 s - rw---n-- 2 0 TCP 10.0.0.2:9051 0.0.0.0:0 + 3561 tor 14 s - rw---n-- 2 0 TCP 10.0.0.2:9050 10.0.0.1:44381 + 3561 tor 15 s - rw---n-- 2 0 TCP 10.0.0.2:33734 38.229.79.2:443 + 3561 tor 16 s - rw---n-- 2 0 TCP 10.0.0.2:47704 68.169.35.102:9001 +""" +
class TestConnection(unittest.TestCase): @patch('stem.util.proc.is_available') @@ -27,7 +103,6 @@ class TestConnection(unittest.TestCase): self.assertEqual([Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS], stem.util.connection.get_system_resolvers('Linux'))
proc_mock.return_value = True - self.assertEqual([Resolver.PROC, Resolver.NETSTAT, Resolver.SOCKSTAT, Resolver.LSOF, Resolver.SS], stem.util.connection.get_system_resolvers('Linux'))
# check that calling without an argument is equivilant to calling for this @@ -35,6 +110,144 @@ class TestConnection(unittest.TestCase):
self.assertEqual(stem.util.connection.get_system_resolvers(platform.system()), stem.util.connection.get_system_resolvers())
+ @patch('stem.util.proc.get_connections') + def test_get_connections_by_proc(self, proc_mock): + """ + Checks the get_connections function with the proc resolver. + """ + + proc_mock.return_value = [ + ('17.17.17.17', 4369, '34.34.34.34', 8738, 'tcp'), + ('187.187.187.187', 48059, '204.204.204.204', 52428, 'tcp'), + ] + + expected = [ + Connection('17.17.17.17', 4369, '34.34.34.34', 8738, 'tcp'), + Connection('187.187.187.187', 48059, '204.204.204.204', 52428, 'tcp'), + ] + + self.assertEqual(expected, stem.util.connection.get_connections(Resolver.PROC, process_pid = 1111)) + + proc_mock.side_effect = IOError('No connections for you!') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.PROC, process_pid = 1111) + + @patch('stem.util.system.call') + def test_get_connections_by_netstat(self, call_mock): + """ + Checks the get_connections function with the netstat resolver. + """ + + call_mock.return_value = NETSTAT_OUTPUT.split('\n') + expected = [Connection('192.168.0.1', 44284, '38.229.79.2', 443, 'tcp')] + self.assertEqual(expected, stem.util.connection.get_connections(Resolver.NETSTAT, process_pid = 15843, process_name = 'tor')) + + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.NETSTAT, process_pid = 15843, process_name = 'stuff') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.NETSTAT, process_pid = 1111, process_name = 'tor') + + call_mock.side_effect = OSError('Unable to call netstat') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.NETSTAT, process_pid = 1111) + + @patch('stem.util.system.call') + def test_get_connections_by_ss(self, call_mock): + """ + Checks the get_connections function with the ss resolver. + """ + + call_mock.return_value = SS_OUTPUT.split('\n') + expected = [ + Connection('192.168.0.1', 44092, '23.112.135.72', 443, 'tcp'), + Connection('192.168.0.1', 44415, '38.229.79.2', 443, 'tcp'), + ] + self.assertEqual(expected, stem.util.connection.get_connections(Resolver.SS, process_pid = 15843, process_name = 'tor')) + + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SS, process_pid = 15843, process_name = 'stuff') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SS, process_pid = 1111, process_name = 'tor') + + call_mock.side_effect = OSError('Unable to call ss') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SS, process_pid = 1111) + + @patch('stem.util.system.call') + def test_get_connections_by_lsof(self, call_mock): + """ + Checks the get_connections function with the lsof resolver. + """ + + call_mock.return_value = LSOF_OUTPUT.split('\n') + expected = [ + Connection('192.168.0.1', 44415, '38.229.79.2', 443, 'tcp'), + Connection('192.168.0.1', 44092, '68.169.35.102', 443, 'tcp'), + ] + self.assertEqual(expected, stem.util.connection.get_connections(Resolver.LSOF, process_pid = 15843, process_name = 'tor')) + + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.LSOF, process_pid = 15843, process_name = 'stuff') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.LSOF, process_pid = 1111, process_name = 'tor') + + call_mock.side_effect = OSError('Unable to call lsof') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.LSOF, process_pid = 1111) + + @patch('stem.util.system.call') + def test_get_connections_by_sockstat(self, call_mock): + """ + Checks the get_connections function with the sockstat resolver. + """ + + call_mock.return_value = SOCKSTAT_OUTPUT.split('\n') + expected = [ + Connection('192.168.0.1', 44415, '38.229.79.2', 443, 'tcp'), + Connection('192.168.0.1', 44092, '68.169.35.102', 443, 'tcp'), + ] + self.assertEqual(expected, stem.util.connection.get_connections(Resolver.SOCKSTAT, process_pid = 15843, process_name = 'tor')) + + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SOCKSTAT, process_pid = 15843, process_name = 'stuff') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SOCKSTAT, process_pid = 1111, process_name = 'tor') + + call_mock.side_effect = OSError('Unable to call sockstat') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.SOCKSTAT, process_pid = 1111) + + @patch('stem.util.system.call') + def test_get_connections_by_sockstat_for_bsd(self, call_mock): + """ + Checks the get_connections function with the bsd variant of the sockstat + resolver. + """ + + call_mock.return_value = BSD_SOCKSTAT_OUTPUT.split('\n') + expected = [ + Connection('172.27.72.202', 54011, '38.229.79.2', 9001, 'tcp'), + Connection('172.27.72.202', 59374, '68.169.35.102', 9001, 'tcp'), + Connection('172.27.72.202', 59673, '213.24.100.160', 9001, 'tcp'), + Connection('172.27.72.202', 51946, '32.188.221.72', 443, 'tcp'), + Connection('172.27.72.202', 60344, '21.89.91.78', 9001, 'tcp'), + ] + self.assertEqual(expected, stem.util.connection.get_connections(Resolver.BSD_SOCKSTAT, process_pid = 4397, process_name = 'tor')) + + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.BSD_SOCKSTAT, process_pid = 4397, process_name = 'stuff') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.BSD_SOCKSTAT, process_pid = 1111, process_name = 'tor') + + call_mock.side_effect = OSError('Unable to call sockstat') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.BSD_SOCKSTAT, process_pid = 1111) + + @patch('stem.util.system.call') + def test_get_connections_by_procstat(self, call_mock): + """ + Checks the get_connections function with the procstat resolver. + """ + + call_mock.return_value = BSD_PROCSTAT_OUTPUT.split('\n') + expected = [ + Connection('10.0.0.2', 9050, '10.0.0.1', 22370, 'tcp'), + Connection('10.0.0.2', 9050, '10.0.0.1', 44381, 'tcp'), + Connection('10.0.0.2', 33734, '38.229.79.2', 443, 'tcp'), + Connection('10.0.0.2', 47704, '68.169.35.102', 9001, 'tcp'), + ] + self.assertEqual(expected, stem.util.connection.get_connections(Resolver.BSD_PROCSTAT, process_pid = 3561, process_name = 'tor')) + + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.BSD_PROCSTAT, process_pid = 3561, process_name = 'stuff') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.BSD_PROCSTAT, process_pid = 1111, process_name = 'tor') + + call_mock.side_effect = OSError('Unable to call procstat') + self.assertRaises(IOError, stem.util.connection.get_connections, Resolver.BSD_PROCSTAT, process_pid = 1111) + def test_is_valid_ipv4_address(self): """ Checks the is_valid_ipv4_address function.
tor-commits@lists.torproject.org