SSHv2 client library for MoonBit with password and publickey authentication, SFTP v3, and local/remote/SOCKS5 port forwarding. / 使用 MoonBit 实现的 SSHv2 客户端库,支持密码/公钥认证、SFTP v3 文件传输及本地/远程/SOCKS5 端口转发
| 能力 | 状态 | 说明 |
|---|---|---|
| TCP 传输层 | ✅ | src/socket/socket.mbt + socket.c(自带 socket FFI,支持 POSIX 与 Winsock2) |
| Banner 交换 | ✅ | Client::connect() |
| 包二进制编解码 | ✅ | src/packet.mbt:length / padding / payload / MAC(EtM 流加密模式) |
| KEXINIT 协商 | ✅ | src/kex.mbt:ecdh-sha2-nistp256 / diffie-hellman-group14-sha256 / diffie-hellman-group14-sha1 |
| 密钥派生 | ✅ | RFC 4253 §7.2 派生(derive_keys()),HMAC-SHA-1/256 扩展 |
| 主机密钥验证 | ✅ | ssh-rsa / rsa-sha2-256 / rsa-sha2-512 / ssh-dss / ecdsa-sha2-nistp256/384/521 / ssh-ed25519 |
| 密码认证 | ✅ | Client::auth_password() 完整流程 |
| 公钥认证 | ✅ | Client::auth_publickey()(two-step 查询 + 签名);私钥支持 RSA / Ed25519 / DSA / ECDSA;公钥文件必须是 OpenSSH 单行格式 |
| keyboard-interactive 认证 | ✅ | Client::auth_keyboard_interactive() |
| 自动回退认证 | ✅ | Client::auth_auto():none → publickey → keyboard-interactive → password |
| exec 命令执行 | ✅ | Client::exec() 返回 (stdout, stderr) |
| shell 通道 | ✅ | Client::shell()(不管理交互式 I/O,仅打开 shell 通道) |
| known_hosts 解析 | ✅ | 基础解析 + 通配符;HMAC-SHA1 哈希形式(\|1\|…)待支持 |
| SFTP | ✅ | SFTP v3 协议实现(src/sftp.mbt) |
| 远程端口转发 (-R) | ✅ | Client::forward_remote_port() — tcpip-forward 全局请求 + forwarded-tcpip 通道 |
| 本地端口转发 (-L) | ✅ | Client::forward_local_port() — direct-tcpip 通道(双向 relay_data 中继) |
| SOCKS5 动态转发 (-D) | ✅ | Client::forward_socks5() — SOCKS5 代理 |
┌─────────────────────────────────────────────────────────┐
│ cmd/{password,key,sftp,forwarding,output}/ │
│ ── CLI 入口(按认证方式/场景分命令)│
└────────────────────────┬────────────────────────────────┘
│ @src.Client / @src.SftpClient
┌────────────────────────▼────────────────────────────────┐
│ src/ssh_client.mbt ── 顶层 API │
│ • ConnectOptions ── 连接选项(含 verifier) │
│ • Client::connect ── Banner 交换 │
│ • Client::kex ── 密钥协商 │
│ • Client::auth_password ── 密码认证 │
│ • Client::auth_publickey── 公钥认证(two-step) │
│ • Client::auth_keyboard_interactive ── kbd-int 认证 │
│ • Client::auth_auto ── 自动回退认证 │
│ • Client::open_session ── 打开 session 通道 │
│ • Client::exec ── 命令执行(stdout,stderr) │
│ • Client::shell ── shell 通道 │
│ • Client::forward_local_port ── 本地转发 (-L) │
│ • Client::forward_remote_port ── 远程转发 (-R) │
│ • Client::forward_socks5 ── SOCKS5 代理 (-D) │
│ • Client::cancel_remote_forward ── 取消远程转发 │
│ • Client::close ── 清理 │
└────────────────────────┬────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
┌──▼────────┐ ┌────────▼─────┐ ┌───────────▼──┐
│ packet.mbt│ │ kex.mbt │ │ auth.mbt │
│ • length │ │ • KexContext│ │ • AuthContext│
│ • padding│ │ • 状态机 │ │ • password │
│ • MAC │ │ • 算法协商 │ │ • publickey │
└─────┬─────┘ └──────┬──────┘ │ • kbd-int │
│ │ └──────┬───────┘
│ │ │
│ ┌──────────▼──────────┐ │
│ │ channel.mbt │ │
│ │ • session / exec │ │
│ │ • shell / pty-req │ │
│ │ • subsystem (sftp) │ │
│ │ • window adjust │ │
│ │ • stdout/stderr │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ sftp.mbt │ │
│ │ • SFTP v3 协议 │ │
│ │ • 文件传输 │ │
│ │ • 目录管理 │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ known_hosts.mbt │ │
│ │ • 通配符匹配 │ │
│ │ • base64 编解码 │ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ log.mbt │ │
│ │ • debug / hex_dump │ │
│ └─────────────────────┘ │
│ │
┌─────▼──────────────────────────────────▼───────────────┐
│ src/socket/* ── TCP 传输层(自实现 socket FFI) │
│ • socket.mbt ── Tcp::connect_to_host / write / … │
│ • socket.c ── Winsock2 / POSIX socket 实现 │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ src/crypto/* ── 密码学原语(FFI 调 OpenSSL libcrypto)│
│ • digest / mac / cipher / pkey / kex / error │
│ • openssl.c ── dlopen libcrypto + EVP_* 包装 │
│ • openssl_loader.mbt / crypto_util.mbt │
└────────────────────────┬────────────────────────────────┘
│ FFI
┌────────────────────────▼────────────────────────────────┐
│ libcrypto.so.3 / libcrypto.dylib / libcrypto-3-x64.dll │
└─────────────────────────────────────────────────────────┘ connect()
│
▼
[INIT] ───────► send_banner / recv_banner
│
▼
[KEXINIT] ◄──► KexContext::drive
│ │ KEXDH_INIT / KEX_ECDH_INIT
│ ▼
│ [KEXDH_REPLY / KEX_ECDH_REPLY]
│ │
│ ▼
│ [NEWKEYS] ──► 启用新密钥
▼
[AUTH] ◄──► AuthContext
│ • none 探测
│ • password / publickey / kbd-int
▼
[CHANNEL] ◄──► Channel::open_session
│ │ exec / shell
│ ▼
│ [DATA + EXIT-STATUS]
▼
[CLOSE]moonbit-ssh-client/
├── AGENTS.md 项目开发规范
├── README.md
├── README.mbt.md mooncakes.io 简介
├── LICENSE Apache-2.0
├── moon.mod 模块清单
├── moon.pkg 根包(仅导入子包)
├── ssh_client.mbt 根包占位
├── ssh_client_test.mbt 黑盒测试
├── ssh_client_wbtest.mbt 白盒测试
├── pkg.generated.mbti 根包生成接口
├── .cnb.yml CNB 开发环境配置(含 docker sshd 自启)
├── src/ ★ 核心库
│ ├── moon.pkg 子包:PaiGack/ssh_client/src
│ ├── pkg.generated.mbti 生成接口
│ ├── log.mbt 调试输出(debug / hex_dump / 开关)
│ ├── ssh_client.mbt 顶层 API(ConnectOptions / Client)
│ ├── packet.mbt 包序列化(length/padding/payload/MAC)+ Reader/Writer
│ ├── kex.mbt KEXINIT 状态机、密钥派生
│ ├── auth.mbt 用户认证(password / publickey / kbd-int / none)
│ ├── channel.mbt 通道(session/exec/shell/pty-req/subsystem,状态机 + stdout/stderr)
│ ├── sftp.mbt SFTP v3 协议(文件传输 / 目录管理)
│ ├── known_hosts.mbt known_hosts 解析 + 通配符匹配
│ ├── socket/ ★ TCP 传输层(自实现 FFI)
│ │ ├── moon.pkg
│ │ ├── pkg.generated.mbti
│ │ ├── socket.mbt Tcp::connect_to_host / write / read_*
│ │ └── socket.c Winsock2 / POSIX socket 实现 + TCP_NODELAY
│ └── crypto/ ★ 密码学子包
│ ├── moon.pkg
│ ├── pkg.generated.mbti
│ ├── openssl.c OpenSSL dlopen + EVP_* / BN_* 包装
│ ├── openssl_loader.mbt 动态加载 libcrypto
│ ├── crypto_util.mbt RAND_bytes / ERR_*
│ ├── digest.mbt SHA-1/256/384/512
│ ├── mac.mbt HMAC-SHA-1 / HMAC-SHA-256
│ ├── cipher.mbt AES-128-CTR
│ ├── pkey.mbt RSA/Ed25519/DSA/ECDSA 签名/验证 + 加载 PEM/OpenSSH 私钥
│ ├── kex.mbt DH Group14 + BigInt FFI(BN_*)
│ └── error.mbt CryptoError
├── cmd/ ★ CLI 入口(按认证方式/场景分多个子命令)
│ ├── output/ Hello MoonBit 演示(println "Hello MoonBit!")
│ ├── utils/ CMD args 工具:tokenize / split_user_host / parse_forward_arg / parse_int
│ ├── password/ 密码认证(auth_auto)
│ ├── key/ Ed25519/RSA/ECDSA 公钥认证
│ ├── sftp/ SFTP 文件传输客户端(ls/get/put/rm/mkdir/rmdir/stat)
│ └── forwarding/ 端口转发(-L / -R / -D),三个独立 run-*.sh
├── docs/
│ ├── prd_000.md 设计 PRD
│ ├── prd_001_ssh-key-types-support-plan.md 密钥类型扩展计划
│ ├── prd_002_sftp-support-plan.md SFTP 实现计划
│ ├── prd_003_port-forwarding-plan.md 端口转发设计文档
│ └── crypto-replacement-plan.md OpenSSL 替换为 MoonBit 原生实现的方案
└── scripts/
└── ssh-server/ 本地 docker sshd 脚本(每个认证方式独立)
├── .env 密码(password="123456")
├── .gitignore 忽略本地生成的密钥对
├── password.sh 密码认证 sshd(端口 1022→2222)
├── key-ed25519.sh 生成 ed25519 密钥 + 公钥认证 sshd(端口 2022→2222)
├── key-rsa.sh 生成 4096 位 rsa 密钥 + 公钥认证 sshd(端口 3022→2222)
├── key-ecdsa.sh 生成 ecdsa 密钥 + 公钥认证 sshd(端口 4022→2222)
└── forwarding.sh 密码认证 sshd(端口 5022→2222,AllowTcpForwarding=yes)+ nginx(1080) 验证目标| 依赖 | 版本 | 说明 |
|---|---|---|
| MoonBit toolchain | ≥ 0.19 | https://www.moonbitlang.com/ |
| OpenSSL libcrypto | 1.1.1+ 或 3.x | 系统库(Linux/macOS 自带;Windows 用 MinGW OpenSSL) |
| Docker(可选) | 任意 | 用于本地启动测试 sshd |
| OpenSSH 客户端(可选) | 任意 | 调试用 ssh -i 验证 sshd |
# add C:\msys64\ucrt64\bin to PATH
pacman -S mingw-w64-ucrt-x86_64-gcc
pacman -S mingw-w64-ucrt-x86_64-opensslapt-get install -y gcc libssl-devbrew install gcc openssl| 脚本 | 用途 | 主机端口 |
|---|---|---|
| password.sh | 启动 lscr.io/linuxserver/openssh-server(密码模式) | 1022 |
| key-ed25519.sh | 生成 ed25519 密钥并启动公钥认证 sshd | 2022 |
| key-rsa.sh | 生成 4096 位 RSA 密钥并启动公钥认证 sshd | 3022 |
| key-ecdsa.sh | 生成 ecdsa 密钥并启动公钥认证 sshd | 4022 |
| forwarding.sh | 启动密码认证 sshd 并开启端口转发 | 5022 |
注意: 每次启动 key-*.sh 都会清空并重新生成同名密钥对(id_ed25519 / id_rsa / id_ecdsa);生成位置在 scripts/ssh-server/ 内,已被 .gitignore 排除。
bash scripts/ssh-server/password.sh
# 在另一个终端:ssh admin@127.0.0.1 -p 1022 # 密码 123456# 前置:bash scripts/ssh-server/password.sh
cd cmd/password
./run.sh
# 内部:
# source .env # 注入 MSSH_HOST=127.0.0.1 MSSH_PORT=1022 MSSH_USERNAME=admin MSSH_PASSWORD=123456
# export MOONBIT_CLI_ARGS="$MSSH_USERNAME@$MSSH_HOST --port $MSSH_PORT --exec 'uname -a' --password $MSSH_PASSWORD"
# moon clean && moon run . --target nativeexport MOONBIT_CLI_ARGS="admin@127.0.0.1 --port 1022 --exec 'uname -a' --password 123456"
cd cmd/password
moon run . --target native# 前置:bash scripts/ssh-server/key-ed25519.sh # 或 key-rsa.sh / key-ecdsa.sh
cd cmd/key
./run-ed25519.sh # 默认 --key ${workspace}/scripts/ssh-server/id_ed25519
./run-rsa.sh # 默认 --key ${workspace}/scripts/ssh-server/id_rsa
./run-ecdsa.sh # 默认 --key ${workspace}/scripts/ssh-server/id_ecdsa
# 内部:
# export MOONBIT_CLI_ARGS="$MSSH_USERNAME@$MSSH_HOST --port $MSSH_PORT --exec 'uname -a' --key ${workspace}/scripts/ssh-server/id_<alg>"
# moon clean && moon run . --target native# 前置:bash scripts/ssh-server/password.sh
cd cmd/sftp
./run.sh
# 内部:
# moon clean && moon build . --target native
# export MOONBIT_CLI_ARGS="$MSSH_USERNAME@$MSSH_HOST --port $MSSH_PORT --password $MSSH_PASSWORD --command ls /"
# ../../_build/native/debug/build/cmd/sftp/sftp.exe
# …(依次 mkdir / ls / put / ls / get / stat / rm / rmdir / ls)注意: 当前 cmd_get 把内容以 hex 预览打印到 stdout,并未真正写入本地文件;cmd_put 上传的是一个固定的演示字符串(Hello from MoonSSH SFTP client!)。
# 前置:bash scripts/ssh-server/forwarding.sh
# (启动 sshd 5022→2222,并把 1080 端口绑定到 nginx 作为统一目标;
# 同时配置 AllowTcpForwarding=yes / GatewayPorts=yes / PermitOpen=any)
cd cmd/forwarding
# 1. 远程转发 (-R):远端 8080 → SSH 隧道 → 本地 localhost:1080 (nginx)
./run-remote.sh
# 内部默认:
# export MOONBIT_CLI_ARGS="$MSSH_USERNAME@$MSSH_HOST --port $MSSH_PORT -R 8080:localhost:1080 --password $MSSH_PASSWORD"
# moon clean && moon run . --target native
# 验证(在 sshd 容器内):
# docker exec openssh-server_forwarding curl http://127.0.0.1:8080
# 2. 本地转发 (-L):本地 2080 → SSH 隧道 → 远端 gateway:1080 (宿主机 nginx)
./run-local.sh
# 内部默认:
# export MOONBIT_CLI_ARGS="$MSSH_USERNAME@$MSSH_HOST --port $MSSH_PORT -L 2080:$GATEWAY_IP:1080 --password $MSSH_PASSWORD"
# 验证(在宿主机):
# curl http://127.0.0.1:2080
# 3. SOCKS5 动态代理 (-D):本地 SOCKS5:3080 → SSH 隧道 → 任意远端目标
./run-socks5.sh
# 内部默认:
# export MOONBIT_CLI_ARGS="$MSSH_USERNAME@$MSSH_HOST --port $MSSH_PORT -D 3080 --password $MSSH_PASSWORD"
# 验证(在宿主机,通过 SOCKS5 访问远端 nginx):
# curl --socks5 127.0.0.1:3080 http://$GATEWAY_IP:1080实现要点:
- 三种模式都共用一个 relay_data() 双向中继实现(select-style 轮询,避免阻塞任一方向)。
- SOCKS5 当前仅支持 IPv4(ATYP=0x01)与域名(ATYP=0x03),IPv6(ATYP=0x04)直接返回 0x08 并关闭。
- relay_data 内部把 channel 的 data_sink 切到 Socket(local_socket) 模式,绕开 Buffer 缓冲。
- 集成测试在 .github/workflows/integration.yml 中跑全:依次 run-remote.sh → run-local.sh → run-socks5.sh。
pub struct ConnectOptions {
host : String
port : Int
user : String
client_banner : String? // 默认 "SSH-2.0-MoonSSH_0.1.0"
host_key_policy : HostKeyPolicy // 默认 Strict, 无条目 -> 拒绝所有未知主机
timeout_ms : Int // 默认 30_000
}| 方法 | 适用场景 |
|---|---|
| .with_strict_host_key_check(content) | 加载 OpenSSH 格式 known_hosts;命中放行,未命中拒绝;key 不一致抛 HostKeyMismatch(MITM 警告)。 |
| .with_trust_on_first_use(content, prompt) | 首次连接由 prompt(host, alg, key) -> Bool 决定是否接受并持久化到 entries;之后按 Strict 校验。 |
| .with_host_key_verifier(f) | 由 (alg, key) -> Bool 回调完全接管校验;适用于自定义指纹格式、HSM / 外部 Trust Store 集成。 |
| .with_insecure_host_key() | 跳过 host key 校验。仅用于受信任内网 / 本地回环。公网禁用。 |
// 1. 严格模式:加载本地 known_hosts
let kh = read_known_hosts("~/.ssh/known_hosts") // 调用方自己读
let opts = @src.ConnectOptions::new("example.com", 22, "alice")
.with_strict_host_key_check(kh)
// 2. TOFU:首次询问用户
let opts2 = @src.ConnectOptions::new("new.host.com", 22, "alice")
.with_trust_on_first_use("", (host, alg, key) => {
let fp = fingerprint(alg, key)
confirm("Trust \{host} (fp=\{fp})? [y/N]")
})
// 3. 自定义校验(HSM / 外部 Trust Store)
let opts3 = @src.ConnectOptions::new("example.com", 22, "alice")
.with_host_key_verifier((alg, key) => hsm.verify("ssh-host", alg, key))
// 4. 内网测试:跳过校验
let opts4 = @src.ConnectOptions::new("127.0.0.1", 2222, "test")
.with_insecure_host_key()| 方法 | 说明 |
|---|---|
| Client::connect(opts) -> Client raise SshError | TCP 连接 + banner 交换 |
| Client::kex() -> Unit raise SshError | 完整 KEX(KEXINIT → DH/ECDH → NEWKEYS → 安装加密) |
| Client::auth_password(pwd) -> Unit raise SshError | 密码认证 |
| Client::auth_publickey(key_path) -> Unit raise SshError | 公钥认证(two-step),自动适配 ssh-rsa / rsa-sha2-256 / rsa-sha2-512 / ssh-ed25519 / ecdsa-sha2-nistp256/384/521 / ssh-dss |
| Client::auth_keyboard_interactive(answer_fn) -> Unit raise SshError | keyboard-interactive 认证 |
| Client::auth_auto(pwd, key_path?) -> Unit raise SshError | none → publickey → kbd-int → password 自动回退 |
| Client::open_session() -> Channel | 创建本地 channel(id 自增) |
| Client::exec(ch, cmd) -> (String, String) raise SshError | 执行命令,返回 (stdout, stderr) |
| Client::shell(ch) -> Unit raise SshError | 打开 shell 通道(不管理交互式 I/O) |
| Client::forward_local_port(local_port, remote_host, remote_port) -> Unit raise SshError | 本地端口转发(-L),阻塞运行 |
| Client::forward_remote_port(remote_port, local_host, local_port) -> Unit raise SshError | 远程端口转发(-R),阻塞运行 |
| Client::forward_socks5(local_port) -> Unit raise SshError | SOCKS5 动态代理(-D),阻塞运行;支持 IPv4 + 域名,不支持 IPv6 |
| Client::cancel_remote_forward(address, port) -> Unit raise SshError | 取消远程端口转发 |
| Client::close() -> Unit | 关闭 TCP 连接 |
pub struct Channel {
id : Int // 本地 channel id
peer_id : Int // 服务端分配的 channel id
state : ChannelState // Closed / Opening / Open / ExecPending / EofReceived / Closing / Done
channel_type : ChannelType // Session / DirectTcpip(host, port) / ForwardedTcpip(host, port)
data_sink : DataSink // Buffer / Socket(Tcp)
// ...
}
pub fn Channel::id(self) -> Int
pub fn Channel::peer_id(self) -> Int
pub fn Channel::state(self) -> ChannelState
pub fn Channel::is_open(self) -> Bool
pub fn Channel::exit_status(self) -> Int?pub struct SftpClient {
// 内部字段
priv client : Client
priv channel : Channel
priv version : Int // SFTP 协议版本,当前固定为 3
priv mut request_id : Int
}
pub struct SftpAttrs {
priv flags : Int
size : Int?
uid : Int?
gid : Int?
permissions : Int?
atime : Int?
mtime : Int?
}
pub struct SftpDirEntry {
filename : String
longname : String
attrs : SftpAttrs
}
// 初始化
pub fn SftpClient::open(client : Client) -> SftpClient raise SshError
// 高层 API
pub fn SftpClient::read_file(path : String) -> Bytes raise SshError
pub fn SftpClient::write_file(path : String, data : Bytes, permissions : Int) -> Unit raise SshError
pub fn SftpClient::listdir(path : String) -> Array[SftpDirEntry] raise SshError
// 底层 API
pub fn SftpClient::readdir(path : String) -> Array[SftpDirEntry] raise SshError
pub fn SftpClient::read(handle : Bytes, offset : Int, length : Int) -> Bytes raise SshError
pub fn SftpClient::write(handle : Bytes, offset : Int, data : Bytes) -> Unit raise SshError
pub fn SftpClient::close_handle(handle : Bytes) -> Unit raise SshError
pub fn SftpClient::stat(path : String) -> SftpAttrs raise SshError
pub fn SftpClient::remove(path : String) -> Unit raise SshError
pub fn SftpClient::mkdir(path : String, permissions : Int) -> Unit raise SshError
pub fn SftpClient::rmdir(path : String) -> Unit raise SshError
pub fn SftpClient::rename(oldpath : String, newpath : String) -> Unit raise SshError
pub fn SftpClient::realpath(path : String) -> String raise SshError
pub fn SftpClient::close() -> Unit当前限制: 文件大小按 32 位 Int 读取,超过 4 GiB 的文件会被截断;SftpAttrs 仅解析 SSH_FILEXFER_ATTR_SIZE / UIDGID / PERMISSIONS / ACCESSTIME / MODIFYTIME,未识别的 flags 字段被忽略。
pub struct KnownHost {
patterns : Array[String]
key_alg : String
key : Bytes
}
pub fn KnownHost::matches(self, host : String, port : Int) -> Bool
pub fn KnownHost::alg(self) -> String
pub fn KnownHost::key_bytes(self) -> Bytesmoon fmt # 格式化
moon info # 更新 .mbti 接口
moon build --target native # 编译
moon test --target native # 测试moon coverage analyze > uncovered.log
# 目标:uncovered.log 中 packet / kex / auth 关键路径为空| 版本 | 内容 | 状态 |
|---|---|---|
| v0.1 | 协议骨架:packet / kex 状态机 / auth(密码 + 公钥 + kbd-int + auto)/ channel / crypto FFI / 自带 socket FFI | ✅ 已发布 |
| v0.2 | SFTP 协议 | ✅ 已发布 |
| v0.3 | 端口转发(remote) | ✅ 已发布 |
| v0.4 | 端口转发(local / SOCKS5) | ✅ 已发布 |
| v0.5 | shell 交互式 I/O / pty-req 对接 | 📋 待开发 |
| v0.6 | X11 转发(x11-req + x11 通道,RFC 4254 §6.3.2) | 📋 待开发 |
| 平台 | 编译 | 运行 | 说明 |
|---|---|---|---|
| Linux glibc | ✅ | ✅ | 完全支持 |
| Windows MinGW (Winsock2) | ✅ | ✅ | 完全支持 |
| macOS | ✅ | ✅ | 完全支持 |
SSHv2 client library for MoonBit with password and publickey authentication, SFTP v3, and local/remote/SOCKS5 port forwarding. / 使用 MoonBit 实现的 SSHv2 客户端库,支持密码/公钥认证、SFTP v3 文件传输及本地/远程/SOCKS5 端口转发