#!/usr/bin/env node
// hypermosh — resilient P2P mosh sessions to peers reached by public key.
//
// hypermosh id print this daemon's server pubkey
// hypermosh serve [--hyperident [--firewall <f> | --disable-firewall]]
// run the session daemon
// hypermosh [user@]<name-or-pubkey> [-- cmd] open a mosh session
// hypermosh add-name <name> <pubkey> remember a name -> pubkey (optional)
// hypermosh names list known names
//
// AUTH — two models:
// • DEFAULT (SSH-style login): the tunnel carries a normal SSH connection to
// the server's system sshd, so password / ssh-key / authorized_keys all work
// exactly like `mosh user@host`. sshd is the auth gate; the hyperdht firewall
// is open. This is what most people want.
// • --hyperident: no SSH — auth is the hyperdht pubkey firewall (allow-list of
// client identity keys). The client presents its ~/.hyperident device key.
//
// A raw public key ALWAYS works; names are an optional convenience.
import process from '#node:process'
import cp from '#node:child_process'
import { serveSession, connectSession } from '../lib/session.mjs'
import { serveSSH, connectSSH } from '../lib/ssh-mode.mjs'
import { loadOrCreateKey, loadDeviceKeyPair, readAllowed, HypercoreId } from '../lib/identity.mjs'
import { resolveAsync as resolveName } from '../lib/names.mjs'
import { addName, loadNames } from '../lib/names.mjs'
function flags (argv) {
const pos = []; const f = {}
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a === '--') { f._rest = argv.slice(i + 1); break }
if (a === '-v' || a === '--verbose') { f.verbose = true; continue }
if (a.startsWith('--')) { const k = a.slice(2); const v = (argv[i + 1] && !argv[i + 1].startsWith('--')) ? argv[++i] : true; f[k] = v }
else pos.push(a)
}
return { pos, f }
}
function usage () {
console.error('usage:\n' +
' hypermosh id\n' +
' hypermosh serve [--hyperident [--firewall <f> | --disable-firewall]]\n' +
' hypermosh [user@]<name-or-pubkey> [-- cmd...] (SSH-style login by default)\n' +
' hypermosh add-name <name> <pubkey>\n' +
' hypermosh names\n' +
'\noptions: -v/--verbose (or HYPERMOSH_DEBUG=1) for connection logging')
process.exit(1)
}
// Split an optional "user@" prefix off the name/pubkey target.
function splitTarget (s) {
const at = s.indexOf('@')
if (at === -1) return { user: null, host: s }
return { user: s.slice(0, at), host: s.slice(at + 1) }
}
async function main () {
const [cmd, ...rest] = process.argv.slice(2)
const { pos, f } = flags(rest)
if (f.verbose) process.env.HYPERMOSH_DEBUG = '1'
if (cmd === 'id') { console.log(HypercoreId.encode(loadOrCreateKey().publicKey)); return }
if (cmd === 'add-name') {
if (!pos[0] || !pos[1]) usage()
addName(pos[0], pos[1])
console.log('added: ' + pos[0] + ' -> ' + pos[1])
return
}
if (cmd === 'names') {
const map = loadNames()
if (map.size === 0) console.log('(no names — add one with: hypermosh add-name <name> <pubkey>)')
else for (const [n, k] of map) console.log(n + ' ' + k)
return
}
if (cmd === 'serve') {
const keyPair = loadOrCreateKey()
if (f.hyperident) {
// Pubkey-firewall mode. Empty allow-list == deny-all — proclaim it loudly.
const allow = f['disable-firewall'] ? true : readAllowed(f.firewall)
const s = await serveSession({ keyPair, allow })
console.log('hypermosh daemon serving as ' + HypercoreId.encode(s.publicKey) + ' [--hyperident]')
if (allow === true) {
console.log(' firewall: DISABLED — ANY peer with this key may connect')
} else if (allow.length === 0) {
console.error(' ⚠ HYPERIDENT FIREWALL ENABLED WITH EMPTY ALLOWLIST — DENYING ALL PEERS.')
console.error(' Nobody can connect. Add client keys with --firewall <file>, or open with --disable-firewall.')
} else {
console.log(' firewall: ' + allow.length + ' allowed key(s)')
}
process.on('SIGINT', async () => { await s.close(); process.exit(0) })
return
}
// DEFAULT: SSH-style. sshd is the auth gate; hyperdht transport is open.
if (f.firewall) console.error(' note: --firewall is ignored without --hyperident (SSH mode auth is your system sshd)')
const s = await serveSSH({ keyPair, allow: true })
console.log('hypermosh daemon serving as ' + HypercoreId.encode(s.publicKey))
console.log(' SSH-style login: tunnels to your system sshd (127.0.0.1:22). Auth = password / ssh-key / authorized_keys.')
console.log(' connect with: hypermosh <user>@' + HypercoreId.encode(s.publicKey))
process.on('SIGINT', async () => { await s.close(); process.exit(0) })
return
}
if (cmd && !cmd.startsWith('-')) {
// one-command client
const { user, host } = splitTarget(cmd)
let remoteKey
try { remoteKey = await resolveName(host) } catch (e) { console.error('hypermosh: ' + e.message); process.exit(1) }
let keyPair = null
try { keyPair = loadDeviceKeyPair() } catch { /* ephemeral identity is fine */ }
const spawnClient = (moshKey, localPort) => {
const env = { ...process.env, MOSH_KEY: moshKey, LC_ALL: process.env.LC_ALL || 'C.UTF-8' }
const child = cp.spawn('mosh-client', ['127.0.0.1', String(localPort)], { stdio: 'inherit', env })
child.on('error', (e) => { console.error('hypermosh: cannot run mosh-client (' + (e.code || e.message) + ') — is mosh installed locally?'); process.exit(1) })
return child
}
if (f.hyperident) {
const start = { term: process.env.TERM || 'xterm', cmd: f._rest || [] }
const session = connectSession({
remoteKey, keyPair, start,
onerror: (e) => { console.error('hypermosh: ' + (e.code || e.message)); process.exit(1) },
onstarted: (moshKey, localPort) => {
const child = spawnClient(moshKey, localPort)
child.on('exit', (code) => { session.close().then(() => process.exit(code || 0)) })
}
})
return
}
// DEFAULT: SSH-style login.
const session = connectSSH({
remoteKey, keyPair, user, cmd: f._rest || [],
onerror: (e) => { console.error('hypermosh: ' + (e.code || e.message)); process.exit(1) },
onstarted: (moshKey, localPort) => {
const child = spawnClient(moshKey, localPort)
child.on('exit', (code) => { session.close().then(() => process.exit(code || 0)) })
}
})
return
}
usage()
}
main().catch((e) => { console.error('hypermosh: ' + e.message); process.exit(1) })