jumpserver是个很受欢迎的开源堡垒机系统,功能丰富,开箱即用。最近公司有需求想要二次开发,调研过程中发现改造还是比较困难的,主要有以下几个问题:

部署麻烦,有core、koko、lion、luna、lina等服务 分别用了python、golang、nodejs语言开发 core服务使用了django-rest-framework库,以配置为主,修改逻辑比较困难 部分前后端未分离 核心的lion未开源 celery处理异步任务,很多任务都不是必要的 ansbile推送系统用户等,需要服务器和网络支持 控制台权限管理不清晰 综上原因,改造比较困难,因此想自己开发一个。 虽然后来发现了更简单轻量的next-terminal,但本着学习的目的,还是自己造了轮子。

堡垒机系统的关键在于通过网页上连接服务器,因此在此记录下方案。 开始想用python实现后端,用过flask + paramiko + flask-socketio方案,尝试下来发现性能不行,ssh延迟比较大。 后来改成了fastapi + asyncssh,效果还行。 因为公司内用golang多些,最终用Gin框架来实现后端。 rdp协议连接用了Guacamole。 前端用了vue + xterm + socketio

paramiko连接ssh部分代码:

import paramiko
import threading
import io

class SshClient:

    def __init__(self, ip, port, username, psd=None, pkey=None):
        self.ip = ip
        self.port = port
        self.username = username  # 用户
        self.psd = psd  #密码
        self.pkey = pkey  # 私钥内容
        self.channel = None

    def send(self, msg):
        '''
        websocket接收到网页发来的消息后,调用此函数发给ssh通道
        '''
        if self.channel:
            return self.channel.send(msg)

    def listening(self):
        while True:
            ret = self.channel.recv(10240)
            print('>>', ret)
            # 收到ssh通道返回的消息,通过websocket返回给网页

    def start(self):
        client = paramiko.SSHClient()
        client.load_system_host_keys()
        client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

        # 私钥登录
        if self.pkey: 
            pkey = paramiko.RSAKey.from_private_key(io.StringIO(self.pkey))
            client.connect(hostname=self.ip, port=self.port, username=self.username, pkey=pkey)
            self.channel = client.invoke_shell()
        # 密码登录
        else:  
            client.connect(hostname=self.ip, port=self.port, username=self.username, password=self.psd)
            self.channel = client.invoke_shell()

        threading._start_new_thread(self.listening, ())

asyncssh连接ssh部分代码:

import asyncio, asyncssh, sys
from fastapi import WebSocket

class MySSHClientSession(asyncssh.SSHClientSession):
    def __init__(self, consumer):
        self.consumer = consumer

    def data_received(self, data, datatype):
        asyncio.ensure_future(self.consumer.send(data))

    def connection_lost(self, exc):
        if exc:
            print('SSH session error: ' + str(exc), file=sys.stderr)

class MySSHClient(asyncssh.SSHClient):
    def connection_made(self, conn):
        print('Connection made to %s.' % conn.get_extra_info('peername')[0])

    def auth_completed(self):
        print('Authentication successful.')

class WsClient():

    def __init__(self, websocket: WebSocket, client_id=None):
        self.websocket = websocket
        self.client_id = client_id
        self.chan = None
        self.conn = None

    async def close(self):
        if self.conn:
            self.conn.close()
        if self.chan:
            self.chan.close()
        self.conn = None
        self.chan = None
        self.websocket = None

    async def send(self, text):
        await self.websocket.send_text(text)

    def write(self, text):
        if self.chan and self.chan._send_state == 'open':
            self.chan.write(text)
        else:
            print("no channel")

    async def run_client(self):
        self.conn, client = await asyncssh.create_connection(MySSHClient, '192.168.64.8', port=22, username='root', password=' ', known_hosts=None)
        self.chan, _ = await self.conn.create_session(lambda:MySSHClientSession(self), term_type='xterm')

前端部分截图