Repository navigation
Expand file tree
/
Copy pathnetutil.py
More file actions
255 lines (229 loc) · 8.93 KB
/
Copy pathnetutil.py
File metadata and controls
255 lines (229 loc) · 8.93 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#!/usr/bin/env python3
# Copyright (c) 2014-present The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Linux, macOS, BSD and illumos network utilities.
Roughly based on https://web.archive.org/web/20190424172231/http://voorloopnul.com/blog/a-python-netstat-in-less-than-100-lines-of-code/ by Ricardo Pascal
"""
import http.client
import ipaddress
import sys
import socket
import struct
import array
import os
# Easily unreachable address. Attempts to connect to it will stay within the machine.
# Used to avoid non-loopback traffic or DNS queries.
UNREACHABLE_PROXY_ARG = '-proxy=127.0.0.1:1'
# STATE_ESTABLISHED = '01'
# STATE_SYN_SENT = '02'
# STATE_SYN_RECV = '03'
# STATE_FIN_WAIT1 = '04'
# STATE_FIN_WAIT2 = '05'
# STATE_TIME_WAIT = '06'
# STATE_CLOSE = '07'
# STATE_CLOSE_WAIT = '08'
# STATE_LAST_ACK = '09'
STATE_LISTEN = '0A'
# STATE_CLOSING = '0B'
# Address manager size constants as defined in addrman_impl.h
ADDRMAN_NEW_BUCKET_COUNT = 1 << 10
ADDRMAN_TRIED_BUCKET_COUNT = 1 << 8
ADDRMAN_BUCKET_SIZE = 1 << 6
# When a test expects a server disconnection, any of these errors are
# acceptable. The specific event is determined by race condition and platform OS.
NETWORK_ERRORS = (
BrokenPipeError, # write to a closed socket/pipe
ConnectionResetError, # connection forcibly closed by peer
ConnectionAbortedError, # connection aborted locally or by network stack
http.client.ResponseNotReady, # server response not ready or connection out of sync
)
def get_socket_inodes(pid):
'''
Get list of socket inodes for process pid.
'''
base = '/proc/%i/fd' % pid
inodes = []
for item in os.listdir(base):
try:
target = os.readlink(os.path.join(base, item))
if target.startswith('socket:'):
inodes.append(int(target[8:-1]))
except FileNotFoundError:
pass
return inodes
def _remove_empty(array):
return [x for x in array if x !='']
def _convert_ip_port(array):
host,port = array.split(':')
# convert host from mangled-per-four-bytes form as used by kernel
host = bytes.fromhex(host)
host_out = ''
for x in range(0, len(host) // 4):
(val,) = struct.unpack('=I', host[x*4:(x+1)*4])
host_out += '%08x' % val
return host_out,int(port,16)
def netstat(typ='tcp'):
'''
Function to return a list with status of tcp connections at linux systems
To get pid of all network process running on system, you must run this script
as superuser
'''
with open('/proc/net/'+typ,'r') as f:
content = f.readlines()
content.pop(0)
result = []
for line in content:
line_array = _remove_empty(line.split(' ')) # Split lines and remove empty spaces.
tcp_id = line_array[0]
l_addr = _convert_ip_port(line_array[1])
r_addr = _convert_ip_port(line_array[2])
state = line_array[3]
inode = int(line_array[9]) # Need the inode to match with process pid.
nline = [tcp_id, l_addr, r_addr, state, inode]
result.append(nline)
return result
def get_bind_addrs(pid):
'''
Get bind addresses as (host,port) tuples for process pid.
'''
if sys.platform == 'linux':
inodes = get_socket_inodes(pid)
bind_addrs = []
for conn in netstat('tcp') + netstat('tcp6'):
if conn[3] == STATE_LISTEN and conn[4] in inodes:
bind_addrs.append(conn[1])
return bind_addrs
# OpenBSD is not included, as it does not ship the lsof utility.
elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "sunos5")):
import re
import subprocess
output = subprocess.check_output(["lsof",
*(["-Di"] if sys.platform.startswith(("freebsd", "netbsd", "sunos5")) else []), # Ignore device cache to avoid stderr warnings.
*(["-w"] if sys.platform.startswith("netbsd") else []), # Ignore point release mismatch warnings.
"-nP", # Keep hosts and ports numeric.
"-a", # Require all filters to match.
"-p", str(pid), # Limit results to the target pid.
"-iTCP", # Only inspect TCP sockets.
"-sTCP:LISTEN", # Only keep listening sockets.
"-Ftn", # Emit machine-readable type and name fields.
], text=True)
return [
(addr_to_hex(("::" if sock_type == "IPv6" else "0.0.0.0") if host == "*" else host.strip("[]")), int(port))
for sock_type, host, port in re.findall(r"t(IPv[46])\nn(\*|\[.+?]|[^:]+):(\d+)", output)
]
else:
raise NotImplementedError(f"get_bind_addrs is not supported on {sys.platform}")
def all_interfaces():
'''
Return all IPv4 interfaces that are up.
'''
if sys.platform == 'linux':
import fcntl # Linux only, so only import when required
is_64bits = sys.maxsize > 2**32
struct_size = 40 if is_64bits else 32
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
max_possible = 8 # initial value
while True:
bytes = max_possible * struct_size
names = array.array('B', b'\0' * bytes)
outbytes = struct.unpack('iL', fcntl.ioctl(
s.fileno(),
0x8912, # SIOCGIFCONF
struct.pack('iL', bytes, names.buffer_info()[0])
))[0]
if outbytes == bytes:
max_possible *= 2
else:
break
namestr = names.tobytes()
return [(namestr[i:i+16].split(b'\0', 1)[0],
socket.inet_ntoa(namestr[i+20:i+24]))
for i in range(0, outbytes, struct_size)]
elif sys.platform.startswith(("darwin", "freebsd", "netbsd", "openbsd", "sunos5")):
import re
import subprocess
output = subprocess.check_output(["ifconfig", "-au"], text=True)
return [
(m["iface"].encode(), ip)
for m in re.finditer(r"(?m)^(?P<iface>\S+):(?P<block>[^\n]*(?:\n[ \t]+[^\n]*)*)", output)
for ip in re.findall(r"inet ([^\s/]+)", m["block"])
]
else:
raise NotImplementedError(f"all_interfaces is not supported on {sys.platform}")
def addr_to_hex(addr):
'''
Convert string IPv4 or IPv6 address to binary address as returned by
get_bind_addrs.
'''
return ipaddress.ip_address(addr).packed.hex()
def test_ipv6_local():
'''
Check for (local) IPv6 support.
'''
# By using SOCK_DGRAM this will not actually make a connection, but it will
# fail if there is no route to IPv6 localhost.
have_ipv6 = True
try:
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
s.connect(('::1', 1))
except socket.error:
have_ipv6 = False
return have_ipv6
def test_unix_socket():
'''Return True if UNIX sockets are available on this platform.'''
try:
socket.AF_UNIX
except AttributeError:
return False
else:
return True
def format_addr_port(addr, port):
'''Return either "addr:port" or "[addr]:port" based on whether addr looks like an IPv6 address.'''
if ":" in addr:
return f"[{addr}]:{port}"
else:
return f"{addr}:{port}"
def format_sock(sock, *, local):
'''
Format either local or remote side of a socket to a human readable string, e.g.
1.2.3.4:8333 or
[11:22::33]:8333 or
/path/to/socket or
@abstract-socket
'''
try:
if local:
name = sock.getsockname()
else:
name = sock.getpeername()
except Exception:
return "n/a"
if sock.family == socket.AF_INET:
return f"{name[0]}:{name[1]}"
if sock.family == socket.AF_INET6:
return f"[{name[0]}]:{name[1]}"
if sock.family == socket.AF_UNIX:
if isinstance(name, bytes):
name = name.decode(errors="backslashreplace")
if name.startswith("\0"):
return f"@{name[1:]}"
return name
return str(name)
def set_ephemeral_port_range(sock):
'''On FreeBSD and OpenBSD, set socket to use the high ephemeral port range (49152-65535).
The default ephemeral port ranges on FreeBSD (10000-65535) and
OpenBSD (1024-49151) overlap with the test framework's static port
range starting at TEST_RUNNER_PORT_MIN (default=11000).
Using IP_PORTRANGE_HIGH avoids this overlap when binding to port 0 for dynamic
port allocation.
'''
if sys.platform.startswith(("freebsd", "openbsd")):
# Constants from FreeBSD/OpenBSD's netinet/in.h and netinet6/in6.h
IP_PORTRANGE = 19
IPV6_PORTRANGE = 14
IP_PORTRANGE_HIGH = 1 # Same value for both IPv4 and IPv6
if sock.family == socket.AF_INET6:
sock.setsockopt(socket.IPPROTO_IPV6, IPV6_PORTRANGE, IP_PORTRANGE_HIGH)
else:
sock.setsockopt(socket.IPPROTO_IP, IP_PORTRANGE, IP_PORTRANGE_HIGH)