hyper-nat
Version:
Securely tunnel UDP and TCP connections over peer-to-peer networks using hyperswarm for NAT traversal
219 lines (194 loc) • 9.4 kB
JavaScript
const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const ConfigManager = require('./config');
/**
* CLI interface and command handling for hyper-nat
*/
class CLI {
/**
* Create and configure the CLI interface
* @returns {Object} Configured yargs instance
*/
static createCLI() {
return yargs(hideBin(process.argv))
.scriptName('hyper-nat')
.usage('$0 <command> [options]')
.command('server', 'Run as server mode', (yargs) => {
return yargs
.option('port', {
alias: 'p',
type: 'string',
demandOption: true,
description: 'Port(s) to forward (comma-separated for multiple: 3000,3001,3002)'
})
.option('host', {
type: 'string',
default: '127.0.0.1',
description: 'Host to forward to'
})
.option('protocol', {
alias: 'proto',
type: 'string',
default: 'udp',
description: 'Protocol(s) to use: tcp, udp, or tcpudp (comma-separated for multiple: tcp,udp,tcpudp)'
})
.option('secret', {
alias: 's',
type: 'string',
description: 'Secret key for authentication (auto-generated if not provided)'
});
})
.command('client', 'Run as client mode', (yargs) => {
return yargs
.option('local-port', {
alias: 'l',
type: 'string',
demandOption: true,
description: 'Local port(s) to bind (comma-separated for multiple: 3000,3001,3002)'
})
.option('remote-port', {
alias: 'r',
type: 'string',
demandOption: true,
description: 'Remote port(s) to connect to (comma-separated for multiple: 80,8080,443)'
})
.option('protocol', {
alias: 'proto',
type: 'string',
default: 'udp',
description: 'Protocol(s) to use: tcp, udp, or tcpudp (comma-separated for multiple: tcp,udp,tcpudp)'
})
.option('public-key', {
alias: 'k',
type: 'string',
demandOption: true,
description: 'Public key from server'
});
})
.option('config', {
alias: 'c',
type: 'string',
description: 'Configuration file path (default: options.json)'
})
.help('h')
.alias('h', 'help')
.example('$0 server -p 3000 -s mysecret', 'Run server on single port 3000')
.example('$0 server -p 3000 --protocol tcpudp -s mysecret', 'Run server with TCP-over-UDP on port 3000')
.example('$0 server -p 3000,3001,3002 --protocol udp,tcp,tcpudp -s mysecret', 'Run server on multiple ports with different protocols')
.example('$0 client -l 8080 -r 80 -k <publickey>', 'Connect local port 8080 to remote port 80')
.example('$0 client -l 8080 -r 80 --protocol tcpudp -k <publickey>', 'Connect using TCP-over-UDP from local 8080 to remote 80')
.example('$0 client -l 8080,8081 -r 80,443 --protocol udp,tcpudp -k <publickey>', 'Connect multiple ports with different protocols')
.example('$0 -c myconfig.json', 'Use configuration file')
.demandCommand(0, 1, 'You can specify a command, use config file, or run without arguments for default server mode')
.argv;
}
/**
* Handle server command
* @param {Object} argv - Parsed command line arguments
*/
static async handleServerCommand(argv) {
const ports = ConfigManager.parsePortList(argv.port);
const protocols = ConfigManager.parseProtocolList(argv.protocol, ports.length);
if (ports.length === 0) {
console.error('Error: No valid ports specified');
process.exit(1);
}
if (protocols.length !== ports.length) {
console.error(`Error: Number of protocols (${protocols.length}) must match number of ports (${ports.length})`);
process.exit(1);
}
// Generate secret if not provided
const secret = argv.secret || ConfigManager.generateRandomSecret();
if (!argv.secret) {
console.log(`Generated random secret: ${secret}\n`);
}
console.log(`Starting server mode with ${ports.length} port(s)`);
// Create multiple configurations and run them
const configurations = ports.map((port, index) => ({
mode: 'server',
proto: protocols[index],
port: port,
host: argv.host,
secret: secret,
showCommands: true
}));
await ConfigManager.runFromConfig(configurations, true);
}
/**
* Handle client command
* @param {Object} argv - Parsed command line arguments
*/
static async handleClientCommand(argv) {
const localPorts = ConfigManager.parsePortList(argv['local-port']);
const remotePorts = ConfigManager.parsePortList(argv['remote-port']);
const protocols = ConfigManager.parseProtocolList(argv.protocol, localPorts.length);
if (localPorts.length === 0 || remotePorts.length === 0) {
console.error('Error: No valid ports specified');
process.exit(1);
}
if (localPorts.length !== remotePorts.length) {
console.error(`Error: Number of local ports (${localPorts.length}) must match number of remote ports (${remotePorts.length})`);
process.exit(1);
}
if (protocols.length !== localPorts.length) {
console.error(`Error: Number of protocols (${protocols.length}) must match number of ports (${localPorts.length})`);
process.exit(1);
}
console.log(`Starting client mode with ${localPorts.length} port mappings`);
// Create multiple configurations and run them
const publicKey = argv.publicKey || argv['public-key'];
const configurations = localPorts.map((localPort, index) => ({
mode: 'client',
proto: protocols[index],
port: remotePorts[index],
localPort: localPort,
publicKey: publicKey
}));
await ConfigManager.runFromConfig(configurations);
}
/**
* Handle default mode (no command specified)
* @param {Object} argv - Parsed command line arguments
*/
static async handleDefaultMode(argv) {
const configPath = argv.config || 'options.json';
const config = ConfigManager.loadConfig(configPath);
if (config && config.schema) {
// Check if config contains multiple server configurations with same secret
const serverConfigs = config.schema.filter(c => c.mode === 'server');
const hasMultipleServersWithSameSecret = serverConfigs.length > 1 &&
serverConfigs.every(c => c.secret === serverConfigs[0].secret);
await ConfigManager.runFromConfig(config.schema, hasMultipleServersWithSameSecret);
return;
}
// Default behavior: Start server mode with both UDP and TCP on localhost:3000
console.log('No command specified - starting default server mode...');
console.log('Server will run on localhost:3000 with both UDP and TCP protocols');
console.log('Use --help to see all available options\n');
const defaultSecret = ConfigManager.generateRandomSecret();
console.log(`Generated random secret: ${defaultSecret}\n`);
const defaultConfigurations = ConfigManager.createDefaultConfigurations(defaultSecret);
console.log('Starting servers...');
await ConfigManager.runFromConfig(defaultConfigurations, true);
}
/**
* Main CLI entry point
*/
static async main() {
const argv = CLI.createCLI();
const command = argv._[0];
try {
if (!command) {
await CLI.handleDefaultMode(argv);
} else if (command === 'server') {
await CLI.handleServerCommand(argv);
} else if (command === 'client') {
await CLI.handleClientCommand(argv);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}
}
}
module.exports = CLI;