'use strict'; exports = module.exports = { testIPv4Config, testIPv6Config, getBlocklist, setBlocklist, getDynamicDns, setDynamicDns, getIPv4Config, setIPv4Config, getIPv6Config, setIPv6Config, getIPv4, hasIPv6, getIPv6 }; const assert = require('assert'), BoxError = require('./boxerror.js'), constants = require('./constants.js'), cron = require('./cron.js'), fs = require('fs'), ipaddr = require('ipaddr.js'), path = require('path'), paths = require('./paths.js'), safe = require('safetydance'), settings = require('./settings.js'), shell = require('./shell.js'), validator = require('validator'); const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh'); function api(provider) { assert.strictEqual(typeof provider, 'string'); switch (provider) { case 'noop': return require('./network/noop.js'); case 'fixed': return require('./network/fixed.js'); case 'network-interface': return require('./network/network-interface.js'); default: return require('./network/generic.js'); } } function hasIPv6() { const IPV6_PROC_FILE = '/proc/net/if_inet6'; // on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0; } async function testIPv4Config(config) { assert.strictEqual(typeof config, 'object'); return await api(config.provider).testIPv4Config(config); } async function testIPv6Config(config) { assert.strictEqual(typeof config, 'object'); return await api(config.provider).testIPv6Config(config); } async function getBlocklist() { const value = await settings.getBlob(settings.FIREWALL_BLOCKLIST_KEY); return value ? value.toString('utf8') : ''; } async function setBlocklist(blocklist, auditSource) { assert.strictEqual(typeof blocklist, 'string'); assert.strictEqual(typeof auditSource, 'object'); const parsedIp = ipaddr.process(auditSource.ip); for (const line of blocklist.split('\n')) { if (!line || line.startsWith('#')) continue; const rangeOrIP = line.trim(); // this checks for IPv4 and IPv6 if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`); if (rangeOrIP.indexOf('/') === -1) { if (auditSource.ip === rangeOrIP) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`); } else { const parsedRange = ipaddr.parseCIDR(rangeOrIP); // returns [addr, range] if (parsedRange[0].kind() === parsedIp.kind() && parsedIp.match(parsedRange)) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`); } } if (constants.DEMO) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'); // store in blob since the value field is TEXT and has 16kb size limit await settings.setBlob(settings.FIREWALL_BLOCKLIST_KEY, Buffer.from(blocklist)); // this is done only because it's easier for the shell script and the firewall service to get the value if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message); const [error] = await safe(shell.promises.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {})); if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`); } async function getDynamicDns() { const enabled = await settings.get(settings.DYNAMIC_DNS_KEY); return enabled ? !!enabled : false; // db holds string values only } async function setDynamicDns(enabled) { assert.strictEqual(typeof enabled, 'boolean'); await settings.set(settings.DYNAMIC_DNS_KEY, enabled ? 'enabled' : ''); // db holds string values only await cron.handleDynamicDnsChanged(enabled); } async function getIPv4Config() { const value = await settings.getJson(settings.IPV4_CONFIG_KEY); return value || { provider: 'generic' }; } async function setIPv4Config(ipv4Config) { assert.strictEqual(typeof ipv4Config, 'object'); if (constants.DEMO) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); const error = await testIPv4Config(ipv4Config); if (error) throw error; await settings.setJson(settings.IPV4_CONFIG_KEY, ipv4Config); } async function getIPv6Config() { const value = await settings.getJson(settings.IPV6_CONFIG_KEY); return value || { provider: 'noop' }; } async function setIPv6Config(ipv6Config) { assert.strictEqual(typeof ipv6Config, 'object'); if (constants.DEMO) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'); const error = await testIPv6Config(ipv6Config); if (error) throw error; await settings.setJson(settings.IPV6_CONFIG_KEY, ipv6Config); } async function getIPv4() { const config = await getIPv4Config(); return await api(config.provider).getIPv4(config); } // returns RFC 5952 formatted address (https://datatracker.ietf.org/doc/html/rfc5952) async function getIPv6() { const config = await getIPv6Config(); const result = await api(config.provider).getIPv6(config); if (!result) return null; return ipaddr.parse(result).toRFC5952String(); }