Repository navigation
Expand file tree
/
Copy pathserver.py
More file actions
128 lines (109 loc) · 4.01 KB
/
Copy pathserver.py
File metadata and controls
128 lines (109 loc) · 4.01 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
#!/usr/bin/env python3
"""TCP Brutal v2 speed test server.
Each connection starts with a 20-byte header from the client:
group ID (u64), rate in bytes/s (u64), duration in seconds (u32).
The server puts the connection into that Brutal group and sends random
data for the duration. All connections sharing a group ID share the rate.
"""
import argparse
import errno
import os
import socket
import struct
import sys
import threading
import time
TCP_CONGESTION = 13
TCP_BRUTAL_PARAMS = 23301
TCP_BRUTAL_VERSION = 23302
CWND_GAIN = 15
HEADER = struct.Struct("!QQI")
CHUNK_SIZE = 65536
def brutal_version(conn):
conn.setsockopt(socket.IPPROTO_TCP, TCP_CONGESTION, b"brutal")
raw = conn.getsockopt(socket.IPPROTO_TCP, TCP_BRUTAL_VERSION, 4)
return struct.unpack("<I", raw)[0]
def check_module():
"""Exit unless the loaded tcp-brutal module is v2 (supports groups)."""
with socket.socket() as listener:
listener.bind(("127.0.0.1", 0))
listener.listen(1)
with socket.create_connection(listener.getsockname()):
conn, _ = listener.accept()
with conn:
try:
version = brutal_version(conn)
except OSError as e:
if e.errno == errno.ENOENT:
sys.exit("error: tcp-brutal kernel module is not loaded")
if e.errno == errno.ENOPROTOOPT:
sys.exit(
"error: tcp-brutal kernel module is v1, v2 is required"
)
sys.exit(f"error: {e}")
if version < 0x020000:
sys.exit(f"error: tcp-brutal kernel module is too old (0x{version:x})")
print(f"tcp-brutal {version >> 16}.{(version >> 8) & 0xFF}.{version & 0xFF}")
def recv_exact(conn, n):
buf = b""
while len(buf) < n:
chunk = conn.recv(n - len(buf))
if not chunk:
return None
buf += chunk
return buf
def handle(conn, addr):
peer = f"{addr[0]}:{addr[1]}"
try:
header = recv_exact(conn, HEADER.size)
if header is None:
return
group_id, rate, duration = HEADER.unpack(header)
try:
conn.setsockopt(socket.IPPROTO_TCP, TCP_CONGESTION, b"brutal")
except PermissionError:
# A "congctl lock" route pinned the algorithm; fine if it is brutal
cc = conn.getsockopt(socket.IPPROTO_TCP, TCP_CONGESTION, 16).rstrip(b"\0")
if cc != b"brutal":
raise
try:
conn.setsockopt(
socket.IPPROTO_TCP,
TCP_BRUTAL_PARAMS,
struct.pack("<QIQ", rate, CWND_GAIN, group_id),
)
print(
f"{peer}: group {group_id:016x}, {rate * 8 / 1e6:.1f} Mbps, {duration}s"
)
except PermissionError:
# A locked destination rule governs this connection: just send
print(f"{peer}: governed by a destination rule, {duration}s")
end = time.monotonic() + duration
while time.monotonic() < end:
conn.sendall(os.urandom(CHUNK_SIZE))
except OSError as e:
print(f"{peer}: {e}")
finally:
conn.close()
print(f"{peer}: done")
def main():
parser = argparse.ArgumentParser(description="TCP Brutal v2 speed test server")
parser.add_argument("-l", "--listen", default="", help="address to listen on")
parser.add_argument(
"-p", "--port", type=int, default=65432, help="port to listen on"
)
args = parser.parse_args()
check_module()
with socket.socket() as listener:
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((args.listen, args.port))
listener.listen()
print(f"listening on {args.listen or '0.0.0.0'}:{args.port}")
while True:
conn, addr = listener.accept()
threading.Thread(target=handle, args=(conn, addr), daemon=True).start()
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
pass