balena-supervisor/test/lib/mocked-iptables.ts
Christina Ying Wang 84a9e7e9ac Replace BALENA-FIREWALL rule in INPUT chain instead of flushing
The issue with the original Supervisor implementation of the firewall is that
on Supervisor start, the Supervisor flushes the INPUT chain of the filter table.
This doesn't play well with services that add to the INPUT chain on startup that
may start up before the Supervisor, such as certain NetworkManager connection
profiles. This change only replaces the BALENA-FIREWALL rule in the INPUT chain,
preserving the other rules as well as their order.

Closes: #1482
Change-type: patch
Signed-off-by: Christina Ying Wang <christina@balena.io>
2023-03-01 13:42:07 -08:00

184 lines
4.4 KiB
TypeScript

import _ = require('lodash');
import { expect } from 'chai';
import { stub } from 'sinon';
import * as childProcess from 'child_process';
import * as firewall from '~/lib/firewall';
import * as iptables from '~/lib/iptables';
import { EventEmitter } from 'events';
import { Writable } from 'stream';
export enum RuleProperty {
NotSet,
}
export type Testable<T> = { [P in keyof T]?: T[P] | RuleProperty | undefined };
class FakeRuleAdaptor {
private rules: iptables.Rule[];
constructor() {
this.rules = [];
}
public getRuleAdaptor(): iptables.RuleAdaptor {
return this.ruleAdaptor.bind(this);
}
private async ruleAdaptor(rules: iptables.Rule[]): Promise<void> {
const handleRule = async (rule: iptables.Rule) => {
// remove any undefined values from the object...
for (const key of Object.getOwnPropertyNames(rule)) {
if ((rule as any)[key] === undefined) {
delete (rule as any)[key];
}
}
this.rules.push(rule);
return '';
};
if (_.isArray(rules)) {
for (const rule of rules) {
await handleRule(rule);
}
}
}
private isSameRule(
testable: Testable<iptables.Rule>,
rule: iptables.Rule,
): boolean {
const props = Object.getOwnPropertyNames(testable);
for (const prop of props) {
if (
_.get(testable, prop) === RuleProperty.NotSet &&
_.get(rule, prop) === undefined
) {
return true;
}
if (
_.get(rule, prop) === undefined ||
!_.isEqual(_.get(rule, prop), _.get(testable, prop))
) {
return false;
}
}
return true;
}
public expectRule(testRule: Testable<iptables.Rule>) {
const matchingIndex = (() => {
for (let i = 0; i < this.rules.length; i++) {
if (this.isSameRule(testRule, this.rules[i])) {
return i;
}
}
return -1;
})();
if (matchingIndex < 0) {
console.log({ testRule, rules: this.rules });
}
expect(matchingIndex).to.be.greaterThan(-1, `Rule has not been applied`);
return matchingIndex;
}
public expectNoRule(testRule: Testable<iptables.Rule>) {
return expect(
_.some(this.rules, (r) => this.isSameRule(testRule, r)),
).to.eq(
false,
`Rule has been applied: ${JSON.stringify(testRule)}\n\n${JSON.stringify(
this.rules,
null,
2,
)}`,
);
}
public clearHistory() {
this.rules = [];
}
}
export const realRuleAdaptor = iptables.getDefaultRuleAdaptor();
const fakeRuleAdaptorManager = new FakeRuleAdaptor();
export const fakeRuleAdaptor = fakeRuleAdaptorManager.getRuleAdaptor();
// @ts-expect-error Assigning to a RO property
iptables.getDefaultRuleAdaptor = () => {
return fakeRuleAdaptor;
};
export interface MockedState {
hasAppliedRules: Promise<void>;
expectRule: (rule: Testable<iptables.Rule>) => number;
expectNoRule: (rule: Testable<iptables.Rule>) => void;
clearHistory: () => void;
}
export type MockedContext = (state: MockedState) => Promise<any>;
const applyFirewallRules = firewall.applyFirewallMode;
export const whilstMocked = async (
context: MockedContext,
ruleAdaptor: iptables.RuleAdaptor = fakeRuleAdaptor,
) => {
const getOriginalDefaultRuleAdaptor = iptables.getDefaultRuleAdaptor;
const spawnStub = stub(childProcess, 'spawn').callsFake(() => {
const fakeProc = new EventEmitter();
(fakeProc as any).stdout = new EventEmitter();
const stdin = new Writable();
stdin._write = (
_chunk: Buffer,
_encoding: string,
callback: (err?: Error) => void,
) => {
// console.log(chunk.toString('utf8'));
callback();
fakeProc.emit('close', 1);
};
(fakeProc as any).stdin = stdin;
return fakeProc as any;
});
// @ts-expect-error Assigning to a RO property
iptables.getDefaultRuleAdaptor = () => {
return ruleAdaptor;
};
fakeRuleAdaptorManager.clearHistory();
const applied = new EventEmitter();
// @ts-expect-error Assigning to a RO property
firewall.applyFirewallMode = async (mode: string) => {
await applyFirewallRules(mode);
applied.emit('applied');
};
await context({
expectRule: (rule) => fakeRuleAdaptorManager.expectRule(rule),
expectNoRule: (rule) => fakeRuleAdaptorManager.expectNoRule(rule),
clearHistory: () => fakeRuleAdaptorManager.clearHistory(),
hasAppliedRules: new Promise((resolve) => {
applied.once('applied', () => resolve());
}),
});
// @ts-expect-error Assigning to a RO property
firewall.applyFirewallMode = applyFirewallRules;
spawnStub.restore();
// @ts-expect-error Assigning to a RO property
iptables.getDefaultRuleAdaptor = getOriginalDefaultRuleAdaptor;
};