├── .gitignore ├── LICENSE ├── README.md ├── bin └── setup ├── iptables-webui ├── iptables ├── enabled.json └── rules.json ├── lib └── iprules.js ├── package.json └── public ├── css ├── bootstrap.min.css ├── custom.css ├── port_forwarding.css ├── rules.css └── status_page.css ├── fonts ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── index.html ├── js ├── ajax.js ├── bootstrap.min.js ├── forwarding_status.js ├── iptables_list.js ├── jquery-1.10.0.min.js ├── jsx_transformer-0.12.2.js ├── page_control.js ├── react-0.12.2-min.js ├── react-0.12.2.js └── source_rules.js └── jsx ├── button.jsx ├── forwarding_status.jsx ├── iptables_list.jsx ├── port_forward_page.jsx ├── port_forward_rule.jsx ├── rule.jsx ├── rule_state_buttons.jsx ├── rules_list.jsx ├── rules_page.jsx ├── source_rules.jsx └── status_page.jsx /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/js/.module-cache 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Robert McLeod 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IP Tables WebUI 2 | 3 | A nice webui for the `iptables` command, written in NodeJS. Currently a work in progress. 4 | 5 | ## Security 6 | 7 | This WebUI is not meant to be used as a general access long running web server. Instead the following flow is assumed: 8 | 9 | 1. The User SSH's into a remote server with a port forward e.g: `ssh myserver.com -L 8099:localhost:8099` 10 | 1. The user starts the iptables web interface `iptables-webui start` 11 | 1. The user navigates to the address in their browser e.g: `http://localhost:8099` 12 | 13 | Pro-tip: Store the server/forwarding details in `~/.ssh/config`: 14 | 15 | ``` 16 | Host myserver 17 | Hostname myserver.com 18 | User me 19 | LocalForward 8099:localhost:8099 20 | ``` 21 | 22 | It would be possible to run it as a typical web server process, but it would not be recommended as good security practice to leave IPTables so wide open. Even if there was authentication and authorization built 23 | into this app. 24 | 25 | ## Planned Features 26 | 27 | * Modular handling of rules (enable and disable named groups of rules) 28 | * Raw rule editing 29 | * Flexible Port Forwarding table 30 | * Simple Pre-built rules (like enable/disable SSH or HTTP in/out) 31 | * Advanced Pre-built rules (internet connection sharing, load balancing) 32 | * Current Status of IP Tables 33 | * View Compiled rules vs Source rules 34 | * Help with enabling/disabling kernel network features (forwarding, masquerading) 35 | 36 | ## Rule Spec 37 | 38 | This is an example of what a rule looks like in JSON: 39 | 40 | * **name**: the name (user set) 41 | * **enabled**: whether the rule is enabled (user set) 42 | * **lines**: the lines that make up the rule, can be either (user set): 43 | * an object that can be parsed by the RuleParser 44 | * or a string of iptables arguments 45 | * **valid**: determined by the result of the last test 46 | * **test_lines**: the lines that made up the file used for the last test 47 | * **error**: the error that occured on the last test 48 | 49 | ```js 50 | { 51 | name: 'my_dumb-rule', 52 | enabled: false, 53 | lines: [ 54 | '# lines in the rule can be JSON or string', 55 | '-A INPUT --dport 22 -j ACCEPT', 56 | {chain: 'input', dport: 80, target: 'accept'} 57 | ] 58 | valid: true, 59 | test_lines: [ 60 | '# iptables-restore test file generated by iprules 2015-01-06 00:00', 61 | '*filter' 62 | '# my_dumb-rule' 63 | '# lines in the rule can be JSON or string', 64 | '-A INPUT --dport 22 -j ACCEPT', 65 | '-A INPUT --dport 80 -j ACCEPT', 66 | '#end' 67 | ], 68 | error: '' 69 | } 70 | ``` 71 | 72 | ### RuleParser spec 73 | 74 | The RuleParser can handle the following arguments (examples shown): 75 | 76 | ```js 77 | { 78 | chain: 'INPUT', 79 | protocol: 'tcp', // default is TCP 80 | sport: false, // this will be ignored during compile time 81 | dport: 8822, 82 | target: 'ACCEPT', 83 | src: '192.168.3.0/24', 84 | dst: '172.16.0.233', 85 | in: 'eth0', 86 | out: 'eth1', 87 | table: 'nat', // default is filter 88 | states: ['new'], 89 | to_dst: '172.16.0.233:22' 90 | } 91 | ``` 92 | 93 | ## API Spec 94 | 95 | This is the API so far, not everything is working: 96 | 97 | ```sh 98 | GET /rules # gets all the rules 99 | POST /rules # creates a rule 100 | GET /rules/:pattern # gets a rule by name or glob pattern 101 | PUT /rules/:name # updates the named rule 102 | DELETE /rules/:name # deletes the named rule 103 | GET /rules/:name/test # tests the named rule 104 | GET /iptables/list # gives iptables -L output 105 | GET /status # various statuses 106 | ``` 107 | 108 | ## You damn kids are just jamming javascript in everywhere! 109 | 110 | I did it in NodeJS and ReactJS because: 111 | 112 | * I don't want to learn a real language like C++ 113 | * I want to learn more about NodeJS an ReactJS 114 | * I want to use it on an ARM and Node is faster than ruby (dammit!) 115 | * I am a sadomasochist -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | home="$1"; 4 | 5 | # Precons 6 | [[ -z "$home" ]] && echo "Must specify setup folder" && exit 1; 7 | [[ -d "$home" ]] && [[ "$2" != "-f" ]] && exit 0; 8 | 9 | # Setup folders 10 | mkdir "$home"; 11 | mkdir "$home/tmp"; 12 | mkdir "$home/rules"; 13 | 14 | # Files 15 | cat "\[\]" > "$home/enabled.json"; 16 | cat "\[\]" > "$home/rules.json"; 17 | 18 | exit 0; -------------------------------------------------------------------------------- /iptables-webui: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nodejs 2 | 3 | /***************************************************\ 4 | * 5 | * IP Tables Web UI 6 | * 7 | * (c) 2015 Robert McLeod released under MIT Licence 8 | * 9 | * You may be thinking the start of this script is 10 | * a bit ugly, ignoring typical convention. This 11 | * is only to make sure thingss start as fast as 12 | * possible. No point waiting for everything to 13 | * load before finding that the port is in use. 14 | * 15 | \***************************************************/ 16 | 17 | // TODO: get command line options working 18 | var PORT = 8099, 19 | HOME = "./iptables", 20 | LOCK = "./iptables-webui.pid"; 21 | // LOCK = "/var/run/iptables-webui.pid"; 22 | 23 | var fs = require("fs"), 24 | exec = require("child_process").exec; 25 | 26 | // trap SIGINT to delete lockfile and exit 27 | process.on("exit", function(code) { 28 | if ( fs.existsSync(LOCK) ) fs.unlinkSync(LOCK); 29 | }); 30 | 31 | // check for root 32 | // if ( process.getuid() != 0 ) { 33 | // console.error("ERROR: Must run as root"); 34 | // process.exit(1); 35 | // } 36 | 37 | // TODO: check if the port is available 38 | 39 | // function for checking/creating log file 40 | var createLockFile = function(lockfile, callback) { 41 | fs.exists(lockfile, function(exists) { 42 | if (exists) { 43 | var pid = fs.readFileSync(lockfile); 44 | exec("ps -p "+pid, function(err, stdout) { // check if it's running 45 | if (err.code == 1) { // the process doesn't exist 46 | fs.writeFile(lockfile, process.pid, function(err){ 47 | callback(err, process.pid); 48 | }); 49 | } else { // the process exists 50 | var err = new Error("IP Tables WebUI is already running on PID "+process.pid); 51 | callback(err, process.pid); 52 | } 53 | }); 54 | } else { 55 | fs.writeFile(lockfile, process.pid, function(err){ 56 | callback(err, process.pid); 57 | }); 58 | } 59 | }); 60 | }; 61 | 62 | // TODO: MAKE THIS SYNCHRONOUS 63 | // check/set the lockfile 64 | createLockFile(LOCK, function(err, pid) { 65 | if (err) { 66 | console.error(err.message); 67 | process.exit(); 68 | } 69 | }); 70 | 71 | 72 | // TODO: run setup here to 73 | 74 | 75 | var http = require("http"), 76 | iprules = require("./lib/iprules"), 77 | express = require("express"); 78 | 79 | var app = express(), 80 | server = http.createServer(app); 81 | 82 | // require faye and attach it to the server 83 | // var _faye = require("faye"), 84 | // bayeux = new _faye.NodeAdapter({mount: '/faye', timeout: 45}), 85 | // faye = _faye.Client("http://localhost:"+PORT+"/faye"); 86 | 87 | // bayeux.attach(server); 88 | 89 | iprules.init(HOME); 90 | 91 | // setup static files 92 | app.use(express.static(__dirname + '/public')); 93 | 94 | app.get("/import", function(req, res) { 95 | if ( !req.params.path ) res.status(400).end(); 96 | 97 | iprules.importRules(req.params.path, function(results) { 98 | res.status(200).json(results).end(); 99 | }); 100 | }); 101 | 102 | app.get("/rules", function(req, res) { 103 | iprules.all(function(err, rules) { 104 | res.json(rules); 105 | }); 106 | }); 107 | 108 | app.get('/rules/:pattern', function(req, res) { 109 | iprules.find(req.params.pattern, function(rules) { 110 | var code = (rules.length > 0 ) ? 200 : 404; 111 | res.json(rules).status(code).end(); 112 | }); 113 | }); 114 | 115 | // create rule 116 | app.post("/rules/:name", function(req, res) { 117 | iprules.create(req.body, function(err, rules) { 118 | if ( !err ) { 119 | res.json({rules: rules}).status(201).end(); 120 | } else if ( err ) { 121 | res.json({error: err.message, rules: rules}).status(500).end(); 122 | } 123 | }); 124 | }); 125 | 126 | // update rule 127 | app.put("/rules/:name", function(req, res) { 128 | 129 | }); 130 | 131 | app.delete('/rules/:name', function(req, res) { 132 | 133 | }); 134 | 135 | app.get('/status', function(req, res) { 136 | var forwarding = {}; 137 | 138 | fs.readdirSync("/proc/sys/net/ipv4/conf").forEach(function(device) { 139 | switch(device) { 140 | case "default": 141 | case "all": 142 | return true; 143 | break; 144 | default: 145 | var state = parseInt(fs.readFileSync("/proc/sys/net/ipv4/conf/"+device+"/forwarding", 'utf-8').trim()); 146 | forwarding[device] = state ? true : false; 147 | return true; 148 | break; 149 | } 150 | }); 151 | 152 | res.json({forwarding: forwarding}).end();; 153 | }); 154 | 155 | app.get('/iptables/list', function(req, res) { 156 | exec('/sbin/iptables -L', function(err, stdout, stderr) { 157 | res.json({list: stdout, error: err && err.message, stderr: stderr}).end(); 158 | }); 159 | }); 160 | 161 | app.put("/rules/:name/enable", function(req, res) { 162 | iprules.enable(req.params.name, function(err) { 163 | res.status(err ? 500 : 200).end(); 164 | }); 165 | }); 166 | 167 | app.put("/rules/:name/disable", function(req, res) { 168 | iprules.disable(req.params.name, function(err) { 169 | res.status(err ? err.status || 500 : 200).end(); 170 | }); 171 | }); 172 | 173 | app.get("/reload", function(req, res) { 174 | iprules.reload(function(err, result) { 175 | if ( err ) { 176 | res.json(err).status(500).end(); 177 | } else { 178 | res.json(result).end(); 179 | } 180 | }); 181 | }); 182 | 183 | app.get("/rules/:name/test", function(req, res) { 184 | iprules.test(req.params.name, function(err) { 185 | if ( err ) { res.status(400).json({message: err.message}).end(); } 186 | res.status(200).end(); 187 | }); 188 | }); 189 | 190 | 191 | // Run setup first 192 | exec("bin/setup "+HOME, function(err, stdout) { 193 | if ( err ) { 194 | console.error(err.message); 195 | process.exit(1); 196 | } 197 | 198 | console.log(stdout); 199 | 200 | // Start the WebUI 201 | app.listen(PORT, function() { 202 | console.log("Started IPTables WebUI on port "+PORT); 203 | }); 204 | }); -------------------------------------------------------------------------------- /iptables/enabled.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /iptables/rules.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /lib/iprules.js: -------------------------------------------------------------------------------- 1 | "use strict" 2 | var fs = require("fs"), 3 | crypto = require("crypto"), 4 | JsonDB = require("node-json-db"), 5 | exec = require("child_process").exec; 6 | 7 | db = new JsonDB("rules", true, false); 8 | 9 | /** 10 | * Basic Interface to the iptables-restore command 11 | */ 12 | var IptablesRestore = { 13 | /** 14 | * Loads the argument text into iptables using 15 | * the iptables-restore command. Must be 16 | * syntactically correct 17 | * 18 | * @param text {string} text formatted into an iptables-restore file 19 | * @callback err 20 | */ 21 | load: function(text, callback) { 22 | // test the data to make sure it's good 23 | this.test(this.build(text), function(err) { 24 | if ( err ) callback(err); 25 | 26 | // test passed so write the file to /etc 27 | // TODO: save mode of 660 28 | var fn = "/etc/iptables.rules"; 29 | fs.writeFile(fn, text, function(err) { 30 | if ( err ) callback(err); 31 | 32 | // the file was written, so load it in for realz 33 | exec("/sbin/iptables-restore < "+fn, function(err, stdout, stderr) { 34 | if ( err ) callback(err); 35 | callback(); 36 | }); 37 | }); 38 | }); 39 | }, 40 | 41 | /** 42 | * Does a test run using the iptables-restore command 43 | * to test the given text 44 | * 45 | * @param text {string} text formatted into an iptables-restore file 46 | * @callback err 47 | * @callback text {string} the text that was used to test with 48 | */ 49 | test: function(text, callback) { 50 | // TODO: save mode of 660 51 | var fn = "/tmp/iptables.rules"; 52 | fs.writeFile(fn, text = this.build(text), function(err) { 53 | if ( err ) callback(err); 54 | 55 | // the file was written so test it 56 | exec("/sbin/iptables-restore --test < "+fn, function(err, stdout, stderr) { 57 | fs.unlink(fn); 58 | if ( err ) callback(err); 59 | callback(null, text); 60 | }); 61 | }); 62 | }, 63 | 64 | build: function(text) { 65 | var lines = [ 66 | "# Generated by ipgroups "+(new Date().toLocaleString()), 67 | "*filter" 68 | ]; 69 | lines.push(text); 70 | lines.push("#end"); 71 | return lines.join("\n"); 72 | } 73 | }; 74 | 75 | /** 76 | * A parser that can translate iptables rule arguments 77 | * into javascript objects and vice-versa. 78 | */ 79 | var RuleParser = { 80 | 81 | /** 82 | * Parses the given iptables rule arguments into 83 | * a JS object 84 | * 85 | * @param rule {string} the iptables rule args 86 | * @return {object} the args split into keys of a JS hash 87 | */ 88 | parse: function(rule) { 89 | var fm = this.firstMatch; 90 | return { 91 | chain: fm(/-A ([A-Z]+)/ , line), 92 | protocol: fm(/-p (\w+)/ , line), 93 | dport: fm(/--dport (\d+)/ , line), 94 | sport: fm(/--sport (\d+)/ , line), 95 | target: fm(/-j (\w+)/ , line), 96 | src: fm(/--src ([\.0-9]+)/ , line), 97 | dst: fm(/--dst ([\.0-9]+)/ , line), 98 | in: fm(/-o ([a-z0-9]+)/ , line), 99 | out: fm(/-i ([a-z0-9]+)/ , line), 100 | table: fm(/-t (\w+)/ , line), 101 | states: fm(/--state ([A-Z,]+)/ , line).split(','), 102 | to_dst: fm(/--to-destination (\S+)/, line) 103 | }; 104 | }, 105 | 106 | /** 107 | * Simply returns the first regexp match in a string 108 | * 109 | * @param regexp {regexp} the regexp to scan for 110 | * @param string {string} the string to scan 111 | * @return {string} the first match 112 | */ 113 | firstMatch: function(regexp, string) { 114 | return regexp.exec(string)[1]; 115 | }, 116 | 117 | /** 118 | * Renders a JS rule into the iptables rule arguments 119 | * 120 | * @param rule {object} the hash representing the rule args 121 | * @return {string} the string of iptables rule args 122 | */ 123 | render: function(rule) { 124 | var args = []; 125 | 126 | // set some defaults 127 | if (!rule.chain) rule.chain = 'INPUT'; 128 | if (!rule.protocol) rule.protocol = 'tcp'; 129 | 130 | // build the args up 131 | if (rule.table) args = args.contact(['-t', , rule.table]); 132 | if (rule.chain) args = args.concat([rule.action , rule.chain]); 133 | if (rule.protocol) args = args.concat(["-p" , rule.protocol]); 134 | if (rule.src) args = args.concat(["--src" , rule.src]); 135 | if (rule.dst) args = args.concat(["--dst" , rule.dst]); 136 | if (rule.sport) args = args.concat(["--sport" , rule.sport]); 137 | if (rule.dport) args = args.concat(["--dport" , rule.dport]); 138 | if (rule.in) args = args.concat(["-i" , rule.in]); 139 | if (rule.out) args = args.concat(["-o" , rule.out]); 140 | if (rule.target) args = args.concat(["-j" , rule.target]); 141 | if (rule.to_dst) args = args.concat(["--to-destination", rule.to_dst]); 142 | if (rule.states) args = args.concat(["-m state --state", rule.states.map(function(s) { return s.toUpperCase(); }).join(",")]) 143 | 144 | return args.join(" "); 145 | } 146 | }; 147 | 148 | module.exports = (function() { 149 | var home = null, 150 | parser = RuleParser, 151 | db = db; 152 | 153 | var init = function(_home) { 154 | home = _home; 155 | 156 | var buildIndex = function() { 157 | db.rules.forEach(function(rule, index) { 158 | db.index.by_name[rule.name] = index; 159 | }); 160 | }; 161 | 162 | /** 163 | * Returns the group by the given name 164 | * 165 | * @param name {string} name of the group 166 | * @return {object} 167 | */ 168 | var get = function(id) { 169 | return db.getData('/groups/'+id); 170 | }; 171 | 172 | /** 173 | * Returns an array of all groups from the db 174 | * 175 | * @return {array} 176 | */ 177 | var all = function() { 178 | return db.getData('/groups'); 179 | }; 180 | 181 | /** 182 | * Find rules by name or glob pattern 183 | * 184 | * @param pattern - the name or pattern to find 185 | * @callback rules - array of rules 186 | */ 187 | var find = function(pattern, callback) { 188 | var rule; 189 | var wildcard = pattern.indexOf("*"); 190 | 191 | if ( wildcard == -1 ) { 192 | if ( (rule = get(pattern)) != null) { 193 | callback(rule); 194 | } else { 195 | callback([]); 196 | } 197 | 198 | return; 199 | } 200 | 201 | var start = (wildcard == 0) ? true : false, 202 | end = (wildcard == pattern.length) ? true : false, 203 | pattern = pattern.replace("*", ''), 204 | rules = []; 205 | 206 | console.log("iprules FIND: "+pattern+" : "+wildcard+" : "+start+" : "+end); 207 | 208 | // check if there was a wildcard specified 209 | if ( wildcard == -1 ) { 210 | if ( db.rules[name] !== undefined ) { 211 | callback([db.rules[name]]); // simply search for the name 212 | } else { 213 | callback([]); // nothing found 214 | } 215 | } else { // there was a wildcard 216 | Object.keys(db.rules).forEach(function(name) { 217 | 218 | if ( start ) { 219 | if (name.indexOf(pattern) == 0) rules.push(db.rules[name]); 220 | } else if ( end ) { 221 | var pos = name.length - pattern.length; // determine the start of the pattern 222 | if (name.indexOf(pattern) == pos ) rules.push(db.rules[name]); 223 | } else { 224 | // TODO: add pattern in the middle support 225 | } 226 | 227 | }); 228 | } 229 | 230 | console.log("iprules FOUND: "+rules.length); 231 | console.log(rules); 232 | 233 | callback(rules); 234 | }; 235 | 236 | /** 237 | * Imports rules from a file or directory and creates groups 238 | * 239 | * @param path {string} 240 | * @callback err 241 | * @callback groups {array} of successfully imported groups 242 | */ 243 | var import = function(path, callback) { 244 | fs.exists(path, function(exists) { 245 | if ( !exists ) { 246 | callback(new Error("Path not found: "+path)); 247 | return false; 248 | } 249 | 250 | // determine if the path given is a directory or file 251 | fs.stat(path, function(err, stats) { 252 | if ( stats.isFile() ) { 253 | importFile(path, function(err, group) { 254 | callback(err, [group]); 255 | }); 256 | } else { 257 | importDirectory(path, callback); 258 | } 259 | }); 260 | 261 | }); 262 | }; 263 | 264 | /** 265 | * Imports rules from a file and creates a group 266 | * 267 | * @param path {string} the path to the file 268 | * @callback err 269 | * @callback group {object} the imported group 270 | */ 271 | var importFile = function(path, callback) { 272 | fs.readFile(path, function(err, text) { 273 | if (err) callback(err); 274 | text = text.replace(/^iptables /g, ''); 275 | 276 | var group = { 277 | name: path, 278 | enabled: false, 279 | lines: text.split("\n"); 280 | } 281 | 282 | create(group, callback); 283 | }); 284 | }; 285 | 286 | /** 287 | * Imports files from a directory 288 | * 289 | * @param path {string} the path name to import from 290 | * @callback err 291 | * @callback groups {array} of successfully imported groups 292 | */ 293 | 294 | var importDirectory = function(path, callback) { 295 | fs.readdir(path, function(err, files) { 296 | if (err) callback && callback(err); 297 | var groups = []; 298 | 299 | files.forEach(function(file) { 300 | importFile(file, function(err, group) { 301 | if (!err) groups.push(group); 302 | }); 303 | }); 304 | 305 | if ( groups.length == 0 ) err = new Error("No rules imported from "+path); 306 | 307 | callback && callback(err, groups); 308 | }); 309 | }; 310 | 311 | /** 312 | * Saves the given group in the DB 313 | * 314 | * @param group {object} 315 | * @callback err 316 | * @callback group {object} the saved group with test data added 317 | */ 318 | var create = function(group, callback) { 319 | if ( typeof group.enabled == undefined ) group.enabled = false; 320 | 321 | test([group], function(err, groups, failed) { // run tests and save results in the groups 322 | // create the group regardless of the test result 323 | var group = groups[0] 324 | 325 | if ( group.id == undefined ) { 326 | group.id = db.getData('/groups').length; 327 | } 328 | 329 | db.push('/groups/'+group.id, group); 330 | 331 | callback(err, group); 332 | }); 333 | }; 334 | 335 | /** 336 | * Updates the given group in the database 337 | * 338 | * @param group {object} the group to update 339 | * @callback err 340 | * @callback group {object} the group after being tested and saved 341 | */ 342 | var update = function(group, callback) { 343 | test([group], function(err, groups, failed) { 344 | if ( err ) callback && callback(err); 345 | db.push('/groups/'+groups[0].id, groups[0]); 346 | callback && callback(err, group[0]); 347 | }); 348 | }; 349 | 350 | /** 351 | * Destroys the rule with the given ID 352 | * 353 | * @param id {integer} 354 | */ 355 | var destroy = function(id) { 356 | db.delete('/groups/'+id); 357 | }; 358 | 359 | /** 360 | * Tests the given groups by using the iptables-restore command 361 | * 362 | * @param groups {array} the groups of rule groups to test 363 | * @callback err 364 | * @callback groups {array} the groups with test data added 365 | * @callback failed {integer} the total number of test failures 366 | */ 367 | var test = function(groups, callback) { 368 | var failed = 0; 369 | 370 | // run the test against each group and save it's results to 371 | groups = groups.map(function(group) { 372 | testGroup(group, function(err, test_lines) { 373 | group.valid = err == null; 374 | group.test_lines = test_lines; 375 | if ( err ) { 376 | failed++; 377 | group.error = err.message; 378 | if ( group.enabled ) group.enabled = false; // disable the group so it doesn't run 379 | } else { 380 | group.error = false; 381 | } 382 | }); 383 | 384 | return group; 385 | }); 386 | 387 | callback(null, groups, failed); 388 | }; 389 | 390 | /** 391 | * Tests a group of rules 392 | * @param group {object} 393 | * @callback err 394 | * @callback lines {array} lines of the file that was tested 395 | */ 396 | var testGroup = function(group, callback) { 397 | IptablesRestore.test(compile([group]), function(err, text) { 398 | callback(err, text.split("\n")); 399 | }); 400 | }; 401 | 402 | var enable = function(name) { 403 | var group = db.getData(name); 404 | group.enabled = true; 405 | db.push('/groups/'+name, group); 406 | }; 407 | 408 | var disable = function(name, callback) { 409 | var group = db.getData(name); 410 | group.enabled = false; 411 | db.push('/groups/'+name, group); 412 | }; 413 | 414 | /* 415 | * Compiles given groups into an iptables-rules file 416 | * represented as an array of lines 417 | * 418 | * @param groups 419 | * @return lines {array} 420 | */ 421 | var compile = function(groups, join) { 422 | var lines = []; 423 | 424 | groups.forEach(function(group) { 425 | lines.push("# "+group.name); 426 | 427 | group.lines.forEach(function(line) { 428 | if ( typeof(line) == "string" ) { 429 | lines.push(line); 430 | } else if ( typeof(line) == "object" ) { 431 | line = RuleParser.render(line); 432 | lines.push(line); 433 | } 434 | }); 435 | 436 | lines.push(""); 437 | }); 438 | 439 | return (join === false) lines : lines.join("\n"); // join by default 440 | }; 441 | 442 | /** 443 | * Reload enabled groups into IP Tables 444 | * 445 | * @callback err 446 | */ 447 | var reload = function(callback) { 448 | var groups = []; 449 | 450 | db.getData("/groups").forEach(function(group) { 451 | if ( group.enabled && group.valid ) groups.push(group); 452 | }); 453 | 454 | 455 | IptablesRestore.load(compile(groups), function(err) { 456 | callback && callback(err, lines); 457 | }); 458 | }; 459 | 460 | return { 461 | init: init, 462 | parser: parser, 463 | all: all, 464 | importRules: importRules, 465 | enable: enable, 466 | disable: disable, 467 | create: create, 468 | update: update, 469 | test: test, 470 | reload: reload, 471 | find: find, 472 | destroy: destroy 473 | } 474 | }()); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "iptables-webui", 3 | "version" : "0.0.1", 4 | "author" : "Robert McLeod", 5 | "licence" : "MIT", 6 | "dependencies": { 7 | "express": "~4.11.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /public/css/custom.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | } 4 | 5 | #container { 6 | padding: 0 100px 0 100px; 7 | } 8 | 9 | .page { 10 | padding-top: 20px; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /public/css/port_forwarding.css: -------------------------------------------------------------------------------- 1 | /*.port_forward { 2 | width: 100%; 3 | background: lightblue; 4 | overflow: hidden; 5 | padding: 10px; 6 | border-radius: 10px; 7 | border: 2px solid black; 8 | } 9 | 10 | .port_forward .in, 11 | .port_forward .dport, 12 | .port_forward .dst, 13 | .port_forward .fport { 14 | float: left; 15 | margin-right: 10px; 16 | } 17 | 18 | .port_forward .controls { 19 | float:right; 20 | } 21 | 22 | .port_forward .dport input, 23 | .port_forward .fport input, 24 | .port_forward .in input { 25 | width: 100px; 26 | } 27 | 28 | 29 | .port_forward p { 30 | font-size: small; 31 | } 32 | 33 | */ -------------------------------------------------------------------------------- /public/css/rules.css: -------------------------------------------------------------------------------- 1 | .rule { 2 | width: 100%; 3 | padding: 5px; 4 | overflow: hidden; 5 | margin-bottom: 10px; 6 | } 7 | 8 | .rule.enabled { 9 | border: 2px solid green; 10 | border-radius: 5px; 11 | background: #D6FFBF; 12 | } 13 | 14 | .rule input.name { 15 | margin: 0px; 16 | width: 100%; 17 | padding: 0px 10px; 18 | background: transparent; 19 | border: 1px solid transparent; 20 | border-radius: 5px; 21 | font-size: 30px; 22 | } 23 | 24 | .rule textarea:hover, 25 | .rule textarea:focus, 26 | .rule input.name:focus, 27 | .rule input.name:hover { 28 | border: 1px solid #c0c0c0; 29 | background: white; 30 | } 31 | 32 | .rule textarea { 33 | width: 100%; 34 | font-size: 16px; 35 | border-radius: 5px; 36 | font-family: monospace; 37 | height: 100px; 38 | background: transparent; 39 | border: 1px solid transparent; 40 | } 41 | 42 | .rule div.btn-group { 43 | /*display: inline-block;*/ 44 | float: right; 45 | } 46 | -------------------------------------------------------------------------------- /public/css/status_page.css: -------------------------------------------------------------------------------- 1 | #device_forwarding { 2 | width: 300px; 3 | overflow: hidden; 4 | } 5 | 6 | #device_forwarding .device { 7 | overflow: hidden; 8 | } 9 | 10 | #device_forwarding .device p { 11 | float: left; 12 | font-weight: bold; 13 | } 14 | 15 | #device_forwarding .device p.state { 16 | float: right; 17 | } 18 | 19 | #device_forwarding .device.on p.state { 20 | color: green; 21 | } 22 | 23 | #device_forwarding .device.off p.state { 24 | color: red; 25 | } 26 | -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /public/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguinpowernz/iptables-webui/f94182e3f2e22143cbabcab1ca4437dc8c3b189e/public/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |t |
Interface | 56 |External Port | 57 |Internal Host | 58 |Internal Port | 59 |60 | |
---|
You need to run this command as root for each device you want to forward on:
10 |echo '1' > /proc/sys/net/ipv4/conf/eth0/forwarding11 |
You can disable forwarding on an interface, by doing the opposite:
12 |echo '0' > /proc/sys/net/ipv4/conf/eth0/forwarding13 | 14 |
This shows what rules IP Tables is currently running with:
16 |This shows what rules in persistence generated the above rules:
20 |