#!/usr/bin/env node
/** @typedef {import('pear-interface')} */ /* global Pear */
/**
* Hyperswarm SOCKS Proxy with Hyperident Profile Support
* Provides SOCKS5 proxy functionality with deterministic addressing via profiles
*/
import { createServer } from 'net'
import { randomBytes } from 'crypto'
import { IdentityManager } from './lib/identity-manager.js'
/**
* SOCKS5 constants
*/
const SOCKS_VERSION = 0x05
const SOCKS_METHODS = {
NO_AUTH: 0x00,
NO_METHODS: 0xFF
}
const SOCKS_COMMANDS = {
CONNECT: 0x01
}
const SOCKS_REPLIES = {
SUCCESS: 0x00,
GENERAL_FAILURE: 0x01,
CONNECTION_NOT_ALLOWED: 0x02,
NETWORK_UNREACHABLE: 0x03,
HOST_UNREACHABLE: 0x04,
CONNECTION_REFUSED: 0x05,
TTL_EXPIRED: 0x06,
COMMAND_NOT_SUPPORTED: 0x07,
ADDRESS_TYPE_NOT_SUPPORTED: 0x08
}
const SOCKS_ADDRESS_TYPES = {
IPV4: 0x01,
DOMAIN: 0x03,
IPV6: 0x04
}
/**
* Simple hyperswarm mock for demonstration
* In a real implementation, this would use actual hyperswarm
*/
class HyperswarmMock {
constructor(topic) {
this.topic = topic
this.connections = new Set()
}
createServer() {
const server = createServer()
server.on('connection', (socket) => {
this.connections.add(socket)
socket.on('close', () => {
this.connections.delete(socket)
})
})
return server
}
connect(topic) {
// Mock connection - in real implementation would connect to hyperswarm
return new Promise((resolve) => {
const mockSocket = createServer().listen(0)
setTimeout(() => resolve(mockSocket), 100)
})
}
destroy() {
this.connections.forEach(conn => conn.destroy())
this.connections.clear()
}
}
/**
* SOCKS5 Proxy Server
*/
class SocksProxy {
constructor(options = {}) {
this.port = options.port || 1080
this.host = options.host || '127.0.0.1'
this.topic = options.topic || null
this.profileName = options.profileName || null
this.server = null
this.hyperswarm = null
}
/**
* Start the SOCKS proxy server
*/
async start() {
this.server = createServer()
this.server.on('connection', this.handleConnection.bind(this))
// Initialize hyperswarm with topic
if (this.topic) {
this.hyperswarm = new HyperswarmMock(this.topic)
}
return new Promise((resolve, reject) => {
this.server.listen(this.port, this.host, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
/**
* Stop the SOCKS proxy server
*/
async stop() {
if (this.hyperswarm) {
this.hyperswarm.destroy()
}
if (this.server) {
return new Promise((resolve) => {
this.server.close(resolve)
})
}
}
/**
* Handle incoming SOCKS connection
*/
handleConnection(socket) {
socket.on('data', (data) => {
this.handleSocksData(socket, data)
})
socket.on('error', (err) => {
console.error('Socket error:', err.message)
})
socket.on('close', () => {
// Connection closed
})
}
/**
* Handle SOCKS protocol data
*/
handleSocksData(socket, data) {
if (data.length < 2) {
socket.destroy()
return
}
const version = data[0]
if (version !== SOCKS_VERSION) {
socket.destroy()
return
}
// Check if this is authentication negotiation
if (data.length >= 2 && data[1] > 0) {
this.handleAuth(socket, data)
} else {
this.handleConnect(socket, data)
}
}
/**
* Handle SOCKS authentication
*/
handleAuth(socket, data) {
const numMethods = data[1]
if (data.length < 2 + numMethods) {
socket.destroy()
return
}
const methods = Array.from(data.slice(2, 2 + numMethods))
// We only support NO_AUTH
if (methods.includes(SOCKS_METHODS.NO_AUTH)) {
const response = Buffer.from([SOCKS_VERSION, SOCKS_METHODS.NO_AUTH])
socket.write(response)
} else {
const response = Buffer.from([SOCKS_VERSION, SOCKS_METHODS.NO_METHODS])
socket.write(response)
socket.destroy()
}
}
/**
* Handle SOCKS CONNECT command
*/
handleConnect(socket, data) {
if (data.length < 4) {
socket.destroy()
return
}
const version = data[0]
const command = data[1]
const addressType = data[3]
if (version !== SOCKS_VERSION || command !== SOCKS_COMMANDS.CONNECT) {
this.sendConnectResponse(socket, SOCKS_REPLIES.COMMAND_NOT_SUPPORTED)
return
}
let address, port, offset
try {
switch (addressType) {
case SOCKS_ADDRESS_TYPES.IPV4:
if (data.length < 10) throw new Error('Invalid IPv4 address')
address = Array.from(data.slice(4, 8)).join('.')
port = data.readUInt16BE(8)
offset = 10
break
case SOCKS_ADDRESS_TYPES.DOMAIN:
if (data.length < 5) throw new Error('Invalid domain address')
const domainLength = data[4]
if (data.length < 5 + domainLength + 2) throw new Error('Invalid domain address')
address = data.slice(5, 5 + domainLength).toString()
port = data.readUInt16BE(5 + domainLength)
offset = 7 + domainLength
break
case SOCKS_ADDRESS_TYPES.IPV6:
if (data.length < 22) throw new Error('Invalid IPv6 address')
// IPv6 handling would go here
this.sendConnectResponse(socket, SOCKS_REPLIES.ADDRESS_TYPE_NOT_SUPPORTED)
return
default:
this.sendConnectResponse(socket, SOCKS_REPLIES.ADDRESS_TYPE_NOT_SUPPORTED)
return
}
// For demo purposes, we'll just send success response
// In real implementation, this would establish hyperswarm connection
this.sendConnectResponse(socket, SOCKS_REPLIES.SUCCESS, address, port)
} catch (error) {
console.error('Connect error:', error.message)
this.sendConnectResponse(socket, SOCKS_REPLIES.GENERAL_FAILURE)
}
}
/**
* Send SOCKS CONNECT response
*/
sendConnectResponse(socket, reply, address = '0.0.0.0', port = 0) {
const response = Buffer.alloc(10)
response[0] = SOCKS_VERSION
response[1] = reply
response[2] = 0x00 // Reserved
response[3] = SOCKS_ADDRESS_TYPES.IPV4
// Write IP address (4 bytes)
const ipParts = address.split('.').map(part => parseInt(part) || 0)
response[4] = ipParts[0]
response[5] = ipParts[1]
response[6] = ipParts[2]
response[7] = ipParts[3]
// Write port (2 bytes)
response.writeUInt16BE(port, 8)
socket.write(response)
if (reply !== SOCKS_REPLIES.SUCCESS) {
socket.destroy()
}
}
}
/**
* Parse command line arguments
*/
function parseArgs(argv) {
const args = argv.slice(2)
const parsed = {
command: null,
flags: {},
positional: []
}
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg === 'server') {
parsed.command = 'server'
} else if (arg === '--profile') {
if (i + 1 < args.length) {
parsed.flags.profile = args[i + 1]
i++ // Skip next argument
} else {
throw new Error('--profile flag requires a profile name')
}
} else if (arg === '--port') {
if (i + 1 < args.length) {
parsed.flags.port = parseInt(args[i + 1])
i++ // Skip next argument
} else {
throw new Error('--port flag requires a port number')
}
} else if (arg === '--help' || arg === '-h') {
parsed.command = 'help'
} else if (!arg.startsWith('-')) {
parsed.positional.push(arg)
}
}
return parsed
}
/**
* Show help message
*/
function showHelp() {
console.log(`
🌐 Hyperswarm SOCKS Proxy with Hyperident Integration
USAGE:
node socks-server.js server [options]
OPTIONS:
--profile <name> Use deterministic address from Hyperident profile
--port <number> SOCKS proxy port (default: 1080)
--help, -h Show this help
EXAMPLES:
node socks-server.js server
# ✅ Hyperswarm SOCKS proxy started
# 🔡 Address: [random-hex].hyperswarm
# 🌐 SOCKS proxy: 127.0.0.1:1080
node socks-server.js server --profile work
# ✅ Hyperswarm SOCKS proxy started
# 🔡 Address: [deterministic-from-work].hyperswarm
# 🌐 SOCKS proxy: 127.0.0.1:1080
HYPERIDENT INTEGRATION:
The --profile flag uses Hyperident for deterministic addressing:
1. Loads identity from ~/.hyperident/
2. Derives topic from profile name
3. Uses consistent hyperswarm address
Setup Hyperident first:
hyperident init
hyperident create-profile work
`)
}
/**
* Get hidden input for passphrase
*/
function getHiddenInput(prompt) {
return new Promise((resolve) => {
process.stdout.write(prompt)
process.stdin.setRawMode(true)
process.stdin.resume()
process.stdin.setEncoding('utf8')
let input = ''
const onData = (char) => {
if (char === '\u0003') { // Ctrl+C
process.exit(1)
} else if (char === '\r' || char === '\n') {
process.stdin.setRawMode(false)
process.stdin.pause()
process.stdin.removeListener('data', onData)
process.stdout.write('\n')
resolve(input)
} else if (char === '\u007f') { // Backspace
if (input.length > 0) {
input = input.slice(0, -1)
process.stdout.write('\b \b')
}
} else {
input += char
process.stdout.write('*')
}
}
process.stdin.on('data', onData)
})
}
/**
* Main server function
*/
async function main() {
try {
const parsed = parseArgs(process.argv)
if (parsed.command === 'help' || !parsed.command) {
showHelp()
return
}
if (parsed.command !== 'server') {
console.error('❌ Error: Unknown command. Use "server" or --help')
process.exit(1)
}
const port = parsed.flags.port || 1080
const profileName = parsed.flags.profile
let topic = null
let address = null
// If profile specified, get deterministic topic from Hyperident
if (profileName) {
console.log(`🔍 Loading profile '${profileName}'...`)
try {
// Check if identity is encrypted and prompt for passphrase if needed
let passphrase = null
try {
await IdentityManager.loadIdentity()
} catch (error) {
if (error.message.includes('encrypted but no passphrase provided')) {
console.log('🔐 Identity is encrypted. Please enter your passphrase.')
passphrase = await getHiddenInput('Enter passphrase: ')
} else {
throw error
}
}
// Get the hyperswarm address for the profile
address = await IdentityManager.getProfileAddress(profileName, passphrase)
topic = Buffer.from(address, 'hex')
console.log(`✅ Profile '${profileName}' loaded`)
console.log(`🔡 Deterministic address: ${address}`)
} catch (error) {
console.error('❌ Failed to load profile:', error.message)
if (error.message.includes('Identity not found')) {
console.error('💡 Run "hyperident init" first to create an identity')
} else if (error.message.includes('not found')) {
console.error('💡 Create the profile: hyperident create-profile ' + profileName)
console.error(' Or list available profiles: hyperident list-profiles')
} else if (error.message.includes('Invalid passphrase')) {
console.error('💡 Please check your passphrase and try again')
}
process.exit(1)
}
} else {
// Generate random topic for non-profile usage
topic = randomBytes(32)
address = topic.toString('hex')
console.log(`🎲 Using random address: ${address}`)
}
// Create and start SOCKS proxy
const proxy = new SocksProxy({
port,
topic,
profileName
})
console.log('🚀 Starting Hyperswarm SOCKS proxy...')
await proxy.start()
console.log('✅ Hyperswarm SOCKS proxy started')
console.log(`🔡 Address: ${address}`)
console.log(`🌐 SOCKS proxy: 127.0.0.1:${port}`)
console.log('')
console.log('💡 Configure your applications to use SOCKS5 proxy:')
console.log(` Host: 127.0.0.1`)
console.log(` Port: ${port}`)
console.log('')
console.log('Press Ctrl+C to stop')
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('\n🛑 Shutting down...')
await proxy.stop()
console.log('✅ SOCKS proxy stopped')
process.exit(0)
})
process.on('SIGTERM', async () => {
console.log('\n🛑 Shutting down...')
await proxy.stop()
console.log('✅ SOCKS proxy stopped')
process.exit(0)
})
} catch (error) {
console.error('❌ Error:', error.message)
if (process.env.DEBUG) {
console.error(error.stack)
}
process.exit(1)
}
}
// Run if this file is executed directly
if (import.meta.url === `file://${process.argv[1]}`) {
main()
}
export { SocksProxy, parseArgs, showHelp, main }