From a6720cfe398264f0fa2541b8140e5002d5dbeb2d Mon Sep 17 00:00:00 2001 From: Justin Lin Date: Sun, 13 Oct 2024 15:59:01 +0800 Subject: [PATCH] feat: connect through ssh --- quests/Dockerfile | 3 +- quests/sshd_config | 121 ++++++++++++++++++++++++++++++++ server/src/api/socket.ts | 20 +++--- server/src/containers/docker.ts | 91 +++++++++++++++--------- server/src/containers/ssh.ts | 0 5 files changed, 192 insertions(+), 43 deletions(-) create mode 100644 quests/sshd_config create mode 100644 server/src/containers/ssh.ts diff --git a/quests/Dockerfile b/quests/Dockerfile index aed6ec0..de56f7b 100644 --- a/quests/Dockerfile +++ b/quests/Dockerfile @@ -25,13 +25,12 @@ COPY ./quests/.includes/zshrc /etc/zsh/zshrc ENV USERNAME=commander RUN adduser --disabled-password --gecos '' --shell /bin/zsh ${USERNAME} +COPY ./quests/sshd_config /etc/ssh/sshd_config COPY ./ssh_pubkey /home/${USERNAME}/.ssh/authorized_keys RUN chmod 600 /home/${USERNAME}/.ssh/authorized_keys \ && chmod 700 /home/${USERNAME}/.ssh \ && chown -R ${USERNAME}:${USERNAME} /home/${USERNAME}/.ssh -RUN apt-get install -y net-tools - RUN service ssh start EXPOSE 22 diff --git a/quests/sshd_config b/quests/sshd_config new file mode 100644 index 0000000..b5063ab --- /dev/null +++ b/quests/sshd_config @@ -0,0 +1,121 @@ +# This is the sshd server system-wide configuration file. See +# sshd_config(5) for more information. + +# This sshd was compiled with PATH=/usr/local/bin:/usr/bin:/bin:/usr/games + +# The strategy used for options in the default sshd_config shipped with +# OpenSSH is to specify options with their default value where +# possible, but leave them commented. Uncommented options override the +# default value. + +Include /etc/ssh/sshd_config.d/*.conf + +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#HostKey /etc/ssh/ssh_host_rsa_key +#HostKey /etc/ssh/ssh_host_ecdsa_key +#HostKey /etc/ssh/ssh_host_ed25519_key + +# Ciphers and keying +#RekeyLimit default none + +# Logging +#SyslogFacility AUTH +#LogLevel INFO + +# Authentication: + +#LoginGraceTime 2m +#PermitRootLogin prohibit-password +#StrictModes yes +#MaxAuthTries 6 +#MaxSessions 10 + +#PubkeyAuthentication yes + +# Expect .ssh/authorized_keys2 to be disregarded by default in future. +#AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2 + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + +# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts +#HostbasedAuthentication no +# Change to yes if you don't trust ~/.ssh/known_hosts for +# HostbasedAuthentication +#IgnoreUserKnownHosts no +# Don't read the user's ~/.rhosts and ~/.shosts files +#IgnoreRhosts yes + +# To disable tunneled clear text passwords, change to no here! +#PasswordAuthentication yes +#PermitEmptyPasswords no + +# Change to yes to enable challenge-response passwords (beware issues with +# some PAM modules and threads) +KbdInteractiveAuthentication no + +# Kerberos options +#KerberosAuthentication no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes +#KerberosGetAFSToken no + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes +#GSSAPIStrictAcceptorCheck yes +#GSSAPIKeyExchange no + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the KbdInteractiveAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via KbdInteractiveAuthentication may bypass +# the setting of "PermitRootLogin prohibit-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and KbdInteractiveAuthentication to 'no'. +UsePAM yes + +#AllowAgentForwarding yes +#AllowTcpForwarding yes +#GatewayPorts no +X11Forwarding yes +#X11DisplayOffset 10 +#X11UseLocalhost yes +#PermitTTY yes +PrintMotd no +#PrintLastLog yes +#TCPKeepAlive yes +#PermitUserEnvironment no +#Compression delayed +#ClientAliveInterval 0 +#ClientAliveCountMax 3 +#UseDNS no +#PidFile /run/sshd.pid +#MaxStartups 10:30:100 +#PermitTunnel no +#ChrootDirectory none +#VersionAddendum none + +# no default banner path +#Banner none + +# Allow client to pass locale environment variables +AcceptEnv LANG LC_* NODE_PATH ZDOTDIR API_ENDPOINT TOKEN + +# override default of no subsystems +Subsystem sftp /usr/lib/openssh/sftp-server + +# Example of overriding settings on a per-user basis +#Match User anoncvs +# X11Forwarding no +# AllowTcpForwarding no +# PermitTTY no +# ForceCommand cvs server diff --git a/server/src/api/socket.ts b/server/src/api/socket.ts index 9ad524b..aff3feb 100644 --- a/server/src/api/socket.ts +++ b/server/src/api/socket.ts @@ -1,8 +1,9 @@ import { Server } from 'socket.io' import type { Socket } from 'socket.io' +import { Duplex } from 'stream' import validator from 'validator' import type { Server as HttpServer } from 'http' -import { Session } from '@linux-odyssey/models' +import { ISession, Session } from '@linux-odyssey/models' import { getAndStartContainer, attachContainer } from '../containers/docker.js' import SessionMiddleware from '../middleware/session.js' import { genJWT } from '../utils/auth.js' @@ -78,30 +79,33 @@ async function connectContainer(socket: Socket, next: (err?: Error) => void) { } next() } catch (err) { - console.error(err) + logger.error(err) next(new Error('Failed to start container.')) } } function onConnect(socket: Socket) { - const { session, stream } = socket.data.context - stream.socket.on('data', (chunk: Buffer) => { + const { session, stream } = socket.data.context as { + session: ISession + stream: Duplex + } + stream.on('data', (chunk: Buffer) => { socket.emit('terminal', chunk.toString()) }) socket.on('terminal', function incoming(message) { - stream.socket.write(message) + stream.write(message) }) const socketCallback = (event: string, ...data: any[]) => { socket.emit(event, ...data) } - listenToSession(session.id, socketCallback) + listenToSession(session._id.toString(), socketCallback) socket.on('disconnect', () => { - stream.socket.write('exit\n') - removeFromSession(session.id, socketCallback) + stream.write('exit\n') + removeFromSession(session._id.toString(), socketCallback) stream.destroy() }) } diff --git a/server/src/containers/docker.ts b/server/src/containers/docker.ts index 2b8a653..d5eae85 100644 --- a/server/src/containers/docker.ts +++ b/server/src/containers/docker.ts @@ -1,3 +1,6 @@ +import fs from 'fs' +import { Duplex } from 'stream' +import { Client } from 'ssh2' import Docker from 'dockerode' import config, { getQuestImage } from '../config.js' import logger from '../utils/logger.js' @@ -11,29 +14,15 @@ const newContainerOptions = ( binds?: string[] } = {} ): Docker.ContainerCreateOptions => ({ - // AttachStdin: true, - // AttachStdout: true, - // AttachStderr: true, - // Tty: true, - // OpenStdin: true, - // StdinOnce: false, name, Image: getQuestImage(imageId), HostConfig: { NetworkMode: 'linux-odyssey-players', // config.docker.network, - // PortBindings: { - // '22/tcp': [ - // { - // HostIP: '0.0.0.0', - // HostPort: '11122', // random port - // }, - // ], - // }, Binds: options.binds, ExtraHosts: ['host.docker.internal:host-gateway'], }, ExposedPorts: { - // '22/tcp': {}, + '22/tcp': {}, }, }) @@ -64,32 +53,68 @@ export async function getAndStartContainer( if (!(await container.inspect()).State.Running) { await container.start() } + await new Promise((resolve) => setTimeout(resolve, 2000)) return container } export async function attachContainer( container: Docker.Container, { token }: { token: string } -) { - const exec = await container.exec({ - AttachStdin: true, - AttachStdout: true, - AttachStderr: true, - Cmd: ['/bin/zsh'], - Tty: true, - Env: [ - `TOKEN=${token}`, - `API_ENDPOINT=${config.backendUrl}`, - 'ZDOTDIR=/etc/zsh', - ], - }) +): Promise { + // console.log('attachContainer', await container.inspect()) + const containerIp = (await container.inspect()).NetworkSettings.Networks[ + 'linux-odyssey-players' + ].IPAddress + console.log('containerIp', containerIp) + console.log('token', token) + const conn = new Client() - const execOutput = await exec.start({ - Detach: false, - Tty: true, + return new Promise((resolve, reject) => { + conn + .on('ready', () => { + console.log('SSH connection ready') + conn.shell( + { + env: { + TOKEN: token, + API_ENDPOINT: 'http://host.docker.internal:3000', + ZDOTDIR: '/etc/zsh', + NODE_PATH: '/usr/src/node_modules', + }, + }, + (err, stream) => { + if (err) { + reject(err) + } + stream.on('error', (err) => { + reject(err) + }) + stream.on('close', () => { + console.log('Stream :: close') + conn.end() + }) + resolve(stream) + } + ) + }) + .on('error', (err) => { + reject(err) + }) + .connect({ + host: containerIp, + port: 22, + username: 'commander', + privateKey: fs.readFileSync('/home/wancat/.ssh/id_ed25519'), + debug: console.log, + }) }) - - return execOutput + // const exec = await container.exec({ + // Env: [ + // `TOKEN=${token}`, + // `API_ENDPOINT=${config.backendUrl}`, + // 'ZDOTDIR=/etc/zsh', + // ], + // }) } export async function deleteContainer(id: string) { diff --git a/server/src/containers/ssh.ts b/server/src/containers/ssh.ts new file mode 100644 index 0000000..e69de29