diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..81f6025 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +env/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..97827a4 --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# Network Emulation + +This is a simple wrapper to concatenate network conditions using `tc-netem ` +by means of a simple configuration file. + +### Configuration Files +The configuration files have the following structure: + +``` +{ + "name": "example", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 5000, + "rules": [ + "clear" + ] + }, + "2": { + "duration": 15000, + "rules": [ + "delay 30ms 10ms distribution normal", + "loss 0.1% 0.25%" + ] + }, + "3": { + "duration": 10000, + "rules": [ + "delay 50ms" + ] + }, + "4": { + "duration": 5000, + "rules": [ + "clear" + ] + } + } +} +``` + +`name`: name of the experiment. + +`interface`: interface where the network conditions will be applied. + +`events`: list of ordered network conditions. + +Each `event` should be tagged by an integer that indicates the order. The +range should be from 1 to N, being N the number of events that will be applied. + +Inside the event `duration` indicates in milliseconds the duration of the +network conditions. + +`rules` is a list of the conditions that will be applied during the event. +This is where the filters from `tc-netem` should be written. For more info, +take a look at the [tc-netem docs](http://man7.org/linux/man-pages/man8/tc-netem.8.html). + +A special rule, that has nothing to do with the `tc-netem` commands has been +created. This rule means that all conditions will be cleared. It is recommended +to always have this rule as the last one to make sure the network conditions are +cleared once the simulation has been finished. It can also be used to add +intervals in the simulation with no conditions applied. + +It is important to note that each event **overwrites** its predecessor. If +a condition needs to be kept from an event, it should be included in the next event. + +### Explanation of [example.json](configs/example.json) + +Here follows an explanation of the example provided in the repo. + +The configuration is named `example` and the conditions will be applied to +the interface `wlp3s0`. It has a total of 4 events: + +1. The first event lasts 5 seconds and there are no conditions applied. +2. Applies a delay that follows a gaussian distribution of mean 30ms and +standard deviation (jitter) of 10ms. It also adds a 0.1% packet loss with +a correlation of 0.25%. This correlation is used to simulate burst errors. This +event has a duration of 15 seconds. +3. This event is 10 second long and adds a constant 50ms delay. +4. The simulation is finished by 5 seconds of cleared conditions, + + + +### Notes + +The code has been tested with python 3.6.9. There is no need of additional python libraries. + +When specifying the jitter with a rule such as `delay 30ms 10ms`, an +erroneous behaviour has been noted, so it is better to fix the distribution, +such as `delay 30ms 10ms distribution normal`. \ No newline at end of file diff --git a/configs/example.json b/configs/example.json new file mode 100644 index 0000000..c7f52af --- /dev/null +++ b/configs/example.json @@ -0,0 +1,31 @@ +{ + "name": "example", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 5000, + "rules": [ + "clear" + ] + }, + "2": { + "duration": 15000, + "rules": [ + "delay 30ms 10ms distribution normal", + "loss 0.1% 0.25%" + ] + }, + "3": { + "duration": 10000, + "rules": [ + "delay 50ms" + ] + }, + "4": { + "duration": 5000, + "rules": [ + "clear" + ] + } + } +} \ No newline at end of file diff --git a/configs/experiment.json b/configs/experiment.json new file mode 100644 index 0000000..5faec2f --- /dev/null +++ b/configs/experiment.json @@ -0,0 +1,60 @@ +{ + "name": "test", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 20000, + "rules": [ + "rate 10mbit" + ] + }, + "2": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 10ms 1ms distribution normal" + ] + }, + "3": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 15ms 1ms distribution normal" + ] + }, + "4": { + "duration": 6000, + "rules": [ + "rate 10mbit", + "delay 20ms 2ms distribution normal", + "loss 1%" + ] + }, + "5": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 15ms 1ms distribution normal" + ] + }, + "6": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 10ms 1ms distribution normal" + ] + }, + "7": { + "duration": 85000, + "rules": [ + "rate 10mbit" + ] + }, + "8": { + "duration": 1000, + "rules": [ + "clear" + ] + } + } +} \ No newline at end of file diff --git a/configs/experiment_long_valley.json b/configs/experiment_long_valley.json new file mode 100644 index 0000000..fb48b88 --- /dev/null +++ b/configs/experiment_long_valley.json @@ -0,0 +1,102 @@ +{ + "name": "test", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 20000, + "rules": [ + "rate 10mbit" + ] + }, + "2": { + "duration": 3000, + "rules": [ + "rate 9mbit" + ] + }, + "3": { + "duration": 3000, + "rules": [ + "rate 8mbit" + ] + }, + "4": { + "duration": 3000, + "rules": [ + "rate 7mbit" + ] + }, + "5": { + "duration": 3000, + "rules": [ + "rate 6mbit" + ] + }, + "6": { + "duration": 3000, + "rules": [ + "rate 5mbit" + ] + }, + "7": { + "duration": 3000, + "rules": [ + "rate 4mbit" + ] + }, + "8": { + "duration": 43000, + "rules": [ + "rate 3mbit" + ] + }, + "9": { + "duration": 3000, + "rules": [ + "rate 4mbit" + ] + }, + "10": { + "duration": 3000, + "rules": [ + "rate 5mbit" + ] + }, + "11": { + "duration": 3000, + "rules": [ + "rate 6mbit" + ] + }, + "12": { + "duration": 3000, + "rules": [ + "rate 7mbit" + ] + }, + "13": { + "duration": 3000, + "rules": [ + "rate 8mbit" + ] + }, + "14": { + "duration": 3000, + "rules": [ + "rate 9mbit" + ] + }, + "15": { + "duration": 261000, + "rules": [ + "rate 10mbit" + ] + }, + "16": { + "duration": 1000, + "rules": [ + "clear" + ] + } + } +} \ No newline at end of file diff --git a/configs/experiment_rate_limit.json b/configs/experiment_rate_limit.json new file mode 100644 index 0000000..45a0e18 --- /dev/null +++ b/configs/experiment_rate_limit.json @@ -0,0 +1,102 @@ +{ + "name": "test", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 20000, + "rules": [ + "rate 10mbit" + ] + }, + "2": { + "duration": 3000, + "rules": [ + "rate 9mbit" + ] + }, + "3": { + "duration": 3000, + "rules": [ + "rate 8mbit" + ] + }, + "4": { + "duration": 3000, + "rules": [ + "rate 7mbit" + ] + }, + "5": { + "duration": 3000, + "rules": [ + "rate 6mbit" + ] + }, + "6": { + "duration": 3000, + "rules": [ + "rate 5mbit" + ] + }, + "7": { + "duration": 3000, + "rules": [ + "rate 4mbit" + ] + }, + "8": { + "duration": 3000, + "rules": [ + "rate 3mbit" + ] + }, + "9": { + "duration": 3000, + "rules": [ + "rate 4mbit" + ] + }, + "10": { + "duration": 3000, + "rules": [ + "rate 5mbit" + ] + }, + "11": { + "duration": 3000, + "rules": [ + "rate 6mbit" + ] + }, + "12": { + "duration": 3000, + "rules": [ + "rate 7mbit" + ] + }, + "13": { + "duration": 3000, + "rules": [ + "rate 8mbit" + ] + }, + "14": { + "duration": 3000, + "rules": [ + "rate 9mbit" + ] + }, + "15": { + "duration": 301000, + "rules": [ + "rate 10mbit" + ] + }, + "16": { + "duration": 1000, + "rules": [ + "clear" + ] + } + } +} \ No newline at end of file diff --git a/configs/experiment_unstable.json b/configs/experiment_unstable.json new file mode 100644 index 0000000..110ce45 --- /dev/null +++ b/configs/experiment_unstable.json @@ -0,0 +1,102 @@ +{ + "name": "test", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 10000, + "rules": [ + "rate 7mbit" + ] + }, + "2": { + "duration": 5000, + "rules": [ + "rate 6mbit" + ] + }, + "3": { + "duration": 5000, + "rules": [ + "rate 5mbit" + ] + }, + "4": { + "duration": 5000, + "rules": [ + "rate 6mbit" + ] + }, + "5": { + "duration": 5000, + "rules": [ + "rate 5mbit" + ] + }, + "6": { + "duration": 5000, + "rules": [ + "rate 6mbit" + ] + }, + "7": { + "duration": 10000, + "rules": [ + "rate 5mbit" + ] + }, + "8": { + "duration": 10000, + "rules": [ + "rate 6mbit" + ] + }, + "9": { + "duration": 10000, + "rules": [ + "rate 5mbit" + ] + }, + "10": { + "duration": 10000, + "rules": [ + "rate 6mbit" + ] + }, + "11": { + "duration": 20000, + "rules": [ + "rate 5mbit" + ] + }, + "12": { + "duration": 20000, + "rules": [ + "rate 6mbit" + ] + }, + "13": { + "duration": 20000, + "rules": [ + "rate 5mbit" + ] + }, + "14": { + "duration": 20000, + "rules": [ + "rate 6mbit" + ] + }, + "15": { + "duration": 205000, + "rules": [ + "rate 7mbit" + ] + }, + "16": { + "duration": 1000, + "rules": [ + "clear" + ] + } + } +} diff --git a/configs/long_experiment.json b/configs/long_experiment.json new file mode 100644 index 0000000..ff7a9a1 --- /dev/null +++ b/configs/long_experiment.json @@ -0,0 +1,106 @@ +{ + "name": "test", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 20000, + "rules": [ + "rate 10mbit" + ] + }, + "2": { + "duration": 5000, + "rules": [ + "rate 10mbit", + "loss 2%" + ] + }, + "3": { + "duration": 15000, + "rules": [ + "rate 10mbit" + ] + }, + "4": { + "duration": 5000, + "rules": [ + "rate 10mbit", + "delay 20ms" + ] + }, + "5": { + "duration": 15000, + "rules": [ + "rate 10mbit" + ] + }, + "6": { + "duration": 5000, + "rules": [ + "rate 10mbit", + "loss 1%", + "delay 20ms" + ] + }, + "7": { + "duration": 20000, + "rules": [ + "rate 10mbit" + ] + }, + "8": { + "duration": 5000, + "rules": [ + "rate 10mbit" + ] + }, + "9": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 10ms 1ms distribution normal" + ] + }, + "10": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 15ms 1ms distribution normal" + ] + }, + "11": { + "duration": 6000, + "rules": [ + "rate 10mbit", + "delay 20ms 2ms distribution normal", + "loss 1%" + ] + }, + "12": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 15ms 1ms distribution normal" + ] + }, + "13": { + "duration": 3000, + "rules": [ + "rate 10mbit", + "delay 10ms 1ms distribution normal" + ] + }, + "14": { + "duration": 10000, + "rules": [ + "rate 10mbit" + ] + }, + "15": { + "duration": 1000, + "rules": [ + "clear" + ] + } + } +} \ No newline at end of file diff --git a/configs/test.json b/configs/test.json new file mode 100644 index 0000000..731e7c4 --- /dev/null +++ b/configs/test.json @@ -0,0 +1,91 @@ +{ + "name": "test", + "interface": "wlp3s0", + "events": { + "1": { + "duration": 10000, + "rules": [ + "delay 10ms" + ] + }, + "2": { + "duration": 5000, + "rules": [ + "clear" + ] + }, + "3": { + "duration": 10000, + "rules": [ + "delay 20ms" + ] + }, + "4": { + "duration": 5000, + "rules": [ + "clear" + ] + }, + "5": { + "duration": 10000, + "rules": [ + "delay 30ms" + ] + }, + "6": { + "duration": 5000, + "rules": [ + "clear" + ] + }, + "7": { + "duration": 10000, + "rules": [ + "delay 40ms", + "loss 0.01%"] + }, + "8": { + "duration": 5000, + "rules": [ + "clear" + ] + }, + "9": { + "duration": 10000, + "rules": [ + "delay 60ms 10ms distribution normal", + "loss 10% 1%" + ] + }, + "10": { + "duration": 5000, + "rules": [ + "clear" + ] + }, + "11": { + "duration": 10000, + "rules": [ + "delay 40ms 30ms distribution normal", + "loss 0.01%" + ] + }, + "12": { + "duration": 10000, + "rules": [ + "clear" + ] + }, + "13": { + "duration": 10000, + "rules": [ + "delay 50ms 1ms"] + }, + "14": { + "duration": 10000, + "rules": [ + "clear" + ] + } + } +} \ No newline at end of file diff --git a/emulate.py b/emulate.py new file mode 100644 index 0000000..6eb6c2f --- /dev/null +++ b/emulate.py @@ -0,0 +1,81 @@ +import json +import time +import argparse +import os +from datetime import datetime + + +def load_config(path: str) -> dict: + with open(path) as json_file: + config = json.load(json_file) + + return config + + +def execute_experiment(config: dict): + n = len(config['events']) + + timeline = {} + + cleared = True + for i in range(1, n + 1): + event = config['events'][str(i)] + print(f'Applying event {i} with configuration:', event) + start = str(datetime.now()) + cleared = apply_condition(config['interface'], event, cleared) + end = str(datetime.now()) + timeline[str(i)] = {'start': start, 'end': end, 'duration': event['duration'], 'rules': event['rules']} + + return timeline + + +def apply_condition(interface: str, event: dict, is_cleared: bool): + if event['rules'][0] == 'clear': + condition = f'sudo tc qdisc del dev {interface} root' + cleared = True + + else: + if is_cleared: + condition = f'sudo tc qdisc add dev {interface} root netem' + else: + os.system(f'sudo tc qdisc del dev {interface} root') + condition = f'sudo tc qdisc add dev {interface} root netem' + + for rule in event['rules']: + condition += ' ' + rule + cleared = False + + os.system(condition) + time.sleep(event['duration']/1000) + + return cleared + + +if __name__ == '__main__': + + parser = argparse.ArgumentParser(description='Network simulation script.') + + parser.add_argument('--config', '-c', default='configs/example.json', + help='Config file for the simulation.') + + parser.add_argument('--output', '-o', + help='Optional output file that saves the timestamps of the events that might be useful for' + 'analysis later.') + + args = parser.parse_args() + + config = load_config(args.config) + + print('Configuration file read:') + print(config, '\n') + timeline = execute_experiment(config) + + if args.output: + print(f'Creating output file {args.output}') + with open(args.output, 'w') as outfile: + json.dump(timeline, outfile) + else: + print('No output file.') + + + diff --git a/network_emulation.py b/network_emulation.py new file mode 100644 index 0000000..338b5c3 --- /dev/null +++ b/network_emulation.py @@ -0,0 +1,19 @@ +import json +import re + + +def get_rate_conditions_from_file(path: str) -> list: + + with open(path, 'r') as json_file: + data = json.load(json_file) + + rates = [] + + for event in data: + duration = data[event]['duration'] + rule = data[event]['rules'][0] + if rule != 'clear': + rate = int(re.findall(r'\d+', rule)[0]) + rates.extend([rate] * (duration // 1000)) + + return rates