Skip to content

Commit

Permalink
Supporting seed nodes: adding support for hostnames, adding parsers f…
Browse files Browse the repository at this point in the history
…or seed node sources
  • Loading branch information
joshuakarp committed Nov 26, 2021
1 parent 40a69ae commit c5f29cf
Show file tree
Hide file tree
Showing 24 changed files with 399 additions and 195 deletions.
2 changes: 1 addition & 1 deletion src/agent/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ function createAgentService({
);
for (const node of closestNodes) {
const addressMessage = new nodesPB.Address();
addressMessage.setHost(node.address.ip);
addressMessage.setHost(node.address.host);
addressMessage.setPort(node.address.port);
// Add the node to the response's map (mapping of node ID -> node address)
response.getNodeTableMap().set(node.id, addressMessage);
Expand Down
86 changes: 85 additions & 1 deletion src/bin/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
* Use PolykeyCommand.addOption or PolykeyCommand.addArgument
* @module
*/
import type { NodeAddress, NodeId, NodeMapping } from '../nodes/types';
import type { Host, Hostname, Port } from '../network/types';
import * as nodesUtils from '../nodes/utils';
import * as networkUtils from '../network/utils';
import * as errors from '../errors';

import commander from 'commander';
import * as parsers from './parsers';
Expand Down Expand Up @@ -38,6 +43,76 @@ const verbose = new commander.Option('-v, --verbose', 'Log Verbose Messages')

const nodeId = new commander.Option('-ni', '--node-id <id>').env('PK_NODE_ID');

/**
* Seed nodes expected to be of form 'nodeId1@host:port;nodeId2@host:port;...'
* By default, any specified seed nodes (in CLI option, or environment variable)
* will overwrite the default nodes in src/config.ts.
* Special flag '<seed-nodes>' in the content indicates that the default seed
* nodes should be added to the starting seed nodes instead of being overwritten.
*/
function parseSeedNodes(rawSeedNodes: string): NodeMapping {
const seedNodeMappings: NodeMapping = {};
// If specifically set no seed nodes, then ensure we start with none
if (rawSeedNodes == '') return seedNodeMappings;
const semicolonSeedNodes = rawSeedNodes.split(';');
for (const rawSeedNode of semicolonSeedNodes) {
// Empty string will occur if there's an extraneous ';' (e.g. at end of env)
if (rawSeedNode == '') continue;
// Append the default seed nodes if we encounter the special flag
if (rawSeedNode == '<seed-nodes>') {
const defaultSeedNodes = getDefaultSeedNodes();
for (const id in defaultSeedNodes) {
seedNodeMappings[id] = defaultSeedNodes[id];
}
}
const idHostPort = rawSeedNode.split(/[@:]/);
if (idHostPort.length != 3) {
throw new commander.InvalidOptionArgumentError(`${rawSeedNode} is not of format 'nodeId@host:port'`)
}
if (!nodesUtils.isNodeId(idHostPort[0])) {
throw new commander.InvalidOptionArgumentError(`${idHostPort[0]} is not a valid node ID`);
}
if (!networkUtils.isValidHostname(idHostPort[1])) {
throw new commander.InvalidOptionArgumentError(`${idHostPort[1]} is not a valid hostname`);
}
const port = parseNumber(idHostPort[2]);
const seedNodeId = idHostPort[0] as NodeId;
const seedNodeAddress: NodeAddress = {
host: idHostPort[1] as Host | Hostname,
port: port as Port,
};
seedNodeMappings[seedNodeId] = seedNodeAddress;
}
return seedNodeMappings;
}

/**
* Acquires the default seed nodes from src/config.ts.
*/
function getDefaultSeedNodes(): NodeMapping {
const seedNodes: NodeMapping = {};
let source;
switch (config.environment) {
case 'testnet':
source = config.seedNodesTest;
break;
case 'mainnet':
source = config.seedNodesMain;
break;
default:
throw new errors.ErrorInvalidConfigEnvironment();
}
for (const id in source) {
const seedNodeId = id as NodeId;
const seedNodeAddress: NodeAddress = {
host: source[seedNodeId].host as Host | Hostname,
port: source[seedNodeId].port as Port,
}
seedNodes[seedNodeId] = seedNodeAddress;
}
return seedNodes;
}

const clientHost = new commander.Option(
'-ch, --client-host <address>',
'Client Host Address',
Expand Down Expand Up @@ -78,6 +153,14 @@ const passwordFile = new commander.Option(
'Path to Password',
);

const seedNodes = new commander.Option(
'-sn, --seed-nodes [nodeId1@host:port;nodeId2@host:port;...]',
'Seed node address mappings',
)
.argParser(parseSeedNodes)
.env('PK_SEED_NODES')
.default(getDefaultSeedNodes());

export {
nodePath,
format,
Expand All @@ -89,4 +172,5 @@ export {
ingressPort,
recoveryCodeFile,
passwordFile,
};
seedNodes,
};
19 changes: 9 additions & 10 deletions src/client/rpcNodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import type * as grpc from '@grpc/grpc-js';
import type * as utils from '../client/utils';
import * as utilsPB from '../proto/js/polykey/v1/utils/utils_pb';
import * as nodesPB from '../proto/js/polykey/v1/nodes/nodes_pb';
import * as nodesUtils from '../nodes/utils';
import { utils as nodesUtils, errors as nodesErrors } from '../nodes';
import * as grpcUtils from '../grpc/utils';
import * as nodesErrors from '../nodes/errors';
import { makeNodeId } from '../nodes/utils';
import * as networkUtils from '../network/utils';

const createNodesRPC = ({
nodeManager,
Expand Down Expand Up @@ -40,14 +39,14 @@ const createNodesRPC = ({
if (!validNodeId) {
throw new nodesErrors.ErrorInvalidNodeId();
}
const validHost = nodesUtils.isValidHost(
const validHost = networkUtils.isValidHost(
call.request.getAddress()!.getHost(),
);
if (!validHost) {
throw new nodesErrors.ErrorInvalidHost();
}
await nodeManager.setNode(makeNodeId(call.request.getNodeId()), {
ip: call.request.getAddress()!.getHost(),
await nodeManager.setNode(nodesUtils.makeNodeId(call.request.getNodeId()), {
host: call.request.getAddress()!.getHost(),
port: call.request.getAddress()!.getPort(),
} as NodeAddress);
} catch (err) {
Expand All @@ -68,7 +67,7 @@ const createNodesRPC = ({
call.sendMetadata(metadata);

const status = await nodeManager.pingNode(
makeNodeId(call.request.getNodeId()),
nodesUtils.makeNodeId(call.request.getNodeId()),
);
response.setSuccess(status);
} catch (err) {
Expand All @@ -90,7 +89,7 @@ const createNodesRPC = ({
const metadata = await authenticate(call.metadata);
call.sendMetadata(metadata);

const remoteNodeId = makeNodeId(call.request.getNodeId());
const remoteNodeId = nodesUtils.makeNodeId(call.request.getNodeId());
const gestaltInvite = await notificationsManager.findGestaltInvite(
remoteNodeId,
);
Expand Down Expand Up @@ -127,12 +126,12 @@ const createNodesRPC = ({
const metadata = await authenticate(call.metadata);
call.sendMetadata(metadata);

const nodeId = makeNodeId(call.request.getNodeId());
const nodeId = nodesUtils.makeNodeId(call.request.getNodeId());
const address = await nodeManager.findNode(nodeId);
response
.setNodeId(nodeId)
.setAddress(
new nodesPB.Address().setHost(address.ip).setPort(address.port),
new nodesPB.Address().setHost(address.host).setPort(address.port),
);
} catch (err) {
callback(grpcUtils.fromError(err), response);
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ const config = {
clientHost: 'localhost',
clientPort: 0,
},
environment: 'testnet',
seedNodesTest: {
'SEEDNODE1' : { host: 'testnet.polykey.io', port: 1314 },
},
seedNodesMain: {
'SEEDNODE1' : { host: 'testnet.polykey.io', port: 1314 },
},
/**
* Polykey OIDs
* These are used by the root X.509 certificates
Expand Down
3 changes: 3 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class ErrorPolykeyClientDestroyed extends ErrorPolykey {}

class ErrorInvalidId extends ErrorPolykey {}

class ErrorInvalidConfigEnvironment extends ErrorPolykey {}

export {
ErrorPolykey,
ErrorPolykeyUndefinedBehaviour,
Expand All @@ -34,6 +36,7 @@ export {
ErrorPolykeyClientNotRunning,
ErrorPolykeyClientDestroyed,
ErrorInvalidId,
ErrorInvalidConfigEnvironment,
};

/**
Expand Down
3 changes: 3 additions & 0 deletions src/network/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ class ErrorCertChainKeyInvalid extends ErrorCertChain {}
*/
class ErrorCertChainSignatureInvalid extends ErrorCertChain {}

class ErrorHostnameResolutionFailed extends ErrorNetwork {}

export {
ErrorNetwork,
ErrorForwardProxyNotStarted,
Expand Down Expand Up @@ -110,4 +112,5 @@ export {
ErrorCertChainNameInvalid,
ErrorCertChainKeyInvalid,
ErrorCertChainSignatureInvalid,
ErrorHostnameResolutionFailed,
};
4 changes: 4 additions & 0 deletions src/network/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import type {
} from '../keys/types';
import type { Opaque } from '../types';

// Host is always an IP address
type Host = Opaque<'Host', string>;
// Specifically for hostname domain names (i.e. to be resolved to an IP address)
type Hostname = Opaque<'Hostname', string>;
type Port = Opaque<'Port', number>;
type Address = Opaque<'Address', string>;

Expand Down Expand Up @@ -42,6 +45,7 @@ type NetworkMessage = PingMessage | PongMessage;

export type {
Host,
Hostname,
Port,
Address,
TLSConfig,
Expand Down
44 changes: 42 additions & 2 deletions src/network/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { Socket } from 'net';
import type { TLSSocket } from 'tls';
import type { Host, Port, Address, NetworkMessage } from './types';
import type { Host, Hostname, Port, Address, NetworkMessage } from './types';
import type { Certificate, PublicKey } from '../keys/types';
import type { NodeId } from '../nodes/types';

import { Buffer } from 'buffer';
import { IPv4, IPv6, Validator } from 'ip-num';
import dns from 'dns';
import * as networkErrors from './errors';
import { isEmptyObject } from '../utils';
import { isEmptyObject, promisify } from '../utils';
import { utils as keysUtils } from '../keys';

const pingBuffer = serializeNetworkMessage({
Expand Down Expand Up @@ -53,6 +54,42 @@ function parseAddress(address: string): [Host, Port] {
return [dstHost as Host, dstPort as Port];
}

/**
* Validates that a provided host address is a valid IPv4 or IPv6 address.
*/
function isValidHost(host: string): boolean {
const [isIPv4] = Validator.isValidIPv4String(host);
const [isIPv6] = Validator.isValidIPv6String(host);
return isIPv4 || isIPv6;
}

/**
* Validates that a provided hostname is valid, as per RFC 1123.
*/
function isValidHostname(hostname: string): boolean {
return (hostname.match(/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/)) ? true : false;
}

/**
* Resolves a provided hostname to its respective IP address (type Host).
*/
async function resolveHost(host: Host | Hostname): Promise<Host> {
// If already IPv4/IPv6 address, return it
if (isValidHost(host)) {
return host as Host;
}
const lookup = promisify(dns.lookup).bind(dns);
let resolvedHost;
try {
// Resolve the hostname and get the IPv4 address
resolvedHost = await lookup(host, 4);
} catch (e) {
throw new networkErrors.ErrorHostnameResolutionFailed(e.message);
}
// Returns an array of [ resolved address, family (4 or 6) ]
return resolvedHost[0] as Host;
}

/**
* Zero IPs should be resolved to localhost when used as the target
* This is usually done automatically, but utp-native doesn't do this
Expand Down Expand Up @@ -317,6 +354,9 @@ export {
toAuthToken,
buildAddress,
parseAddress,
isValidHost,
isValidHostname,
resolveHost,
resolvesZeroIP,
serializeNetworkMessage,
unserializeNetworkMessage,
Expand Down
Loading

0 comments on commit c5f29cf

Please sign in to comment.