mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-01-22 04:18:10 +00:00
Merge pull request #2135 from balena-os/fix-iptables-input-flush
Replace BALENA-FIREWALL rule in INPUT chain instead of flushing
This commit is contained in:
commit
d06b8b7de8
@ -170,6 +170,18 @@ export async function applyFirewallMode(mode: string) {
|
|||||||
}
|
}
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// Get position of BALENA-FIREWALL rule in the INPUT chain for both iptables & ip6tables
|
||||||
|
const v4Position = await iptables.getRulePosition(
|
||||||
|
'INPUT',
|
||||||
|
'BALENA-FIREWALL',
|
||||||
|
4,
|
||||||
|
);
|
||||||
|
const v6Position = await iptables.getRulePosition(
|
||||||
|
'INPUT',
|
||||||
|
'BALENA-FIREWALL',
|
||||||
|
6,
|
||||||
|
);
|
||||||
|
|
||||||
// get an adaptor to manipulate iptables rules...
|
// get an adaptor to manipulate iptables rules...
|
||||||
const ruleAdaptor = iptables.getDefaultRuleAdaptor();
|
const ruleAdaptor = iptables.getDefaultRuleAdaptor();
|
||||||
|
|
||||||
@ -178,6 +190,37 @@ export async function applyFirewallMode(mode: string) {
|
|||||||
.build()
|
.build()
|
||||||
.forTable('filter', (filter) =>
|
.forTable('filter', (filter) =>
|
||||||
filter
|
filter
|
||||||
|
.forChain('INPUT', (chain) => {
|
||||||
|
// Delete & insert to v4 tables
|
||||||
|
if (v4Position !== -1) {
|
||||||
|
chain.addRule({
|
||||||
|
action: iptables.RuleAction.Delete,
|
||||||
|
target: 'BALENA-FIREWALL',
|
||||||
|
family: 4,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
chain.addRule({
|
||||||
|
action: iptables.RuleAction.Insert,
|
||||||
|
id: v4Position > 0 ? v4Position : 1,
|
||||||
|
target: 'BALENA-FIREWALL',
|
||||||
|
family: 4,
|
||||||
|
});
|
||||||
|
// Delete & insert to v6 tables
|
||||||
|
if (v6Position !== -1) {
|
||||||
|
chain.addRule({
|
||||||
|
action: iptables.RuleAction.Delete,
|
||||||
|
target: 'BALENA-FIREWALL',
|
||||||
|
family: 6,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
chain.addRule({
|
||||||
|
action: iptables.RuleAction.Insert,
|
||||||
|
id: v6Position > 0 ? v6Position : 1,
|
||||||
|
target: 'BALENA-FIREWALL',
|
||||||
|
family: 6,
|
||||||
|
});
|
||||||
|
return chain;
|
||||||
|
})
|
||||||
.forChain(BALENA_FIREWALL_CHAIN, (chain) =>
|
.forChain(BALENA_FIREWALL_CHAIN, (chain) =>
|
||||||
chain
|
chain
|
||||||
.addRule(prepareChain)
|
.addRule(prepareChain)
|
||||||
@ -190,16 +233,6 @@ export async function applyFirewallMode(mode: string) {
|
|||||||
action: iptables.RuleAction.Append,
|
action: iptables.RuleAction.Append,
|
||||||
target: 'REJECT',
|
target: 'REJECT',
|
||||||
}),
|
}),
|
||||||
)
|
|
||||||
.forChain('INPUT', (chain) =>
|
|
||||||
chain
|
|
||||||
.addRule({
|
|
||||||
action: iptables.RuleAction.Flush,
|
|
||||||
})
|
|
||||||
.addRule({
|
|
||||||
action: iptables.RuleAction.Append,
|
|
||||||
target: 'BALENA-FIREWALL',
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.apply(ruleAdaptor);
|
.apply(ruleAdaptor);
|
||||||
|
@ -3,6 +3,9 @@ import { spawn } from 'child_process';
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
import { TypedError } from 'typed-error';
|
import { TypedError } from 'typed-error';
|
||||||
|
|
||||||
|
import { exec } from './fs-utils';
|
||||||
|
import log from './supervisor-console';
|
||||||
|
|
||||||
export class IPTablesRuleError extends TypedError {
|
export class IPTablesRuleError extends TypedError {
|
||||||
public constructor(err: string | Error, public ruleset: string) {
|
public constructor(err: string | Error, public ruleset: string) {
|
||||||
super(err);
|
super(err);
|
||||||
@ -13,6 +16,7 @@ export enum RuleAction {
|
|||||||
Insert = '-I',
|
Insert = '-I',
|
||||||
Append = '-A',
|
Append = '-A',
|
||||||
Flush = '-F',
|
Flush = '-F',
|
||||||
|
Delete = '-D',
|
||||||
}
|
}
|
||||||
export interface Rule {
|
export interface Rule {
|
||||||
id?: number;
|
id?: number;
|
||||||
@ -96,6 +100,14 @@ export function convertToRestoreRulesFormat(rules: Rule[]): string {
|
|||||||
}
|
}
|
||||||
if (rule.chain) {
|
if (rule.chain) {
|
||||||
args.push(rule.chain);
|
args.push(rule.chain);
|
||||||
|
// Optionally push a rule to a specific position in the chain
|
||||||
|
if (
|
||||||
|
(rule.action === RuleAction.Insert ||
|
||||||
|
rule.action === RuleAction.Delete) &&
|
||||||
|
rule.id
|
||||||
|
) {
|
||||||
|
args.push(rule.id?.toString() ?? '1');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (rule.proto) {
|
if (rule.proto) {
|
||||||
args.push(`-p ${rule.proto}`);
|
args.push(`-p ${rule.proto}`);
|
||||||
@ -291,3 +303,34 @@ async function applyRules(rules: Rule | Rule[], adaptor: RuleAdaptor) {
|
|||||||
|
|
||||||
await adaptor(processedRules);
|
await adaptor(processedRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StdError extends TypedError {}
|
||||||
|
|
||||||
|
export async function getRulePosition(
|
||||||
|
chain: string,
|
||||||
|
ruleMatch: string,
|
||||||
|
family: 4 | 6,
|
||||||
|
): Promise<number> {
|
||||||
|
const cmd = `ip${
|
||||||
|
family === 6 ? '6' : ''
|
||||||
|
}tables -L ${chain} -n --line-numbers`;
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await exec(cmd);
|
||||||
|
if (stderr) {
|
||||||
|
throw new StdError(stderr);
|
||||||
|
}
|
||||||
|
const line = stdout.split('\n').find((l) => l.includes(ruleMatch));
|
||||||
|
if (!line) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return parseInt(line.split(' ')[0], 10);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
log.error(
|
||||||
|
`Received ${
|
||||||
|
e instanceof StdError ? 'stderr' : 'error'
|
||||||
|
} querying iptables ${chain} chain:`,
|
||||||
|
(e as Error).message ?? e ?? 'Unknown error',
|
||||||
|
);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,6 +10,7 @@ import * as iptablesMock from '~/test-lib/mocked-iptables';
|
|||||||
import * as dbFormat from '~/src/device-state/db-format';
|
import * as dbFormat from '~/src/device-state/db-format';
|
||||||
|
|
||||||
import constants = require('~/lib/constants');
|
import constants = require('~/lib/constants');
|
||||||
|
import * as iptables from '~/lib/iptables';
|
||||||
import { RuleAction, Rule } from '~/lib/iptables';
|
import { RuleAction, Rule } from '~/lib/iptables';
|
||||||
import { log } from '~/lib/supervisor-console';
|
import { log } from '~/lib/supervisor-console';
|
||||||
|
|
||||||
@ -86,7 +87,7 @@ describe('lib/firewall', function () {
|
|||||||
|
|
||||||
// expect that we jump to the firewall chain...
|
// expect that we jump to the firewall chain...
|
||||||
expectRule({
|
expectRule({
|
||||||
action: RuleAction.Append,
|
action: RuleAction.Insert,
|
||||||
target: 'BALENA-FIREWALL',
|
target: 'BALENA-FIREWALL',
|
||||||
chain: 'INPUT',
|
chain: 'INPUT',
|
||||||
family: 4,
|
family: 4,
|
||||||
@ -122,7 +123,7 @@ describe('lib/firewall', function () {
|
|||||||
|
|
||||||
// expect that we jump to the firewall chain...
|
// expect that we jump to the firewall chain...
|
||||||
expectRule({
|
expectRule({
|
||||||
action: RuleAction.Append,
|
action: RuleAction.Insert,
|
||||||
target: 'BALENA-FIREWALL',
|
target: 'BALENA-FIREWALL',
|
||||||
chain: 'INPUT',
|
chain: 'INPUT',
|
||||||
family: 4,
|
family: 4,
|
||||||
@ -179,7 +180,7 @@ describe('lib/firewall', function () {
|
|||||||
|
|
||||||
// expect that we jump to the firewall chain...
|
// expect that we jump to the firewall chain...
|
||||||
expectRule({
|
expectRule({
|
||||||
action: RuleAction.Append,
|
action: RuleAction.Insert,
|
||||||
target: 'BALENA-FIREWALL',
|
target: 'BALENA-FIREWALL',
|
||||||
chain: 'INPUT',
|
chain: 'INPUT',
|
||||||
family: 4,
|
family: 4,
|
||||||
@ -238,7 +239,7 @@ describe('lib/firewall', function () {
|
|||||||
|
|
||||||
// expect that we jump to the firewall chain...
|
// expect that we jump to the firewall chain...
|
||||||
expectRule({
|
expectRule({
|
||||||
action: RuleAction.Append,
|
action: RuleAction.Insert,
|
||||||
target: 'BALENA-FIREWALL',
|
target: 'BALENA-FIREWALL',
|
||||||
chain: 'INPUT',
|
chain: 'INPUT',
|
||||||
family: 4,
|
family: 4,
|
||||||
@ -269,6 +270,65 @@ describe('lib/firewall', function () {
|
|||||||
expect(loggerSpy.called).to.be.true;
|
expect(loggerSpy.called).to.be.true;
|
||||||
}, iptablesMock.realRuleAdaptor);
|
}, iptablesMock.realRuleAdaptor);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: The way that iptables & mocked-iptables are implemented does not
|
||||||
|
// translate to well-written, side-effect free tests. While this test passes,
|
||||||
|
// it's not able to check that BALENA-FIREWALL was inserted at the correct
|
||||||
|
// position in the INPUT rule chain.
|
||||||
|
// We should refactor the test setup for iptables to write rules to a file,
|
||||||
|
// and test whether that file can be applied successfully with `iptables-restore --test`.
|
||||||
|
it('should only replace the BALENA-FIREWALL rule from INPUT chain', async () => {
|
||||||
|
const acceptRuleOnPort = (port: number) => ({
|
||||||
|
action: RuleAction.Append,
|
||||||
|
proto: 'tcp',
|
||||||
|
matches: [`--dport ${port}`],
|
||||||
|
target: 'ACCEPT',
|
||||||
|
});
|
||||||
|
|
||||||
|
await iptablesMock.whilstMocked(
|
||||||
|
async ({ hasAppliedRules, expectRule }) => {
|
||||||
|
// clear the spies...
|
||||||
|
loggerSpy.resetHistory();
|
||||||
|
logSpy.resetHistory();
|
||||||
|
|
||||||
|
// Add some rules to INPUT: 3 unrelated rules,
|
||||||
|
// and BALENA-FIREWALL at position 4.
|
||||||
|
await iptables
|
||||||
|
.build()
|
||||||
|
.forTable('filter', (filter) =>
|
||||||
|
filter.forChain('INPUT', (chain) =>
|
||||||
|
chain
|
||||||
|
.addRule(acceptRuleOnPort(80))
|
||||||
|
.addRule(acceptRuleOnPort(443))
|
||||||
|
.addRule(acceptRuleOnPort(22))
|
||||||
|
.addRule({
|
||||||
|
action: RuleAction.Append,
|
||||||
|
target: 'BALENA-FIREWALL',
|
||||||
|
chain: 'INPUT',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.apply(iptablesMock.fakeRuleAdaptor);
|
||||||
|
|
||||||
|
// set the firewall to be in off mode...
|
||||||
|
await config.set({ firewallMode: 'off' });
|
||||||
|
await hasAppliedRules;
|
||||||
|
|
||||||
|
// Unrelated rules should not have been flushed
|
||||||
|
expectRule(acceptRuleOnPort(80));
|
||||||
|
expectRule(acceptRuleOnPort(443));
|
||||||
|
expectRule(acceptRuleOnPort(22));
|
||||||
|
// TODO: this should be testable but isn't because of the reason above.
|
||||||
|
// expectRule({
|
||||||
|
// action: RuleAction.Insert,
|
||||||
|
// target: 'BALENA-FIREWALL',
|
||||||
|
// chain: 'INPUT',
|
||||||
|
// id: 4,
|
||||||
|
// });
|
||||||
|
},
|
||||||
|
iptablesMock.fakeRuleAdaptor,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Service rules', () => {
|
describe('Service rules', () => {
|
||||||
|
@ -107,7 +107,7 @@ class FakeRuleAdaptor {
|
|||||||
export const realRuleAdaptor = iptables.getDefaultRuleAdaptor();
|
export const realRuleAdaptor = iptables.getDefaultRuleAdaptor();
|
||||||
|
|
||||||
const fakeRuleAdaptorManager = new FakeRuleAdaptor();
|
const fakeRuleAdaptorManager = new FakeRuleAdaptor();
|
||||||
const fakeRuleAdaptor = fakeRuleAdaptorManager.getRuleAdaptor();
|
export const fakeRuleAdaptor = fakeRuleAdaptorManager.getRuleAdaptor();
|
||||||
|
|
||||||
// @ts-expect-error Assigning to a RO property
|
// @ts-expect-error Assigning to a RO property
|
||||||
iptables.getDefaultRuleAdaptor = () => {
|
iptables.getDefaultRuleAdaptor = () => {
|
||||||
@ -121,11 +121,11 @@ export interface MockedState {
|
|||||||
clearHistory: () => void;
|
clearHistory: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MockedConext = (state: MockedState) => Promise<any>;
|
export type MockedContext = (state: MockedState) => Promise<any>;
|
||||||
|
|
||||||
const applyFirewallRules = firewall.applyFirewallMode;
|
const applyFirewallRules = firewall.applyFirewallMode;
|
||||||
export const whilstMocked = async (
|
export const whilstMocked = async (
|
||||||
context: MockedConext,
|
context: MockedContext,
|
||||||
ruleAdaptor: iptables.RuleAdaptor = fakeRuleAdaptor,
|
ruleAdaptor: iptables.RuleAdaptor = fakeRuleAdaptor,
|
||||||
) => {
|
) => {
|
||||||
const getOriginalDefaultRuleAdaptor = iptables.getDefaultRuleAdaptor;
|
const getOriginalDefaultRuleAdaptor = iptables.getDefaultRuleAdaptor;
|
||||||
|
Loading…
Reference in New Issue
Block a user