├── .gitattributes ├── app ├── precache.json ├── robots.txt ├── sw-import.js ├── favicon.ico ├── images │ └── touch │ │ ├── icon-128x128.png │ │ ├── ms-icon-144x144.png │ │ ├── apple-touch-icon.png │ │ ├── chrome-touch-icon-192x192.png │ │ └── ms-touch-icon-144x144-precomposed.png ├── manifest.json ├── elements │ ├── duplication-cfg.html │ ├── corruption-cfg.html │ ├── loss-cfg.html │ ├── dev-cfg.html │ ├── distrib-cfg.html │ ├── routing.html │ ├── device-cfg.html │ ├── float-slider.html │ ├── percent-correlation │ │ └── percent-correlation.html │ ├── reorder-cfg.html │ ├── elements.html │ ├── delay-cfg.html │ ├── rate-cfg.html │ └── app-theme.html ├── styles │ └── main.css ├── parse.py ├── index.html └── scripts │ └── app.js ├── .bowerrc ├── screenshots ├── network-shaper-1.png ├── network-shaper-2.png ├── network-shaper-3.png ├── network-shaper-4.png └── network-shaper-5.png ├── .jscsrc ├── .gitignore ├── server ├── network-shaper.service ├── Makefile ├── cfg.go ├── util.go ├── netem.go └── shaper.go ├── Makefile ├── .editorconfig ├── .jshintrc ├── wct.conf.js ├── package.json ├── sample.json ├── LICENSE.md ├── bower.json ├── README.md └── gulpfile.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /app/precache.json: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | Disallow: 5 | -------------------------------------------------------------------------------- /app/sw-import.js: -------------------------------------------------------------------------------- 1 | importScripts('bower_components/platinum-sw/service-worker.js'); 2 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/images/touch/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/app/images/touch/icon-128x128.png -------------------------------------------------------------------------------- /screenshots/network-shaper-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/screenshots/network-shaper-1.png -------------------------------------------------------------------------------- /screenshots/network-shaper-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/screenshots/network-shaper-2.png -------------------------------------------------------------------------------- /screenshots/network-shaper-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/screenshots/network-shaper-3.png -------------------------------------------------------------------------------- /screenshots/network-shaper-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/screenshots/network-shaper-4.png -------------------------------------------------------------------------------- /screenshots/network-shaper-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/screenshots/network-shaper-5.png -------------------------------------------------------------------------------- /app/images/touch/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/app/images/touch/ms-icon-144x144.png -------------------------------------------------------------------------------- /app/images/touch/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/app/images/touch/apple-touch-icon.png -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "excludeFiles": ["node_modules/**"] 5 | } 6 | -------------------------------------------------------------------------------- /app/images/touch/chrome-touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/app/images/touch/chrome-touch-icon-192x192.png -------------------------------------------------------------------------------- /app/images/touch/ms-touch-icon-144x144-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codekoala/network-shaper/HEAD/app/images/touch/ms-touch-icon-144x144-precomposed.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | test/temp 4 | bower_components 5 | .tmp 6 | test/bower_components/ 7 | server/network-shaper 8 | server/bindata*.go 9 | *.rpm 10 | *.tar.xz* 11 | -------------------------------------------------------------------------------- /server/network-shaper.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Network Traffic Shaper 3 | 4 | [Service] 5 | ExecStart=/usr/bin/network-shaper -c /etc/network-shaper.json 6 | Restart=always 7 | RestartSec=5 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | server: 2 | $(MAKE) -C server 3 | 4 | build: 5 | gulp 6 | find dist/bower_components \( \( -type f -not -name "webcomponents.min.js" \) -o \( -type d -empty \) \) -delete 7 | 8 | assets: 9 | $(MAKE) -C server assets 10 | 11 | dist: build 12 | $(MAKE) -C server dist 13 | 14 | rpm: 15 | $(MAKE) -C server rpm 16 | 17 | arch: 18 | $(MAKE) -C server arch 19 | 20 | clean: 21 | find . -type f \( -iname "*.rpm" -o -iname "*.tar.xz*" \) -delete 22 | gulp clean 23 | 24 | .PHONY: server build 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "undef": true, 15 | "unused": true, 16 | "globals": { 17 | "wrap": true, 18 | "unwrap": true, 19 | "Polymer": true, 20 | "Platform": true, 21 | "page": true, 22 | "_": true, 23 | "$": true, 24 | "app": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Network Shaper", 3 | "short_name": "Network Shaper", 4 | "icons": [{ 5 | "src": "images/touch/icon-128x128.png", 6 | "sizes": "128x128" 7 | }, { 8 | "src": "images/touch/apple-touch-icon.png", 9 | "sizes": "152x152" 10 | }, { 11 | "src": "images/touch/ms-touch-icon-144x144-precomposed.png", 12 | "sizes": "144x144" 13 | }, { 14 | "src": "images/touch/chrome-touch-icon-192x192.png", 15 | "sizes": "192x192" 16 | }], 17 | "start_url": "/#!/", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /server/Makefile: -------------------------------------------------------------------------------- 1 | OUT := network-shaper 2 | ASSETS := ../dist 3 | VERSION := 0.0.5 4 | 5 | all: 6 | go build -ldflags '-X main.VERSION=$(VERSION)' -o $(OUT) 7 | 8 | assets: 9 | go-bindata-assetfs $(ASSETS)/... 10 | 11 | upx: 12 | goupx $(OUT) 13 | 14 | dist: assets all upx 15 | 16 | rpm: dist 17 | fpm \ 18 | -n stc-network-shaper \ 19 | -v $(VERSION) \ 20 | -s dir \ 21 | -t rpm \ 22 | ./network-shaper=/usr/sbin/network-shaper \ 23 | ./network-shaper.service=/usr/lib/systemd/system/network-shaper.service 24 | 25 | arch: 26 | sed -i 's/^\(pkgver=\).*/\1$(VERSION)/' PKGBUILD 27 | makepkg --clean 28 | -------------------------------------------------------------------------------- /wct.conf.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 3 | This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 4 | The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 5 | The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 6 | Code distributed by Google as part of the polymer project is also 7 | subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 8 | */ 9 | 10 | module.exports = { 11 | root: 'app', 12 | suites: ['test'] 13 | }; 14 | -------------------------------------------------------------------------------- /app/elements/duplication-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /app/elements/corruption-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /app/elements/loss-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 27 | -------------------------------------------------------------------------------- /app/elements/dev-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 30 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /app/styles/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #fafafa; 3 | font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; 4 | color: #333; 5 | } 6 | 7 | summary { 8 | display: block; 9 | padding-top: 10px; 10 | font-weight: lighter; 11 | } 12 | 13 | subtitle { 14 | display: block; 15 | font-weight: lighter; 16 | font-size: 0.86em; 17 | } 18 | 19 | paper-menu paper-button { 20 | width: calc(100% - 20px); 21 | margin: inherit auto; 22 | } 23 | 24 | iron-pages section > div { 25 | max-width: 800px; 26 | margin: 0px auto 65px; 27 | } 28 | 29 | iron-pages section dev-cfg > paper-material { 30 | max-width: 800px; 31 | background-color: #fff; 32 | border-radius: 5px; 33 | } 34 | 35 | hr { 36 | border: none; 37 | height: 1px; 38 | background-color: #bbb; 39 | } 40 | 41 | .fabs paper-fab { color: #444; } 42 | 43 | paper-fab.disable { 44 | background-color: pink; 45 | } 46 | 47 | paper-fab.reload { 48 | background-color: lightblue; 49 | } 50 | 51 | paper-fab.apply { 52 | background-color: lightgreen; 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polymer-starter-kit", 3 | "version": "0.0.0", 4 | "dependencies": {}, 5 | "devDependencies": { 6 | "apache-server-configs": "^2.7.1", 7 | "browser-sync": "^2.6.4", 8 | "del": "^1.1.1", 9 | "glob": "^5.0.6", 10 | "gulp": "^3.8.5", 11 | "gulp-autoprefixer": "^2.1.0", 12 | "gulp-cache": "^0.2.8", 13 | "gulp-changed": "^1.0.0", 14 | "gulp-cssmin": "^0.1.7", 15 | "gulp-flatten": "0.0.4", 16 | "gulp-if": "^1.2.1", 17 | "gulp-imagemin": "^2.2.1", 18 | "gulp-jshint": "^1.6.3", 19 | "gulp-load-plugins": "^0.10.0", 20 | "gulp-minify-html": "^1.0.2", 21 | "gulp-rename": "^1.2.0", 22 | "gulp-replace": "^0.5.3", 23 | "gulp-size": "^1.0.0", 24 | "gulp-uglify": "^1.2.0", 25 | "gulp-uncss": "^1.0.1", 26 | "gulp-useref": "^1.1.2", 27 | "gulp-vulcanize": "^6.0.0", 28 | "jshint-stylish": "^2.0.0", 29 | "merge-stream": "^0.1.7", 30 | "opn": "^1.0.0", 31 | "require-dir": "^0.3.0", 32 | "run-sequence": "^1.0.2", 33 | "vulcanize": ">= 1.4.2", 34 | "web-component-tester": "^3.1.3" 35 | }, 36 | "engines": { 37 | "node": ">=0.10.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/elements/distrib-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | 32 | -------------------------------------------------------------------------------- /sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": "0.0.0.0", 3 | "port": 8080, 4 | "allow_no_ip": true, 5 | "inbound": { 6 | "device": "br0", 7 | "label": "br0", 8 | "netem": { 9 | "delay": 0, 10 | "delay_unit": "ms", 11 | "delay_jitter": 0, 12 | "delay_jitter_unit": "ms", 13 | "delay_corr": 0, 14 | "loss_pct": 0, 15 | "loss_corr": 0, 16 | "dupe_pct": 0, 17 | "dupe_corr": 0, 18 | "corrupt_pct": 0, 19 | "corrupt_corr": 0, 20 | "reorder_pct": 0, 21 | "reorder_corr": 0, 22 | "reorder_gap": 0, 23 | "rate": 0, 24 | "rate_unit": "kbit", 25 | "rate_pkt_overhead": 0, 26 | "rate_cell_size": 0, 27 | "rate_cell_overhead": 0 28 | } 29 | }, 30 | "outbound": { 31 | "device": "lxcbr0", 32 | "label": "Outbound", 33 | "netem": { 34 | "delay": 0, 35 | "delay_unit": "ms", 36 | "delay_jitter": 0, 37 | "delay_jitter_unit": "ms", 38 | "delay_corr": 0, 39 | "loss_pct": 0, 40 | "loss_corr": 0, 41 | "dupe_pct": 0, 42 | "dupe_corr": 0, 43 | "corrupt_pct": 0, 44 | "corrupt_corr": 0, 45 | "reorder_pct": 0, 46 | "reorder_corr": 0, 47 | "reorder_gap": 0, 48 | "rate": 0, 49 | "rate_unit": "kbit", 50 | "rate_pkt_overhead": 0, 51 | "rate_cell_size": 0, 52 | "rate_cell_overhead": 0 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Everything in this repo is BSD style license unless otherwise specified. 4 | 5 | Copyright (c) 2015 The Polymer Authors. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following disclaimer 13 | in the documentation and/or other materials provided with the 14 | distribution. 15 | * Neither the name of Google Inc. nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /app/elements/routing.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 56 | -------------------------------------------------------------------------------- /app/elements/device-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 38 | 39 | 40 | 68 | -------------------------------------------------------------------------------- /app/elements/float-slider.html: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 46 | 47 | 48 | 81 | -------------------------------------------------------------------------------- /app/elements/percent-correlation/percent-correlation.html: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | 72 | -------------------------------------------------------------------------------- /app/elements/reorder-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 37 | 38 | 39 | 49 | -------------------------------------------------------------------------------- /app/parse.py: -------------------------------------------------------------------------------- 1 | from pprint import pprint 2 | import re 3 | import subprocess 4 | 5 | 6 | VALID_UNITS = ( 7 | 'bit', 8 | 'kbit', 9 | 'mbit', 10 | 'gbit', 11 | 'tbit', 12 | 'bps', 13 | 'kbps', 14 | 'mbps', 15 | 'gbps', 16 | 'tbps', 17 | ) 18 | UNITS = '|'.join(VALID_UNITS) 19 | 20 | 21 | def pct_corr(keyword, varname=None): 22 | if varname is None: 23 | varname = keyword 24 | 25 | return re.compile(( 26 | r'{keyword}\s+(?P<{varname}_pct>\d+(\.\d+)?)%' 27 | r'(\s+(?P<{varname}_corr>\d+(\.\d+)?)%)?' 28 | ).format(keyword=keyword, varname=varname)) 29 | 30 | # qdisc netem 8008: root refcnt 2 limit 1000 delay 200.0ms 50.0ms loss 50% 10% duplicate 40% 60% reorder 75% 50% corrupt 10% 50% rate 500Kbit gap 5 31 | # qdisc netem 800c: root refcnt 2 limit 1000 delay 200.0ms 50.0ms loss 50% 10% duplicate 40% 60% reorder 75% 50% corrupt 10% 50% rate 500Kbit packetoverhead 10 cellsize 10 gap 10 32 | 33 | LIMIT_RE = re.compile(r'limit\s+(?P\d+)') 34 | DELAY_RE = re.compile( 35 | r'delay\s+(?P\d+(\.\d+)?)(?P(?:m|u)s)' 36 | r'(\s+(?P\d+(\.\d+)?)(?P(?:m|u)s)' 37 | r'(\s+(?P\d+(\.\d+)?)%)?)?' 38 | ) 39 | LOSS_RE = pct_corr('loss') 40 | DUPE_RE = pct_corr('duplicate', 'dup') 41 | REORDER_RE = pct_corr('reorder') 42 | GAP_RE = re.compile(r'gap\s+(?P\d+)') 43 | CORRUPT_RE = pct_corr('corrupt') 44 | RATE_RE = re.compile(( 45 | r'rate\s+(?P\d+(\.\d+)?)(?P{units})' 46 | r'(\s+packetoverhead\s+(?P\d+)' 47 | r'(\s+cellsize\s+(?P\d+)' 48 | r'(\s+celloverhead\s+(?P\d+))?)?)?' 49 | ).format(units=UNITS), re.I) 50 | 51 | 52 | PARSERS = ( 53 | LIMIT_RE, 54 | DELAY_RE, 55 | LOSS_RE, 56 | DUPE_RE, 57 | REORDER_RE, GAP_RE, 58 | CORRUPT_RE, 59 | RATE_RE, 60 | ) 61 | 62 | 63 | rules = subprocess.check_output('tc qdisc show dev em1 | grep netem | head -1', shell=True).decode() 64 | 65 | print('Found rules:', rules) 66 | 67 | settings = {} 68 | for parser in PARSERS: 69 | print(parser.pattern) 70 | m = parser.search(rules) 71 | if m: 72 | settings.update(m.groupdict()) 73 | 74 | 75 | pprint(settings) 76 | -------------------------------------------------------------------------------- /app/elements/elements.html: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /app/elements/delay-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 65 | 66 | 67 | 76 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polymer-starter-kit", 3 | "version": "0.0.0", 4 | "license": "http://polymer.github.io/LICENSE.txt", 5 | "dependencies": { 6 | "iron-elements": "PolymerElements/iron-elements#1.0.0", 7 | "paper-elements": "PolymerElements/paper-elements#1.0.1", 8 | "platinum-elements": "PolymerElements/platinum-elements#1.0.0", 9 | "neon-elements": "PolymerElements/neon-elements#1.0.0", 10 | "page": "visionmedia/page.js#~1.6.3", 11 | "jquery": "2.1.4" 12 | }, 13 | "devDependencies": { 14 | "web-component-tester": "*", 15 | "test-fixture": "PolymerElements/test-fixture#^1.0.0" 16 | }, 17 | "resolutions": { 18 | "polymer": "^1.0.0", 19 | "webcomponentsjs": "^0.7.2", 20 | "iron-ajax": "^1.0.0", 21 | "promise-polyfill": "^1.0.0", 22 | "iron-autogrow-textarea": "^1.0.0", 23 | "iron-validatable-behavior": "^1.0.0", 24 | "iron-behaviors": "^1.0.0", 25 | "iron-a11y-keys-behavior": "^1.0.0", 26 | "iron-collapse": "^1.0.0", 27 | "iron-component-page": "^1.0.0", 28 | "paper-toolbar": "^1.0.0", 29 | "paper-header-panel": "^1.0.0", 30 | "paper-styles": "^1.0.0", 31 | "iron-doc-viewer": "^1.0.1", 32 | "marked-element": "^1.0.0", 33 | "prism-element": "^1.0.2", 34 | "iron-fit-behavior": "^1.0.0", 35 | "iron-flex-layout": "^1.0.0", 36 | "iron-icon": "^1.0.0", 37 | "iron-icons": "^1.0.0", 38 | "iron-iconset": "^1.0.0", 39 | "iron-iconset-svg": "^1.0.0", 40 | "iron-image": "^1.0.0", 41 | "iron-input": "^1.0.0", 42 | "iron-jsonp-library": "^1.0.0", 43 | "iron-localstorage": "^1.0.0", 44 | "iron-media-query": "^1.0.0", 45 | "iron-meta": "^1.0.0", 46 | "iron-overlay-behavior": "^1.0.0", 47 | "iron-pages": "^1.0.0", 48 | "iron-resizable-behavior": "^1.0.0", 49 | "iron-selector": "^1.0.0", 50 | "iron-signals": "^1.0.0", 51 | "iron-test-helpers": "^1.0.0", 52 | "paper-behaviors": "^1.0.0", 53 | "paper-button": "^1.0.0", 54 | "paper-checkbox": "^1.0.0", 55 | "paper-dialog-scrollable": "^1.0.0", 56 | "paper-drawer-panel": "^1.0.0", 57 | "paper-fab": "^1.0.0", 58 | "paper-icon-button": "^1.0.0", 59 | "paper-input": "^1.0.0", 60 | "iron-form-element-behavior": "^1.0.0", 61 | "paper-item": "^1.0.0", 62 | "paper-material": "^1.0.0", 63 | "paper-menu": "^1.0.0", 64 | "iron-menu-behavior": "^1.0.0", 65 | "paper-radio-button": "^1.0.0", 66 | "paper-radio-group": "^1.0.0", 67 | "paper-ripple": "^1.0.0", 68 | "paper-spinner": "^1.0.0", 69 | "paper-tabs": "^1.0.0", 70 | "paper-toast": "^1.0.0", 71 | "iron-a11y-announcer": "^1.0.0", 72 | "paper-toggle-button": "^1.0.0", 73 | "platinum-sw": "~1.0.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server/cfg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "io/ioutil" 7 | "log" 8 | ) 9 | 10 | const DEFAULT_CFG = ` 11 | { 12 | "host": "0.0.0.0", 13 | "port": 80, 14 | "allow_no_ip": false, 15 | "inbound": { 16 | "device": "eth0", 17 | "label": "Inbound", 18 | "netem": { 19 | "delay": 0, 20 | "delay_unit": "", 21 | "delay_jitter": 0, 22 | "delay_jitter_unit": "", 23 | "delay_corr": 0, 24 | "loss_pct": 0, 25 | "loss_corr": 0, 26 | "dupe_pct": 0, 27 | "dupe_corr": 0, 28 | "corrupt_pct": 0, 29 | "corrupt_corr": 0, 30 | "reorder_pct": 0, 31 | "reorder_corr": 0, 32 | "reorder_gap": 0, 33 | "rate": 0, 34 | "rate_unit": "", 35 | "rate_pkt_overhead": 0, 36 | "rate_cell_size": 0, 37 | "rate_cell_overhead": 0 38 | } 39 | }, 40 | "outbound": { 41 | "device": "eth1", 42 | "label": "Outbound", 43 | "netem": { 44 | "delay": 50, 45 | "delay_unit": "ms", 46 | "delay_jitter": 100, 47 | "delay_jitter_unit": "ms", 48 | "delay_corr": 0, 49 | "loss_pct": 0, 50 | "loss_corr": 0, 51 | "dupe_pct": 0, 52 | "dupe_corr": 0, 53 | "corrupt_pct": 0, 54 | "corrupt_corr": 0, 55 | "reorder_pct": 0, 56 | "reorder_corr": 0, 57 | "reorder_gap": 0, 58 | "rate": 0, 59 | "rate_unit": "", 60 | "rate_pkt_overhead": 0, 61 | "rate_cell_size": 0, 62 | "rate_cell_overhead": 0 63 | } 64 | } 65 | } 66 | ` 67 | 68 | type ( 69 | ShaperConfig struct { 70 | Host string `json:"host"` 71 | Port int `json:"port"` 72 | AllowNoIp bool `json:"allow_no_ip"` 73 | Inbound NetemConfig `json:"inbound"` 74 | Outbound NetemConfig `json:"outbound"` 75 | } 76 | 77 | NetemConfig struct { 78 | Device string `json:"device"` 79 | Label string `json:"label"` 80 | Netem Netem `json:"netem"` 81 | } 82 | ) 83 | 84 | // GetConfig attempts to read configuration from a file, falling back to the 85 | // default config if no such file is present and/or readable 86 | func GetConfig(path string) *ShaperConfig { 87 | buf, err := ioutil.ReadFile(path) 88 | if err != nil { 89 | log.Printf("ERR: %s\n", err.Error()) 90 | log.Println("Using default configuration") 91 | buf = append(buf, []byte(DEFAULT_CFG)...) 92 | } 93 | 94 | var cfg ShaperConfig 95 | if err := json.Unmarshal(buf, &cfg); err != nil { 96 | log.Fatalf("Failed to load configuration: %s\n", err.Error()) 97 | } 98 | 99 | return &cfg 100 | } 101 | 102 | // SaveConfig serializes the specified configuration as json and writes it to 103 | // the specified file. 104 | func SaveConfig(cfg *ShaperConfig, path string) (success bool) { 105 | buf, err := json.MarshalIndent(*cfg, "", " ") 106 | if err != nil { 107 | log.Printf("Failed to serialize config: %s\n", err.Error()) 108 | } else if err = ioutil.WriteFile(path, buf, 0644); err != nil { 109 | log.Printf("Failed to write config to '%s': %s\n", path, err.Error()) 110 | } 111 | 112 | return success 113 | } 114 | -------------------------------------------------------------------------------- /server/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // VALID_TIME_UNITS is a mapping of acceptable packet delay units based on 12 | // tc(8) 13 | VALID_TIME_UNITS = map[string]string{ 14 | "usecs": "us", 15 | "usec": "us", 16 | "us": "us", 17 | "msecs": "ms", 18 | "msec": "ms", 19 | "ms": "ms", 20 | "secs": "s", 21 | "sec": "s", 22 | "s": "s", 23 | } 24 | 25 | // VALID_RATE_UNITS is a mapping of acceptable packet rate units based on 26 | // tc(8) 27 | VALID_RATE_UNITS = map[string]string{ 28 | "bit": "bit", 29 | "kbit": "kbit", 30 | "mbit": "mbit", 31 | "gbit": "gbit", 32 | "tbit": "tbit", 33 | "bps": "bps", 34 | "kbps": "kbps", 35 | "mbps": "mbps", 36 | "gbps": "gbps", 37 | "tbps": "tbps", 38 | } 39 | ) 40 | 41 | // GetTimeUnit is a helper function to get the correct unit of time that is 42 | // acceptable to tc, or to fallback to a default unit of time. 43 | func GetTimeUnit(unit, def string) string { 44 | u, ok := VALID_TIME_UNITS[unit] 45 | if !ok { 46 | u = def 47 | } 48 | 49 | return u 50 | } 51 | 52 | // GetRateUnit is a helper function to get the correct unit of speed that is 53 | // acceptable to tc, or to fallback to a default unit of speed. 54 | func GetRateUnit(unit, def string) string { 55 | u, ok := VALID_RATE_UNITS[unit] 56 | if !ok { 57 | u = def 58 | } 59 | 60 | return u 61 | } 62 | 63 | // ParseCurrentNetem runs tc to get the current netem configuration for the 64 | // specified device and then attempts to parse it into a Netem object 65 | func ParseCurrentNetem(device string) *Netem { 66 | out, err := exec.Command("tc", "qdisc", "show", "dev", device).Output() 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | rules := strings.ToLower(string(out)) 72 | 73 | return ParseNetem(rules) 74 | } 75 | 76 | // ParseNetem will attempt to parse the specified netem configuration 77 | func ParseNetem(rule string) *Netem { 78 | netem := Netem{} 79 | netem.Parse(rule) 80 | 81 | return &netem 82 | } 83 | 84 | // RemoveNetemConfig runs a tc command to remove any netem settings applied to 85 | // the specified device 86 | func RemoveNetemConfig(device string) error { 87 | cmd := exec.Command("tc", "qdisc", "del", "dev", device, "root") 88 | out, err := cmd.CombinedOutput() 89 | if err != nil && err.Error() != "exit status 2" { 90 | log.Println("Failed to remove netem settings: " + err.Error()) 91 | log.Println(string(out)) 92 | return err 93 | } 94 | 95 | log.Println("Successfully removed netem settings for", device) 96 | SaveConfig(config, *cfgPath) 97 | 98 | return nil 99 | } 100 | 101 | func str2f(val string) float64 { 102 | fval, _ := strconv.ParseFloat(val, 32) 103 | return fval 104 | } 105 | 106 | func str2i(val string) int64 { 107 | ival, _ := strconv.ParseInt(val, 10, 0) 108 | return ival 109 | } 110 | 111 | func f2str(val float64) string { 112 | return strconv.FormatFloat(val, 'f', 2, 32) 113 | } 114 | 115 | func UnitToMs(value float64, unit string) (float64, string) { 116 | if unit == "us" { 117 | value /= 1000 118 | } else if unit == "s" { 119 | value *= 1000 120 | } 121 | 122 | unit = "ms" 123 | return value, unit 124 | } 125 | -------------------------------------------------------------------------------- /app/elements/rate-cfg.html: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 77 | 78 | 79 | 134 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Network Shaper 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 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | -------------------------------------------------------------------------------- /app/elements/app-theme.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 229 | -------------------------------------------------------------------------------- /app/scripts/app.js: -------------------------------------------------------------------------------- 1 | (function (document) { 2 | 'use strict'; 3 | 4 | // Grab a reference to our auto-binding template 5 | // and give it some initial binding values 6 | // Learn more about auto-binding templates at http://goo.gl/Dx1u2g 7 | var app = document.querySelector('#app'); 8 | 9 | app.displayInstalledToast = function() { 10 | document.querySelector('#caching-complete').show(); 11 | }; 12 | 13 | // Listen for template bound event to know when bindings 14 | // have resolved and content has been stamped to the page 15 | app.addEventListener('dom-change', function() { 16 | app.onRefresh(); 17 | }); 18 | 19 | // See https://github.com/Polymer/polymer/issues/1381 20 | window.addEventListener('WebComponentsReady', function() { 21 | document.querySelector('body').removeAttribute('unresolved'); 22 | }); 23 | 24 | // Close drawer after menu item is selected if drawerPanel is narrow 25 | app.onMenuSelect = function() { 26 | var drawerPanel = document.querySelector('#paperDrawerPanel'); 27 | if (drawerPanel.narrow) { 28 | drawerPanel.closeDrawer(); 29 | } 30 | }; 31 | 32 | // Display a message for the user 33 | app.showToast = function(text) { 34 | var t = $('#toaster')[0]; 35 | t.set('text', text); 36 | t.show(); 37 | }; 38 | 39 | // Reset all netem settings 40 | app.onReset = function() { 41 | $.post('/remove', function() { 42 | app.showToast('Settings reset successfully'); 43 | }); 44 | }; 45 | 46 | // Reload all netem settings and populate the form 47 | app.onRefresh = function() { 48 | $.get('/refresh', app.restoreSettings); 49 | }; 50 | 51 | // Populate the form with configuration values 52 | app.restoreSettings = function(allData) { 53 | var sliders = { 54 | delay: ['jitter', 'corr'], 55 | reorder: ['pct', 'corr', 'gap'], 56 | rate: ['pkt_overhead', 'cell_size', 'cell_overhead'], 57 | corrupt: ['pct', 'corr'], 58 | dupe: ['pct', 'corr'], 59 | loss: ['pct', 'corr'] 60 | }; 61 | 62 | _.each(['inbound', 'outbound'], function(dir) { 63 | var dirEl = $('section[data-route=' + dir + ']'), 64 | dev = $('paper-menu#' + dir + '-device')[0], 65 | data = allData[dir].netem; 66 | 67 | _.find(dev.items, function(item, i) { 68 | if (!_.isEqual(item.name, allData[dir].device)) { 69 | return false; 70 | } 71 | 72 | dev.select(i); 73 | return true; 74 | }); 75 | 76 | _.each(sliders, function(sub, section) { 77 | _.each(sub, function(name) { 78 | var sName = section + '_' + name, 79 | slider = dirEl.find('float-slider[name=' + sName + ']'); 80 | 81 | if (!_.isEmpty(slider)) { 82 | slider[0].set('value', data[sName]); 83 | } 84 | }); 85 | }); 86 | 87 | dirEl.find('float-slider[name=delay_time]')[0].set('value', data.delay); 88 | dirEl.find('float-slider[name=rate_speed]')[0].set('value', data.rate); 89 | 90 | dirEl.find('paper-checkbox[name=chk-delay]')[0].set('checked', data.delay > 0); 91 | dirEl.find('paper-checkbox[name=chk-reorder]')[0].set('checked', data.reorder_pct > 0); 92 | dirEl.find('paper-checkbox[name=chk-rate]')[0].set('checked', data.rate > 0); 93 | dirEl.find('paper-checkbox[name=chk-corrupt]')[0].set('checked', data.corrupt_pct > 0); 94 | dirEl.find('paper-checkbox[name=chk-dupe]')[0].set('checked', data.dupe_pct > 0); 95 | dirEl.find('paper-checkbox[name=chk-loss]')[0].set('checked', data.loss_pct > 0); 96 | }); 97 | 98 | app.showToast('Settings restored successfully'); 99 | }; 100 | 101 | // Apply the selected setting to the host 102 | app.onApply = function() { 103 | var sliders = { 104 | delay: ['time', 'jitter', 'corr'], 105 | reorder: ['pct', 'corr', 'gap'], 106 | rate: ['speed', 'pkt_overhead', 'cell_size', 'cell_overhead'], 107 | corrupt: ['pct', 'corr'], 108 | dupe: ['pct', 'corr'], 109 | loss: ['pct', 'corr'] 110 | }; 111 | 112 | var inDev = $('paper-menu#inbound-device')[0], 113 | outDev = $('paper-menu#outbound-device')[0], 114 | allowNoIP = $('paper-checkbox#allow-no-ip')[0].checked; 115 | 116 | if (!inDev.selectedItem) { 117 | app.showToast('Please select an inbound device'); 118 | return; 119 | } else { 120 | inDev = inDev.selectedItem.name; 121 | } 122 | 123 | if (!outDev.selectedItem) { 124 | app.showToast('Please select an outbound device'); 125 | return; 126 | } else { 127 | outDev = outDev.selectedItem.name; 128 | } 129 | 130 | var payload = { 131 | inbound: { 132 | delay_unit: 'ms', 133 | delay_jitter_unit: 'ms', 134 | rate_unit: 'kbit' 135 | }, 136 | outbound: { 137 | delay_unit: 'ms', 138 | delay_jitter_unit: 'ms', 139 | rate_unit: 'kbit' 140 | } 141 | }; 142 | 143 | _.each(['inbound', 'outbound'], function(dir) { 144 | var dirEl = $('section[data-route=' + dir + ']'); 145 | 146 | _.each(sliders, function(sub, section) { 147 | if (!dirEl.find('paper-checkbox[name=chk-' + section + ']')[0].checked) { 148 | return; 149 | } 150 | 151 | _.each(sub, function(name) { 152 | var sName = section + '_' + name, 153 | slider = dirEl.find('float-slider[name=' + sName + ']'); 154 | 155 | if (!_.isEmpty(slider)) { 156 | payload[dir][sName] = slider[0].value; 157 | } 158 | }); 159 | }); 160 | 161 | var dist = $('paper-radio-group[name=distribution]'); 162 | if (!_.isEmpty(dist)) { 163 | payload[dir].distribution = dist[0].selected; 164 | } 165 | 166 | payload[dir].delay = payload[dir].delay_time; 167 | delete payload[dir].delay_time; 168 | 169 | payload[dir].rate = payload[dir].rate_speed; 170 | delete payload[dir].rate_speed; 171 | }); 172 | 173 | $.ajax({ 174 | url: '/apply', 175 | dataType: 'json', 176 | contentType: 'application/json', 177 | method: 'POST', 178 | data: JSON.stringify({ 179 | allow_no_ip: allowNoIP, 180 | inbound: { 181 | device: inDev, 182 | netem: payload.inbound 183 | }, 184 | outbound: { 185 | device: outDev, 186 | netem: payload.outbound 187 | } 188 | }), 189 | success: function() { 190 | app.showToast('Settings applied successfully'); 191 | }, 192 | error: function(msg) { 193 | app.showToast(msg.responseText); 194 | } 195 | }); 196 | 197 | return false; 198 | }; 199 | })(document); 200 | 201 | // TODO: Decide if we still want to suggest wrapping as it requires 202 | // using webcomponents.min.js. 203 | // wrap document so it plays nice with other libraries 204 | // http://www.polymer-project.org/platform/shadow-dom.html#wrappers 205 | // )(wrap(document)); 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Network Shaper 2 | ============== 3 | 4 | This project is a very simplistic user interface for 5 | [netem](http://www.linuxfoundation.org/collaborate/workgroups/networking/netem). 6 | It is designed to allow users to apply basic traffic shaping rules to simulate 7 | poor network conditions without being required to learn the ``netem`` command 8 | line interface. 9 | 10 | The UI is written using [polymer](https://www.polymer-project.org/1.0/), mostly 11 | as an excuse for me to become familiar with polymer. The backend is written 12 | in [Go](https://golang.org), and should have no external dependencies when 13 | built (aside from ``tc``, of course). 14 | 15 | I'm sure there are plenty of things that can be done to improve both the UI and 16 | backend of this project. Contributions are more than welcome, particularly in 17 | the area of cleaning out cruft from this repository (which was created using 18 | the contents of the [polymer starter kit](https://github.com/PolymerElements/polymer-starter-kit/releases). 19 | 20 | Assumptions 21 | ----------- 22 | 23 | This tool was built for a particular scenario, and some assumptions are made 24 | because of it. 25 | 26 | * It is assumed that the service will run as root in order to apply changes to 27 | the network interfaces. No special logic currently exists to ensure that the 28 | executable can do what it needs to before starting to handle requests from 29 | the UI. 30 | * It is assumed that the server will reside on a machine with two physical 31 | network interfaces, one for "external" traffic coming into the network 32 | controlled by this tool, and another interface for "internal" traffic leaving 33 | the controlled network. 34 | 35 | This tool has only been tested on a machine with two physical Gigabit network 36 | interfaces. 37 | 38 | Screenshots 39 | ----------- 40 | 41 | ![All sections](screenshots/network-shaper-1.png?raw=true "All sections") 42 | ![Delay/Reorder packets](screenshots/network-shaper-2.png?raw=true "Delay/Reorder packets") 43 | ![Rate limiting](screenshots/network-shaper-3.png?raw=true "Rate limiting") 44 | ![Packet corruption, duplication, and loss](screenshots/network-shaper-4.png?raw=true "Packet corruption, duplication, and loss") 45 | ![Devices](screenshots/network-shaper-5.png?raw=true "Devices") 46 | 47 | Building 48 | -------- 49 | 50 | The requirements for building this project are: 51 | 52 | * [nodejs](https://nodejs.org) for npm, to install other dependencies 53 | * [gulp](http://gulpjs.com/) as a build tool 54 | * [bower](http://bower.io/), a package manager 55 | * Go 1.x (currently built with 1.4.2) 56 | * [go-bindata](https://github.com/jteeuwen/go-bindata) to bundle static assets 57 | into the Go binary 58 | * [go-bindata-assetfs](https://github.com/elazarl/go-bindata-assetfs) to easily 59 | serve the web UI 60 | * [upx](http://upx.sourceforge.net/) to compress binaries 61 | * [goupx](https://github.com/pwaller/goupx) to fix a bug in upx to handle 62 | 64-bit Go binaries 63 | 64 | The steps to build this tool are: 65 | 66 | ```sh 67 | $ npm install gulp # install gulp 68 | $ npm install bower # install bower 69 | $ npm install # install polymer starter kit dependencies 70 | $ bower install # install UI dependencies 71 | $ go get github.com/jteeuwen/go-bindata/... # for bundling the UI 72 | $ go get github.com/elazarl/go-bindata-assetfs/... # for serving the UI 73 | $ make dist # compile UI and executable 74 | ``` 75 | 76 | At this point, the ``network-shaper`` binary should appear in the ``server/`` 77 | directory. This is the final executable. 78 | 79 | You can proceed to build an RPM using the following command: 80 | 81 | ```sh 82 | $ make rpm 83 | ``` 84 | 85 | Or you may build an ArchLinux package: 86 | 87 | ```sh 88 | $ make arch 89 | ``` 90 | 91 | Using This Tool 92 | --------------- 93 | 94 | Once you have the ``network-shaper`` binary, you may invoke it using the 95 | command line as such: 96 | 97 | ```sh 98 | # network-shaper -c config.json 99 | ``` 100 | 101 | A sample configuration file can be found in the repository as ``sample.json`` 102 | and looks like this: 103 | 104 | ```json 105 | { 106 | "host": "0.0.0.0", 107 | "port": 80, 108 | "inbound": { 109 | "device": "enp2s0", 110 | "netem": { 111 | "delay": 0, 112 | "delay_unit": "ms", 113 | "delay_jitter": 0, 114 | "delay_jitter_unit": "ms", 115 | "delay_corr": 0, 116 | "loss_pct": 0, 117 | "loss_corr": 0, 118 | "dupe_pct": 0, 119 | "dupe_corr": 0, 120 | "corrupt_pct": 0, 121 | "corrupt_corr": 0, 122 | "reorder_pct": 0, 123 | "reorder_corr": 0, 124 | "reorder_gap": 0, 125 | "rate": 0, 126 | "rate_unit": "kbit", 127 | "rate_pkt_overhead": 0, 128 | "rate_cell_size": 0, 129 | "rate_cell_overhead": 0 130 | } 131 | }, 132 | "outbound": { 133 | "device": "enp4s0", 134 | "netem": { 135 | "delay": 0, 136 | "delay_unit": "ms", 137 | "delay_jitter": 0, 138 | "delay_jitter_unit": "ms", 139 | "delay_corr": 0, 140 | "loss_pct": 0, 141 | "loss_corr": 0, 142 | "dupe_pct": 0, 143 | "dupe_corr": 0, 144 | "corrupt_pct": 0, 145 | "corrupt_corr": 0, 146 | "reorder_pct": 0, 147 | "reorder_corr": 0, 148 | "reorder_gap": 0, 149 | "rate": 0, 150 | "rate_unit": "kbit", 151 | "rate_pkt_overhead": 0, 152 | "rate_cell_size": 0, 153 | "rate_cell_overhead": 0 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | The most basic configuration file would look like this: 160 | 161 | ```json 162 | { 163 | "host": "0.0.0.0", 164 | "port": 80, 165 | "inbound": { 166 | "device": "enp2s0" 167 | }, 168 | "outbound": { 169 | "device": "enp4s0" 170 | } 171 | } 172 | ``` 173 | 174 | The rest of the configuration file is generated by the tool once you apply 175 | settings. 176 | 177 | The purpose of these configuration values are: 178 | 179 | * ``host``: the IP to bind the web UI to. ``0.0.0.0`` means that the server 180 | will accept requests on any interface on the host machine. ``127.0.0.1`` 181 | means the server will only accept requests made from the host itself. 182 | * ``port``: the TCP port on which the server will accept requests. Note that 183 | if you have any other web server software installed and running, such as 184 | Apache or Nginx, port 80 will likely conflict with their default 185 | configuration. 186 | * ``inbound.device``: the name of the network interface connected to the 187 | "internal" network, or the network that *is* influenced by the rules set 188 | by this tool. 189 | * ``outbound.device``: the name of the network interface connected to the 190 | "external" network, or the network that *is not* influenced by the rules set 191 | by this tool. 192 | 193 | Contributing 194 | ------------ 195 | 196 | We welcome your bug reports, PRs for improvements, docs and anything you think 197 | would improve the experience for other developers. 198 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Include Gulp & Tools We'll Use 4 | var gulp = require('gulp'); 5 | var $ = require('gulp-load-plugins')(); 6 | var del = require('del'); 7 | var runSequence = require('run-sequence'); 8 | var browserSync = require('browser-sync'); 9 | var reload = browserSync.reload; 10 | var merge = require('merge-stream'); 11 | var path = require('path'); 12 | var fs = require('fs'); 13 | var glob = require('glob'); 14 | var plumber = require('gulp-plumber'); 15 | var uglify = require('gulp-uglify'); 16 | var pump = require('pump'); 17 | 18 | var AUTOPREFIXER_BROWSERS = [ 19 | 'ie >= 10', 20 | 'ie_mob >= 10', 21 | 'ff >= 30', 22 | 'chrome >= 34', 23 | 'safari >= 7', 24 | 'opera >= 23', 25 | 'ios >= 7', 26 | 'android >= 4.4', 27 | 'bb >= 10' 28 | ]; 29 | 30 | var styleTask = function (stylesPath, srcs) { 31 | return gulp.src(srcs.map(function(src) { 32 | return path.join('app', stylesPath, src); 33 | })) 34 | .pipe($.changed(stylesPath, {extension: '.css'})) 35 | .pipe($.autoprefixer(AUTOPREFIXER_BROWSERS)) 36 | .pipe(gulp.dest('.tmp/' + stylesPath)) 37 | .pipe($.if('*.css', $.cssmin())) 38 | .pipe(gulp.dest('dist/' + stylesPath)) 39 | .pipe($.size({title: stylesPath})); 40 | }; 41 | 42 | // Compile and Automatically Prefix Stylesheets 43 | gulp.task('styles', function () { 44 | return styleTask('styles', ['**/*.css']); 45 | }); 46 | 47 | gulp.task('elements', function () { 48 | return styleTask('elements', ['**/*.css']); 49 | }); 50 | 51 | // Lint JavaScript 52 | gulp.task('jshint', function () { 53 | return gulp.src([ 54 | 'app/scripts/**/*.js', 55 | 'app/elements/**/*.js', 56 | 'app/elements/**/*.html' 57 | ]) 58 | .pipe(reload({stream: true, once: true})) 59 | .pipe($.jshint.extract()) // Extract JS from .html files 60 | .pipe($.jshint()) 61 | .pipe($.jshint.reporter('jshint-stylish')) 62 | .pipe($.if(!browserSync.active, $.jshint.reporter('fail'))); 63 | }); 64 | 65 | // Optimize Images 66 | gulp.task('images', function () { 67 | return gulp.src('app/images/**/*') 68 | .pipe($.cache($.imagemin({ 69 | progressive: true, 70 | interlaced: true 71 | }))) 72 | .pipe(gulp.dest('dist/images')) 73 | .pipe($.size({title: 'images'})); 74 | }); 75 | 76 | // Copy All Files At The Root Level (app) 77 | gulp.task('copy', function () { 78 | var app = gulp.src([ 79 | 'app/*', 80 | '!app/test', 81 | '!app/precache.json', 82 | 'node_modules/apache-server-configs/dist/.htaccess' 83 | ], { 84 | dot: true 85 | }).pipe(gulp.dest('dist')); 86 | 87 | var bower = gulp.src([ 88 | 'bower_components/**/*' 89 | ]).pipe(gulp.dest('dist/bower_components')); 90 | 91 | var elements = gulp.src(['app/elements/**/*.html']) 92 | .pipe(gulp.dest('dist/elements')); 93 | 94 | var swBootstrap = gulp.src(['bower_components/platinum-sw/bootstrap/*.js']) 95 | .pipe(gulp.dest('dist/elements/bootstrap')); 96 | 97 | var swToolbox = gulp.src(['bower_components/sw-toolbox/*.js']) 98 | .pipe(gulp.dest('dist/sw-toolbox')); 99 | 100 | var vulcanized = gulp.src(['app/elements/elements.html']) 101 | .pipe($.rename('elements.vulcanized.html')) 102 | .pipe(gulp.dest('dist/elements')); 103 | 104 | return merge(app, bower, elements, vulcanized, swBootstrap, swToolbox) 105 | .pipe($.size({title: 'copy'})); 106 | }); 107 | 108 | // Copy Web Fonts To Dist 109 | gulp.task('fonts', function () { 110 | return gulp.src(['app/fonts/**']) 111 | .pipe(gulp.dest('dist/fonts')) 112 | .pipe($.size({title: 'fonts'})); 113 | }); 114 | 115 | gulp.task('uglify', function (cb) { 116 | pump([ 117 | gulp.src('*.js'), 118 | uglify(), 119 | gulp.dest('dist') 120 | ], cb); 121 | }); 122 | 123 | // Scan Your HTML For Assets & Optimize Them 124 | gulp.task('html', function () { 125 | var assets = $.useref.assets({searchPath: ['.tmp', 'app', 'dist']}); 126 | 127 | return gulp.src(['app/**/*.html', '!app/{elements,test}/**/*.html']) 128 | .pipe(plumber()) 129 | // Replace path for vulcanized assets 130 | .pipe($.if('*.html', $.replace('elements/elements.html', 'elements/elements.vulcanized.html'))) 131 | .pipe(assets) 132 | // Concatenate And Minify JavaScript 133 | //.pipe($.if('*.js', $.uglify())) 134 | // Concatenate And Minify Styles 135 | // In case you are still using useref build blocks 136 | .pipe($.if('*.css', $.cssmin())) 137 | .pipe(assets.restore()) 138 | .pipe($.useref()) 139 | // Minify Any HTML 140 | .pipe($.if('*.html', $.minifyHtml({ 141 | quotes: true, 142 | empty: true, 143 | spare: true 144 | }))) 145 | // Output Files 146 | .pipe(gulp.dest('dist')) 147 | .pipe($.size({title: 'html'})); 148 | }); 149 | 150 | // Vulcanize imports 151 | gulp.task('vulcanize', function () { 152 | var DEST_DIR = 'dist/elements'; 153 | 154 | return gulp.src('dist/elements/elements.vulcanized.html') 155 | .pipe($.vulcanize({ 156 | dest: DEST_DIR, 157 | strip: true, 158 | inlineCss: true, 159 | inlineScripts: true 160 | })) 161 | .pipe(gulp.dest(DEST_DIR)) 162 | .pipe($.size({title: 'vulcanize'})); 163 | }); 164 | 165 | // Generate a list of files that should be precached when serving from 'dist'. 166 | // The list will be consumed by the element. 167 | gulp.task('precache', function (callback) { 168 | var dir = 'dist'; 169 | 170 | glob('{elements,scripts,styles}/**/*.*', {cwd: dir}, function(error, files) { 171 | if (error) { 172 | callback(error); 173 | } else { 174 | files.push( 175 | 'index.html', 176 | './', 177 | 'bower_components/webcomponentsjs/webcomponents.min.js', 178 | 'bower_components/lodash/lodash.min.js' 179 | ); 180 | var filePath = path.join(dir, 'precache.json'); 181 | fs.writeFile(filePath, JSON.stringify(files), callback); 182 | } 183 | }); 184 | }); 185 | 186 | // Clean Output Directory 187 | gulp.task('clean', del.bind(null, ['.tmp', 'dist'])); 188 | 189 | // Watch Files For Changes & Reload 190 | gulp.task('serve', ['styles', 'elements', 'images'], function () { 191 | browserSync({ 192 | notify: false, 193 | // Run as an https by uncommenting 'https: true' 194 | // Note: this uses an unsigned certificate which on first access 195 | // will present a certificate warning in the browser. 196 | // https: true, 197 | server: { 198 | baseDir: ['.tmp', 'app'], 199 | routes: { 200 | '/bower_components': 'bower_components' 201 | } 202 | } 203 | }); 204 | 205 | gulp.watch(['app/**/*.html'], reload); 206 | gulp.watch(['app/styles/**/*.css'], ['styles', reload]); 207 | gulp.watch(['app/elements/**/*.css'], ['elements', reload]); 208 | gulp.watch(['app/{scripts,elements}/**/*.js'], ['jshint']); 209 | gulp.watch(['app/images/**/*'], reload); 210 | }); 211 | 212 | // Build and serve the output from the dist build 213 | gulp.task('serve:dist', ['default'], function () { 214 | browserSync({ 215 | notify: false, 216 | // Run as an https by uncommenting 'https: true' 217 | // Note: this uses an unsigned certificate which on first access 218 | // will present a certificate warning in the browser. 219 | // https: true, 220 | server: 'dist' 221 | }); 222 | }); 223 | 224 | // Build Production Files, the Default Task 225 | gulp.task('default', ['clean'], function (cb) { 226 | runSequence( 227 | ['copy', 'styles'], 228 | 'elements', 229 | ['jshint', 'images', 'fonts', 'uglify', 'html'], 230 | 'vulcanize', 'precache', 231 | cb); 232 | }); 233 | 234 | // Load tasks for web-component-tester 235 | // Adds tasks for `gulp test:local` and `gulp test:remote` 236 | try { require('web-component-tester').gulp.init(gulp); } catch (err) {} 237 | 238 | // Load custom tasks from the `tasks` directory 239 | try { require('require-dir')('tasks'); } catch (err) {} 240 | -------------------------------------------------------------------------------- /server/netem.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os/exec" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | var ( 12 | // DELAY_RE is used to parse packet delay configuration from tc 13 | DELAY_RE = regexp.MustCompile(`delay\s+(?P\d+(?:\.\d+)?)(?P(?:m|u)?s)(?:\s+(?P\d+(?:\.\d+)?)(?P(?:m|u)?s)(?:\s+(?P\d+(?:\.\d+)?)%)?)?`) 14 | 15 | // LOSS_RE is used to parse packet loss configuration from tc 16 | LOSS_RE = regexp.MustCompile(`loss\s+(?P\d+(?:\.\d+)?)%(?:\s+(?P\d+(?:\.\d+)?)%)?`) 17 | 18 | // DUPE_RE is used to parse packet duplication configuration from tc 19 | DUPE_RE = regexp.MustCompile(`duplicate\s+(?P\d+(?:\.\d+)?)%(?:\s+(?P\d+(?:\.\d+)?)%)?`) 20 | 21 | // REORDER_RE is used to parse packet reordering configuration from tc 22 | REORDER_RE = regexp.MustCompile(`reorder\s+(?P\d+(?:\.\d+)?)%(?:\s+(?P\d+(?:\.\d+)?)%)?`) 23 | 24 | // GAP_RE is used to parse packet reordering gap configuration from tc 25 | GAP_RE = regexp.MustCompile(`gap\s+(?P\d+)`) 26 | 27 | // CORRUPT_RE is used to parse packet corruption configuration from tc 28 | CORRUPT_RE = regexp.MustCompile(`corrupt\s+(?P\d+(?:\.\d+)?)%(?:\s+(?P\d+(?:\.\d+)?)%)?`) 29 | 30 | // RATE_RE is used to parse rate limiting configuration from tc 31 | RATE_RE = regexp.MustCompile(`rate\s+(?P\d+(?:\.\d+)?)(?Pbit|kbit|mbit|gbit|tbit|bps|kbps|mbps|gbps|tbps)(?:\s+packetoverhead\s+(?P\d+)(?:\s+cellsize\s+(?P\d+)(?:\s+celloverhead\s+(?P\d+))?)?)?`) 32 | ) 33 | 34 | // Netem represents the netem configuration of a specific network interface 35 | type Netem struct { 36 | // packet delay configuration 37 | Delay float64 `json:"delay"` 38 | DelayUnit string `json:"delay_unit"` 39 | DelayJitter float64 `json:"delay_jitter"` 40 | DelayJitterUnit string `json:"delay_jitter_unit"` 41 | DelayCorr float64 `json:"delay_corr"` 42 | 43 | // packet loss configuration 44 | LossPct float64 `json:"loss_pct"` 45 | LossCorr float64 `json:"loss_corr"` 46 | 47 | // packet duplication configuration 48 | DupePct float64 `json:"dupe_pct"` 49 | DupeCorr float64 `json:"dupe_corr"` 50 | 51 | // packet corruption configuration 52 | CorruptPct float64 `json:"corrupt_pct"` 53 | CorruptCorr float64 `json:"corrupt_corr"` 54 | 55 | // packet reordering configuration 56 | ReorderPct float64 `json:"reorder_pct"` 57 | ReorderCorr float64 `json:"reorder_corr"` 58 | ReorderGap int64 `json:"reorder_gap"` 59 | 60 | // rate limiting configuration 61 | Rate float64 `json:"rate"` 62 | RateUnit string `json:"rate_unit"` 63 | RatePktOverhead int64 `json:"rate_pkt_overhead"` 64 | RateCellSize int64 `json:"rate_cell_size"` 65 | RateCellOverhead int64 `json:"rate_cell_overhead"` 66 | } 67 | 68 | func (n *Netem) Parse(rule string) { 69 | n.ParseDelay(rule) 70 | n.ParseLoss(rule) 71 | n.ParseDuplication(rule) 72 | n.ParseCorruption(rule) 73 | n.ParseReorder(rule) 74 | n.ParseRate(rule) 75 | } 76 | 77 | func (n *Netem) Apply(device string) error { 78 | var ( 79 | args []string 80 | unit string 81 | ) 82 | 83 | if n.Delay > 0 { 84 | unit = GetTimeUnit(n.DelayUnit, "ms") 85 | args = append(args, "delay") 86 | args = append(args, f2str(n.Delay)+unit) 87 | 88 | if n.DelayJitter > 0 { 89 | unit = GetTimeUnit(n.DelayJitterUnit, "ms") 90 | args = append(args, f2str(n.DelayJitter)+unit) 91 | 92 | if n.DelayCorr > 0 { 93 | args = append(args, f2str(n.DelayCorr)+"%") 94 | } 95 | } 96 | 97 | // packet reordering requires a delay to be specified 98 | if n.ReorderPct > 0 { 99 | args = append(args, "reorder") 100 | args = append(args, f2str(n.ReorderPct)+"%") 101 | if n.ReorderCorr > 0 { 102 | args = append(args, f2str(n.ReorderCorr)+"%") 103 | } 104 | 105 | if n.ReorderGap > 0 { 106 | args = append(args, "gap") 107 | args = append(args, strconv.FormatInt(n.ReorderGap, 10)) 108 | } 109 | } 110 | 111 | } 112 | 113 | if n.CorruptPct > 0 { 114 | args = append(args, "corrupt") 115 | args = append(args, f2str(n.CorruptPct)+"%") 116 | if n.CorruptCorr > 0 { 117 | args = append(args, f2str(n.CorruptCorr)+"%") 118 | } 119 | } 120 | 121 | if n.DupePct > 0 { 122 | args = append(args, "duplicate") 123 | args = append(args, f2str(n.DupePct)+"%") 124 | if n.DupeCorr > 0 { 125 | args = append(args, f2str(n.DupeCorr)+"%") 126 | } 127 | } 128 | 129 | if n.LossPct > 0 { 130 | args = append(args, "loss") 131 | args = append(args, f2str(n.LossPct)+"%") 132 | if n.LossCorr > 0 { 133 | args = append(args, f2str(n.LossCorr)+"%") 134 | } 135 | } 136 | 137 | if n.Rate > 0 { 138 | args = append(args, "rate") 139 | unit = GetRateUnit(n.RateUnit, "kbit") 140 | args = append(args, f2str(n.Rate)+unit) 141 | 142 | // packet overhead can be negative or positive 143 | if n.RatePktOverhead != 0 { 144 | args = append(args, strconv.FormatInt(n.RatePktOverhead, 10)) 145 | 146 | // cell size is unsigned 147 | if n.RateCellSize > 0 { 148 | args = append(args, strconv.FormatInt(n.RateCellSize, 10)) 149 | 150 | // cell overhead can be negative or positive 151 | if n.RateCellOverhead != 0 { 152 | args = append(args, strconv.FormatInt(n.RateCellOverhead, 10)) 153 | } 154 | } 155 | } 156 | } 157 | 158 | // try to apply the settings if we have any to set 159 | if len(args) > 0 { 160 | defArgs := []string{"qdisc", "replace", "dev", device, "root", "netem"} 161 | args = append(defArgs, args...) 162 | 163 | log.Println("Applying: tc", strings.Join(args, " ")) 164 | out, err := exec.Command("tc", args...).CombinedOutput() 165 | if err != nil { 166 | log.Println("Error: ", string(out)) 167 | return err 168 | } 169 | 170 | return nil 171 | } 172 | 173 | // if we don't have any valid netem configuration, we're effectively 174 | // removing our netem policy 175 | return RemoveNetemConfig(device) 176 | } 177 | 178 | func (n *Netem) ParseDelay(rule string) { 179 | match := DELAY_RE.FindStringSubmatch(rule) 180 | if len(match) >= 3 { 181 | n.Delay, n.DelayUnit = UnitToMs(str2f(match[1]), match[2]) 182 | 183 | if len(match) >= 5 { 184 | n.DelayJitter, n.DelayJitterUnit = UnitToMs(str2f(match[3]), match[4]) 185 | 186 | if len(match) == 6 { 187 | n.DelayCorr = str2f(match[5]) 188 | } 189 | } 190 | } 191 | } 192 | 193 | func (n *Netem) ParseLoss(rule string) { 194 | match := LOSS_RE.FindStringSubmatch(rule) 195 | if len(match) >= 2 { 196 | n.LossPct = str2f(match[1]) 197 | 198 | if len(match) == 3 { 199 | n.LossCorr = str2f(match[2]) 200 | } 201 | } 202 | } 203 | 204 | func (n *Netem) ParseDuplication(rule string) { 205 | match := DUPE_RE.FindStringSubmatch(rule) 206 | if len(match) >= 2 { 207 | n.DupePct = str2f(match[1]) 208 | 209 | if len(match) == 3 { 210 | n.DupeCorr = str2f(match[2]) 211 | } 212 | } 213 | } 214 | 215 | func (n *Netem) ParseCorruption(rule string) { 216 | match := CORRUPT_RE.FindStringSubmatch(rule) 217 | if len(match) >= 2 { 218 | n.CorruptPct = str2f(match[1]) 219 | 220 | if len(match) == 3 { 221 | n.CorruptCorr = str2f(match[2]) 222 | } 223 | } 224 | } 225 | 226 | func (n *Netem) ParseReorder(rule string) { 227 | match := REORDER_RE.FindStringSubmatch(rule) 228 | if len(match) >= 2 { 229 | n.ReorderPct = str2f(match[1]) 230 | 231 | if len(match) == 3 { 232 | n.ReorderCorr = str2f(match[2]) 233 | } 234 | 235 | match = GAP_RE.FindStringSubmatch(rule) 236 | if len(match) == 2 { 237 | n.ReorderGap = str2i(match[1]) 238 | } 239 | } 240 | } 241 | 242 | func (n *Netem) ParseRate(rule string) { 243 | match := RATE_RE.FindStringSubmatch(rule) 244 | if len(match) >= 3 { 245 | n.Rate = str2f(match[1]) 246 | n.RateUnit = match[2] 247 | 248 | if len(match) >= 4 { 249 | n.RatePktOverhead = str2i(match[3]) 250 | 251 | if len(match) >= 5 { 252 | n.RateCellSize = str2i(match[4]) 253 | 254 | if len(match) == 6 { 255 | n.RateCellOverhead = str2i(match[5]) 256 | } 257 | } 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /server/shaper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "html/template" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "strings" 13 | 14 | "github.com/elazarl/go-bindata-assetfs" 15 | ) 16 | 17 | var ( 18 | VERSION = "dev" 19 | 20 | // cfgPath is the path to the file containing configuration values for this 21 | // application 22 | cfgPath = flag.String("c", "/etc/network-shaper.json", "Path to configuration file") 23 | 24 | config *ShaperConfig 25 | ) 26 | 27 | func init() { 28 | var inValid, exValid bool 29 | 30 | log.Println("Starting Network Shaper", VERSION) 31 | 32 | flag.Parse() 33 | config = GetConfig(*cfgPath) 34 | 35 | nics, _ := net.Interfaces() 36 | for _, nic := range nics { 37 | if nic.Name == config.Inbound.Device { 38 | inValid = true 39 | } 40 | if nic.Name == config.Outbound.Device { 41 | exValid = true 42 | } 43 | } 44 | 45 | if !inValid { 46 | log.Fatalln("Invalid internal network interface name:", config.Inbound.Device) 47 | } 48 | 49 | if !exValid { 50 | log.Fatalln("Invalid external network interface name:", config.Outbound.Device) 51 | } 52 | 53 | if config.Inbound.Device == config.Outbound.Device { 54 | log.Fatalln("You must specify different NICs for your internal and external networks") 55 | } 56 | 57 | if config.Inbound.Label == "" { 58 | config.Inbound.Label = "Inbound" 59 | } 60 | 61 | if config.Outbound.Label == "" { 62 | config.Outbound.Label = "Outbound" 63 | } 64 | } 65 | 66 | var ( 67 | templateFuncs = template.FuncMap{ 68 | "route": func() string { 69 | return `{{route}}` 70 | }, 71 | } 72 | 73 | staticFs = http.FileServer(&assetfs.AssetFS{ 74 | Asset: Asset, 75 | AssetDir: AssetDir, 76 | Prefix: "../dist", 77 | }) 78 | ) 79 | 80 | func main() { 81 | var ( 82 | tpl []byte 83 | index *template.Template 84 | err error 85 | ) 86 | 87 | if tpl, err = Asset("../dist/index.html"); err != nil { 88 | log.Fatalf("unable to load index template: %s", err) 89 | } 90 | 91 | if index, err = template.New("index").Funcs(templateFuncs).Parse(string(tpl)); err != nil { 92 | log.Fatalf("unable to parse index template: %s", err) 93 | } 94 | 95 | // allow netem configuration to be removed 96 | http.HandleFunc("/remove", removeConfig) 97 | 98 | // allow netem configuration to be updated 99 | http.HandleFunc("/apply", applyConfig) 100 | 101 | // expose the current netem configuration 102 | http.HandleFunc("/refresh", refreshConfig) 103 | 104 | // allow the user to select from the NICs available on this system 105 | http.HandleFunc("/nics", getValidNics) 106 | 107 | // serve static files for the web UI 108 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 109 | if r.URL.Path == "/" { 110 | index.Execute(w, config) 111 | } else { 112 | staticFs.ServeHTTP(w, r) 113 | } 114 | }) 115 | 116 | // begin accepting web requests 117 | bind := fmt.Sprintf("%s:%d", config.Host, config.Port) 118 | log.Printf("Listening at http://%s\n", bind) 119 | log.Println("Internal NIC:", config.Inbound.Device) 120 | log.Println("External NIC:", config.Outbound.Device) 121 | 122 | log.Println("Restoring inbound shaping...") 123 | config.Inbound.Netem.Apply(config.Inbound.Device) 124 | 125 | log.Println("Restoring outbound shaping...") 126 | config.Outbound.Netem.Apply(config.Outbound.Device) 127 | 128 | if err := http.ListenAndServe(bind, nil); err != nil { 129 | log.Fatalln(err) 130 | } 131 | } 132 | 133 | // refreshConfig queries the current netem configuration and returns it as JSON 134 | // to the client. Configuration may be requested using any HTTP method. 135 | func refreshConfig(w http.ResponseWriter, req *http.Request) { 136 | config.Inbound.Netem = *ParseCurrentNetem(config.Inbound.Device) 137 | config.Outbound.Netem = *ParseCurrentNetem(config.Outbound.Device) 138 | 139 | j, err := json.Marshal(config) 140 | if err != nil { 141 | msg := "Failed to parse netem configuration: " + err.Error() 142 | log.Println(msg) 143 | 144 | w.WriteHeader(500) 145 | w.Write([]byte(msg)) 146 | return 147 | } 148 | 149 | w.Header().Set("Content-Type", "application/json") 150 | w.Write(j) 151 | } 152 | 153 | // applyConfig tries to update netem configuration. Changes must be submitted 154 | // as an HTTP POST request. 155 | func applyConfig(w http.ResponseWriter, req *http.Request) { 156 | if req.Method != "POST" { 157 | w.WriteHeader(405) 158 | return 159 | } 160 | 161 | var msg string 162 | body, err := ioutil.ReadAll(req.Body) 163 | if err != nil { 164 | msg = "Failed to read request: " + err.Error() 165 | log.Println(msg) 166 | 167 | w.WriteHeader(400) 168 | w.Write([]byte(msg)) 169 | return 170 | } 171 | 172 | // parse the new netem settings sent by the client 173 | var newConfig ShaperConfig 174 | err = json.Unmarshal(body, &newConfig) 175 | if err != nil { 176 | msg = "Failed to parse request: " + err.Error() 177 | log.Println(msg) 178 | 179 | w.WriteHeader(400) 180 | w.Write([]byte(msg)) 181 | return 182 | } 183 | 184 | // sanity check! 185 | if newConfig.Inbound.Device == newConfig.Outbound.Device { 186 | msg = "Inbound and outbound devices must not be the same" 187 | log.Println(msg) 188 | 189 | w.WriteHeader(400) 190 | w.Write([]byte(msg)) 191 | return 192 | } 193 | 194 | // apply the new settings 195 | err = newConfig.Inbound.Netem.Apply(newConfig.Inbound.Device) 196 | if err != nil { 197 | w.WriteHeader(400) 198 | msg = "Failed to apply inbound settings: " + err.Error() 199 | } else { 200 | err = newConfig.Outbound.Netem.Apply(newConfig.Outbound.Device) 201 | if err != nil { 202 | w.WriteHeader(400) 203 | msg = "Failed to apply outbound settings: " + err.Error() 204 | } else { 205 | w.WriteHeader(202) 206 | msg = "Settings applied successfully" 207 | 208 | config.AllowNoIp = newConfig.AllowNoIp 209 | 210 | config.Inbound.Device = newConfig.Inbound.Device 211 | config.Inbound.Label = newConfig.Inbound.Label 212 | config.Inbound.Netem = newConfig.Inbound.Netem 213 | 214 | config.Outbound.Device = newConfig.Outbound.Device 215 | config.Outbound.Label = newConfig.Outbound.Label 216 | config.Outbound.Netem = newConfig.Outbound.Netem 217 | 218 | SaveConfig(config, *cfgPath) 219 | } 220 | } 221 | 222 | log.Println(msg) 223 | w.Write([]byte(msg)) 224 | } 225 | 226 | // removeConfig will remove our netem settings, reverting back to the default 227 | // configuration. Once complete, the new netem settings are sent back to the 228 | // client. 229 | func removeConfig(w http.ResponseWriter, req *http.Request) { 230 | if req.Method != "POST" { 231 | w.WriteHeader(405) 232 | return 233 | } 234 | 235 | log.Println("Removing netem configuration") 236 | RemoveNetemConfig(config.Inbound.Device) 237 | RemoveNetemConfig(config.Outbound.Device) 238 | 239 | refreshConfig(w, req) 240 | } 241 | 242 | type SimpleNic struct { 243 | Name string `json:"name"` 244 | Ip string `json:"ip"` 245 | Label string `json:"label"` 246 | } 247 | 248 | type ValidNicResponse struct { 249 | AllowNoIp bool `json:"allow_no_ip"` 250 | InboundLabel string `json:"inbound_label"` 251 | OutboundLabel string `json:"outbound_label"` 252 | AllNics []SimpleNic `json:"all_devices"` 253 | } 254 | 255 | // getValidNics offers a list of NICs that are present on this system 256 | func getValidNics(w http.ResponseWriter, req *http.Request) { 257 | if req.Method != "GET" { 258 | w.WriteHeader(405) 259 | return 260 | } 261 | 262 | body := ValidNicResponse{ 263 | AllowNoIp: config.AllowNoIp, 264 | InboundLabel: config.Inbound.Label, 265 | OutboundLabel: config.Outbound.Label, 266 | } 267 | 268 | nics, _ := net.Interfaces() 269 | for _, nic := range nics { 270 | added := false 271 | addrs, _ := nic.Addrs() 272 | for _, addrO := range addrs { 273 | addr := strings.Split(addrO.String(), "/")[0] 274 | ip := net.ParseIP(addr) 275 | if ip.To4() == nil { 276 | continue 277 | } 278 | 279 | // we have a good NIC! 280 | body.AllNics = append(body.AllNics, SimpleNic{ 281 | Name: nic.Name, 282 | Ip: addr, 283 | Label: fmt.Sprintf("%s: %s", nic.Name, addr), 284 | }) 285 | added = true 286 | break 287 | } 288 | 289 | if !added && config.AllowNoIp { 290 | body.AllNics = append(body.AllNics, SimpleNic{ 291 | Name: nic.Name, 292 | Label: nic.Name, 293 | }) 294 | } 295 | } 296 | 297 | nicsJson, err := json.Marshal(body) 298 | if err != nil { 299 | msg := "Failed to serialize NICs: " + err.Error() 300 | log.Println(msg) 301 | 302 | w.WriteHeader(500) 303 | fmt.Fprintf(w, msg) 304 | } else { 305 | fmt.Fprintf(w, "%s", nicsJson) 306 | } 307 | } 308 | --------------------------------------------------------------------------------