mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2024-12-28 01:28:52 +00:00
84a9e7e9ac
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>
184 lines
4.4 KiB
TypeScript
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;
|
|
};
|