-
Notifications
You must be signed in to change notification settings - Fork 5
Description
Specification
In order to establish a unidirectional connection between two nodes (a 'client' and a 'server'), both nodes need to send each other hole-punching packets (that is, we require a relay message to be passed to the 'server' node to send relay packets back to the 'client' node). It's not sufficient enough for a node to simply be in its node table to establish a connection - the node needs to be aware that a connection is desired.
For a brand new keynode in the Polykey network, it will rely on the seed node/s to establish connections to other nodes in this manner. (However, note that once it becomes aware of other nodes in the network through Kademlia, it can send relay messages to these other nodes, with the aim that the relay message will eventually converge on the target node by "hopping" between nodes.)
This scenario of 3 keynodes needs to be incorporated into the NodeConnection testing suite (currently we only have automated tests for a direct connection between two nodes).
For example, for a node A to connect to node B, we require another node Seed to facilitate the connection. This process is depicted in the following diagram:
┌────────┐
┌──────► Seed ├──────┐
│ └────────┘ │
│1 2│
┌───┴───┐ 1 ┌───▼───┐
│ ├──────────────► │
│ A │ │ B │
│ ◄──────────────┤ │
└───────┘ 3 └───────┘
We make the assumption that direct, unidirectional connections are already established from A to Seed, and from Seed to B. We also assume that A already knows B's host and port (but B may not know of A's host and port, or even A's node ID). Then, it follows the process:
Asends the relay message toSeed(via its direct connection), and simultaneously begins sending hole-punching packets toB's IPSeedreceives the relay message, noting the target node ID. It relays this hole-punching message toB(via its direct connection)Breceives the hole-punching message, and begins sending hole-punching packets back toA.
Assuming that A is still sending hole-punching packets when B begins sending their own back to A, then a unidirectional connection is established from A to B.
The above process was previously manually verified by a collection of 3 scratchpad test files:
test-triple-client.ts:
import type { Host, Port, TLSConfig } from './src/network/types';
import type { NodeId, NodeAddress, NodeBucket } from './src/nodes/types';
import type { PrivateKeyPem, CertificatePemChain } from './src/keys/types';
import { ForwardProxy, ReverseProxy, utils as networkUtils } from './src/network';
import { NodeManager } from './src/nodes';
import { KeyManager, utils as keysUtils } from './src/keys';
import os from 'os';
import path from 'path';
import fs from 'fs';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
async function main() {
const authToken = 'AUTH';
const fwd = new ForwardProxy({
authToken: authToken
});
// Keys and cert generation
//const keyPair = await keysUtils.generateDeterministicKeyPair(4098, 'a');
const keyPair = await keysUtils.generateKeyPair(4098);
const keyPairPem = keysUtils.keyPairToPem(keyPair);
const cert = keysUtils.generateCertificate(
keyPair.publicKey,
keyPair.privateKey,
keyPair.privateKey,
12332432423
);
const certPem = keysUtils.certToPem(cert);
const nodeId = networkUtils.certNodeId(cert);
const tlsConfig = {
keyPrivatePem: keyPairPem.privateKey as PrivateKeyPem,
certChainPem: certPem as CertificatePemChain
} as TLSConfig;
// --------------------------------------------------
await fwd.start({
proxyHost: '127.0.0.1' as Host,
proxyPort: 44709 as Port,
egressHost: '127.0.0.1' as Host,
egressPort: 31111 as Port,
tlsConfig: tlsConfig
});
// --------------------------------------------------
// Setup
const logger = new Logger('NodeClientTest', LogLevel.WARN, [
new StreamHandler(),
]);
let dataDir: string;
let keysPath: string;
let nodesPath: string;
dataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'TripleTestClient'));
keysPath = path.join(dataDir, 'keys');
nodesPath = path.join(dataDir, 'nodes');
let keyManager = new KeyManager({
keysPath: keysPath,
fs: fs,
logger: logger
});
// Only needed to pass into nodeManager constructor
const revProxy = new ReverseProxy({
logger: logger
});
let nodeManager = new NodeManager({
nodesPath: nodesPath,
keyManager: keyManager,
fwdProxy: fwd,
revProxy: revProxy,
fs: fs,
logger: logger
});
let brokers: NodeBucket = {};
// Set the console output of "BROKER NODE ID:" here (from test-triple-broker.ts)
const brokerNodeId = 'h2xWGAVWDo67c3RB3vnfvASjL7HqdR+vcrSSmX09JbA=' as NodeId;
brokers[brokerNodeId] = {
address: { ip: '127.2.0.1' as Host, port: 2222 as Port },
lastUpdated: new Date()
};
await keyManager.start({ password: 'password' });
await nodeManager.start({
nodeId: nodeId,
brokerNodes: brokers
});
// --------------------------------------------------
console.log("CLIENT NODE ID:", nodeId);
// Client has no known nodes in its database (apart from what's discovered from
// the initial calibration with brokers)
const buckets = await nodeManager.getAllBuckets();
console.log(`KNOWN NODES:`);
buckets.forEach((bucket) => {
for (const nodeId of Object.keys(bucket) as Array<NodeId>) {
console.log(`${nodeId} -> { ${bucket[nodeId].address.ip}:${bucket[nodeId].address.port} }`);
}
});
// We want to find 'NodeId3'.
console.log("ATTEMPTING TO LOCATE: NodeId3");
console.log("Calling nodeManager.getClosestGlobalNodes('NodeId3')");
const found = await nodeManager.getClosestGlobalNodes('NodeId3' as NodeId);
console.log(`KNOWN NODES:`);
const newBuckets = await nodeManager.getAllBuckets();
newBuckets.forEach((bucket) => {
for (const nodeId of Object.keys(bucket) as Array<NodeId>) {
console.log(`${nodeId} -> { ${bucket[nodeId].address.ip}:${bucket[nodeId].address.port} }`);
}
});
// console.log(`FOUND NodeId3?: ${found}`)
}
main();test-triple-broker.ts:
import type { Host, Port, TLSConfig } from './src/network/types';
import type { PrivateKeyPem, CertificatePemChain } from './src/keys/types';
import ReverseProxy from './src/network/ReverseProxy';
import { ForwardProxy, utils as networkUtils } from './src/network';
import { KeyManager, utils as keysUtils } from './src/keys';
import { VaultManager } from './src/vaults';
import { NodeManager } from './src/nodes';
import type { NodeId, NodeAddress } from './src/nodes/types';
import GRPCServer from './src/grpc/GRPCServer';
import { AgentService, createAgentService } from './src/agent';
import { GitBackend } from './src/git';
import os from 'os';
import path from 'path';
import fs from 'fs';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
import { NodeIdMessage } from './src/proto/js/Agent_pb';
async function main() {
const authToken = 'AUTH';
const fwd = new ForwardProxy({
authToken: authToken
});
// Keys and cert generation
//const keyPair = await keysUtils.generateDeterministicKeyPair(4098, 'b');
const keyPair = await keysUtils.generateKeyPair(4098);
const keyPairPem = keysUtils.keyPairToPem(keyPair);
const cert = keysUtils.generateCertificate(
keyPair.publicKey,
keyPair.privateKey,
keyPair.privateKey,
12332432423
);
const certPem = keysUtils.certToPem(cert);
const nodeId = networkUtils.certNodeId(cert);
const tlsConfig = {
keyPrivatePem: keyPairPem.privateKey as PrivateKeyPem,
certChainPem: certPem as CertificatePemChain
} as TLSConfig;
// --------------------------------------------------
await fwd.start({
proxyHost: '127.2.0.1' as Host,
proxyPort: 32221 as Port,
egressHost: '127.2.0.1' as Host,
egressPort: 32222 as Port,
tlsConfig: tlsConfig
});
// --------------------------------------------------
// Setup
const logger = new Logger('NodeClientTest', LogLevel.WARN, [
new StreamHandler(),
]);
let dataDir: string;
let keysPath: string;
let vaultsPath: string;
let nodesPath: string;
let keyManager: KeyManager;
let vaultManager: VaultManager;
let nodeManager: NodeManager;
dataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'TripleTestBroker'));
keysPath = path.join(dataDir, 'keys');
vaultsPath = path.join(dataDir, 'vaults');
nodesPath = path.join(dataDir, 'nodes');
keyManager = new KeyManager({
keysPath: keysPath,
fs: fs,
logger: logger
});
vaultManager = new VaultManager({
vaultsPath: vaultsPath,
keyManager: keyManager,
fs: fs,
logger: logger,
});
const rev = new ReverseProxy({
logger: logger
});
nodeManager = new NodeManager({
nodesPath: nodesPath,
keyManager: keyManager,
fwdProxy: fwd,
revProxy: rev,
fs: fs,
logger: logger
});
const gitBackend = new GitBackend({
getVault: vaultManager.getVault.bind(vaultManager),
getVaultID: vaultManager.getVaultIds.bind(vaultManager),
getVaultNames: vaultManager.listVaults.bind(vaultManager),
logger: logger,
});
await keyManager.start({ password: 'password' });
await vaultManager.start({});
await nodeManager.start({ nodeId: nodeId });
// --------------------------------------------------
console.log("BROKER NODE ID:", nodeId);
// Broker has one node in its database (the 'server' node)
// Set the console output of "SERVER NODE ID:" here (from test-triple-server.ts)
const serverNodeId = 'VUF66MkpoATLR9qu21ttS6rQdUbGcsL7gms/PhivD5Y=' as NodeId;
await nodeManager.setNode(
serverNodeId,
{ip: '127.3.0.1', port: 33333} as NodeAddress
);
await nodeManager.createConnectionToNode(
serverNodeId,
{ip: '127.3.0.1', port: 33333} as NodeAddress
);
const buckets = await nodeManager.getAllBuckets();
console.log(`KNOWN NODES:`);
buckets.forEach((bucket) => {
for (const nodeId of Object.keys(bucket) as Array<NodeId>) {
console.log(`${nodeId} -> { ${bucket[nodeId].address.ip}:${bucket[nodeId].address.port} }`);
}
});
const agentService = createAgentService({
keyManager: keyManager,
vaultManager: vaultManager,
nodeManager: nodeManager,
git: gitBackend
});
const server = new GRPCServer({
services: [
[ AgentService, agentService ]
]
});
await server.start({
host: '127.2.0.1' as Host,
});
console.log('GRPC HOST', server.getHost());
console.log('GRPC PORT', server.getPort());
// ingress host and port
// upstream host and port is the GRPC server
await rev.start({
ingressHost: '127.2.0.1' as Host,
ingressPort: 2222 as Port,
serverHost: '127.2.0.1' as Host,
serverPort: server.getPort(),
tlsConfig: tlsConfig
});
console.log('INGRESS HOST', rev.getIngressHost());
console.log('INGRESS PORT', rev.getIngressPort());
}
main();test-triple-server.ts:
import type { Host, Port, TLSConfig } from './src/network/types';
import type { PrivateKeyPem, CertificatePemChain } from './src/keys/types';
import ReverseProxy from './src/network/ReverseProxy';
import { ForwardProxy, utils as networkUtils } from './src/network';
import { KeyManager, utils as keysUtils } from './src/keys';
import { VaultManager } from './src/vaults';
import { NodeManager } from './src/nodes';
import type { NodeId, NodeAddress } from './src/nodes/types';
import GRPCServer from './src/grpc/GRPCServer';
import { AgentService, createAgentService } from './src/agent';
import { GitBackend } from './src/git';
import os from 'os';
import path from 'path';
import fs from 'fs';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
import { NodeIdMessage } from './src/proto/js/Agent_pb';
async function main () {
const rev = new ReverseProxy();
// Keys and cert generation
//const keyPair = await keysUtils.generateDeterministicKeyPair(4098, 'c');
const keyPair = await keysUtils.generateKeyPair(4098);
const keyPairPem = keysUtils.keyPairToPem(keyPair);
const cert = keysUtils.generateCertificate(
keyPair.publicKey,
keyPair.privateKey,
keyPair.privateKey,
12332432423
);
// can be used to get node ID
const certPem = keysUtils.certToPem(cert);
const nodeId = networkUtils.certNodeId(cert);
console.log('SERVER NODE ID:', nodeId);
// --------------------------------------------------
// Setup
const logger = new Logger('NodeServerTest', LogLevel.WARN, [
new StreamHandler(),
]);
let dataDir: string;
let keysPath: string;
let vaultsPath: string;
let nodesPath: string;
let keyManager: KeyManager;
let vaultManager: VaultManager;
let nodeManager: NodeManager;
dataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'polykey-test-'));
keysPath = path.join(dataDir, 'keys');
vaultsPath = path.join(dataDir, 'vaults');
nodesPath = path.join(dataDir, 'nodes');
keyManager = new KeyManager({
keysPath,
fs: fs,
logger: logger,
});
vaultManager = new VaultManager({
vaultsPath: vaultsPath,
keyManager: keyManager,
fs: fs,
logger: logger,
});
// Only needed to pass into nodeManager constructor - won't be forwarding calls
// so no need to start
const fwdProxy = new ForwardProxy({
authToken: '',
logger: logger,
});
nodeManager = new NodeManager({
nodesPath: nodesPath,
keyManager: keyManager,
fwdProxy: fwdProxy,
revProxy: rev,
fs: fs,
logger: logger
});
const gitBackend = new GitBackend({
getVault: vaultManager.getVault.bind(vaultManager),
getVaultID: vaultManager.getVaultIds.bind(vaultManager),
getVaultNames: vaultManager.listVaults.bind(vaultManager),
logger: logger,
});
await keyManager.start({ password: 'password' });
await vaultManager.start({});
await nodeManager.start({ nodeId: nodeId });
// --------------------------------------------------
const agentService = createAgentService({
keyManager: keyManager,
vaultManager: vaultManager,
nodeManager: nodeManager,
git: gitBackend
});
const server = new GRPCServer({
services: [
[ AgentService, agentService ]
]
});
await server.start({
host: '127.3.0.1' as Host,
});
console.log('GRPC HOST', server.getHost());
console.log('GRPC PORT', server.getPort());
const tlsConfig = {
keyPrivatePem: keyPairPem.privateKey as PrivateKeyPem,
certChainPem: certPem as CertificatePemChain
} as TLSConfig;
// ingress host and port
// upstream host and port is the GRPC server
await rev.start({
ingressHost: '127.3.0.1' as Host,
ingressPort: 33333 as Port,
serverHost: '127.3.0.1' as Host,
serverPort: server.getPort(),
tlsConfig: tlsConfig
});
console.log('INGRESS HOST', rev.getIngressHost());
console.log('INGRESS PORT', rev.getIngressPort());
// Server has some known node 'NodeId3'
await nodeManager.setNode(
'NodeId3' as NodeId,
{ ip: '3.3.3.3', port: 3333 } as NodeAddress
);
const buckets = await nodeManager.getAllBuckets();
console.log(`KNOWN NODES:`);
buckets.forEach((bucket) => {
for (const nodeId of Object.keys(bucket) as Array<NodeId>) {
console.log(`${nodeId} -> { ${bucket[nodeId].address.ip}:${bucket[nodeId].address.port} }`);
}
});
}
main();Additional context
- some original discussion of this from an old nodes MR https://gitlab.com/MatrixAI/Engineering/Polykey/js-polykey/-/merge_requests/178#note_561046259
- some further discussion of mocking 3 nodes in some scratchpad test files https://gitlab.com/MatrixAI/Engineering/Polykey/js-polykey/-/merge_requests/178#note_577565530
Tasks
- Implement a test that mocks 2 other nodes (a "seed" and a "target node" to connect to) to test the above process.