· 5 years ago · Sep 25, 2020, 12:40 PM
1import datetime
2import json
3import random
4import signal
5import sqlite3
6import subprocess
7import time
8
9import requests
10
11"""
12Installation:
13# To run the Speed Test
14sudo apt install speedtest-cli
15
16To list all the network
17ip netns list
18
19Resources:
20Network Interfaces: https://man7.org/linux/man-pages/man8/ip-netns.8.html
21"""
22
23
24def download_file(url, file_name):
25 image_url = url
26
27 r = requests.get(image_url) # create HTTP response object
28
29 with open(file_name, "wb") as f:
30 f.write(r.content)
31
32
33class Network:
34 """
35 Class to get the details of each network connected to the system
36 """
37
38 def __init__(self, TIMEOUT=60, DNS_OR_IP="www.google.com", PACKETS=5):
39 self.TIMEOUT = TIMEOUT # Limiting the time for the execution of ping
40 self.DNS_OR_IP = DNS_OR_IP # Checking ping against this DNS or IP
41 self.PACKETS = PACKETS # Checking for this number of packets
42
43 def ping(self, name_space) -> dict:
44 """
45 Checking ping against each network interfaces
46 :param name_space:
47 :return:
48 """
49 # Getting uplink and downlink from the speedtest-cli
50 download, upload = self.speed_test(name_space)
51
52 run_ping = subprocess.run(
53 [
54 "ip",
55 "netns",
56 "exec",
57 f"{name_space}",
58 "timeout",
59 f"{self.TIMEOUT}",
60 "ping",
61 f"{self.DNS_OR_IP}",
62 "-c",
63 f"{self.PACKETS}",
64 ],
65 stdout=subprocess.PIPE,
66 universal_newlines=True,
67 )
68 info = dict()
69
70 # Output of the ping command is in the last two lines
71 ping = run_ping.stdout.splitlines()[-2:]
72 if ping:
73 try:
74 info["rtt"] = float(ping[1].split("=")[-1].split("/")[1].strip())
75 info["packet_loss"] = float(
76 ping[0].split(",")[-1].split("%")[0].strip()
77 )
78 info["jitter"] = float(
79 ping[1].split("=")[-1].split("/")[3][:-2].strip()
80 )
81 info["dns_or_ip"] = self.DNS_OR_IP
82 info["downlink"], info["uplink"] = download, upload
83 info["timestamp"] = datetime.datetime.now()
84 except Exception as e:
85 print(f"Network Assess Failed due to {e}")
86 info = dict()
87 return info
88
89 def speed_test(self, name_space):
90 """
91 Speed Test against each network interfaces to get uplink and downlink
92 Need to install speedtest-cli
93
94 sudo apt install speedtest-cli
95
96 :param name_space:
97 :return:
98 """
99 run_speed_test = subprocess.run(
100 [
101 "ip",
102 "netns",
103 "exec",
104 f"{name_space}",
105 "speedtest-cli",
106 "--json",
107 "--timeout",
108 f"{self.TIMEOUT}",
109 ],
110 stdout=subprocess.PIPE,
111 universal_newlines=True,
112 )
113
114 try:
115 run_speed_test_res = json.loads(run_speed_test.stdout.splitlines()[0])
116 download, upload = (
117 int(run_speed_test_res["download"]) / 10 ** 6,
118 int(run_speed_test_res["upload"]) / 10 ** 6,
119 )
120 except Exception as e:
121 print(f"Speed Test Failed due to {e}")
122 download, upload = 0.0, 0.0
123
124 # Speed is in is in Mbits/s
125 return download, upload
126
127
128class NetworkInterface:
129 def __init__(self, name_space, ip, port_type="eth0"):
130 self.NAME_SPACE = name_space # Name of your network interface
131 self.IP = ip # Ip of the connected connection
132 self.PORT_TYPE = port_type # Tyoe of port default is ethernet port
133
134 def create_netns(self) -> None:
135 """
136 Creating network name space
137 :return:
138 """
139 cmd = subprocess.run(
140 ["ip", "netns", "add", f"{self.NAME_SPACE}",],
141 stdout=subprocess.PIPE,
142 universal_newlines=True,
143 )
144
145 def link_interface_to_netns(self) -> None:
146 """
147 Linking interface to name space
148 :return:
149 """
150 cmd = subprocess.run(
151 ["ip", "link", "set", f"{self.PORT_TYPE}", "netns" f"{self.NAME_SPACE}",],
152 stdout=subprocess.PIPE,
153 universal_newlines=True,
154 )
155
156 def set_ip_in_namespace(self) -> None:
157 """
158 Setting up IP in Name Space
159 :return:
160 """
161 cmd = subprocess.run(
162 [
163 "ip",
164 "netns",
165 "exec",
166 f"{self.NAME_SPACE}",
167 "ifconfig",
168 f"{self.PORT_TYPE}",
169 f"{self.IP}",
170 "up",
171 ],
172 stdout=subprocess.PIPE,
173 universal_newlines=True,
174 )
175
176 def set_default_interface(self, default_ip) -> None:
177 """
178 Connect namespace to the default interface
179 :param device:
180 :return:
181 """
182 cmd = subprocess.run(
183 [
184 "ip",
185 "link",
186 "set",
187 "dev",
188 f"{default_ip}",
189 "netns",
190 f"{self.NAME_SPACE}",
191 ],
192 stdout=subprocess.PIPE,
193 universal_newlines=True,
194 )
195
196 def delete_namespace(self) -> None:
197 """
198 Delete Name Space once done
199 :return:
200 """
201 cmd = subprocess.run(
202 ["ip", "netns", "delete", f"{self.NAME_SPACE}"],
203 stdout=subprocess.PIPE,
204 universal_newlines=True,
205 )
206
207
208class DataBase:
209 """
210 To handle Data Base for logging
211 """
212
213 def __init__(self, DBNAME, uri=False):
214 self.DBNAME = DBNAME
215 self.db = sqlite3.connect(
216 self.DBNAME, uri=uri
217 ) # For existing db provide the path of the sqlite3 file and set uri to True
218
219 def drop_table(self, table_name):
220 c = self.db.cursor()
221 c.execute(f"""DROP TABLE {table_name};""")
222 self.db.commit()
223 c.close()
224
225 def create_table(self, table_name, fields):
226 c = self.db.cursor()
227 c.execute(f"""CREATE TABLE IF NOT EXISTS {table_name} ({", ".join(fields)});""")
228 self.db.commit()
229 c.close()
230
231 def insert_data(self, table_name, fields, values):
232 c = self.db.cursor()
233 c.execute(
234 f"""INSERT INTO {table_name} ({', '.join(fields)}) VALUES ({', '.join(values)})"""
235 )
236 self.db.commit()
237 c.close()
238
239
240class CompareConnections:
241 """
242 Comparing two connections based upon their threshold values
243 """
244
245 def __init__(self, jitter=1.0, rtt=30.0, packet_loss=10, uplink=0.5, downlink=0.2):
246 self.JITTER = jitter
247 self.RTT = rtt
248 self.PACKET_LOSS = packet_loss
249 self.UPLINK = uplink
250 self.DOWNLINK = downlink
251
252 def compare(self, records):
253 flag_list = [True, True]
254 for ind, record in enumerate(records):
255 flag = True
256 if record["ping"]["jitter"] < self.JITTER:
257 flag = False
258 if record["ping"]["rtt"] < self.RTT:
259 flag = False
260 if record["ping"]["packet_loss"] > self.PACKET_LOSS:
261 flag = False
262 if record["ping"]["uplink"] < self.UPLINK:
263 flag = False
264 if record["ping"]["downlink"] < self.DOWNLINK:
265 flag = False
266 flag_list[ind] = flag
267 return flag_list
268
269
270def signal_handler(signal, frame):
271 global interrupted
272 interrupted = True
273
274
275if __name__ == "__main__":
276 # Default IP of the default network
277 DEFAULT_IP = ""
278
279 # Creating Two different interfaces
280 connA = NetworkInterface("connectionA", "192.168.0.10/24", port_type="eth0")
281 connB = NetworkInterface("connectionB", "192.168.43.136/24", port_type="eth1")
282
283 # Creting namespace
284 connA.create_netns()
285 connB.create_netns()
286
287 # Linking Interface to netns
288 connA.link_interface_to_netns()
289 connB.link_interface_to_netns()
290
291 # Set IP in NameSpace
292 connA.set_ip_in_namespace()
293 connB.set_ip_in_namespace()
294
295 # Initializing network class
296 network = Network()
297
298 # Database for maintaining logs
299 ping_db = DataBase("ping.sqlite3")
300 # ping_db.drop_table("ping") # To get the new logs
301 PING_FIELDS = [
302 "timestamp text",
303 "connection text",
304 "rtt DECIMAL(8,6)",
305 "packet_loss DECIMAL(8,6)",
306 "jitter DECIMAL(8,6)",
307 "dns_or_ip TEXT",
308 "downlink DECIMAL(8,6)",
309 "uplink DECIMAL(8,6)",
310 ]
311 ping_db.create_table("ping", PING_FIELDS)
312
313 signal.signal(signal.SIGINT, signal_handler)
314 interrupted = False
315 while True:
316 time.sleep(120)
317
318 track = []
319 for connection in [connA, connB]:
320 ping = network.ping(connection)
321 ping["connection"] = str(connection.NAME_SPACE)
322 track.append({"ping": ping, "connection": connection})
323 ping_db.insert_data("ping", ping.keys(), ping.values())
324
325 # Compare both the connections
326 status = CompareConnections().compare(track)
327 if status[0] and status[1]:
328 strongest_connection = random.choice(track)["connection"]
329 else:
330 strongest_connection = track[0 if status[0] else 1]["connection"]
331
332 strongest_connection.set_default_interface(default_ip=DEFAULT_IP)
333
334 download_file(
335 "https://wallpaperaccess.com/full/799794.jpg", "picture.jpg",
336 )
337
338 if interrupted:
339 """If interrupted it will delete the created namespace"""
340 connA.delete_namespace()
341 connB.delete_namespace()
342 break
343