From 20e206fa43149bc4be308cae3e7a2d7793b5073d Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Mon, 14 Sep 2020 10:29:48 -0700 Subject: [PATCH] migrate blocklist to a txt file this allows easy copy/pasting of existing deny lists which contain comments and blank lines --- CHANGES | 3 ++ .../20200914172345-split-firewall-config.js | 27 ++++++++++++++ package-lock.json | 17 ++++++--- package.json | 1 + setup/start.sh | 1 + setup/start/cloudron-firewall.sh | 4 +-- src/network.js | 35 +++++++++++-------- src/paths.js | 1 + src/routes/network.js | 6 ++-- src/scripts/setblocklist.sh | 12 ++++--- 10 files changed, 78 insertions(+), 29 deletions(-) create mode 100644 migrations/20200914172345-split-firewall-config.js diff --git a/CHANGES b/CHANGES index a0623166e..30e73144e 100644 --- a/CHANGES +++ b/CHANGES @@ -2087,3 +2087,6 @@ * ovh: add sydney region * s3: makes multi-part copies in parallel +[5.6.1] +* Blocklists are now stored in a text file instead of json + diff --git a/migrations/20200914172345-split-firewall-config.js b/migrations/20200914172345-split-firewall-config.js new file mode 100644 index 000000000..87de4405a --- /dev/null +++ b/migrations/20200914172345-split-firewall-config.js @@ -0,0 +1,27 @@ +'use strict'; + +const OLD_FIREWALL_CONFIG_JSON = '/home/yellowtent/boxdata/firewall-config.json'; +const PORTS_FILE = '/home/yellowtent/boxdata/firewall/ports.json'; +const BLOCKLIST_FILE = '/home/yellowtent/boxdata/firewall/blocklist.txt'; + +const fs = require('fs'); + +exports.up = function (db, callback) { + if (!fs.existsSync(OLD_FIREWALL_CONFIG_JSON)) return callback(); + + try { + const dataJson = fs.readFileSync(OLD_FIREWALL_CONFIG_JSON, 'utf8'); + const data = JSON.parse(dataJson); + fs.writeFileSync(BLOCKLIST_FILE, data.blocklist.join('\n') + '\n', 'utf8'); + fs.writeFileSync(PORTS_FILE, JSON.stringify({ allowed_tcp_ports: data.allowed_tcp_ports }, null, 4), 'utf8'); + fs.unlinkSync(OLD_FIREWALL_CONFIG_JSON); + } catch (error) { + console.log('Error migrating old firewall config', error); + } + + callback(); +}; + +exports.down = function (db, callback) { + callback(); +}; diff --git a/package-lock.json b/package-lock.json index eb794a781..ec079e7e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2438,9 +2438,9 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.0.tgz", + "integrity": "sha512-S54H9mIj0rbxRIyrDMEuuER86LdlgUg9FSeZ8duQb6CUG2iRrA36MYVQBSprTF/ZeAwvyQ5mDGuNvIPM0BIl3w==" }, "is-arguments": { "version": "1.0.4", @@ -3779,6 +3779,13 @@ "requires": { "forwarded": "~0.1.2", "ipaddr.js": "1.9.0" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + } } }, "proxy-middleware": { @@ -4635,8 +4642,8 @@ "resolved": "https://registry.npmjs.org/prettyjson/-/prettyjson-1.2.1.tgz", "integrity": "sha1-/P+rQdGcq0365eV15kJGYZsS0ok=", "requires": { - "colors": "1.2.1", - "minimist": "1.2.0" + "colors": "^1.1.2", + "minimist": "^1.2.0" } } } diff --git a/package.json b/package.json index d5842dd58..7d05d385e 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "ejs": "^2.6.1", "ejs-cli": "^2.2.0", "express": "^4.17.1", + "ipaddr.js": "^2.0.0", "js-yaml": "^3.14.0", "json": "^9.0.6", "ldapjs": "^1.0.2", diff --git a/setup/start.sh b/setup/start.sh index a2833bf3e..0b7516b62 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -57,6 +57,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \ mkdir -p "${PLATFORM_DATA_DIR}/update" mkdir -p "${BOX_DATA_DIR}/appicons" +mkdir -p "${BOX_DATA_DIR}/firewall" mkdir -p "${BOX_DATA_DIR}/profileicons" mkdir -p "${BOX_DATA_DIR}/certs" mkdir -p "${BOX_DATA_DIR}/acme" # acme keys diff --git a/setup/start/cloudron-firewall.sh b/setup/start/cloudron-firewall.sh index 89221342c..52c70aac4 100755 --- a/setup/start/cloudron-firewall.sh +++ b/setup/start/cloudron-firewall.sh @@ -20,8 +20,8 @@ iptables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443 -j ACCEPT # 202 is the alternate ssh port # whitelist any user ports -user_firewall_json="/home/yellowtent/boxdata/firewall-config.json" -if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${user_firewall_json}', 'utf8')).allowed_tcp_ports.join(','))" 2>/dev/null); then +ports_json="/home/yellowtent/boxdata/firewall/ports.json" +if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_tcp_ports.join(','))" 2>/dev/null); then [[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT fi diff --git a/src/network.js b/src/network.js index 753db6fe7..2810e2bd1 100644 --- a/src/network.js +++ b/src/network.js @@ -7,6 +7,7 @@ exports = module.exports = { const assert = require('assert'), BoxError = require('./boxerror.js'), + ipaddr = require('ipaddr.js'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), @@ -19,27 +20,33 @@ const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh'); function getBlocklist(callback) { assert.strictEqual(typeof callback, 'function'); - const data = safe.fs.readFileSync(paths.FIREWALL_CONFIG_FILE, 'utf8'); - const config = safe.JSON.parse(data); - const blocklist = config && config.blocklist ? config.blocklist : []; - - callback(null, blocklist); + const data = safe.fs.readFileSync(paths.FIREWALL_BLOCKLIST_FILE, 'utf8'); + callback(null, data); } -function setBlocklist(blocklist, callback) { - assert(Array.isArray(blocklist)); +function setBlocklist(blocklist, auditSource, callback) { + assert.strictEqual(typeof blocklist, 'string'); + assert.strictEqual(typeof auditSource, 'object'); assert.strictEqual(typeof callback, 'function'); - if (!blocklist.every(x => validator.isIP(x) || validator.isIPRange(x))) return callback(new BoxError(BoxError.BAD_FIELD, 'blocklist must contain IP or IP range')); + const parsedIp = ipaddr.process(auditSource.ip); + + for (const line of blocklist.split('\n')) { + if (!line || line.startsWith('#')) continue; + const rangeOrIP = line.trim(); + if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`)); + + if (rangeOrIP.indexOf('/') === -1) { + if (auditSource.ip === rangeOrIP) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`)); + } else { + const parsedRange = ipaddr.parseCIDR(rangeOrIP); + if (parsedIp.match(parsedRange)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`)); + } + } if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode')); - const data = safe.fs.readFileSync(paths.FIREWALL_CONFIG_FILE, 'utf8'); - const config = safe.JSON.parse(data) || {}; - - config.blocklist = blocklist; - - if (!safe.fs.writeFileSync(paths.FIREWALL_CONFIG_FILE, JSON.stringify(config, null, 4), 'utf8')) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); + if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist, 'utf8')) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message)); shell.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {}, function (error) { if (error) return callback(new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`)); diff --git a/src/paths.js b/src/paths.js index bfa16a964..9c6856508 100644 --- a/src/paths.js +++ b/src/paths.js @@ -46,6 +46,7 @@ exports = module.exports = { CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'), UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'), ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'), + FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'boxdata/firewall/blocklist.txt'), FIREWALL_CONFIG_FILE: path.join(baseDir(), 'boxdata/firewall-config.json'), LOG_DIR: path.join(baseDir(), 'platformdata/logs'), diff --git a/src/routes/network.js b/src/routes/network.js index d7a77c794..5201dbbec 100644 --- a/src/routes/network.js +++ b/src/routes/network.js @@ -6,6 +6,7 @@ exports = module.exports = { }; var assert = require('assert'), + auditSource = require('../auditsource.js'), BoxError = require('../boxerror.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, @@ -22,12 +23,11 @@ function getBlocklist(req, res, next) { function setBlocklist(req, res, next) { assert.strictEqual(typeof req.body, 'object'); - if (!Array.isArray(req.body.blocklist)) return next(new HttpError(400, 'blocklist is required')); - if (!req.body.blocklist.every(x => typeof x === 'string')) return next(new HttpError(400, 'blocklist must be array of strings')); + if (typeof req.body.blocklist !== 'string') return next(new HttpError(400, 'blocklist must be a string')); req.clearTimeout(); // can take a while if there is a lot of network ranges - network.setBlocklist(req.body.blocklist, function (error) { + network.setBlocklist(req.body.blocklist, auditSource.fromRequest(req), function (error) { if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); diff --git a/src/scripts/setblocklist.sh b/src/scripts/setblocklist.sh index 08c2e2297..53e117e79 100755 --- a/src/scripts/setblocklist.sh +++ b/src/scripts/setblocklist.sh @@ -14,10 +14,12 @@ fi ipset flush cloudron_blocklist -user_firewall_json="/home/yellowtent/boxdata/firewall-config.json" -if blocklist=$(node -e "console.log(JSON.parse(fs.readFileSync('${user_firewall_json}', 'utf8')).blocklist.join(' '))" 2>/dev/null); then +user_firewall_json="/home/yellowtent/boxdata/firewall/blocklist.txt" - for ip in ${blocklist}; do - ipset add cloudron_blocklist "${ip}" - done +if [[ -f "${user_firewall_json}" ]]; then + while read -r line; do + [[ -z "${line}" ]] && continue # ignore empty lines + [[ "$line" =~ ^#.*$ ]] && continue # ignore lines starting with # + ipset add cloudron_blocklist "${line}" + done < "${user_firewall_json}" fi