From 28fe33ed5a507cd118383610a94e0771b04c29db Mon Sep 17 00:00:00 2001 From: David White Date: Wed, 21 Feb 2024 08:33:48 +0000 Subject: [PATCH 01/30] [BUGFIX] fix iptables rules to make them idempotent (#1214) * Update iptables rules to make them idempotent * Add missing return statements * Fix catch logic error * Only catch one statement with try * Remove broken catches (sync code) * Move to explictly denying flux 172.23.0.0/16 and use DOCKER-USER chain * Add a bit more to docstring * Remove erroneous extra function call from testing * Greatly simplify how default gateway / local subnet is determined * Lint * Remove requirement for , will protect all operator networks * Modify rules slightly to match iptables output, add tests * Add allow for Flux networks, remove RETURN that docker keeps adding * Full refactor - see below This commit now blocks 100% of access to private address space, while maintaining isolation for each Flux docker network. Now apps can be sure no other app is snooping their traffic, and operators can be sure that apps do not have access to ANY private network they are routing. Tests will all be broken - I'll fix up in next commit. * Lint * Update tests * Move the docker interface fetch up a level to avoid circular * Add dockerService to serviceManager, fix up tests * Fix typing for return * Stub console output (from Flux log) so it doesn't clog up the testing output * Add missing remove private stanza for softInstallLocally * Update compatibility with older iptables 1.8.4 - see below Older iptables (legacy) on ubuntu 20.04 operates slightly differently than the nf_tables version, the check output command doesn't return anything. Some of the output strings are different, so we don't check those anymore. Have also added a check to make sure the iptables binary is in the root users path. * Add iptables exists check and fix up tests --- ZelBack/src/services/appsService.js | 16 ++ ZelBack/src/services/dockerService.js | 42 +++ ZelBack/src/services/fluxNetworkHelper.js | 179 ++++++++++++- ZelBack/src/services/serviceHelper.js | 38 +++ ZelBack/src/services/serviceManager.js | 6 +- tests/unit/fluxNetworkHelper.test.js | 300 +++++++++++++++++++++- 6 files changed, 565 insertions(+), 16 deletions(-) diff --git a/ZelBack/src/services/appsService.js b/ZelBack/src/services/appsService.js index eb00537d3..099d442a6 100644 --- a/ZelBack/src/services/appsService.js +++ b/ZelBack/src/services/appsService.js @@ -3391,6 +3391,14 @@ async function registerAppLocally(appSpecs, componentSpecs, res) { throw new Error(`Flux App network of ${appName} failed to initiate. Range already assigned to different application.`); } log.info(serviceHelper.ensureString(fluxNet)); + const fluxNetworkInterfaces = await dockerService.getFluxDockerNetworkPhysicalInterfaceNames(); + const accessRemoved = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable(fluxNetworkInterfaces); + const accessRemovedRes = { + status: accessRemoved ? `Private network access removed for ${appName}` : `Error removing private network access for ${appName}`, + }; + if (res) { + res.write(serviceHelper.ensureString(accessRemovedRes)); + } const fluxNetResponse = { status: `Docker network of ${appName} initiated.`, }; @@ -3764,6 +3772,14 @@ async function softRegisterAppLocally(appSpecs, componentSpecs, res) { throw new Error(`Flux App network of ${appName} failed to initiate. Range already assigned to different application`); } log.info(serviceHelper.ensureString(fluxNet)); + const fluxNetworkInterfaces = await dockerService.getFluxDockerNetworkPhysicalInterfaceNames(); + const accessRemoved = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable(fluxNetworkInterfaces); + const accessRemovedRes = { + status: accessRemoved ? `Private network access removed for ${appName}` : `Error removing private network access for ${appName}`, + }; + if (res) { + res.write(serviceHelper.ensureString(accessRemovedRes)); + } const fluxNetResponse = { status: `Docker network of ${appName} initiated.`, }; diff --git a/ZelBack/src/services/dockerService.js b/ZelBack/src/services/dockerService.js index 08d40beb0..d2958677e 100644 --- a/ZelBack/src/services/dockerService.js +++ b/ZelBack/src/services/dockerService.js @@ -821,6 +821,46 @@ async function createFluxDockerNetwork() { return response; } +/** + * + * @returns {Promise} + */ +async function getFluxDockerNetworks() { + const fluxNetworks = await docker.listNetworks({ + filters: JSON.stringify({ + name: ['fluxDockerNetwork'], + }), + }); + + return fluxNetworks; +} + +/** + * + * @returns {Promise} + */ +async function getFluxDockerNetworkPhysicalInterfaceNames() { + const fluxNetworks = await getFluxDockerNetworks(); + + const interfaceNames = fluxNetworks.map((network) => { + // the physical interface name is br- + const intName = `br-${network.Id.slice(0, 12)}`; + return intName; + }); + + return interfaceNames; +} + +/** + * + * @returns {Promise} + */ +async function getFluxDockerNetworkSubnets() { + const fluxNetworks = await getFluxDockerNetworks(); + const subnets = fluxNetworks.map((network) => network.IPAM.Config[0].Subnet); + return subnets; +} + /** * Creates flux application docker network if doesn't exist * @@ -973,6 +1013,8 @@ module.exports = { createFluxDockerNetwork, getDockerContainerOnly, getDockerContainerByIdOrName, + getFluxDockerNetworkPhysicalInterfaceNames, + getFluxDockerNetworkSubnets, createFluxAppDockerNetwork, removeFluxAppDockerNetwork, pruneNetworks, diff --git a/ZelBack/src/services/fluxNetworkHelper.js b/ZelBack/src/services/fluxNetworkHelper.js index 4c1cdc9ad..dc6b84155 100644 --- a/ZelBack/src/services/fluxNetworkHelper.js +++ b/ZelBack/src/services/fluxNetworkHelper.js @@ -1345,21 +1345,174 @@ async function purgeUFW() { } /** - * This fix a docker security issue where docker containers can access host network, for example to create port forwarding on hosts + * This fix a docker security issue where docker containers can access private node operator networks, for example to create port forwarding on hosts. + * + * Docker should create a DOCKER-USER chain. If this doesn't exist - we create it, then jump to this chain immediately from the FORWARD CHAIN. + * This allows rules to be added via -I (insert) and -A (append) to the DOCKER-USER chain individually, so we can ALWAYS append the + * drop traffic rule, and insert the ACCEPT rules. If no matches are found in the DOCKER-USER chain, rule evaluation continues + * from the next rule in the FORWARD chain. + * + * If needed in the future, we can actually create a JUMP from the DOCKER-USER chain to a custom chain. The reason why we MUST use the DOCKER-USER + * chain is that whenever docker creates a new network, it re-jumps the DOCKER-USER chain at the head of the FORWARD chain. + * + * As can be seen in this example: + * + * Originally, was using the FLUX chain, but you can see docker inserted the br-72d1725e481c network ahead, as well as the JUMP to DOCKER-USER, + * which invalidates any rules in the FLUX chain, as there is basically an accept any: + * + * FORWARD -i br-72d1725e481c ! -o br-72d1725e481c -j ACCEPT + * + * ```bash + * -A INPUT -j ufw-track-input + * -A FORWARD -j DOCKER-USER + * -A FORWARD -j DOCKER-ISOLATION-STAGE-1 + * -A FORWARD -o br-72d1725e481c -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + * -A FORWARD -o br-72d1725e481c -j DOCKER + * -A FORWARD -i br-72d1725e481c ! -o br-72d1725e481c -j ACCEPT + * -A FORWARD -i br-72d1725e481c -o br-72d1725e481c -j ACCEPT + * -A FORWARD -j FLUX + * -A FORWARD -o br-048fde111132 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT + * -A FORWARD -o br-048fde111132 -j DOCKER + * -A FORWARD -i br-048fde111132 ! -o br-048fde111132 -j ACCEPT + * -A FORWARD -i br-048fde111132 -o br-048fde111132 -j ACCEPT + *``` + * This means if a user or someone was to delete a single rule, we are able to recover correctly from it. + * + * The other option - is just to Flush all rules on every run, and reset them all. This is what we are doing now. + * + * @param {string[]} fluxNetworkInterfaces The network interfaces, br-<12 character string> + * @returns {Promise} */ -async function removeDockerContainerAccessToHost() { +async function removeDockerContainerAccessToNonRoutable(fluxNetworkInterfaces) { + const cmdAsync = util.promisify(nodecmd.get); + + const checkIptables = 'sudo iptables --version'; + const iptablesInstalled = await cmdAsync(checkIptables).catch(() => { + log.error('Unable to find iptables binary'); + return false + }); + + if (!iptablesInstalled) return false; + + // check if rules have been created, as iptables is NOT idempotent. + const checkDockerUserChain = 'sudo iptables -L DOCKER-USER'; + // iptables 1.8.4 doesn't return anything - so have updated command a little + const checkJumpChain = 'sudo iptables -C FORWARD -j DOCKER-USER && echo true'; + + const dockerUserChainExists = await cmdAsync(checkDockerUserChain).catch(async () => { + try { + await cmdAsync('sudo iptables -N DOCKER-USER'); + log.info('IPTABLES: DOCKER-USER chain created'); + } catch (err) { + log.error('IPTABLES: Error adding DOCKER-USER chain'); + // if we can't add chain, we can't proceed + return new Error(); + } + return null; + }); + + if (dockerUserChainExists instanceof Error) return false; + if (dockerUserChainExists) log.info('IPTABLES: DOCKER-USER chain already created'); + + const checkJumpToDockerChain = await cmdAsync(checkJumpChain).catch(async () => { + // Ubuntu 20.04 @ iptables 1.8.4 Error: "iptables: No chain/target/match by that name." + // Ubuntu 22.04 @ iptables 1.8.7 Error: "iptables: Bad rule (does a matching rule exist in that chain?)." + const jumpToFluxChain = 'sudo iptables -I FORWARD -j DOCKER-USER'; + try { + await cmdAsync(jumpToFluxChain); + log.info('IPTABLES: New rule in FORWARD inserted to jump to DOCKER-USER chain'); + } catch (err) { + log.error('IPTABLES: Error inserting FORWARD jump to DOCKER-USER chain'); + // if we can't jump, we need to bail out + return new Error(); + } + + return null; + }); + + if (checkJumpToDockerChain instanceof Error) return false; + if (checkJumpToDockerChain) log.info('IPTABLES: Jump to DOCKER-USER chain already enabled'); + + const rfc1918Networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']; + const fluxSrc = '172.23.0.0/16'; + + const baseDropCmd = `sudo iptables -A DOCKER-USER -s ${fluxSrc} -d #DST -j DROP`; + const baseAllowToFluxNetworksCmd = 'sudo iptables -I DOCKER-USER -i #INT -o #INT -j ACCEPT'; + const baseAllowEstablishedCmd = `sudo iptables -I DOCKER-USER -s ${fluxSrc} -d #DST -m state --state RELATED,ESTABLISHED -j ACCEPT`; + const baseAllowDnsCmd = `sudo iptables -I DOCKER-USER -s ${fluxSrc} -d #DST -p udp --dport 53 -j ACCEPT`; + + const addReturnCmd = 'sudo iptables -A DOCKER-USER -j RETURN'; + const flushDockerUserCmd = 'sudo iptables -F DOCKER-USER'; + try { - const cmdAsync = util.promisify(nodecmd.get); - const dropAccessToHostNetwork = "sudo iptables -I FORWARD -i docker0 -d $(ip route | grep \"src $(ip addr show dev $(ip route | awk '/default/ {print $5}') | grep \"inet\" | awk 'NR==1{print $2}' | cut -d'/' -f 1)\" | awk '{print $1}') -j DROP"; - await cmdAsync(dropAccessToHostNetwork).catch((error) => log.error(`Error executing dropAccessToHostNetwork command:${error}`)); - const giveHostAccessToDockerNetwork = "sudo iptables -I FORWARD -i docker0 -d $(ip route | grep \"src $(ip addr show dev $(ip route | awk '/default/ {print $5}') | grep \"inet\" | awk 'NR==1{print $2}' | cut -d'/' -f 1)\" | awk '{print $1}') -m state --state ESTABLISHED,RELATED -j ACCEPT"; - await cmdAsync(giveHostAccessToDockerNetwork).catch((error) => log.error(`Error executing giveHostAccessToDockerNetwork command:${error}`)); - const giveContainerAccessToDNS = "sudo iptables -I FORWARD -i docker0 -p udp -d $(ip route | grep \"src $(ip addr show dev $(ip route | awk '/default/ {print $5}') | grep \"inet\" | awk 'NR==1{print $2}' | cut -d'/' -f 1)\" | awk '{print $1}') --dport 53 -j ACCEPT"; - await cmdAsync(giveContainerAccessToDNS).catch((error) => log.error(`Error executing giveContainerAccessToDNS command:${error}`)); - log.info('Access to host from containers removed'); - } catch (error) { - log.error(error); + await cmdAsync(flushDockerUserCmd); + log.info('IPTABLES: DOCKER-USER table flushed'); + } catch (err) { + log.error(`IPTABLES: Error flushing DOCKER-USER table. ${err}`); + return false; + } + + // add for legacy apps + fluxNetworkInterfaces.push('docker0'); + + // eslint-disable-next-line no-restricted-syntax + for (const int of fluxNetworkInterfaces) { + // if this errors, we need to bail, as if the deny succeedes, we may cut off access + const giveFluxNetworkAccess = baseAllowToFluxNetworksCmd.replace(/#INT/g, int); + try { + // eslint-disable-next-line no-await-in-loop + await cmdAsync(giveFluxNetworkAccess); + log.info(`IPTABLES: Traffic on Flux interface ${int} accepted`); + } catch (err) { + log.error(`IPTABLES: Error allowing traffic on Flux interface ${int}. ${err}`); + return false; + } + } + + // eslint-disable-next-line no-restricted-syntax + for (const network of rfc1918Networks) { + // if any of these error, we need to bail, as if the deny succeedes, we may cut off access + + const giveHostAccessToDockerNetwork = baseAllowEstablishedCmd.replace('#DST', network); + try { + // eslint-disable-next-line no-await-in-loop + await cmdAsync(giveHostAccessToDockerNetwork); + log.info(`IPTABLES: Access to Flux containers from ${network} accepted`); + } catch (err) { + log.error(`IPTABLES: Error allowing access to Flux containers from ${network}. ${err}`); + return false; + } + + const giveContainerAccessToDNS = baseAllowDnsCmd.replace('#DST', network); + try { + // eslint-disable-next-line no-await-in-loop + await cmdAsync(giveContainerAccessToDNS); + log.info(`IPTABLES: DNS access to ${network} from Flux containers accepted`); + } catch (err) { + log.error(`IPTABLES: Error allowing DNS access to ${network} from Flux containers. ${err}`); + return false; + } + + // This always gets appended, so the drop is at the end + const dropAccessToHostNetwork = baseDropCmd.replace('#DST', network); + try { + // eslint-disable-next-line no-await-in-loop + await cmdAsync(dropAccessToHostNetwork); + log.info(`IPTABLES: Access to ${network} from Flux containers removed`); + } catch (err) { + log.error(`IPTABLES: Error denying access to ${network} from Flux containers. ${err}`); + return false; + } } + + try { + await cmdAsync(addReturnCmd); + log.info('IPTABLES: DOCKER-USER explicit return to FORWARD chain added'); + } catch (err) { + log.error(`IPTABLES: Error adding explicit return to Forward chain. ${err}`); + return false; + } + return true; } const lruRateOptions = { @@ -1492,5 +1645,5 @@ module.exports = { isPortUserBlocked, allowNodeToBindPrivilegedPorts, installNetcat, - removeDockerContainerAccessToHost, + removeDockerContainerAccessToNonRoutable, }; diff --git a/ZelBack/src/services/serviceHelper.js b/ZelBack/src/services/serviceHelper.js index 85dc168ae..efe354839 100644 --- a/ZelBack/src/services/serviceHelper.js +++ b/ZelBack/src/services/serviceHelper.js @@ -200,6 +200,42 @@ function commandStringToArray(command) { return splitargs(command); } +/** + * + * @param {*} ip ip address to check + * @returns {Boolean} + */ +function validIpv4Address(ip) { + // first octet must start with 1-9, then next 3 can be 0. + const ipv4Regex = /^[1-9]\d{0,2}\.(\d{0,3}\.){2}\d{0,3}$/; + + if (!ipv4Regex.test(ip)) return false; + + const octets = ip.split('.'); + const isValid = octets.every((octet) => parseInt(octet, 10) < 256); + return isValid; +} + +/** + * To confirm if ip is in subnet + * @param {string} ip + * @param {string} subnet + * @returns {Boolean} + */ +function ipInSubnet(ip, subnet) { + const [network, mask] = subnet.split('/'); + + if (!validIpv4Address(ip) || !validIpv4Address(network)) return false; + + // eslint-disable-next-line no-bitwise + const ipAsInt = Number(ip.split('.').reduce((ipInt, octet) => (ipInt << 8) + parseInt(octet || 0, 10), 0)); + // eslint-disable-next-line no-bitwise + const networkAsInt = Number(network.split('.').reduce((ipInt, octet) => (ipInt << 8) + parseInt(octet || 0, 10), 0)); + const maskAsInt = parseInt('1'.repeat(mask) + '0'.repeat(32 - mask), 2); + // eslint-disable-next-line no-bitwise + return (ipAsInt & maskAsInt) === (networkAsInt & maskAsInt); +} + module.exports = { ensureBoolean, ensureNumber, @@ -212,4 +248,6 @@ module.exports = { isDecimalLimit, dockerBufferToString, commandStringToArray, + validIpv4Address, + ipInSubnet, }; diff --git a/ZelBack/src/services/serviceManager.js b/ZelBack/src/services/serviceManager.js index c9c6fefef..6a0f3b049 100644 --- a/ZelBack/src/services/serviceManager.js +++ b/ZelBack/src/services/serviceManager.js @@ -14,6 +14,7 @@ const geolocationService = require('./geolocationService'); const upnpService = require('./upnpService'); const syncthingService = require('./syncthingService'); const pgpService = require('./pgpService'); +const dockerService = require('./dockerService'); const apiPort = userconfig.initial.apiport || config.server.apiport; const development = userconfig.initial.development || false; @@ -91,10 +92,11 @@ async function startFluxFunctions() { setTimeout(() => { fluxCommunicationUtils.constantlyUpdateDeterministicFluxList(); // updates deterministic flux list for communication every 2 minutes, so we always trigger cache and have up to date value }, 15 * 1000); - setTimeout(() => { + setTimeout(async () => { log.info('Rechecking firewall app rules'); fluxNetworkHelper.purgeUFW(); - fluxNetworkHelper.removeDockerContainerAccessToHost(); + const fluxNetworkInterfaces = await dockerService.getFluxDockerNetworkPhysicalInterfaceNames(); + fluxNetworkHelper.removeDockerContainerAccessToNonRoutable(fluxNetworkInterfaces); appsService.testAppMount(); // test if our node can mount a volume }, 30 * 1000); setTimeout(() => { diff --git a/tests/unit/fluxNetworkHelper.test.js b/tests/unit/fluxNetworkHelper.test.js index 31a4bc859..a475298e8 100644 --- a/tests/unit/fluxNetworkHelper.test.js +++ b/tests/unit/fluxNetworkHelper.test.js @@ -3,9 +3,9 @@ global.userconfig = require('../../config/userconfig'); const chai = require('chai'); const sinon = require('sinon'); const WebSocket = require('ws'); -const proxyquire = require('proxyquire'); const path = require('path'); const chaiAsPromised = require('chai-as-promised'); +const proxyquire = require('proxyquire'); const fs = require('fs').promises; const util = require('util'); const log = require('../../ZelBack/src/lib/log'); @@ -18,6 +18,7 @@ const daemonServiceFluxnodeRpcs = require('../../ZelBack/src/services/daemonServ const fluxCommunicationUtils = require('../../ZelBack/src/services/fluxCommunicationUtils'); const benchmarkService = require('../../ZelBack/src/services/benchmarkService'); const verificationHelper = require('../../ZelBack/src/services/verificationHelper'); + const { outgoingConnections, outgoingPeers, incomingPeers, incomingConnections, } = require('../../ZelBack/src/services/utils/establishedConnections'); @@ -2018,4 +2019,301 @@ describe('fluxNetworkHelper tests', () => { expect(fluxUptime.data).to.be.lte(utb); }); }); + + describe('remove flux container access to private address space tests', () => { + let utilStub; + let funcStub; + let infoLogSpy; + let errorLogSpy; + beforeEach(() => { + // hide console output from logs, but still get logging spy + sinon.stub(console, 'log'); + utilStub = sinon.stub(util, 'promisify'); + infoLogSpy = sinon.spy(log, 'info'); + errorLogSpy = sinon.spy(log, 'error'); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should return false if the iptables binary does not exist', async () => { + funcStub = sinon.fake(async (cmd) => { + // chain doesn't exists + if (cmd.includes('sudo iptables --version')) { + throw new Error(); + } + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + expect(result).to.eql(false); + sinon.assert.calledOnceWithExactly(funcStub, 'sudo iptables --version'); + sinon.assert.calledWith(errorLogSpy, 'Unable to find iptables binary'); + + }) + + it('should add the DOCKER-USER chain to iptables if it is missing', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } + else if (cmd.includes('-L')) { + // chain doesn't exists + throw new Error(); + } + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + expect(result).to.eql(true); + + sinon.assert.calledWith(funcStub, 'sudo iptables -L DOCKER-USER'); + sinon.assert.calledWith(funcStub, 'sudo iptables -N DOCKER-USER'); + sinon.assert.calledWith(infoLogSpy, 'IPTABLES: DOCKER-USER chain created'); + sinon.assert.notCalled(errorLogSpy); + }); + + it('should skip addding the DOCKER-USER chain to iptables if it already exists', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } + else if (cmd.includes('-L')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } + return undefined; + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(true); + sinon.assert.calledWith(funcStub, 'sudo iptables -L DOCKER-USER'); + sinon.assert.neverCalledWith(funcStub, 'sudo iptables -N DOCKER-USER'); + sinon.assert.calledWith(infoLogSpy, 'IPTABLES: DOCKER-USER chain already created'); + sinon.assert.notCalled(errorLogSpy); + }); + + it('should bail out if there is an error addding the DOCKER-USER chain to iptables', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else { + // throw for both -L and -N (throwing on -L is normal) + throw new Error(); + } + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(false); + sinon.assert.calledWith(funcStub, 'sudo iptables -L DOCKER-USER'); + sinon.assert.calledWith(funcStub, 'sudo iptables -N DOCKER-USER'); + sinon.assert.notCalled(infoLogSpy); + sinon.assert.calledOnceWithExactly(errorLogSpy, 'IPTABLES: Error adding DOCKER-USER chain'); + }); + + it('should add the jump to DOCKER-USER chain from FORWARD chain to iptables if it is missing', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + // chain doesn't exists + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } else if (cmd.includes('sudo iptables -C FORWARD -j DOCKER-USER && echo true')) { + throw new Error('iptables: Bad rule (does a matching rule exist in that chain?).'); + } else { + return 'DOCKER-USER all opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0'; + } + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(true); + sinon.assert.calledWith(funcStub, 'sudo iptables -C FORWARD -j DOCKER-USER && echo true'); + sinon.assert.calledWith(funcStub, 'sudo iptables -I FORWARD -j DOCKER-USER'); + + sinon.assert.calledWith(infoLogSpy, 'IPTABLES: New rule in FORWARD inserted to jump to DOCKER-USER chain'); + sinon.assert.notCalled(errorLogSpy); + }); + + it('should skip adding the jump to DOCKER-USER chain from FORWARD chain to iptables if it already exists', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } + return 'DOCKER-USER all opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0'; + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(true); + sinon.assert.neverCalledWith(funcStub, 'sudo iptables -I FORWARD -j DOCKER-USER'); + + sinon.assert.calledWith(infoLogSpy, 'IPTABLES: Jump to DOCKER-USER chain already enabled'); + sinon.assert.notCalled(errorLogSpy); + }); + + it('should bail out if there is an error addding the DOCKER-USER chain to iptables', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } else if (cmd.includes('sudo iptables -C FORWARD -j DOCKER-USER')) { + throw new Error('iptables: Bad rule (does a matching rule exist in that chain?).'); + } else { + throw new Error(); + } + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(false); + sinon.assert.calledWith(funcStub, 'sudo iptables -C FORWARD -j DOCKER-USER && echo true'); + sinon.assert.calledWith(funcStub, 'sudo iptables -I FORWARD -j DOCKER-USER'); + + sinon.assert.neverCalledWith(infoLogSpy, 'IPTABLES: New rule in FORWARD inserted to jump to DOCKER-USER chain'); + expect(infoLogSpy.callCount).to.eql(1); + sinon.assert.calledOnceWithExactly(errorLogSpy, 'IPTABLES: Error inserting FORWARD jump to DOCKER-USER chain'); + }); + + it('should flush the DOCKER-USER chain', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } + return undefined; + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(true); + sinon.assert.calledWith(funcStub, 'sudo iptables -F DOCKER-USER'); + sinon.assert.neverCalledWith(errorLogSpy); + }); + + it('should bail out if there is an error flushing the DOCKER-USER chain', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } else if (cmd.includes('sudo iptables -C FORWARD -j DOCKER-USER && echo true')) { + return 'DOCKER-USER all opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0'; + } + throw new Error(); + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(false); + sinon.assert.calledWith(funcStub, 'sudo iptables -F DOCKER-USER'); + sinon.assert.calledOnceWithExactly(errorLogSpy, 'IPTABLES: Error flushing DOCKER-USER table. Error'); + expect(funcStub.callCount).to.eql(4); + }); + + it('should add two allow and one drop rule for each private network', async () => { + const networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']; + + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } + return null; + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(true); + + // eslint-disable-next-line no-restricted-syntax + for (const network of networks) { + sinon.assert.calledWith(funcStub, `sudo iptables -I DOCKER-USER -s 172.23.0.0/16 -d ${network} -p udp --dport 53 -j ACCEPT`); + sinon.assert.calledWith(funcStub, `sudo iptables -I DOCKER-USER -s 172.23.0.0/16 -d ${network} -m state --state RELATED,ESTABLISHED -j ACCEPT`); + sinon.assert.calledWith(funcStub, `sudo iptables -A DOCKER-USER -s 172.23.0.0/16 -d ${network} -j DROP`); + } + + // 1 for the CHAIN rules, 1 FLUSH, 1 docker0 allow, 9 for the adds and 1 for the RETURN + expect(infoLogSpy.callCount).to.eql(13); + sinon.assert.notCalled(errorLogSpy); + }); + + it('should add an allow for intra-network traffic per docker network', async () => { + const interfaces = ['br-aaf87aa57b20', 'br-098bac43a7f1']; + + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } + return null; + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable(interfaces); + + expect(result).to.eql(true); + + // eslint-disable-next-line no-restricted-syntax + for (const int of interfaces) { + sinon.assert.calledWith(funcStub, `sudo iptables -I DOCKER-USER -i ${int} -o ${int} -j ACCEPT`); + } + sinon.assert.calledWith(funcStub, 'sudo iptables -I DOCKER-USER -i docker0 -o docker0 -j ACCEPT'); + + // 1 for the CHAIN rules, 1 FLUSH, 1 docker0 allow 2 interface allows, 9 for the adds and 1 for the RETURN + expect(infoLogSpy.callCount).to.eql(15); + sinon.assert.notCalled(errorLogSpy); + }); + + it('should bail out as soon as a rule errors out', async () => { + funcStub = sinon.fake(async (cmd) => { + if (cmd.includes('sudo iptables --version')) { + return 'iptables v1.8.7 (nf_tables)' + } else if (cmd.includes('sudo iptables -L DOCKER-USER')) { + return `Chain DOCKER-USER (0 references) + target prot opt source destination`; + } if (cmd.includes('sudo iptables -C FORWARD -j DOCKER-USER')) { + return 'DOCKER-USER all opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0'; + } if (cmd.includes('sudo iptables -F DOCKER-USER')) { + // this is the rule under test + return undefined; + } if (cmd.includes('sudo iptables -I DOCKER-USER -i docker0 -o docker0 -j ACCEPT')) { + throw new Error(); + } + return undefined; + }); + utilStub.returns(funcStub); + + const result = await fluxNetworkHelper.removeDockerContainerAccessToNonRoutable([]); + + expect(result).to.eql(false); + expect(funcStub.callCount).to.eql(5); + + sinon.assert.calledOnceWithExactly(errorLogSpy, 'IPTABLES: Error allowing traffic on Flux interface docker0. Error'); + }); + }); }); From 43c9c8f02f99d3d39bab728d22d3a4256043c2a5 Mon Sep 17 00:00:00 2001 From: Cabecinha84 Date: Wed, 21 Feb 2024 11:42:30 +0000 Subject: [PATCH 02/30] Try different local docker network addresses for the app network before failing app install --- ZelBack/src/services/appsService.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/ZelBack/src/services/appsService.js b/ZelBack/src/services/appsService.js index 099d442a6..bee654c3b 100644 --- a/ZelBack/src/services/appsService.js +++ b/ZelBack/src/services/appsService.js @@ -3386,9 +3386,17 @@ async function registerAppLocally(appSpecs, componentSpecs, res) { if (res) { res.write(serviceHelper.ensureString(fluxNetworkStatus)); } - const fluxNet = await dockerService.createFluxAppDockerNetwork(appName, dockerNetworkAddrValue).catch((error) => log.error(error)); + let fluxNet = null; + for (let i = 0; i <= 20; i += 1) { + // eslint-disable-next-line no-await-in-loop + fluxNet = await dockerService.createFluxAppDockerNetwork(appName, dockerNetworkAddrValue).catch((error) => log.error(error)); + if (fluxNet || appsThatMightBeUsingOldGatewayIpAssignment.includes(appName)) { + break; + } + dockerNetworkAddrValue = Math.floor(Math.random() * 256); + } if (!fluxNet) { - throw new Error(`Flux App network of ${appName} failed to initiate. Range already assigned to different application.`); + throw new Error(`Flux App network of ${appName} failed to initiate. Not possible to create docker application network.`); } log.info(serviceHelper.ensureString(fluxNet)); const fluxNetworkInterfaces = await dockerService.getFluxDockerNetworkPhysicalInterfaceNames(); @@ -3767,9 +3775,17 @@ async function softRegisterAppLocally(appSpecs, componentSpecs, res) { if (res) { res.write(serviceHelper.ensureString(fluxNetworkStatus)); } - const fluxNet = await dockerService.createFluxAppDockerNetwork(appName, dockerNetworkAddrValue).catch((error) => log.error(error)); + let fluxNet = null; + for (let i = 0; i <= 20; i += 1) { + // eslint-disable-next-line no-await-in-loop + fluxNet = await dockerService.createFluxAppDockerNetwork(appName, dockerNetworkAddrValue).catch((error) => log.error(error)); + if (fluxNet || appsThatMightBeUsingOldGatewayIpAssignment.includes(appName)) { + break; + } + dockerNetworkAddrValue = Math.floor(Math.random() * 256); + } if (!fluxNet) { - throw new Error(`Flux App network of ${appName} failed to initiate. Range already assigned to different application`); + throw new Error(`Flux App network of ${appName} failed to initiate. Not possible to create docker application network.`); } log.info(serviceHelper.ensureString(fluxNet)); const fluxNetworkInterfaces = await dockerService.getFluxDockerNetworkPhysicalInterfaceNames(); From 099215ca5dc1145f2442260dd0d7acf24de34d2a Mon Sep 17 00:00:00 2001 From: Cabecinha84 Date: Thu, 22 Feb 2024 12:20:28 +0000 Subject: [PATCH 03/30] Fix reverted changes by accident on v4.31.0 --- HomeUI/src/views/apps/Management.vue | 76 +++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/HomeUI/src/views/apps/Management.vue b/HomeUI/src/views/apps/Management.vue index f09754a46..be71977dc 100644 --- a/HomeUI/src/views/apps/Management.vue +++ b/HomeUI/src/views/apps/Management.vue @@ -108,8 +108,8 @@ :number="callResponse.data.height + (callResponse.data.expire || 22000)" />
- + +
+ +
+ +
+
+
{{ getExpireLabel || (appUpdateSpecification.expire ? `${appUpdateSpecification.expire} blocks` : '1 month') }}
@@ -3632,7 +3659,7 @@ :max="5" :step="1" /> - +

Cont. Data @@ -4534,7 +4561,7 @@