diff --git a/bad_network_start.sh b/bad_network_start.sh new file mode 100755 index 0000000..e8d6966 --- /dev/null +++ b/bad_network_start.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Oneliner to find script's directory. Please note - last path component should NOT be symlink. +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; + +/usr/bin/python3 ${SCRIPT_DIR}/src/utils_network_impairment.py --start + diff --git a/bad_network_stop.sh b/bad_network_stop.sh new file mode 100755 index 0000000..9a7d1d3 --- /dev/null +++ b/bad_network_stop.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +# Oneliner to find script's directory. Please note - last path component should NOT be symlink. +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; + +/usr/bin/python3 ${SCRIPT_DIR}/src/utils_network_impairment.py --stop diff --git a/debug_node.sh b/debug_node.sh new file mode 100755 index 0000000..71bd884 --- /dev/null +++ b/debug_node.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Oneliner to find script's directory. Please note - last path component should NOT be symlink. +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; + + +DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus + +# DBUS_SESSION_BUS_PID=`cat /run/dbus/pid` + +export DBUS_SESSION_BUS_ADDRESS +# export DBUS_SESSION_BUS_PID + +# To avoid problems with pulseaudio +pkill pulseaudio + +while true; do + read -p "Do you wish to enable bad network simulation for gsm.sevana.biz ? " yn + case $yn in + [Yy]* ) $SCRIPT_DIR/start_bad_network.sh; break;; + [Nn]* ) exit;; + * ) echo "Please answer yes or no.";; + esac +done + +# Ensure BT stack is here +python3 -u $SCRIPT_DIR/src/bt_preconnect.py $SCRIPT_DIR/config/agent.yaml +python3 -u $SCRIPT_DIR/src/agent_gsm.py --config $SCRIPT_DIR/config/agent.yaml --test diff --git a/src/utils_bt_audio.py b/src/utils_bt_audio.py old mode 100755 new mode 100644 diff --git a/src/utils_cache.py b/src/utils_cache.py new file mode 100644 index 0000000..4ccc2af --- /dev/null +++ b/src/utils_cache.py @@ -0,0 +1,94 @@ +#!/usr/bin/python3 +from pathlib import Path +from utils_qualtest import Phone, TaskList +import os +import shutil +import json +import uuid + +class InfoCache: + dir: Path + + def __init__(self, dir: Path) -> None: + self.dir = dir + if dir is not None and not dir.exists(): + os.mkdir(dir) + + + def is_active(self) -> bool: + return self.dir is not None + + + def add_reference_audio(self, audio_id: int, src_path: Path): + if not self.is_active(): + return + + p = self.dir / f'ref_{audio_id}.wav' + if not p.exists(): + shutil.copy(src_path, p) + + + def get_reference_audio(self, audio_id: int, dst_path: Path) -> bool: + if not self.is_active(): + return False + + p = self.dir / f'ref_{audio_id}.wav' + if p.exists(): + shutil.copy(p, dst_path) + return True + + return False + + + def add_recorded_audio(self, src_path: Path, probe_id: str) -> Path: + if not self.is_active(): + return None + + p = self.dir / f'audio_{probe_id}.wav' + + shutil.copy(src_path, p) + return p + + + def get_recorded_audio(self, probe_id: str) -> Path: + if not self.is_active(): + return None + + p = self.dir / f'audio_{probe_id}.wav' + if p.exists(): + return p + else: + return None + + def put_phone(self, phone: Phone): + if self.is_active(): + with open(self.dir / f'phone_{phone.name}.json', 'wt') as f: + f.write(phone.dump()) + + def get_phone(self, name: str) -> Phone: + p = self.dir / f'phone_{name}.json', 'wt' + if p.exists(): + with open(p, 'rt') as f: + return Phone.make(f.read()) + + def put_tasks(self, name: str, tasks: TaskList): + p = self.dir / f'tasks_{name}.json' + with open(p, 'wt') as f: + f.write(json.dumps(tasks.tasks)) + + + def get_tasks(self, name: str) -> TaskList: + p = self.dir / f'tasks_{name}.json' + + + def add_report(self, report: dict) -> str: + if not self.is_active(): + return None + + # Generate UUID manually and save under this name to cache dir + probe_id = uuid.uuid1().urn[9:] + with open(self.dir / f'{probe_id}.json', 'wt') as f: + f.write(json.dumps(report, indent=4)) + return probe_id + + \ No newline at end of file diff --git a/src/utils_network_impairment.py b/src/utils_network_impairment.py new file mode 100644 index 0000000..77be509 --- /dev/null +++ b/src/utils_network_impairment.py @@ -0,0 +1,237 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- + +# FLEDGE_BEGIN +# See: http://fledge-iot.readthedocs.io/ +# FLEDGE_END + +""" Module for applying network impairments.""" + + +__author__ = "Deepanshu Yadav" +__copyright__ = "Copyright (c) 2022 Dianomic Systems Inc." +__license__ = "Apache 2.0" +__version__ = "${VERSION}" + +# References +# 1. http://myconfigure.blogspot.com/2012/03/traffic-shaping.html +# 2. https://lartc.org/howto/lartc.qdisc.classful.html +# 3. https://lartc.org/howto/lartc.qdisc.filters.html +# 4. https://serverfault.com/a/841865 +# 5. https://serverfault.com/a/906499 +# 6. https://wiki.linuxfoundation.org/networking/netem +# 7. https://srtlab.github.io/srt-cookbook/how-to-articles/using-netem-to-emulate-networks/ +# 8. https://wiki.linuxfoundation.org/networking/netem + +import subprocess +import multiprocessing +import datetime +import time +import socket +import argparse + +def check_for_interface(interface): + """Checks for given interface if present in output of ifconfig""" + for tup in socket.if_nameindex(): + if tup[1] == interface: + return True + + return False + + +class Distortion(multiprocessing.Process): + def __init__(self, run_cmd_list, clear_cmd, duration): + super(Distortion, self).__init__() + self.run_cmd_list = run_cmd_list + self.duration = duration + self.clear_cmd = clear_cmd + + @staticmethod + def run_command(command): + """Executes a shell command using subprocess module.""" + try: + process = subprocess.Popen(command, cwd=None, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE) + except Exception as inst: + print("Problem running command : \n ", str(command)) + return False + + [stdoutdata, stderrdata] = process.communicate(None) + if process.returncode: + print(stderrdata) + print("Problem running command : \n ", str(command), " ", process.returncode) + return False + + return True + + def run(self) -> None: + + # Make sure we are in clean state. Ignore error if there. + _ = Distortion.run_command(self.clear_cmd) + + for run_cmd in self.run_cmd_list: + print("Executing {}".format(run_cmd), flush=True) + ret_val = Distortion.run_command(run_cmd) + if not ret_val: + print("Could not perform execution of command {}".format(run_cmd), flush=True) + return + + end_time = datetime.datetime.now() + datetime.timedelta(seconds=self.duration) + while datetime.datetime.now() < end_time: + time.sleep(0.5) + + print("Executing {}".format(self.clear_cmd), flush=True) + ret_val = Distortion.run_command(self.clear_cmd) + if not ret_val: + print("Could not perform execution of command {}".format(self.clear_cmd), flush=True) + return + + print("Network Impairment complete.", flush=True) + + +def reset_network(interface): + """ + Reset the network in the middle of impairment. + :param interface: The interface of the network. + :type interface: string + :return: True/False If successful. + :rtype: boolean + """ + if not check_for_interface(interface): + raise Exception("Could not find given {} among present interfaces.".format(interface)) + + clear_cmd = "sudo tc qdisc del dev {} root".format(interface) + ret_val = Distortion.run_command(command=clear_cmd) + + if ret_val: + print("Network has been reset.") + else: + print("Could not reset the network.") + + +def distort_network(interface, duration, rate_limit, latency, ip=None, port=None, + traffic=''): + """ + + :param interface: Interface on which network impairment will be applied. See ifconfig in + your linux machine to decide. + :type interface: string + :param duration: The duration (in seconds) for which impairment will be applied. Note it will + get auto cleared after application. + :type duration: integer + :param traffic: If inbound then the given ip and port will be used to filter packets coming + from destination. For these packets only the impairment will be applied. + If outbound then we are talking about packets leaving this machine for destination. + This is exactly the opposite of first case. + :type traffic: inbound/ outbound string + :param ip: The ip of machine where packets are coming / leaving to filter. Keep None + if no filter required. + :type ip: string + :param port: The port of machine where packets are coming / leaving to filter. Keep None + if no filter required. + :type port: integer + :param rate_limit: The restriction in rate in kbps. Use value 20 for 20 kbps. + :type rate_limit: integer + :param latency: The delay to cause for every packet leaving/ coming from machine in + milliseconds. Use something like 300 for causing a delay for 300 milliseconds. + :type latency: integer + :return: None + :rtype: None + """ + + if not check_for_interface(interface): + raise Exception("Could not find given {} among present interfaces.".format(interface)) + + if not latency and not rate_limit: + raise Exception("Could not find latency or rate_limit.") + + if latency: + latency_converted = str(latency) + 'ms' + else: + latency_converted = None + + if rate_limit: + rate_limit_converted = str(rate_limit) + 'Kbit' + else: + rate_limit_converted = None + + if not (ip and port): + if rate_limit_converted and latency_converted: + run_cmd = "sudo tc qdisc add dev {} root netem" \ + " delay {} rate {}".format(interface, latency_converted, + rate_limit_converted) + elif rate_limit_converted and not latency_converted: + run_cmd = "sudo tc qdisc add dev {} root netem" \ + " rate {}".format(interface, rate_limit_converted) + elif not rate_limit_converted and latency_converted: + run_cmd = "sudo tc qdisc add dev {} root netem" \ + "delay {}".format(interface, latency_converted) + + clear_cmd = "sudo tc qdisc del dev {} root".format(interface) + p = Distortion([run_cmd], clear_cmd, duration) + p.daemon = True + p.start() + + else: + r1 = "sudo tc qdisc add dev {} root handle 1: prio" \ + " priomap 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2".format(interface) + if latency_converted and rate_limit_converted: + r2 = "sudo tc qdisc add dev {} parent 1:1 " \ + "handle 10: netem delay {} rate {}".format(interface, + latency_converted, + rate_limit_converted) + elif not latency_converted and rate_limit_converted: + r2 = "sudo tc qdisc add dev {} parent 1:1 " \ + "handle 10: netem rate {}".format(interface, + rate_limit_converted) + elif latency_converted and not rate_limit_converted: + r2 = "sudo tc qdisc add dev {} parent 1:1 " \ + "handle 10: netem delay {} ".format(interface, + latency_converted) + + if traffic.lower() == 'outbound': + ip_param = 'dst' + port_param = 'dport' + + elif traffic.lower() == "inbound": + ip_param = 'src' + port_param = 'sport' + else: + raise Exception("For ip and port are given then traffic has to be either inbound or outbound." + " But got other than these two. ") + + r3 = "sudo tc filter add dev {} protocol ip parent 1:0 prio 1 u32 " \ + "match ip {} {}/32 match ip {} {} 0xffff flowid 1:1".format(interface, ip_param, + ip, port_param, port) + clear_cmd = "sudo tc qdisc del dev {} root".format(interface) + run_cmd_list = [r1, r2, r3] + p = Distortion(run_cmd_list, clear_cmd, duration) + p.daemon = True + p.start() + + +""" -------------------------Usage -------------------------------------""" + +# from network_impairment import distort_network, reset_network +# distort_network(interface="wlp2s0", duration=40, rate_limit=20, latency=300, +# ip="192.168.1.80", port=8081, traffic="inbound") +# +# distort_network(interface="wlp2s0", duration=40, rate_limit=20, latency=300, +# ip="192.168.1.80", port=8081, traffic="outbound") +# +# distort_network(interface="wlp2s0", duration=40, rate_limit=20, latency=300) + +# reset_network(interface="wlp2s0") + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--stop', action='store_true') + parser.add_argument('--start', action='store_true') + parser.add_argument('--ip') + + config = parser.parse_args() + interface_name = 'wlp2s0' + if config.start and config.ip: + distort_network(interface='', duration=300, rate_limit=20, latency=300, ip=config.ip, port=443, traffic='outbound') + distort_network(interface='', duration=300, rate_limit=20, latency=300, ip=config.ip, port=443, traffic='inbound') + elif config.stop: + reset_network(interface='') \ No newline at end of file