Skip to content

Commit

Permalink
WIP nat tests
Browse files Browse the repository at this point in the history
  • Loading branch information
emmacasolin committed Mar 14, 2022
1 parent 928b633 commit 53aeadc
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 74 deletions.
59 changes: 45 additions & 14 deletions tests/nat/noNAT.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import type { RecoveryCode } from '@/keys/types';
import os from 'os';
import path from 'path';
import fs from 'fs';
import readline from 'readline';
import * as jestMockProps from 'jest-mock-props';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
import PolykeyAgent from '@/PolykeyAgent';
import Status from '@/status/Status';
import config from '@/config';
import * as statusErrors from '@/status/errors';
import * as testBinUtils from '../bin/utils';
import * as testUtils from '../utils';
import * as testNatUtils from './utils';

describe('no NAT', () => {
const logger = new Logger('no nat test', LogLevel.WARN, [new StreamHandler()]);
const password = 'password';
let dataDir: string;
beforeEach(async () => {
let nodePath1: string;
let nodePath2: string;
beforeAll(async () => {
dataDir = await fs.promises.mkdtemp(
path.join(os.tmpdir(), 'polykey-test-'),
);
nodePath1 = path.join(dataDir, 'node1');
nodePath2 = path.join(dataDir, 'node2');
});
afterEach(async () => {
afterAll(async () => {
await fs.promises.rm(dataDir, {
force: true,
recursive: true,
Expand All @@ -30,7 +24,44 @@ describe('no NAT', () => {
test(
'two nodes can connect',
async () => {

const vethN1 = 'veth-n1';
const vethN2 = 'veth-n2';
const ipN1 = '10.0.0.1/24';
const ipN2 = '10.0.0.2/24';
const usrns = testNatUtils.createUserNamespace();
const node1 = testNatUtils.createNodeNamespace(
usrns.pid,
nodePath1,
password,
);
const node2 = testNatUtils.createNodeNamespace(
usrns.pid,
nodePath2,
password,
);
console.log(usrns.pid);
console.log(node1.pid);
console.log(node2.pid);
testNatUtils.linkNamespaces(
usrns.pid,
node1.pid,
node2.pid,
vethN1,
vethN2,
ipN1,
ipN2,
);
const node1Id = await testNatUtils.getOwnNodeId(
usrns.pid,
node1.pid,
nodePath1,
password,
);
expect(node1Id).toBeDefined();
console.log(node1Id);
node2.kill('SIGTERM');
node1.kill('SIGTERM');
usrns.kill('SIGTERM');
},
global.defaultTimeout * 2,
);
Expand Down
184 changes: 124 additions & 60 deletions tests/nat/utils.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,145 @@
import { exec } from "child_process";
import { exec } from 'child_process';

const execLogger = (error, stdout, stderr) => {
if (error) {
console.log(`error: ${error.message}`);
return;
}
if (stderr) {
console.log(`stderr: ${stderr}`);
return;
}
console.log(`stdout: ${stdout}`);
/**
* Formats the command to enter a namespace to run a process inside it
*/
const nsenter = (usrnsPid: number, netnsPid: number) => {
return `nsenter --target ${usrnsPid} --user --preserve-credentials `.concat(
`nsenter --target ${netnsPid} --net `,
);
};

/**
* Create a linux network namespace with a given name and bring up its
* loopback interface
* Create a user namespace from which network namespaces can be created without
* requiring sudo
*/
function createNamespace(name: string) {
// Create namespace
exec(`ip netns add ${name}`, execLogger);
// Bring up loopback
exec(`ip netns exec ${name} ip link set lo up`, execLogger);
function createUserNamespace() {
return exec('unshare --user --map-root-user');
}

/**
* Delete a namespace by its name (including all of its interfaces and
* iptables rules)
* Create a network namespace inside an existing user namespace and start an
* agent inside it
* Returns the process so that the agent can be killed
* Killing the agent will also destroy the namespace and any network setup/
* interfaces it contains
*/
function deleteNamespace(name: string) {
exec(`ip netns del ${name}`);
function createNodeNamespace(
usrnsPid: number,
nodePath: string,
password: string,
) {
const command = ''.concat(
`nsenter --target ${usrnsPid} --user --preserve-credentials `,
'unshare --net ',
'npm run polykey ',
'-- agent start ',
`--node-path=${nodePath} `,
`--password-file=<(echo '${password}') `,
'--client-host=0.0.0.0',
);
return exec(command);
}

/**
* Create a veth pair linking two existing namespaces
* Namespaces must be identitied by their pid
* Brings up loopback for both namespaces as well
*/
function linkNamespaces(ns1: string, ns2: string, veth1: string, veth2: string, ip1: string, ip2: string) {
function linkNamespaces(
usrnsPid: number,
netnsPid1: number,
netnsPid2: number,
veth1: string,
veth2: string,
host1: string,
host2: string,
) {
// Bring up loopback
exec(nsenter(usrnsPid, netnsPid1) + `ip link set lo up`);
exec(nsenter(usrnsPid, netnsPid2) + `ip link set lo up`);
// Create veth pair to link the namespaces
exec(`ip link add ${veth1} type veth peer name ${veth2}`);
exec(
nsenter(usrnsPid, netnsPid1) +
`ip link add ${veth1} type veth peer name ${veth2}`,
);
// Link up the ends to the correct namespaces
exec(`ip link set ${veth1} netns ${ns1}`);
exec(`ip link set ${veth2} netns ${ns2}`);
exec(
nsenter(usrnsPid, netnsPid1) +
`ip link set dev ${veth2} netns ${netnsPid2}`,
);
// Bring up each end
exec(`ip netns exec ${ns1} ip link set ${veth1} up`);
exec(`ip netns exec ${ns2} ip link set ${veth2} up`);
exec(nsenter(usrnsPid, netnsPid1) + `ip link set ${veth1} up`);
exec(nsenter(usrnsPid, netnsPid2) + `ip link set ${veth2} up`);
// Assign ip addresses to each end
exec(`ip netns exec ${ns1} ip addr add ${ip1} dev ${veth1}`);
exec(`ip netns exec ${ns2} ip addr add ${ip2} dev ${veth2}`);
exec(nsenter(usrnsPid, netnsPid1) + `ip addr add ${host1} dev ${veth1}`);
exec(nsenter(usrnsPid, netnsPid2) + `ip addr add ${host2} dev ${veth2}`);
}

/**
* Run `pk agent start` in an existing namespace
* Returns the process so that the agent can be killed
* Ping a node from a namespace
*/
function startNodeInNamespace(namespace: string, nodePath: string, password: string) {
return exec(`ip netns exec ${namespace} npm run polykey -- agent start --node-path=${nodePath} --password-file=<(echo '${password}') --client-host=0.0.0.0`);
}

/**
* Ping a node in one namespace from a node in another namespace
*/
function pingNodeFromNamespace(namespace: string, targetHost: string, targetNodeId: string, nodePath: string, password: string): Promise<{
async function pingNodeFromNamespace(
usrnsPid: number,
netnsPid: number,
clientHost: string,
nodeId: string,
nodePath: string,
password: string,
): Promise<{
exitCode: number;
stdout: string;
stderr: string;
}> {
const command = nsenter(usrnsPid, netnsPid).concat(
'npm run polykey ',
`-- nodes ping ${nodeId} `,
`--node-path=${nodePath} `,
`--password-file=<(echo '${password}') `,
`--client-host=${clientHost}`,
);
return new Promise((resolve, reject) => {
exec(`ip netns exec ${namespace} npm run polykey -- nodes ping ${targetNodeId} --node-path=${nodePath} --password-file=<(echo '${password}') --client-host=${targetHost}`,
(error, stdout, stderr) => {
if (error != null && error.code === undefined) {
// This can only happen when the command is killed
return reject(error);
} else {
// Success and Unsuccessful exits are valid here
return resolve({
exitCode: error && error.code != null ? error.code : 0,
stdout,
stderr,
});
}
},
);
exec(command, (error, stdout, stderr) => {
if (error != null && error.code === undefined) {
// This can only happen when the command is killed
return reject(error);
} else {
// Success and Unsuccessful exits are valid here
return resolve({
exitCode: error && error.code != null ? error.code : 0,
stdout,
stderr,
});
}
});
});
}

/**
* Run the agent status command to get own node id for agent running in a
* namespace
*/
async function getOwnNodeId(
usrnsPid: number,
netnsPid: number,
nodePath: string,
password: string,
): Promise<string | undefined> {
const command = nsenter(usrnsPid, netnsPid).concat(
'npm run polykey ',
`-- agent status `,
`--node-path=${nodePath} `,
`--password-file=<(echo '${password}') `,
`--format=json`,
);
return new Promise((resolve) => {
exec(command, (error, stdout) => {
if (error) {
return resolve(undefined);
} else {
return resolve(JSON.parse(stdout).nodeId);
}
});
});
}

Expand All @@ -91,16 +151,20 @@ function pingNodeFromNamespace(namespace: string, targetHost: string, targetNode
*/
function makeFullConeNAT(clientip: string, router: string, routerip: string) {
// Any packets leaving the router coming from the client should be made to look like they're coming from the router
exec(`iptables -t nat -A POSTROUTING -s ${clientip} -o ${router} -j SNAT --to-source ${routerip}`);
exec(
`iptables -t nat -A POSTROUTING -s ${clientip} -o ${router} -j SNAT --to-source ${routerip}`,
);
// Any packets arriving to the router addressed to the router should be redirected to the client
exec(`iptables -t nat -A PREROUTING -d ${routerip} -i ${router} -j DNAT --to-destination ${clientip}`);
exec(
`iptables -t nat -A PREROUTING -d ${routerip} -i ${router} -j DNAT --to-destination ${clientip}`,
);
}

export {
createNamespace,
deleteNamespace,
createUserNamespace,
createNodeNamespace,
linkNamespaces,
startNodeInNamespace,
getOwnNodeId,
pingNodeFromNamespace,
makeFullConeNAT,
};

0 comments on commit 53aeadc

Please sign in to comment.