commit 511023952629dff75d59c8d07a3ff6e53fe12136 Author: nitred Date: Tue Sep 14 10:29:50 2021 +0200 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c69648c --- /dev/null +++ b/.gitignore @@ -0,0 +1,140 @@ +.idea + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cf1f976 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog +This project adheres to [Semantic Versioning](http://semver.org/). + +## tag: 0.1.0 / 2021-09-14 +- First working release \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a7b3193 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Nitish K Reddy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..37ec116 --- /dev/null +++ b/Makefile @@ -0,0 +1,36 @@ +.DEFAULT_GOAL := help + +help: ## Show available options with this Makefile + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' + +.PHONY : test +test: ## Run all the tests +test: + python setup.py test + +.PHONY : recreate_pyenv +recreate_pyenv: ## Create the python environment. Recreates if the env exists already. +recreate_pyenv: + conda env create --force -f dev_environment.yml + +.PHONY : readme_to_rst +readme_to_rst: ## Convert README.md to README.rst for the sake of pip documentation. +readme_to_rst: + m2r --overwrite README.md + +.PHONY : upload_test_pypi +upload_test_pypi: ## Build and upload distribution to testpypi server +upload_test_pypi: readme_to_rst + python setup.py bdist_wheel --dist-dir dist && \ + twine upload --skip-existing --repository testpypi dist/* + +.PHONY : upload_pypi +upload_pypi: ## Build and upload distribution to pypi server +upload_pypi: readme_to_rst + python setup.py bdist_wheel --dist-dir dist && \ + twine upload --skip-existing --repository pypi dist/* + +.PHONY : lint +lint: ## Run flake8 linter +lint: + flake8 nr_pypackage diff --git a/README.md b/README.md new file mode 100644 index 0000000..c30bfc1 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# About +A python 3 project to help find optimal MTUs for a Wireguard peer and server. + +***Please read the following documentation carefully, especially the WARNING section***. + + +* This project offers no warranties, therefore do not use in production. Ideally trying using two VMs that are similar to your production setup. +* The project was developed and tested against a WG peer and WG server running Ubuntu 20.04. + + +# Warning +***WARNING: This project contains scripts that run shell commands using root access. DO NOT USE IN PRODUCTION.*** + +***WARNING: This project tears down and spins up the Wireguard interface in the order of a thousand times. DO NOT USE IN PRODUCTION.*** + + +That being said, if you're an experienced python developer, please go through the code to verify that it meets your security standards. + + +# Example Bandwidth Plot +![Bandwidth Plot](./examples/example.png) + +# Installation + +Install the following on both the WG server and WG peer +* Install `ping` + ```bash + # Ubuntu + sudo apt install iputils-ping + ``` +* Install `iperf3` + ```bash + # Source: https://iperf.fr/iperf-download.php + ``` +* Install `sed` + ```bash + # Ubuntu + sudo apt install sed + ``` +* Install `wg-quick` + ```bash + # Should come installed when you install Wireguard + ``` +* Install the project + ```bash + # Use your environment manager of choice like virtualenv or conda to pre-create an environment + # This package has the following dependencies: pandas, matplotlib, pydantic, requests, flask + pip install git+https://github.com/nitred/nr-wg-mtu-finder.git --upgrade + ``` + +# Usage + +### Prerequisites +1. Follow the installation instructions above for both WG server and WG peer +1. The project assumes that you already have a working WG installation on both the WG peer and WG server. +1. The project assumes that you already have a WG interface like `wg0`. +1. The project assumes that you already have a WG conf file like `/etc/wireguard/wg0.conf`. ***Take a backup of these files***. +1. Start the WG server script before the WG peer script + +### On the WG Server +1. Let your firewall accept connections on port 5201 from IPs within your WG interface. This port is used by the iperf3 server. + ```text + # Replace 10.2.0.0/24 with your interface's IP range + ufw allow proto tcp from 10.2.0.0/24 to any port 5201 + ``` +1. Let your firewall accept connections on port 5000 from IPs within your WG interface. This port is used by the flask server. + ```text + # Replace 10.2.0.0/24 with your interface's IP range + ufw allow proto tcp from 10.2.0.0/24 to any port 5000 + ``` +1. Add the MTU setting to the WG conf file i.e. `/etc/wireguard/wg0.conf`. Choose any random MTU, it will be replaced by the script anyway: + ```text + [Interface] + ... + MTU = 1420 # <----- ADD THIS LINE IF NOT ALREADY EXISTS + + [Peer] + ... + ``` +1. Start the server script with the following command. + ```bash + # Example: The script cycles peer MTUs from 1280 to 1290 in steps of 2 + nr-wg-mtu-finder --mode server --mtu-min 1280 --mtu-max 1290 --mtu-step 2 --server-ip 10.2.0.1 + ``` + +### On the WG Peer +1. Add the MTU setting to the WG conf file i.e. `/etc/wireguard/wg0.conf`. Choose any random MTU, it will be replaced by the script anyway: + ```text + [Interface] + ... + MTU = 1420 # <----- ADD THIS LINE IF NOT ALREADY EXISTS + + [Peer] + ... + ``` +1. Start the server script with the following command. + ```bash + # Example: The script cycles peer MTUs from 1280 to 1290 in steps of 2 + nr-wg-mtu-finder --mode peer --mtu-min 1280 --mtu-max 1290 --mtu-step 2 --server-ip 10.2.0.1 + ``` + +# How it works? + +* Two python scripts need to be running simultaneously, one of the WG server and one on the WG peer. Let's call them *server script* and *peer script*. +* The both scripts use `subprocess.Popen` to run shell commands. The following commands are used and expected to be pre-installed if not already available: + * `ping` + * `iperf3` + * `wg-quick` + * `sed` +* The server script also runs a `flask` server and the peer script uses `requests` to communicate with the flask server. + + +### How does the server script work? +1. The flow for the server script is defined in the method `MTUFinder.run_server_mode()`. +1. First, a flask server called a `sync_server` is run is the background on a separate process. + * The `sync_server's` listens for requests and commands from the peer script so that they can synchronize with each other. + * The peer script waits for the `sync_server` to be available before running any upload or download tests. + * The peer scripts get the status and MTU of the server script from the `sync_server`. + * The peer script tells the `sync_server` that it is done with its cycling through all of its MTUs and is ready for the server script to change its MTU so that it can start a fresh cycle. + * The `sync_server` informs the peer script that the server script is finished with cycling through all MTUs and that it is going to shut itself down. The peer script uses this signal to shut itself down as well. +1. When the server script receives an `INTIALIZE` signal, it runs the following shell commands + * First, terminate an `iperf3` server process if it is already running. + * Spin down the WG interface + ``` + wg-quick down wg0 + ``` + * Replace the MTU in the WG conf file with the next MTU in the list + ``` + # 1421 is the new MTU + sed -i s/MTU.*/MTU = 1421/ /etc/wireguard/wg0.conf + ``` + * Spin up the WG interface + ``` + wg-quick up wg0 + ``` + * Run iperf3 in server mode + ``` + iperf3 -s + ``` +1. If the server has finishing cycling through all of its MTUs and then receives a request from peer script that it is ready for a new cycle, then the server sends a `SHUTDOWN` signal to the peer script via the `sync_server`. + + +### How does the server script work? +* On start, the peer script checks if the `sync_server` is reachable. Once it is reachable, it sends a `peer/ready` request to the server script. +* The peer script then waits for the `iperf3` server to start on the server side. Once it recognizes that the iperf3 server has started, and then the peer script starts cycling through each of its MTUs. + * For each MTU, the peer script runs an upload and download test using the following command + ``` + # Upload test + iperf3 -c 10.2.0.1 -J -t 5 -i 5 + # Download test + iperf3 -c 10.2.0.1 -J -t 5 -i 5 -R + ``` + * After each download and upload test, the peer script parses the output and stores the bandwidth results in a bandwidth log file. +* Once the peer script is finished cycling through all of its MTU, it sends another `peer/ready` request to the server script and restarts the whole process again with the next server MTU. +* If the server script is finished cycling through all of its MTUs, then it sends a `SHUTDOWN` signal to the peer script as a reply to the `peer/ready` request. The server shuts down after a short delay as does the peer script. +* Finall the user can check the bandwidth log file to see the results. + +## License +MIT + + diff --git a/examples/example.csv b/examples/example.csv new file mode 100644 index 0000000..3411732 --- /dev/null +++ b/examples/example.csv @@ -0,0 +1,530 @@ +server_mtu,peer_mtu,upload_rcv_mbps,upload_send_mbps,download_rcv_mbps,download_send_mbps +1280,1280,20.737,21.706,357.680,361.111 +1280,1290,14.765,14.975,452.098,454.813 +1280,1300,23.878,25.598,441.084,443.618 +1280,1310,24.075,26.939,347.185,351.830 +1280,1320,22.004,23.316,427.644,431.565 +1280,1330,17.544,18.535,358.896,362.756 +1280,1340,28.138,28.864,454.672,459.084 +1280,1350,32.121,35.410,422.873,427.440 +1280,1360,21.791,22.495,357.659,361.045 +1280,1370,23.719,24.942,385.588,389.351 +1280,1380,21.030,22.956,383.594,388.121 +1280,1390,26.248,27.473,386.102,390.638 +1280,1400,23.327,24.402,362.564,365.865 +1280,1410,16.620,18.005,214.110,216.537 +1280,1420,20.246,21.264,374.027,377.710 +1280,1430,19.317,20.879,401.521,404.392 +1280,1440,22.405,23.765,461.278,464.945 +1280,1450,22.616,23.532,371.722,375.896 +1280,1460,30.835,31.758,402.537,405.522 +1280,1470,21.576,23.630,401.294,403.768 +1280,1480,19.692,21.963,422.196,424.874 +1280,1490,23.925,25.043,445.973,449.313 +1280,1500,19.811,21.244,347.357,350.900 +1290,1280,35.771,36.419,359.940,363.446 +1290,1290,14.878,15.232,348.246,352.510 +1290,1300,20.231,23.871,373.139,376.295 +1290,1310,19.999,21.902,370.460,374.907 +1290,1320,29.591,31.197,391.984,395.072 +1290,1330,18.162,19.165,442.891,446.690 +1290,1340,15.905,16.341,440.876,444.084 +1290,1350,37.672,41.055,398.939,402.280 +1290,1360,24.898,25.466,432.273,436.181 +1290,1370,23.111,23.450,395.508,398.189 +1290,1380,32.917,34.623,456.721,460.339 +1290,1390,21.104,21.620,391.805,396.177 +1290,1400,19.260,20.488,382.249,387.130 +1290,1410,18.015,19.383,319.812,322.884 +1290,1420,15.490,15.991,360.272,363.151 +1290,1430,15.413,17.090,317.617,320.606 +1290,1440,14.045,14.497,371.438,376.006 +1290,1450,20.657,22.855,374.898,379.608 +1290,1460,14.877,15.531,368.144,371.331 +1290,1470,20.951,22.841,386.190,390.560 +1290,1480,18.031,19.109,371.938,375.809 +1290,1490,10.701,11.142,344.828,347.840 +1290,1500,11.389,11.762,349.931,354.006 +1300,1280,13.912,14.242,368.976,372.335 +1300,1290,16.644,17.094,378.847,382.403 +1300,1300,14.438,15.614,370.253,374.128 +1300,1310,18.677,20.098,454.495,457.000 +1300,1320,17.009,17.821,493.933,498.110 +1300,1330,17.590,19.050,395.682,400.577 +1300,1340,16.146,16.976,346.854,350.555 +1300,1350,11.636,12.014,378.019,380.927 +1300,1360,13.592,13.829,436.955,441.551 +1300,1370,17.050,17.547,463.782,467.986 +1300,1380,19.308,20.228,384.729,387.436 +1300,1390,17.105,18.469,351.818,356.274 +1300,1400,18.335,19.690,400.307,404.786 +1300,1410,20.246,21.018,477.726,480.120 +1300,1420,21.012,21.693,384.489,388.038 +1300,1430,17.447,18.867,475.123,477.586 +1300,1440,19.544,20.909,375.938,379.178 +1300,1450,20.206,21.519,371.267,374.434 +1300,1460,13.834,14.221,454.383,457.228 +1300,1470,23.662,25.031,377.847,381.450 +1300,1480,21.179,22.387,407.507,411.387 +1300,1490,22.752,24.140,464.428,468.342 +1300,1500,20.137,21.153,407.764,410.721 +1310,1280,24.554,25.418,450.094,453.958 +1310,1290,32.799,34.445,334.064,338.373 +1310,1300,18.880,19.592,450.908,453.416 +1310,1310,20.892,21.180,455.059,458.640 +1310,1320,11.837,12.268,391.793,394.896 +1310,1330,21.693,22.963,398.557,403.305 +1310,1340,15.346,15.806,376.985,380.019 +1310,1350,10.285,10.521,359.544,363.674 +1310,1360,16.308,17.094,436.644,440.071 +1310,1370,20.998,21.963,395.040,397.771 +1310,1380,25.056,26.647,356.214,360.911 +1310,1390,21.321,22.030,363.237,367.955 +1310,1400,20.981,21.657,393.627,396.955 +1310,1410,20.216,21.856,366.811,370.610 +1310,1420,21.114,22.476,358.938,362.589 +1310,1430,25.139,27.542,409.005,412.270 +1310,1440,23.208,24.030,422.830,427.122 +1310,1450,16.493,17.245,429.245,433.080 +1310,1460,21.970,23.217,472.925,476.874 +1310,1470,16.617,16.947,395.209,397.926 +1310,1480,25.573,27.158,389.331,394.088 +1310,1490,16.971,17.310,348.305,352.080 +1310,1500,20.932,22.227,240.437,245.590 +1320,1280,22.555,23.129,347.619,350.488 +1320,1290,17.876,18.358,453.234,457.504 +1320,1300,21.877,22.309,392.929,396.702 +1320,1310,11.208,11.362,463.303,465.583 +1320,1320,26.435,27.986,374.603,377.338 +1320,1330,21.705,23.432,390.230,393.351 +1320,1340,19.203,20.336,416.539,421.234 +1320,1350,21.474,22.404,422.342,426.601 +1320,1360,19.118,19.786,370.611,375.007 +1320,1370,19.497,20.164,359.026,363.846 +1320,1380,24.963,25.694,490.747,494.928 +1320,1390,23.934,24.712,400.657,404.155 +1320,1400,13.130,13.527,380.067,383.977 +1320,1410,19.924,20.442,366.813,371.311 +1320,1420,23.818,26.556,469.225,473.164 +1320,1430,22.512,24.571,406.034,408.337 +1320,1440,19.834,21.557,422.145,426.182 +1320,1450,16.514,16.869,398.187,402.474 +1320,1460,26.042,28.406,370.707,373.627 +1320,1470,20.684,21.048,396.256,399.315 +1320,1480,21.472,22.134,386.412,389.312 +1320,1490,25.173,26.481,375.125,378.413 +1320,1500,18.153,18.715,374.005,378.667 +1330,1280,20.093,20.868,403.183,407.583 +1330,1290,14.580,15.093,344.065,348.965 +1330,1300,36.932,39.153,421.392,425.640 +1330,1310,24.103,25.515,427.476,430.501 +1330,1320,27.390,28.713,482.112,485.813 +1330,1330,26.995,28.842,344.585,348.289 +1330,1340,16.882,17.327,405.615,409.948 +1330,1350,18.251,19.114,371.292,374.626 +1330,1360,38.493,39.926,346.768,351.425 +1330,1370,15.419,15.928,373.667,377.447 +1330,1380,17.419,17.822,395.268,399.278 +1330,1390,37.203,40.082,381.329,384.662 +1330,1400,28.339,29.352,417.808,420.815 +1330,1410,22.143,22.930,312.897,317.504 +1330,1420,21.320,22.567,353.455,356.800 +1330,1430,22.229,22.991,414.613,418.926 +1330,1440,23.547,24.781,409.729,412.782 +1330,1450,18.566,19.112,403.791,406.411 +1330,1460,46.150,47.614,397.481,401.810 +1330,1470,44.288,47.172,364.955,368.548 +1330,1480,42.831,45.948,380.175,384.812 +1330,1490,43.132,45.724,384.511,388.324 +1330,1500,42.910,46.234,377.012,380.413 +1340,1280,40.970,43.932,384.070,387.031 +1340,1290,43.337,46.401,462.216,465.164 +1340,1300,44.782,47.163,375.905,380.045 +1340,1310,46.314,49.230,447.525,452.002 +1340,1320,43.528,45.867,340.500,345.585 +1340,1330,46.099,49.260,366.380,369.329 +1340,1340,47.438,49.624,377.512,382.420 +1340,1350,42.376,44.671,367.266,371.305 +1340,1360,44.972,47.974,374.273,378.996 +1340,1370,47.177,49.416,430.201,432.883 +1340,1380,42.313,44.696,389.294,392.299 +1340,1390,41.436,44.065,454.095,456.777 +1340,1400,41.991,44.780,468.950,473.464 +1340,1410,47.348,50.726,409.560,414.111 +1340,1420,44.536,47.657,486.635,490.753 +1340,1430,47.298,50.350,400.488,404.877 +1340,1440,43.741,46.008,376.270,379.751 +1340,1450,45.536,48.624,290.047,293.968 +1340,1460,43.048,46.014,412.570,416.066 +1340,1470,43.631,46.594,443.560,446.974 +1340,1480,47.208,50.441,401.051,404.399 +1340,1490,43.400,46.435,395.608,399.117 +1340,1500,44.388,47.259,436.558,440.839 +1350,1280,47.135,50.406,331.239,335.948 +1350,1290,46.615,49.222,379.506,383.526 +1350,1300,47.373,48.545,416.031,420.804 +1350,1310,43.271,45.530,428.074,431.355 +1350,1320,45.384,48.386,358.959,363.826 +1350,1330,43.518,45.720,384.338,388.132 +1350,1340,43.564,46.047,350.386,354.929 +1350,1350,46.288,48.670,461.497,464.137 +1350,1360,43.749,46.049,381.902,385.044 +1350,1370,46.941,49.384,413.764,417.004 +1350,1380,47.234,50.546,385.482,388.407 +1350,1390,42.600,45.327,347.027,351.447 +1350,1400,46.934,49.908,401.345,404.402 +1350,1410,43.787,46.729,421.364,425.265 +1350,1420,43.569,46.089,433.664,437.179 +1350,1430,43.453,45.653,418.746,422.000 +1350,1440,46.947,49.610,466.914,471.127 +1350,1450,46.873,49.084,362.836,366.796 +1350,1460,43.974,47.215,345.236,348.277 +1350,1470,45.248,47.831,406.501,410.286 +1350,1480,44.284,46.920,394.578,397.082 +1350,1490,43.874,46.473,391.074,394.978 +1350,1500,46.366,49.011,409.418,413.614 +1360,1280,46.606,49.073,413.245,417.799 +1360,1290,47.184,50.474,373.364,376.969 +1360,1300,44.819,47.350,391.410,395.155 +1360,1310,43.253,45.511,459.712,462.539 +1360,1320,46.719,49.520,386.740,391.172 +1360,1330,46.544,49.689,388.020,392.924 +1360,1340,47.355,50.336,465.833,469.530 +1360,1350,47.016,49.851,296.911,300.098 +1360,1360,47.488,49.803,391.799,394.708 +1360,1370,47.286,49.702,420.187,423.353 +1360,1380,47.794,50.218,362.679,366.414 +1360,1390,47.006,49.912,413.945,417.139 +1360,1400,47.669,50.107,482.327,486.410 +1360,1410,47.351,50.008,455.142,458.010 +1360,1420,47.341,50.732,443.398,446.091 +1360,1430,47.666,49.890,411.488,415.658 +1360,1440,47.137,49.708,489.588,492.898 +1360,1450,46.444,49.634,357.538,361.253 +1360,1460,43.260,45.937,355.947,359.599 +1360,1470,46.710,49.494,372.000,375.415 +1360,1480,46.479,49.112,381.690,386.446 +1360,1490,46.814,49.443,348.627,352.467 +1360,1500,45.926,48.908,489.397,493.555 +1370,1280,46.079,49.217,341.215,346.083 +1370,1290,46.324,49.333,354.585,358.135 +1370,1300,45.948,49.007,325.264,328.921 +1370,1310,45.760,48.819,402.222,405.064 +1370,1320,41.845,43.926,439.608,443.271 +1370,1330,43.544,45.916,362.986,367.136 +1370,1340,44.261,47.049,358.705,362.964 +1370,1350,47.638,50.211,339.365,342.812 +1370,1360,46.072,48.675,344.933,348.692 +1370,1370,44.038,46.339,325.494,330.452 +1370,1380,46.809,47.801,380.272,383.394 +1370,1390,44.417,47.149,398.422,401.229 +1370,1400,46.917,49.615,377.657,382.451 +1370,1410,47.453,49.923,374.671,378.186 +1370,1420,47.578,50.772,457.411,461.173 +1370,1430,44.567,47.322,363.931,367.982 +1370,1440,43.752,46.977,403.047,405.826 +1370,1450,41.883,44.379,382.507,385.486 +1370,1460,43.066,45.629,407.615,411.008 +1370,1470,47.484,50.120,399.682,402.568 +1370,1480,44.844,47.582,402.227,405.917 +1370,1490,43.137,45.268,404.491,408.429 +1370,1500,39.914,42.826,390.582,393.641 +1380,1280,47.054,50.305,375.697,379.837 +1380,1290,47.243,50.164,361.712,365.192 +1380,1300,43.303,45.753,379.332,382.083 +1380,1310,47.166,49.422,373.085,376.683 +1380,1320,39.164,41.863,428.841,432.676 +1380,1330,44.732,47.537,391.169,394.933 +1380,1340,44.493,47.739,483.757,486.409 +1380,1350,45.202,47.964,413.985,417.853 +1380,1360,40.612,43.343,505.719,509.206 +1380,1370,46.763,49.657,369.036,373.138 +1380,1380,42.127,45.232,492.606,495.466 +1380,1390,44.039,47.137,390.771,394.201 +1380,1400,44.837,47.133,419.354,422.332 +1380,1410,46.748,48.894,468.828,472.945 +1380,1420,45.195,47.964,406.479,410.919 +1380,1430,43.412,46.124,419.994,424.013 +1380,1440,24.789,25.478,438.730,443.086 +1380,1450,46.675,49.598,468.888,471.852 +1380,1460,47.209,50.218,420.342,423.961 +1380,1470,47.403,50.535,434.837,438.704 +1380,1480,44.192,46.901,489.425,491.684 +1380,1490,46.814,49.507,439.317,442.391 +1380,1500,43.724,46.862,361.066,365.678 +1390,1280,42.911,45.507,406.398,410.654 +1390,1290,46.795,49.158,364.768,368.048 +1390,1300,43.435,46.346,343.994,347.175 +1390,1310,44.852,47.265,366.856,370.823 +1390,1320,45.848,48.477,389.515,392.635 +1390,1330,47.126,49.378,354.994,357.823 +1390,1340,42.225,45.023,375.905,379.187 +1390,1350,41.725,44.251,382.117,385.106 +1390,1360,44.995,47.614,432.551,435.303 +1390,1370,43.850,46.213,450.180,453.250 +1390,1380,44.317,47.477,402.016,406.168 +1390,1390,47.798,50.054,391.631,394.646 +1390,1400,44.562,47.465,404.887,408.516 +1390,1410,47.752,50.316,393.029,396.575 +1390,1420,44.435,46.676,346.765,351.394 +1390,1430,42.803,45.276,466.015,469.179 +1390,1440,46.385,48.688,430.222,434.576 +1390,1450,41.577,44.052,395.744,399.414 +1390,1460,45.516,48.374,488.142,490.588 +1390,1470,47.181,49.237,393.392,397.167 +1390,1480,46.888,49.944,400.137,404.074 +1390,1490,43.894,46.102,378.468,381.859 +1390,1500,43.907,46.912,395.456,398.778 +1400,1280,46.104,48.460,359.593,362.682 +1400,1290,43.489,46.718,367.730,370.555 +1400,1300,42.327,44.667,416.976,419.959 +1400,1310,46.524,49.534,367.941,371.630 +1400,1320,42.031,44.275,462.915,466.266 +1400,1330,47.160,50.219,317.624,321.332 +1400,1340,43.276,45.653,391.912,396.419 +1400,1350,45.284,46.558,417.397,420.441 +1400,1360,46.616,49.617,485.362,487.805 +1400,1370,47.180,50.361,434.505,438.943 +1400,1380,46.935,49.849,448.249,452.473 +1400,1390,47.865,49.164,441.358,444.339 +1400,1400,47.402,50.056,420.547,422.776 +1400,1410,0.002,0.116,447.504,450.294 +1400,1420,0.002,0.116,487.645,490.899 +1400,1430,0.002,0.116,429.633,432.702 +1400,1440,0.000,0.116,395.876,400.381 +1400,1450,0.002,0.116,392.369,397.164 +1400,1460,0.002,0.116,433.513,436.706 +1400,1470,0.002,0.116,368.857,372.625 +1400,1480,0.002,0.116,382.472,386.120 +1400,1490,0.002,0.116,432.998,436.962 +1400,1500,0.002,0.116,459.141,462.302 +1410,1280,42.989,46.367,442.547,445.498 +1410,1290,46.365,49.114,332.335,336.748 +1410,1300,47.163,49.815,444.285,448.088 +1410,1310,37.480,39.737,333.152,336.146 +1410,1320,45.235,48.091,400.229,404.294 +1410,1330,46.520,48.732,355.929,359.483 +1410,1340,46.149,49.215,398.291,402.335 +1410,1350,46.775,49.807,475.651,478.461 +1410,1360,46.371,49.075,356.632,359.879 +1410,1370,47.111,50.241,364.984,368.451 +1410,1380,47.058,49.229,387.655,392.184 +1410,1390,43.873,46.028,392.432,396.199 +1410,1400,47.655,50.580,98.980,100.422 +1410,1410,0.002,0.117,200.022,205.480 +1410,1420,0.000,0.117,133.354,136.554 +1410,1430,0.002,0.117,305.256,308.611 +1410,1440,0.002,0.117,118.640,120.651 +1410,1450,0.002,0.117,229.342,234.068 +1410,1460,0.000,0.117,97.275,102.622 +1410,1470,0.000,0.117,173.014,178.180 +1410,1480,0.002,0.117,146.648,152.365 +1410,1490,0.000,0.117,196.239,201.461 +1410,1500,0.002,0.117,296.940,301.425 +1420,1280,44.282,46.680,383.941,387.031 +1420,1290,47.374,50.846,354.061,357.530 +1420,1300,46.821,49.491,420.790,423.298 +1420,1310,41.964,44.671,472.767,475.471 +1420,1320,46.513,49.140,343.647,348.050 +1420,1330,46.020,48.255,426.009,430.308 +1420,1340,46.884,50.156,388.693,392.733 +1420,1350,46.894,49.139,444.956,448.583 +1420,1360,47.474,50.502,457.816,460.175 +1420,1370,44.183,46.871,396.187,400.121 +1420,1380,47.738,49.948,380.511,384.911 +1420,1390,47.782,50.110,397.849,402.594 +1420,1400,47.522,50.287,174.540,178.564 +1420,1410,0.002,0.113,223.213,227.528 +1420,1420,0.000,0.114,114.871,116.596 +1420,1430,0.002,0.114,89.193,90.702 +1420,1440,0.002,0.114,251.005,255.191 +1420,1450,0.002,0.114,209.876,214.953 +1420,1460,0.000,0.114,164.545,168.716 +1420,1470,0.000,0.114,200.798,204.326 +1420,1480,0.002,0.114,213.964,218.615 +1420,1490,0.002,0.114,83.747,87.399 +1420,1500,0.002,0.114,96.928,100.633 +1430,1280,46.594,49.298,364.530,367.544 +1430,1290,43.464,45.882,338.075,343.091 +1430,1300,45.593,48.437,401.153,404.951 +1430,1310,43.348,45.899,339.975,343.794 +1430,1320,42.838,45.082,349.725,354.740 +1430,1330,46.583,49.481,401.968,404.797 +1430,1340,47.388,49.843,430.043,432.719 +1430,1350,47.740,49.912,335.411,339.093 +1430,1360,43.761,46.099,358.030,361.012 +1430,1370,47.807,50.792,439.497,443.603 +1430,1380,46.835,49.789,388.182,391.607 +1430,1390,47.802,50.616,501.649,503.888 +1430,1400,33.385,36.691,309.061,312.914 +1430,1410,0.002,0.113,318.991,323.005 +1430,1420,0.002,0.114,78.752,80.259 +1430,1430,0.000,0.115,284.417,288.465 +1430,1440,0.000,0.115,143.378,144.648 +1430,1450,0.002,0.115,200.704,204.512 +1430,1460,0.002,0.115,197.756,202.444 +1430,1470,0.002,0.115,54.259,56.274 +1430,1480,0.002,0.115,328.764,332.320 +1430,1490,0.002,0.115,237.407,241.869 +1430,1500,0.002,0.115,209.705,213.681 +1440,1280,42.074,44.501,406.357,410.903 +1440,1290,46.385,49.328,412.699,416.557 +1440,1300,44.579,47.315,398.179,401.939 +1440,1310,47.453,50.454,408.335,411.398 +1440,1320,47.493,50.236,448.419,451.166 +1440,1330,47.094,49.362,418.514,422.355 +1440,1340,44.521,46.795,401.347,404.221 +1440,1350,43.905,47.317,420.556,423.635 +1440,1360,47.740,50.280,406.278,410.948 +1440,1370,43.983,46.206,386.788,389.480 +1440,1380,47.509,50.661,486.314,489.483 +1440,1390,39.922,42.185,429.133,432.874 +1440,1400,47.557,49.842,243.538,248.218 +1440,1410,0.002,0.117,122.151,124.606 +1440,1420,0.002,0.114,208.142,212.654 +1440,1430,0.002,0.115,331.651,335.410 +1440,1440,0.000,0.115,368.121,372.060 +1440,1450,0.000,0.115,110.465,114.932 +1440,1460,0.002,0.115,295.627,299.630 +1440,1470,0.002,0.115,220.861,225.743 +1440,1480,0.002,0.115,66.580,68.696 +1440,1490,0.000,0.115,217.765,221.178 +1440,1500,0.000,0.115,209.181,213.834 +1450,1280,46.175,51.393,327.369,332.297 +1450,1290,47.383,52.631,410.074,412.935 +1450,1300,46.219,51.205,349.221,353.839 +1450,1310,47.379,53.607,408.749,411.850 +1450,1320,47.561,53.506,390.459,393.384 +1450,1330,47.133,52.920,344.771,349.021 +1450,1340,45.378,50.812,374.855,379.606 +1450,1350,46.815,52.474,376.302,379.277 +1450,1360,45.127,50.739,428.621,431.826 +1450,1370,44.750,49.395,423.730,428.298 +1450,1380,44.515,49.418,447.861,452.282 +1450,1390,43.850,45.547,440.119,444.401 +1450,1400,46.162,51.550,262.339,267.229 +1450,1410,0.002,0.163,230.559,235.369 +1450,1420,0.002,0.164,241.786,246.065 +1450,1430,0.002,0.165,289.238,292.380 +1450,1440,0.000,0.166,295.184,299.770 +1450,1450,37.938,40.089,19.853,21.148 +1450,1460,41.110,44.118,8.600,9.087 +1450,1470,38.742,40.944,17.073,19.671 +1450,1480,38.429,40.829,12.197,13.490 +1450,1490,33.310,34.765,9.039,9.531 +1450,1500,38.997,41.829,8.692,8.962 +1460,1280,43.509,47.887,451.779,454.999 +1460,1290,42.584,47.424,485.101,488.950 +1460,1300,44.387,49.034,509.394,512.145 +1460,1310,47.436,53.484,496.417,499.625 +1460,1320,40.401,45.530,462.502,465.300 +1460,1330,46.312,50.769,425.412,428.465 +1460,1340,45.085,51.029,469.161,471.493 +1460,1350,47.656,51.454,534.816,537.539 +1460,1360,47.056,51.987,408.523,412.333 +1460,1370,45.192,50.867,395.506,399.835 +1460,1380,47.665,51.989,72.788,73.925 +1460,1390,47.783,52.151,516.615,520.145 +1460,1400,46.494,51.692,134.252,137.861 +1460,1410,0.002,0.163,285.622,290.335 +1460,1420,0.002,0.164,95.419,96.763 +1460,1430,0.002,0.165,334.494,337.763 +1460,1440,0.000,0.166,283.256,286.536 +1460,1450,39.177,42.046,12.626,13.806 +1460,1460,37.356,39.928,14.528,16.183 +1460,1470,37.672,40.817,7.526,8.012 +1460,1480,37.605,39.720,7.288,7.780 +1460,1490,40.869,43.941,15.936,18.340 +1460,1500,37.051,39.339,7.276,7.906 +1470,1280,43.361,48.700,411.680,416.226 +1470,1290,42.887,48.857,506.792,510.003 +1470,1300,46.596,51.133,443.212,446.737 +1470,1310,42.926,47.469,339.886,344.595 +1470,1320,44.740,50.418,444.137,447.900 +1470,1330,47.335,51.939,406.395,410.032 +1470,1340,47.501,52.632,417.795,422.391 +1470,1350,46.801,51.984,470.573,473.815 +1470,1360,44.855,50.619,379.513,383.737 +1470,1370,47.153,51.313,374.597,378.850 +1470,1380,43.283,49.471,402.552,406.743 +1470,1390,47.852,52.600,361.754,364.733 +1470,1400,47.142,51.334,321.534,325.217 +1470,1410,0.002,0.169,262.148,265.686 +1470,1420,0.002,0.164,225.842,229.224 +1470,1430,0.000,0.165,271.334,275.624 +1470,1440,0.002,0.166,220.754,225.484 +1470,1450,35.827,37.228,7.388,8.015 +1470,1460,38.572,40.810,6.776,7.244 +1470,1470,41.453,44.202,8.394,9.355 +1470,1480,36.455,38.593,8.378,9.045 +1470,1490,39.469,42.335,9.610,10.446 +1470,1500,26.675,27.821,8.623,9.425 +1480,1280,41.379,47.404,386.087,389.791 +1480,1290,47.116,52.921,372.710,376.986 +1480,1300,43.591,48.554,441.520,444.398 +1480,1310,44.763,49.952,419.570,422.882 +1480,1320,46.656,52.989,475.603,479.614 +1480,1330,39.676,45.840,358.419,362.049 +1480,1340,43.780,49.423,384.640,388.027 +1480,1350,47.621,52.558,518.682,521.277 +1480,1360,45.319,51.154,446.399,450.470 +1480,1370,45.888,50.197,442.774,445.599 +1480,1380,47.074,53.112,529.057,532.387 +1480,1390,47.739,52.665,392.739,397.445 +1480,1400,47.815,51.894,154.429,156.783 +1480,1410,0.002,0.163,59.773,60.546 +1480,1420,0.002,0.164,272.349,277.010 +1480,1430,0.002,0.165,100.013,101.855 +1480,1440,0.002,0.166,269.456,272.814 +1480,1450,38.350,41.022,18.162,20.158 +1480,1460,36.197,38.729,8.087,8.880 +1480,1470,38.112,40.513,10.513,11.547 +1480,1480,38.360,40.842,15.024,16.395 +1480,1490,38.981,41.590,15.305,17.625 +1480,1500,34.672,36.138,17.419,19.126 +1490,1280,47.125,51.977,376.595,381.008 +1490,1290,46.827,51.365,435.473,439.195 +1490,1300,39.790,45.828,356.390,361.268 +1490,1310,47.100,52.067,356.747,360.857 +1490,1320,46.172,50.884,435.792,440.180 +1490,1330,47.401,53.229,407.655,411.750 +1490,1340,46.210,50.751,371.442,375.445 +1490,1350,46.564,51.538,381.831,384.885 +1490,1360,38.658,44.039,452.159,455.878 +1490,1370,38.329,42.662,401.988,406.855 +1490,1380,40.187,43.111,485.876,488.792 +1490,1390,36.109,41.719,445.349,449.066 +1490,1400,38.746,44.183,153.668,155.962 +1490,1410,0.002,0.163,57.885,58.614 +1490,1420,0.002,0.164,217.184,221.444 +1490,1430,0.002,0.165,81.774,82.932 +1490,1440,0.000,0.166,246.123,250.524 +1490,1450,37.250,41.590,7.605,8.391 +1490,1460,36.461,38.006,11.789,12.702 +1490,1470,37.499,40.086,12.297,13.536 +1490,1480,39.907,42.394,7.958,8.584 +1490,1490,37.969,42.336,12.657,14.180 +1490,1500,35.612,37.221,9.601,10.411 +1500,1280,45.898,51.074,381.356,385.851 +1500,1290,46.155,51.279,447.346,450.937 +1500,1300,47.081,53.054,455.394,458.827 +1500,1310,45.750,50.849,472.877,476.415 +1500,1320,46.826,51.109,403.040,407.518 +1500,1330,41.654,46.665,384.903,388.987 +1500,1340,45.405,50.982,422.598,425.510 +1500,1350,47.062,53.009,367.359,372.020 +1500,1360,33.811,38.690,432.314,436.518 +1500,1370,33.253,38.044,460.230,464.132 +1500,1380,46.189,50.603,448.953,451.416 +1500,1390,46.487,50.754,405.929,408.691 +1500,1400,47.207,51.748,280.478,284.156 +1500,1410,0.000,0.163,164.343,169.619 +1500,1420,0.000,0.164,233.938,238.295 +1500,1430,0.000,0.165,144.196,147.032 +1500,1440,0.002,0.166,134.236,135.898 +1500,1450,37.032,39.162,7.104,7.501 +1500,1460,40.111,42.441,9.935,11.265 +1500,1470,37.090,38.387,14.869,16.335 +1500,1480,40.173,43.382,17.456,19.305 +1500,1490,39.987,42.603,14.543,16.166 +1500,1500,40.490,43.420,11.558,12.189 diff --git a/examples/example.png b/examples/example.png new file mode 100644 index 0000000..6369611 Binary files /dev/null and b/examples/example.png differ diff --git a/nr_wg_mtu_finder/__init__.py b/nr_wg_mtu_finder/__init__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/nr_wg_mtu_finder/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/nr_wg_mtu_finder/main.py b/nr_wg_mtu_finder/main.py new file mode 100644 index 0000000..d03ba8c --- /dev/null +++ b/nr_wg_mtu_finder/main.py @@ -0,0 +1,105 @@ +import argparse +import signal +import time +import sys +from pydantic import BaseModel, StrictStr, StrictInt, root_validator +from typing_extensions import Literal +from .mtu_finder import MTUFinder + + +def signal_handler(sig, frame): + """Handle ctrl+c interrupt. + + Without this handler, everytime a ctrl+c interrupt is received, the server shutdowns and + proceeds to the next iteration in the loop rather than exiting the program altogether. + """ + print("************Received CTRL-C. Will exit in 1 second************") + time.sleep(1) + sys.exit(0) + + +signal.signal(signal.SIGINT, signal_handler) + + +class ArgsModel(BaseModel): + mode: Literal["server", "peer"] + mtu_min: int + mtu_max: int + mtu_step: int + + server_ip: StrictStr + server_port: int = 5000 + + interface: StrictStr = "wg0" + conf_file: StrictStr = "/etc/wireguard/wg0.conf" + + @root_validator(pre=False) + def validate(cls, values): + """Generic validations.""" + mtu_min, mtu_max, mtu_step = ( + values.get("mtu_min", None), + values.get("mtu_max", None), + values.get("mtu_step", None), + ) + + if not (1280 <= mtu_min <= 1500): + raise ValueError(f"mtu_min: {mtu_min} must be in range [1280, 1500].") + + if not (1280 <= mtu_max <= 1500): + raise ValueError(f"mtu_max: {mtu_max} must be in range [1280, 1500].") + + if not (mtu_min <= mtu_max): + raise ValueError(f"mtu_min: {mtu_min} must be less than or equal to mtu_max: {mtu_max}") + + return values + + class Config: + orm_mode = True + + +def setup_args(): + """Setup args.""" + parser = argparse.ArgumentParser( + description="nr-wg-mtu-finder - Helps find the optimal Wireguard MTU between Server and Peer." + ) + parser.add_argument( + "--mode", + help=( + "Mode is 'server' if you are running this script on the WG Server, " + "else the mode is 'peer' if you are running this script on the WG Peer." + ), + required=True, + ) + parser.add_argument( + "--mtu-min", help="Min MTU.", required=True, + ) + parser.add_argument( + "--mtu-max", help="Max MTU.", required=True, + ) + parser.add_argument( + "--mtu-step", help="By how much to increment the MTU between loops.", required=True, + ) + parser.add_argument( + "--server-ip", help="The IP address of the WG server and flask server.", required=True, + ) + parser.add_argument( + "--server-port", help="The port for the flask server.", required=False, default=5000, + ) + parser.add_argument( + "--interface", help="The WG interface name. Default: 'wg0'", required=False, default="wg0" + ) + parser.add_argument( + "--conf-file", + help="The WG interface name. Default: '/etc/wireguard/wg0.conf'", + required=False, + default="/etc/wireguard/wg0.conf", + ) + args = parser.parse_args() + return args + + +def run(): + args = setup_args() + args = ArgsModel.from_orm(args) + + MTUFinder(**args.dict()) diff --git a/nr_wg_mtu_finder/mtu_finder.py b/nr_wg_mtu_finder/mtu_finder.py new file mode 100644 index 0000000..8e127e7 --- /dev/null +++ b/nr_wg_mtu_finder/mtu_finder.py @@ -0,0 +1,397 @@ +import subprocess +import time +import sys +import json +from datetime import datetime +import requests + +# Set to either client or server +from nr_wg_mtu_finder.sync_server import run_sync_server +from nr_wg_mtu_finder.plot import plot_log + + +class MTUFinder(object): + def __init__( + self, mode, server_ip, server_port, interface, conf_file, mtu_max, mtu_min, mtu_step + ): + """Init.""" + self.mode = mode + + self.server_ip = server_ip + self.server_port = server_port + self.interface = interface + self.conf_file = conf_file + + self.mtu_max = mtu_max + self.mtu_min = mtu_min + self.mtu_step = mtu_step + + self.peer_mtu = None + self.server_mtu = None + self.current_mtu = None + + self.log_filename = ( + f"wg_mtu_finder_{self.mode}_{datetime.now().strftime('%Y%m%dT%H%M%S')}.csv" + ) + self.plot_filename = ( + f"wg_mtu_finder_{self.mode}_{datetime.now().strftime('%Y%m%dT%H%M%S')}.png" + ) + + if self.mode == "server": + self.run_server_mode() + elif self.mode == "peer": + self.run_peer_mode() + else: + raise NotImplementedError() + + def create_log(self): + """Create an empty CSV log file with the headers. + + This log file will be used to store all bandwidth information for each MTU test. + """ + msg = f"Creating log file: {self.log_filename}" + print(f"{msg:<50s}", end=": ") + with open(self.log_filename, "w") as f: + f.write( + f"server_mtu," + f"peer_mtu," + f"upload_rcv_mbps," + f"upload_send_mbps," + f"download_rcv_mbps," + f"download_send_mbps\n" + ) + print("SUCCESS") + + def append_log_with_bandwidth_info(self, up_rcv_bps, up_snd_bps, down_rcv_bps, down_snd_bps): + """Append the bandwidth information to the log file.""" + if self.mode == "server": + raise NotImplementedError() + + msg = f"Appending log for MTU: {self.current_mtu}" + print(f"{msg:<50s}", end=": ") + + with open(self.log_filename, "a") as f: + f.write( + f"{self.server_mtu}," + f"{self.peer_mtu}," + f"{up_rcv_bps / 1000000:0.3f}," + f"{up_snd_bps / 1000000:0.3f}," + f"{down_rcv_bps / 1000000:0.3f}," + f"{down_snd_bps / 1000000:0.3f}\n" + ) + + print("SUCCESS") + + def wg_quick_down(self): + """Spin down the interface using wg-quick.""" + msg = "WG Interface Down" + print(f"{msg:<50s}", end=": ") + process = subprocess.Popen( + ["wg-quick", "down", f"{self.interface}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = process.communicate() + print("SUCCESS" if process.returncode == 0 else f"FAILED with code {process.returncode}") + + def wg_quick_up(self): + """Spin up the interface using wg-quick.""" + msg = "WG Interface Up" + print(f"{msg:<50s}", end=": ") + process = subprocess.Popen( + ["wg-quick", "up", f"{self.interface}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + stdout, stderr = process.communicate() + print("SUCCESS" if process.returncode == 0 else f"FAILED with code {process.returncode}") + + def __validate_conf_file(self): + """Validate that a line `MTU =` exists in the wireguard conf.""" + with open(self.conf_file, "r") as f: + for line in f.readlines(): + if line.startswith("MTU ="): + return + + # If no line starts with "MTU = ", then raise an error. + raise ValueError( + f"Expected to find a line that begins with 'MTU =' in {self.conf_file} file." + ) + + def update_mtu_in_conf_file(self): + """Update the MTU setting in the WG Conf. + + Find a line that starts with 'MTU =***' and replace it with 'MTU = ' + """ + self.__validate_conf_file() + + msg = f"Setting MTU to {self.current_mtu} in /etc/wireguard/wg0.conf" + print(f"{msg:<50s}", end=": ") + process = subprocess.Popen( + ["sed", "-i", f"s/MTU.*/MTU = {self.current_mtu}/", f"{self.conf_file}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + stdout, stderr = process.communicate() + print("SUCCESS" if process.returncode == 0 else f"FAILED with code {process.returncode}") + + def run_iperf3_upload_test(self): + """Run iperf3 upload test.""" + msg = f"Running peer upload" + print(f"{msg:<50s}", end=": ") + command = ["iperf3", "-c", f"{self.server_ip}", "-J", "-t", "5", "-i", "5"] + # print(f"command: {' '.join(command)}") + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, + ) + + # Wait iperf3 test to be done. + stdout, stderr = process.communicate() + + print("SUCCESS" if process.returncode == 0 else f"FAILED with code {process.returncode}") + + # load iperf3 output json which results from the -J flag + output = json.loads(stdout) + return ( + output["end"]["streams"][0]["receiver"]["bits_per_second"], + output["end"]["streams"][0]["sender"]["bits_per_second"], + ) + + def run_iperf3_download_test(self): + """Run iperf3 upload test.""" + msg = f"Running peer download" + print(f"{msg:<50s}", end=": ") + process = subprocess.Popen( + ["iperf3", "-c", f"{self.server_ip}", "-J", "-t", "5", "-i", "5", "-R"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + # Wait iperf3 test to be done. + stdout, stderr = process.communicate() + + print("SUCCESS" if process.returncode == 0 else f"FAILED with code {process.returncode}") + + # load iperf3 output json which results from the -J flag + output = json.loads(stdout) + return ( + output["end"]["streams"][0]["receiver"]["bits_per_second"], + output["end"]["streams"][0]["sender"]["bits_per_second"], + ) + + def __peer_mode__wait_for_server_init(self): + """Get server mtu. + + Raises: + - requests.Timeout, requests.ConnectionError or KeyError if there is something wrong + with the Flask server running on the WG server. + """ + while True: + msg = f"Waiting for server init and status" + print(f"{msg:<50s}", end=": ") + try: + resp = requests.get( + f"http://{self.server_ip}:{self.server_port}/server/status", + verify=False, + timeout=5, + ) + + server_mtu, server_status = resp.json()["server_mtu"], resp.json()["server_status"] + + if (server_status == "INITIALIZED") or (server_status == "SHUTDOWN"): + print(f"SUCCESS, SERVER_MTU: {server_mtu}, SERVER_STATUS: {server_status}") + return server_mtu, server_status + else: + print(f"FAILED, SERVER_STATUS: {server_status}, Retrying...") + time.sleep(1) + continue + except requests.exceptions.ConnectTimeout: + print("FAILED, ConnectTimeout, Retrying...") + time.sleep(1) + continue + + def __peer_mode__send_server_peer_ready(self): + """Send restart signal to flask server and get back server status.""" + msg = f"Send peer ready for next loop to server" + print(f"{msg:<50s}", end=": ") + resp = requests.get( + f"http://{self.server_ip}:{self.server_port}/peer/ready", verify=False, timeout=5 + ) + server_mtu, server_status = resp.json()["server_mtu"], resp.json()["server_status"] + print("SUCCESS") + return server_mtu, server_status + + def __peer_mode__ping_server(self): + """Ping server to reestablish connection between peer and server. + + After server interface is spun down and spun up again, the peer is not guaranteed to be + connected to the server. Therefore we force a ping to make sure peer sends packets + on this network. + """ + msg = f"Pinging server to establish connection" + print(f"{msg:<50s}", end=": ") + process = subprocess.Popen( + ["ping", "-c", "1", f"{self.server_ip}"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + stdout, stderr = process.communicate() + + print("SUCCESS" if process.returncode == 0 else f"FAILED with code {process.returncode}") + + def run_peer_mode(self): + """Run all steps for peer mode. + + 1. Peer is the one that logs bandwidth + """ + self.create_log() + while True: + # Ping IP address of server to flush connection + self.__peer_mode__ping_server() + + # Tell server that peer is ready for next loop. + self.__peer_mode__send_server_peer_ready() + + # Ping IP address of server to flush connection + self.__peer_mode__ping_server() + + # Start a fresh loop of cycling through all peer MTUs + # At start, find what the current server_mtu is. + self.server_mtu, server_status = self.__peer_mode__wait_for_server_init() + + if server_status == "INITIALIZED": + pass + elif server_status == "SHUTDOWN": + print(f"Server has shutdown... Shutting down peer script.") + print(f"Check final bandwidth log: {self.log_filename}") + plot_log(log_filename=self.log_filename, plot_filename=self.plot_filename) + print(f"Check final bandwidth plot: {self.plot_filename}") + sys.exit(0) + else: + raise NotImplementedError() + + for current_mtu in range(self.mtu_min, self.mtu_max + 1, self.mtu_step): + if self.server_mtu is None: + raise NotImplementedError() + + self.current_mtu = current_mtu + self.peer_mtu = current_mtu + + print("-" * 80) + self.wg_quick_down() + self.update_mtu_in_conf_file() + self.wg_quick_up() + + # Wait a short while after interface is spun up. + time.sleep(1) + up_rcv_bps, up_snd_bps = self.run_iperf3_upload_test() + time.sleep(1) + down_rcv_bps, down_snd_bps = self.run_iperf3_download_test() + + self.append_log_with_bandwidth_info( + up_rcv_bps, up_snd_bps, down_rcv_bps, down_snd_bps + ) + + def run_iperf3_server_test(self): + """Run iperf3 upload test.""" + msg = f"Running iperf3 server" + print(f"{msg:<50s}", end=": ") + process = subprocess.Popen( + ["iperf3", "-s"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + + time.sleep(1) + print("SUCCESS") + + return process + + def run_server_mode(self): + """Run all steps for server mode.""" + import multiprocessing as mp + + pool = mp.Pool(1) + manager = mp.Manager() + + to_server_queue = manager.Queue(5) + from_server_queue = manager.Queue(5) + + pool.apply_async( + run_sync_server, + kwds={ + "host": self.server_ip, + "port": self.server_port, + "to_server_queue": to_server_queue, + "from_server_queue": from_server_queue, + }, + ) + + iperf3_server_process = None + mtu_range = list(range(self.mtu_min, self.mtu_max + 1, self.mtu_step)) + mtu_range_iter = iter(mtu_range) + + while True: + print("-" * 80) + # Wait for init command from sync server + sync_server_status = from_server_queue.get(block=True) + + # Any time a message is received from the sync_server, the iperf3 server + # must be terminated. + if iperf3_server_process: + iperf3_server_process.terminate() + + if sync_server_status == "INITIALIZE": + # We receive INITIALIZE from the peer but sometimes the connection is spun down too + # quickly before a response could be sent. Therefore we'll wait for a little while + # until the request has been handled. + time.sleep(1) + + try: + self.current_mtu = next(mtu_range_iter) + except StopIteration: + # Done with cycling through all MTUs + # Send Shutdown signal to the sync_server + # And go back to waiting for shutdown signal from sync_server + to_server_queue.put( + {"server_mtu": self.server_mtu, "server_status": "SHUTDOWN"} + ) + continue + + self.server_mtu = self.current_mtu + + self.wg_quick_down() + self.update_mtu_in_conf_file() + self.wg_quick_up() + + iperf3_server_process = self.run_iperf3_server_test() + + # Wait a short while after interface is spun up. + time.sleep(1) + + to_server_queue.put({"server_mtu": self.server_mtu, "server_status": "INITIALIZED"}) + + # Now wait for peer to ping our server + # Peer will get a response that tells it that the iperf3 server is ready with + # the current_mtu + # Peer will start cycling through all of its MTUs + # Peer will send another "init" command if it needs the server to + + elif sync_server_status == "SHUTDOWN": + time.sleep(2) + print("Received 'SHUTDOWN' signal from sync server. Shutting down.") + sys.exit(0) + + else: + raise NotImplementedError() + + # Code should not reach here. + raise NotImplementedError() diff --git a/nr_wg_mtu_finder/plot.py b/nr_wg_mtu_finder/plot.py new file mode 100644 index 0000000..d3f51bb --- /dev/null +++ b/nr_wg_mtu_finder/plot.py @@ -0,0 +1,84 @@ +import matplotlib.pyplot as plt +import seaborn as sns +import pandas as pd + + +def plot_log(log_filename, plot_filename): + f, axes = plt.subplots(nrows=2, ncols=2, figsize=(12, 12)) + + df = pd.read_csv(log_filename) + + ax = axes[0, 0] + dfx = df.pivot(index="server_mtu", columns="peer_mtu", values="upload_rcv_mbps",) + sns.heatmap( + dfx.values, + linewidth=0.5, + ax=ax, + cmap="Greens_r", + xticklabels=list(dfx.index), + yticklabels=list(dfx.columns), + ) + ax.tick_params(axis="x", rotation=45) + ax.tick_params(axis="y", rotation=0) + ax.set(xlabel="Server MTU", ylabel="Peer MTU") + ax.set_title("Upload Rcv Bandwidth (Mbps)") + ax.invert_yaxis() + + ax = axes[0, 1] + dfx = df.pivot(index="server_mtu", columns="peer_mtu", values="upload_send_mbps") + sns.heatmap( + dfx.values, + linewidth=0.5, + ax=ax, + cmap="Greens_r", + xticklabels=list(dfx.index), + yticklabels=list(dfx.columns), + ) + ax.tick_params(axis="x", rotation=45) + ax.tick_params(axis="y", rotation=0) + ax.set(xlabel="Server MTU", ylabel="Peer MTU") + ax.set_title("Upload Send Bandwidth (Mbps)") + ax.invert_yaxis() + + ax = axes[1, 0] + dfx = df.pivot(index="server_mtu", columns="peer_mtu", values="download_rcv_mbps") + sns.heatmap( + dfx.values, + linewidth=0.5, + ax=ax, + cmap="Greens_r", + xticklabels=list(dfx.index), + yticklabels=list(dfx.columns), + ) + ax.tick_params(axis="x", rotation=45) + ax.tick_params(axis="y", rotation=0) + ax.set(xlabel="Server MTU", ylabel="Peer MTU") + ax.set_title("Download Rcv Bandwidth (Mbps)") + ax.invert_yaxis() + + ax = axes[1, 1] + dfx = df.pivot(index="server_mtu", columns="peer_mtu", values="download_send_mbps") + sns.heatmap( + dfx.values, + linewidth=0.5, + ax=ax, + cmap="Greens_r", + xticklabels=list(dfx.index), + yticklabels=list(dfx.columns), + ) + ax.tick_params(axis="x", rotation=45) + ax.tick_params(axis="y", rotation=0) + ax.set(xlabel="Server MTU", ylabel="Peer MTU") + ax.set_title("Download Send Bandwidth (Mbps)") + ax.invert_yaxis() + + f.suptitle("Peer MTU vs Server MTU Bandwidth (Mbps)") + f.tight_layout() + f.savefig(plot_filename, dpi=300) + + +if __name__ == "__main__": + plot_log( + "/home/nitred/projects/group-nr/nr-wg-mtu-finder/examples/example.csv", + "/home/nitred/projects/group-nr/nr-wg-mtu-finder/examples/example.png", + ) diff --git a/nr_wg_mtu_finder/sync_server.py b/nr_wg_mtu_finder/sync_server.py new file mode 100644 index 0000000..8f07c5a --- /dev/null +++ b/nr_wg_mtu_finder/sync_server.py @@ -0,0 +1,64 @@ +import traceback + +from flask import Flask, jsonify, request +from typing_extensions import Literal +from typing import Optional + +status: Literal["NOT_INITIALIZED", "INITIALIZED", "SHUTDOWN"] = "NOT_INITIALIZED" +mtu: Optional[int] = None + + +def run_sync_server(host, port, to_server_queue, from_server_queue): + """Run a temporary flask/http server that returns server mtu and status and restarts server.""" + app = Flask(__name__) + + def shutdown_server(): + shutdown = request.environ.get("werkzeug.server.shutdown") + if shutdown is None: + raise RuntimeError("Not running with the Werkzeug Server") + shutdown() + + @app.route("/server/status", methods=["GET"]) + def server_status(): + global mtu, status + print("RECEIVED REQUEST /server/status") + + msg = None if to_server_queue.empty() else to_server_queue.get() + + if msg: + # Update global state + mtu, status = msg["server_mtu"], msg["server_status"] + + if status == "SHUTDOWN": + # App will shutdown after sending one last response + shutdown_server() + from_server_queue.put("SHUTDOWN") + elif status == "INITIALIZED": + pass + else: + raise NotImplementedError() + + return jsonify({"server_mtu": mtu, "server_status": status}) + else: + # Return current state + return jsonify({"server_mtu": mtu, "server_status": status}) + + @app.route("/peer/ready", methods=["GET"]) + def peer_ready(): + """Peer is done with its cycle and is waiting for next cycle.""" + global mtu, status + print("RECEIVED REQUEST /peer/ready") + status = "NOT_INITIALIZED" + + from_server_queue.put("INITIALIZE") + return jsonify({"server_mtu": mtu, "server_status": status}) + + # @app.route("/server/shutdown", methods=["GET"]) + # def server_restart(): + # from_server_queue.put("shutdown") + # shutdown_server() + # return jsonify(jsonify({"server_mtu": mtu, "server_status": "shutdown"})) + + # Blocking call until the server received a GET request on /server/shutdown after which + # the flask server is shutdown + app.run(host=host, port=port) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..708f37d --- /dev/null +++ b/setup.py @@ -0,0 +1,50 @@ +import re +from codecs import open # To use a consistent encoding +from os import path + +from setuptools import find_packages, setup + +here = path.abspath(path.dirname(__file__)) + +# Get the long description from the relevant file +with open(path.join(here, "README.md"), encoding="utf-8") as f: + long_description = f.read() + + +# Get version without importing, which avoids dependency issues +def get_version(): + with open("nr_wg_mtu_finder/__init__.py") as version_file: + return re.search( + r"""__version__\s+=\s+(['"])(?P.+?)\1""", version_file.read() + ).group("version") + + +install_requires = [ + "pandas>=0.23.4", + "matplotlib", + "seaborn", + "pydantic", + "requests", + "flask", +] + + +setup( + name="nr-wg-mtu-finder", + description="Scripts to find the optimal MTU for Wireguard server and peers.", + long_description=long_description, + version=get_version(), + include_package_data=True, + install_requires=install_requires, + setup_requires=["pytest-runner"], + entry_points=""" + [console_scripts] + nr-wg-mtu-finder=nr_wg_mtu_finder.main:run + """, + packages=find_packages(), + zip_safe=False, + author="Nitish K Reddy", + author_email="nitish.k.reddy@gmail.com", + # download_url="github.com/nitred/nr-wg-mtu-finder/archive/{}.tar.gz".format(get_version()), + classifiers=["Programming Language :: Python :: 3.7"], +)