aboutsummaryrefslogtreecommitdiff
path: root/docker/netiquette
diff options
context:
space:
mode:
authorQuentin Dufour <quentin@deuxfleurs.fr>2019-12-04 18:04:30 +0100
committerQuentin Dufour <quentin@deuxfleurs.fr>2019-12-04 18:04:30 +0100
commitfcc2328a3bf49eb5310413058cc9ebaf8e7819f8 (patch)
treec6faabae8badd96455453b72472ac08a2fe0b1c2 /docker/netiquette
parent0b3eb8ec1b3ba3691410744f6397437c9832e74d (diff)
downloadinfrastructure-fcc2328a3bf49eb5310413058cc9ebaf8e7819f8.tar.gz
infrastructure-fcc2328a3bf49eb5310413058cc9ebaf8e7819f8.zip
WIP netiquette
Diffstat (limited to 'docker/netiquette')
-rw-r--r--docker/netiquette/.gitignore1
-rw-r--r--docker/netiquette/README.md24
-rw-r--r--docker/netiquette/index.mjs59
-rw-r--r--docker/netiquette/package-lock.json74
-rw-r--r--docker/netiquette/package.json18
-rw-r--r--docker/netiquette/src/catalog/consul.mjs30
-rw-r--r--docker/netiquette/src/catalog/control_loop.mjs10
-rw-r--r--docker/netiquette/src/injector/iptables.mjs53
-rw-r--r--docker/netiquette/src/injector/upnp.mjs0
-rw-r--r--docker/netiquette/src/io/files.mjs8
-rw-r--r--docker/netiquette/src/io/run.mjs9
-rw-r--r--docker/netiquette/static.iptables10
-rw-r--r--docker/netiquette/test/io.mjs10
-rw-r--r--docker/netiquette/test/iptables.mjs28
-rw-r--r--docker/netiquette/test/runner.mjs28
15 files changed, 362 insertions, 0 deletions
diff --git a/docker/netiquette/.gitignore b/docker/netiquette/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/docker/netiquette/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/docker/netiquette/README.md b/docker/netiquette/README.md
new file mode 100644
index 0000000..e4c2f27
--- /dev/null
+++ b/docker/netiquette/README.md
@@ -0,0 +1,24 @@
+# netiquette
+
+```
+npm install
+npm test
+```
+
+You will probably need to run consul in parallel:
+
+```
+consul agent -dev
+```
+
+You can register services like that:
+
+```
+consul services register -name=toto -tag="public_port=4848"
+```
+
+You will need some arguments to run the software:
+
+```
+sudo npm start node=rincevent ipt_base=./static.iptables
+```
diff --git a/docker/netiquette/index.mjs b/docker/netiquette/index.mjs
new file mode 100644
index 0000000..6aca6e4
--- /dev/null
+++ b/docker/netiquette/index.mjs
@@ -0,0 +1,59 @@
+'use strict'
+import consul from 'consul'
+import { exec } from './src/io/run.mjs'
+import { readFile } from './src/io/files.mjs'
+
+import ctlg_control_loop from './src/catalog/control_loop.mjs'
+import ctlg_consul from './src/catalog/consul.mjs'
+import inj_iptables from './src/injector/iptables.mjs'
+
+const get_args = () => process
+ .argv
+ .slice(2)
+ .map(a => a.split('='))
+ .reduce((dict, tuple) => {
+ dict[tuple[0]] = tuple.length > 1 ? tuple[1] : null
+ return dict
+ }, {})
+
+/**
+ * If we have multiple catalogs
+ * we cache the results of the other ones
+ */
+function* notifications_aggregator(injectors) {
+ const states = []
+ for(let idx = 0; true; idx++) {
+ yield async (tag_list) => {
+ states[idx] = tag_list
+ const merged = states.reduce((acc, tag) => [...acc, ...tag], [])
+ await Promise.all(injectors.map(notify => notify(merged)))
+ }
+ }
+}
+
+const main = async () => {
+ try {
+ const args = get_args()
+
+ // Initialize all injectors
+ const injectors = [
+ await inj_iptables(args.ipt_base, readFile, exec, console.log),
+ // await inj_upnp
+ ]
+
+ // Initialize all catalogs and map them to the injectors
+ const aggr = notifications_aggregator(injectors)
+ const catalogs = [
+ // this catalog is used to defeat deriving config due to single resource updated async. by multiple prog or by external program not tracked by catalogs
+ await ctlg_control_loop(setInterval, 60000, aggr.next().value),
+ await ctlg_consul(args.node, consul(), console.log, aggr.next().value)
+ ]
+
+ console.log("[main] initialized")
+ } catch(e) {
+ console.error("initialization failed", e)
+ process.exit(1)
+ }
+}
+
+main()
diff --git a/docker/netiquette/package-lock.json b/docker/netiquette/package-lock.json
new file mode 100644
index 0000000..a4d30b9
--- /dev/null
+++ b/docker/netiquette/package-lock.json
@@ -0,0 +1,74 @@
+{
+ "name": "consul-to-igd",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "dev": true
+ },
+ "chai": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
+ "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
+ "dev": true,
+ "requires": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^3.0.1",
+ "get-func-name": "^2.0.0",
+ "pathval": "^1.1.0",
+ "type-detect": "^4.0.5"
+ }
+ },
+ "check-error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+ "dev": true
+ },
+ "consul": {
+ "version": "0.34.1",
+ "resolved": "https://registry.npmjs.org/consul/-/consul-0.34.1.tgz",
+ "integrity": "sha512-xCLBzPQBgnDgC2LdYnrT/Fc6PglRU6u7EBkpW0ExAx3Am/CdtKcP5o/3jfwOy7PBAwBqnJk3AYdwwGg+arriiQ==",
+ "requires": {
+ "papi": "^0.29.0"
+ }
+ },
+ "deep-eql": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+ "dev": true,
+ "requires": {
+ "type-detect": "^4.0.0"
+ }
+ },
+ "get-func-name": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+ "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
+ "dev": true
+ },
+ "papi": {
+ "version": "0.29.1",
+ "resolved": "https://registry.npmjs.org/papi/-/papi-0.29.1.tgz",
+ "integrity": "sha512-Y9ipSMfWuuVFO3zY9PlxOmEg+bQ7CeJ28sa9/a0veYNynLf9fwjR3+3fld5otEy7okUaEOUuCHVH62MyTmACXQ=="
+ },
+ "pathval": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
+ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
+ "dev": true
+ },
+ "type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true
+ }
+ }
+}
diff --git a/docker/netiquette/package.json b/docker/netiquette/package.json
new file mode 100644
index 0000000..02fa0b3
--- /dev/null
+++ b/docker/netiquette/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "netiquette",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.mjs",
+ "dependencies": {
+ "consul": "^0.34.1"
+ },
+ "devDependencies": {
+ "chai": "^4.2.0"
+ },
+ "scripts": {
+ "start": "node --experimental-modules ./index.mjs",
+ "test": "node --experimental-modules ./test/runner.mjs"
+ },
+ "author": "Quentin",
+ "license": "AGPL-3.0-or-later"
+}
diff --git a/docker/netiquette/src/catalog/consul.mjs b/docker/netiquette/src/catalog/consul.mjs
new file mode 100644
index 0000000..655c61f
--- /dev/null
+++ b/docker/netiquette/src/catalog/consul.mjs
@@ -0,0 +1,30 @@
+'use strict'
+
+let l
+export default l = async (node, consul, log, notify) => {
+ const watch = consul.watch({ method: consul.catalog.node.services, options: {node: node}})
+
+ const extract_tags = data =>
+ data ?
+ Object
+ .keys(data.Services)
+ .map(k => data.Services[k].Tags)
+ .reduce((acc, v) => [...acc, ...v], []) :
+ []
+
+ watch.on('error', err => {
+ console.error('error', err)
+ })
+
+ watch.on('change', async (data, res) => {
+ try {
+ const tags = extract_tags(data)
+ log(`[consul] new update, detected ${tags.length} tags`)
+ await notify(tags)
+ } catch(e) {
+ console.error('failed to notify target', e)
+ }
+ })
+
+ log('[consul] initialized')
+}
diff --git a/docker/netiquette/src/catalog/control_loop.mjs b/docker/netiquette/src/catalog/control_loop.mjs
new file mode 100644
index 0000000..56ad6f5
--- /dev/null
+++ b/docker/netiquette/src/catalog/control_loop.mjs
@@ -0,0 +1,10 @@
+'use strict'
+
+let l
+export default l = async (timer, interval, notify) => {
+ timer(() => {
+ notify([])
+ console.log(`[control_loop] actuation (triggered every ${interval} ms)`)
+ }, interval)
+ console.log("[control_loop] initialized")
+}
diff --git a/docker/netiquette/src/injector/iptables.mjs b/docker/netiquette/src/injector/iptables.mjs
new file mode 100644
index 0000000..584b560
--- /dev/null
+++ b/docker/netiquette/src/injector/iptables.mjs
@@ -0,0 +1,53 @@
+'use strict'
+
+let l;
+export default l = async (path, readFile, exec, log) => {
+
+ const load_static_rules = async path =>
+ (await readFile(path, 'utf-8'))
+ .split('\n')
+ .filter(e => e)
+
+ const get_current_rules = async () =>
+ (await exec('iptables -S INPUT'))
+ .stdout
+ .split('\n')
+ .filter(e => e.match(/^-A INPUT/g))
+
+ const compute_rules_to_add = (current, target) =>
+ target.filter(r => !current.includes(r))
+
+ const compute_rules_to_del = (current, target) =>
+ current
+ .filter(r => !target.includes(r))
+ .map(r => r.replace(/^-A INPUT/g, '-D INPUT'))
+
+ const update_rules = async (current, target) =>
+ await Promise.all([
+ ...compute_rules_to_del(current, target),
+ ...compute_rules_to_add(current, target)
+ ].map(r => exec(`iptables ${r}`)))
+
+ const build_target_rules = (tag_list) =>
+ tag_list
+ .map(t => /^public_port=(\d+)(-(\d+))?\/(udp|tcp)/g.exec(t))
+ .filter(t => t)
+ .map(t => new Object({ start: t[1], stop: t[3], protocol: t[4] }))
+ .map(t => t.stop
+ ? `-A INPUT -p ${t.protocol} --match multiport --dports ${t.start}:${t.stop} -j ACCEPT`
+ : `-A INPUT -p ${t.protocol} --dport ${t.start} -j ACCEPT`)
+
+ const do_log = (tag_list, r) => {
+ //log('[iptables]', tag_list)
+ log(`[iptables] ran ${r.length} commands`)
+ }
+
+ const static_rules = path ? await load_static_rules(path) : []
+ log(`[iptables] initialized with ${static_rules.length} static rules`)
+ return async tag_list =>
+ do_log(
+ tag_list,
+ await update_rules(
+ await get_current_rules(),
+ [...static_rules, ...build_target_rules(tag_list)]))
+}
diff --git a/docker/netiquette/src/injector/upnp.mjs b/docker/netiquette/src/injector/upnp.mjs
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docker/netiquette/src/injector/upnp.mjs
diff --git a/docker/netiquette/src/io/files.mjs b/docker/netiquette/src/io/files.mjs
new file mode 100644
index 0000000..c3eca1b
--- /dev/null
+++ b/docker/netiquette/src/io/files.mjs
@@ -0,0 +1,8 @@
+'use strict'
+
+import fs from 'fs'
+
+export const readFile = (file, opts) =>
+ new Promise((resolve, reject) =>
+ fs.readFile(file, opts, (err, data) =>
+ err ? reject(err) : resolve(data)))
diff --git a/docker/netiquette/src/io/run.mjs b/docker/netiquette/src/io/run.mjs
new file mode 100644
index 0000000..8774043
--- /dev/null
+++ b/docker/netiquette/src/io/run.mjs
@@ -0,0 +1,9 @@
+'use strict'
+
+import child_process from 'child_process'
+
+export const exec = (cmd, opts) =>
+ new Promise((resolve, reject) =>
+ child_process.exec(cmd, opts, (error, stdout, stderr) =>
+ error ? reject({err: error, stdout: stdout, stderr: stderr}) : resolve({stdout: stdout, stderr: stderr})))
+
diff --git a/docker/netiquette/static.iptables b/docker/netiquette/static.iptables
new file mode 100644
index 0000000..d9e7d38
--- /dev/null
+++ b/docker/netiquette/static.iptables
@@ -0,0 +1,10 @@
+-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
+-A INPUT -i docker0 -j ACCEPT
+-A INPUT -s 127.0.0.0/8 -j ACCEPT
+-A INPUT -s 192.168.1.2/32 -j ACCEPT
+-A INPUT -s 192.168.1.3/32 -j ACCEPT
+-A INPUT -s 192.168.1.4/32 -j ACCEPT
+-A INPUT -p udp -m udp --dport 53 -j ACCEPT
+-A INPUT -p tcp -m tcp --dport 53 -j ACCEPT
+-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
+-A INPUT -p tcp -m tcp --dport 110 -j ACCEPT
diff --git a/docker/netiquette/test/io.mjs b/docker/netiquette/test/io.mjs
new file mode 100644
index 0000000..d88ad15
--- /dev/null
+++ b/docker/netiquette/test/io.mjs
@@ -0,0 +1,10 @@
+import chai from 'chai'
+import { readFile } from '../src/io/files.mjs'
+const expect = chai.expect
+
+export default [
+ (async () => {
+ const dirname = import.meta.url.replace(/^file:\/\//g, '').replace(/io.mjs$/g, '')
+ expect(await readFile(`${dirname}/../package.json`, 'utf-8')).to.include('Quentin')
+ })
+]
diff --git a/docker/netiquette/test/iptables.mjs b/docker/netiquette/test/iptables.mjs
new file mode 100644
index 0000000..1ae1cb0
--- /dev/null
+++ b/docker/netiquette/test/iptables.mjs
@@ -0,0 +1,28 @@
+'use strict'
+
+import chai from 'chai'
+import iptables from '../src/injector/iptables.mjs'
+const expect = chai.expect
+
+export default [
+ (async () => {
+ const effective_actions = []
+ const expected_actions = [
+ 'iptables -A INPUT -p tcp --dport 56 -j ACCEPT',
+ 'iptables -A INPUT -p tcp --dport 53 -j ACCEPT',
+ 'iptables -A INPUT -p udp --match multiport --dports 25630:25999 -j ACCEPT',
+ 'iptables -D INPUT -p tcp --dport 54 -j ACCEPT'
+ ]
+
+ const mockLog = () => {}
+ const mockReadFile = (file, opt) => '-A INPUT -p tcp --dport 53 -j ACCEPT'
+ const mockExecCommand = (cmd, opts) => {
+ if (cmd.match(/^iptables -S/g)) return { stdout: '-A INPUT -p tcp --dport 54 -j ACCEPT' }
+ else effective_actions.push(cmd)
+ return { stdout: '' } }
+
+ const fw = await iptables('static', mockReadFile, mockExecCommand, mockLog)
+ await fw(['public_port=56/tcp', 'public_port=25630-25999/udp', 'public_port=13', 'traefik.entrypoints=Host:im.deuxfleurs.fr;PathPrefix:/_matrix'])
+ expect(effective_actions).to.have.members(expected_actions)
+ })
+]
diff --git a/docker/netiquette/test/runner.mjs b/docker/netiquette/test/runner.mjs
new file mode 100644
index 0000000..b4da1de
--- /dev/null
+++ b/docker/netiquette/test/runner.mjs
@@ -0,0 +1,28 @@
+'use strict'
+
+import io from './io.mjs'
+import iptables from './iptables.mjs'
+
+(async () => {
+ const res = await [
+ ...io,
+ ...iptables
+ ].map(async f => {
+ try {
+ await f()
+ return 'passed'
+ }
+ catch(e) {
+ console.error(e)
+ return 'failed'
+ }
+ }).reduce(async (acc, r) => {
+ const accumulator = await acc
+ const result = await r
+ accumulator.total++
+ accumulator[result]++
+ return accumulator
+ }, {total: 0, passed: 0, failed: 0})
+
+ console.log(`Done. passed: ${res.passed}, failed: ${res.failed}, total: ${res.total}`)
+})()