Skip to content

Commit

Permalink
fix: private IP check on MTR (#61)
Browse files Browse the repository at this point in the history
* ref: run private ip check pre mtr query

* add: test private ip logic
  • Loading branch information
patrykcieszkowski authored Jun 17, 2022
1 parent 2301c86 commit 30115a0
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 46 deletions.
70 changes: 27 additions & 43 deletions src/command/mtr-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import {InvalidOptionsException} from './exception/invalid-options-exception.js'
import type {HopType, ResultType} from './handlers/mtr/types.js';
import MtrParser, {NEW_LINE_REG_EXP} from './handlers/mtr/parser.js';

type MtrOptions = {
export type MtrOptions = {
type: string;
target: string;
protocol: string;
port: number;
packets: number;
};

type DnsResolver = (addr: string, rrtype: string) => Promise<string[]>;
type DnsResolver = (addr: string, rrtype?: string) => Promise<string[]>;

const mtrOptionsSchema = Joi.object<MtrOptions>({
type: Joi.string().valid('mtr'),
Expand All @@ -28,7 +28,7 @@ const mtrOptionsSchema = Joi.object<MtrOptions>({
port: Joi.number(),
});

export const getResultInitState = () => ({hops: [], rawOutput: ''});
export const getResultInitState = () => ({hops: [], rawOutput: '', data: []});

export const mtrCmd = (options: MtrOptions): ExecaChildProcess => {
const protocolArg = options.protocol === 'icmp' ? null : options.protocol;
Expand Down Expand Up @@ -58,40 +58,22 @@ export const mtrCmd = (options: MtrOptions): ExecaChildProcess => {
export class MtrCommand implements CommandInterface<MtrOptions> {
constructor(private readonly cmd: typeof mtrCmd, readonly dnsResolver: DnsResolver = dns.promises.resolve) {}

async run(socket: Socket, measurementId: string, testId: string, options: unknown): Promise<void> {
async run(socket: Socket, measurementId: string, testId: string, options: MtrOptions): Promise<void> {
const {value: cmdOptions, error} = mtrOptionsSchema.validate(options);

if (error) {
throw new InvalidOptionsException('mtr', error);
}

const cmd = this.cmd(cmdOptions);
let isResultPrivate = false;
const result: ResultType = {
hops: [],
data: [],
rawOutput: '',
};

const emitProgress = () => {
isResultPrivate = !this.validateResult(result.hops, cmd);
const cmd = this.cmd(cmdOptions);
const result: ResultType = getResultInitState();

cmd.stdout?.on('data', async (data: Buffer) => {
if (isResultPrivate) {
return;
}

socket.emit('probe:measurement:progress', {
testId,
measurementId,
overwrite: true,
result: {
hops: result.hops,
rawOutput: result.rawOutput,
},
});
};

cmd.stdout?.on('data', async (data: Buffer) => {
for (const line of data.toString().split(NEW_LINE_REG_EXP)) {
if (!line) {
continue;
Expand All @@ -104,21 +86,36 @@ export class MtrCommand implements CommandInterface<MtrOptions> {
result.hops = hops;
result.rawOutput = rawOutput;

emitProgress();
socket.emit('probe:measurement:progress', {
testId,
measurementId,
overwrite: true,
result: {
hops: result.hops,
rawOutput: result.rawOutput,
},
});
});

try {
await this.checkForPrivateDest(options.target);

await cmd;
const [hops, rawOutput] = await this.parseResult(result.hops, result.data, true);
result.hops = hops;
result.rawOutput = rawOutput;
} catch (error: unknown) {
if (error instanceof Error && error.message === 'private destination') {
isResultPrivate = true;
}

const output = isExecaError(error) ? error.stderr.toString() : '';
result.rawOutput = output;
}

if (isResultPrivate) {
result.hops = [];
result.data = [];
result.rawOutput = 'Private IP ranges are not allowed';
}

Expand Down Expand Up @@ -191,24 +188,11 @@ export class MtrCommand implements CommandInterface<MtrOptions> {
return result.flat()[0];
}

private hasResultPrivateIp(hops: HopType[]): boolean {
const privateResults = hops.filter((hop: HopType) => isIpPrivate(hop?.host ?? ''));

if (privateResults.length > 0) {
return true;
}

return false;
}

private validateResult(hops: HopType[], cmd: ExecaChildProcess): boolean {
const hasPrivateIp = this.hasResultPrivateIp(hops.slice(1)); // First hop is always gateway
private async checkForPrivateDest(target: string): Promise<void> {
const [ipAddress] = await this.dnsResolver(target);

if (hasPrivateIp) {
cmd.kill('SIGKILL');
return false;
if (isIpPrivate(String(ipAddress))) {
throw new Error('private destination');
}

return true;
}
}
9 changes: 9 additions & 0 deletions test/mocks/mtr-fail-private-ip.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"testId": "test",
"measurementId": "measurement",
"result": {
"hops": [],
"data": [],
"rawOutput": "Private IP ranges are not allowed"
}
}
34 changes: 31 additions & 3 deletions test/unit/cmd/mtr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,19 @@ import {expect} from 'chai';
import {Socket} from 'socket.io-client';
import {getCmdMock, getCmdMockResult, execaPromise} from '../../utils.js';
import {MtrCommand} from '../../../src/command/mtr-command.js';
import type {MtrOptions} from '../../../src/command/mtr-command.js';

const dnsResolver = async (_addr: string, _type: string) => ['123 | abc | abc'];
const dnsResolver = (isPrivate: boolean) => async (_addr: string, type = 'A') => {
if (type === 'TXT') {
return ['123 | abc | abc'];
}

if (isPrivate) {
return ['192.168.0.1'];
}

return ['1.1.1.1'];
};

describe('mtr command executor', () => {
const sandbox = sinon.createSandbox();
Expand Down Expand Up @@ -40,11 +51,28 @@ describe('mtr command executor', () => {

const mockCmd = execaPromise({stdout: stream}, promise);

const mtr = new MtrCommand((): any => mockCmd, dnsResolver);
await mtr.run(mockedSocket as any, 'measurement', 'test', options);
const mtr = new MtrCommand((): any => mockCmd, dnsResolver(false));
await mtr.run(mockedSocket as any, 'measurement', 'test', options as MtrOptions);

expect(mockedSocket.emit.args.length).to.equal(rawOutputLines.length + 1); // Progress + result
expect(mockedSocket.emit.lastCall.args[0]).to.equal('probe:measurement:result');
expect(mockedSocket.emit.lastCall.args[1]).to.deep.equal(expectedResult);
});

it('should detect Private IP and stop', async () => {
const testCase = 'mtr-fail-private-ip';
const options = {
type: 'mtr' as const,
target: 'jsdelivr.net',
};

const expectedResult = getCmdMockResult(testCase);
const mockCmd = execaPromise({stdout: new PassThrough()}, Promise.resolve());

const mtr = new MtrCommand((): any => mockCmd, dnsResolver(true));
await mtr.run(mockedSocket as any, 'measurement', 'test', options as MtrOptions);

expect(mockedSocket.emit.lastCall.args[0]).to.equal('probe:measurement:result');
expect(mockedSocket.emit.lastCall.args[1]).to.deep.equal(expectedResult);
});
});

0 comments on commit 30115a0

Please sign in to comment.