Last active
January 25, 2026 23:11
-
-
Save maietta/d10baad8986ca7af5afcd30719877545 to your computer and use it in GitHub Desktop.
Connect to a server via Cloudflare Tunnel programmatically through Bun.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Client } from 'ssh2'; | |
| import { Duplex } from 'node:stream'; | |
| import { spawn } from 'node:child_process'; | |
| import { bin as cloudflaredBin } from 'cloudflared'; | |
| import { generateKeyPairSync } from 'node:crypto'; | |
| const hostname = 'ssh.somehostname.com'; | |
| const port = 22; | |
| const username = 'root'; | |
| const password = 'secret'; | |
| console.log('Starting cloudflared...'); | |
| console.log(`Using cloudflared binary at: ${cloudflaredBin}`); | |
| // Start cloudflared access ssh | |
| const args = ['access', 'ssh', '--hostname', hostname]; | |
| const proc = spawn(cloudflaredBin, args, { | |
| env: process.env, | |
| stdio: ['pipe', 'pipe', 'pipe'] | |
| }); | |
| let cloudflaredErrors: string[] = []; | |
| // Capture stderr | |
| proc.stderr.on('data', (data: Buffer) => { | |
| const errorMsg = data.toString(); | |
| cloudflaredErrors.push(errorMsg); | |
| console.error(`[cloudflared stderr] ${errorMsg.trim()}`); | |
| }); | |
| proc.on('error', (e: Error) => { | |
| console.error(`[cloudflared] Process error: ${e.message}`); | |
| }); | |
| proc.on('exit', (code: number, signal: string | null) => { | |
| if (code !== 0) { | |
| console.error(`[cloudflared] Exited with code ${code}${cloudflaredErrors.length > 0 ? ': ' + cloudflaredErrors.join(' ') : ''}`); | |
| } | |
| }); | |
| // Create duplex stream | |
| const duplex = new Duplex({ | |
| read() {}, | |
| write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null) => void) { | |
| proc.stdin.write(chunk, encoding, callback); | |
| } | |
| }); | |
| proc.stdout.on('data', (c: Buffer) => duplex.push(c)); | |
| proc.stdout.on('end', () => duplex.push(null)); | |
| // Create SSH client | |
| const conn = new Client(); | |
| // Generate a test public key | |
| import { generateKeyPair } from 'node:crypto'; | |
| const { publicKey } = generateKeyPairSync('rsa', { | |
| modulusLength: 2048, | |
| publicKeyEncoding: { type: 'spki', format: 'pem' }, | |
| privateKeyEncoding: { type: 'pkcs8', format: 'pem' } | |
| }); | |
| // Convert to OpenSSH format (simplified - in real code we use ssh-keygen) | |
| // For testing, we'll use a dummy key | |
| const testPublicKey = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...test-key'; | |
| conn.on('ready', () => { | |
| console.log('SSH connection established!'); | |
| // First test basic connection | |
| conn.exec('whoami', (err, stream) => { | |
| if (err) { | |
| console.error('Exec error:', err); | |
| conn.end(); | |
| proc.kill(); | |
| process.exit(1); | |
| } | |
| stream.on('data', (data: Buffer) => { | |
| console.log('Output:', data.toString()); | |
| }); | |
| stream.on('close', (code: number) => { | |
| console.log(`whoami exited with code ${code}`); | |
| // Now test the key installation command | |
| console.log('\nTesting key installation command...'); | |
| const command = `mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && grep -qF "test-key-installation" ~/.ssh/authorized_keys || echo "test-key-installation" >> ~/.ssh/authorized_keys`; | |
| conn.exec(command, (err2, stream2) => { | |
| if (err2) { | |
| console.error('Key install exec error:', err2); | |
| conn.end(); | |
| proc.kill(); | |
| process.exit(1); | |
| } | |
| let output = ''; | |
| stream2.on('data', (data: Buffer) => { | |
| output += data.toString(); | |
| }); | |
| stream2.stderr.on('data', (data: Buffer) => { | |
| console.error('stderr:', data.toString()); | |
| }); | |
| stream2.on('close', (code2: number) => { | |
| console.log(`Key installation command exited with code ${code2}`); | |
| if (output) console.log('Output:', output); | |
| conn.end(); | |
| proc.kill(); | |
| process.exit(code2 === 0 ? 0 : 1); | |
| }); | |
| }); | |
| }); | |
| }); | |
| }); | |
| conn.on('error', (err: Error) => { | |
| console.error(`[SSH] Connection error: ${err.message}`); | |
| proc.kill(); | |
| process.exit(1); | |
| }); | |
| conn.on('keyboard-interactive', (name, instructions, lang, prompts, finish) => { | |
| console.log('Keyboard-interactive prompt'); | |
| if (prompts.length > 0) { | |
| finish([password]); | |
| } else { | |
| finish([]); | |
| } | |
| }); | |
| // Set timeout | |
| const timeout = setTimeout(() => { | |
| console.error('Connection timeout after 10 seconds'); | |
| conn.end(); | |
| proc.kill(); | |
| process.exit(1); | |
| }, 10000); | |
| conn.on('ready', () => { | |
| clearTimeout(timeout); | |
| }); | |
| console.log('Connecting via SSH over Cloudflare tunnel...'); | |
| conn.connect({ | |
| username, | |
| password, | |
| readyTimeout: 10000, | |
| keepaliveInterval: 1000, | |
| tryKeyboard: true, | |
| sock: duplex | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment