pssh ssh 补全 ProxyJump 相关选项与文档: 新增 -W host:port(stdio/netcat 转发)与 -N(仅端口转发不执行命令); -J 支持逗号分隔多跳语法但对多级级联降级为仅第一跳(同步 I/O 架构下多跳应用数据阶段不稳定,实测验证并如实降级); 修复目标认证失败时跳板 io ctx 未释放的泄漏; 单跳 -J/直接连接回归通过, 构建无警告; STATUS/AGENT/CHANGELOG 同步更新

这个提交包含在:
JGZYES
2026-08-14 11:59:43 +08:00
父节点 50386fcf8d
当前提交 ef86a4c201
修改 4 个文件,包含 193 行新增67 行删除
+8 -2
查看文件
@@ -59,5 +59,11 @@ STATUS.md 完成度总表 AGENT.md 本文件
- `ecdsa.c` `bits2int`:输入短于群阶 qlen 时应右对齐而非左移(否则 P-384+SHA-256 验签错误) - `ecdsa.c` `bits2int`:输入短于群阶 qlen 时应右对齐而非左移(否则 P-384+SHA-256 验签错误)
- `tls_conn.c` 新增 `paze_tls_store_peer_cert`:对端证书 DER 拷贝到自有缓冲,避免 issuer/subject 指针指向被记录层复用的 `app_buf` 导致链验证失败(TLS 1.3 与 1.2 均在 Certificate 解析处使用) - `tls_conn.c` 新增 `paze_tls_store_peer_cert`:对端证书 DER 拷贝到自有缓冲,避免 issuer/subject 指针指向被记录层复用的 `app_buf` 导致链验证失败(TLS 1.3 与 1.2 均在 Certificate 解析处使用)
### 3. SSH ProxyJump ### 3. SSH ProxyJump(✅ 基础完成)
客户端连接管理(经跳板机转发),中等。
客户端经跳板机转发连接目标主机,核心链路已实现(`apps/ssh/main.c` 初始版本即含):
- `-J <user@host[:port]>``connect_target()` 在跳板会话上用 `direct-tcpip` 通道承载目标 SSH 握手(嵌套会话),主事件循环直接轮询跳板 socket。
- config 文件 `ProxyJump` 字段解析(`src/ssh/ssh_config.c`)与匹配。
- 补全(2026-08-14):`pssh ssh -W host:port`stdio/netcat 转发,目标 TCP 隧道桥接本地 stdio)、`-N`(仅端口转发不执行远程命令,配 `-L/-R/-D`)。
已知限制:`-J` 接受逗号分隔的多跳语法,但多级级联跳板在同步 I/O 架构下的应用数据阶段交互不稳定,实测多跳握手可完成但握手后易卡;故当前对多跳打印警告并**退化为仅第一跳的稳定单跳**。单跳与 `-W`/`-N` 均已实测验证。
+1
查看文件
@@ -25,6 +25,7 @@
- 新增 `CHANGELOG.md`;更新 `STATUS.md` / `AGENT.md` - 新增 `CHANGELOG.md`;更新 `STATUS.md` / `AGENT.md`
- 规则:Git 推送仅发往 parlz(gitea),不再提交 origin(GitHub)。 - 规则:Git 推送仅发往 parlz(gitea),不再提交 origin(GitHub)。
- **SSH ProxyJump**:确认 `-J` 代理跳板(单跳)核心链路已实现并同步到文档;新增 `pssh ssh -W host:port`(标准 I/O 转发/netcat 模式,目标 TCP 隧道桥接本地 stdio)与 `-N`(不执行远程命令、仅端口转发,配合 `-L/-R/-D`)。说明:`-J` 接受逗号分隔的多跳语法,但多级级联跳板在同步 I/O 架构下的应用数据阶段交互不稳定,当前会打印警告并退化为**仅第一跳**的稳定单跳。
## [功能阶段] — 2026-08TLS 1.3 完善期) ## [功能阶段] — 2026-08TLS 1.3 完善期)
+1 -1
查看文件
@@ -41,7 +41,7 @@
- 通道:sessionexec/shell 真实进程执行)、direct-tcpip(出站桥接) - 通道:sessionexec/shell 真实进程执行)、direct-tcpip(出站桥接)
- **SFTP 子系统 v3**OPEN/READ/WRITE/OPENDIR/READDIR/STAT/REALPATH 等,与 OpenSSH sftp / scp -s 互操作(Git OpenSSH 实测通过) - **SFTP 子系统 v3**OPEN/READ/WRITE/OPENDIR/READDIR/STAT/REALPATH 等,与 OpenSSH sftp / scp -s 互操作(Git OpenSSH 实测通过)
- 客户端命令: - 客户端命令:
- `pssh ssh` — 远程 shell / 执行命令,`-L`/`-R`/`-D` 端口转发 - `pssh ssh` — 远程 shell / 执行命令,`-L`/`-R`/`-D` 端口转发`-J` 代理跳板(单跳)、`-W host:port` stdio 转发、`-N` 仅转发
- `pssh scp` — SFTP 优先(OpenSSH 9.x 默认),回退 legacy SCP;`-r` 递归、`-p` 保留时间戳 - `pssh scp` — SFTP 优先(OpenSSH 9.x 默认),回退 legacy SCP;`-r` 递归、`-p` 保留时间戳
- `pssh keygen` / `keyscan` / `copy-id` — 密钥工具 - `pssh keygen` / `keyscan` / `copy-id` — 密钥工具
- `pssh agent` / `add` / `ssha` — agent 守护进程与密钥管理 - `pssh agent` / `add` / `ssha` — agent 守护进程与密钥管理
+183 -64
查看文件
@@ -172,11 +172,13 @@ static int auth_session(ssh_session_t *s, const char *user,
return ok; return ok;
} }
/* ---------------- 代理跳转 (-J):跳板通道上的嵌套会话 ---------------- */ /* ---------------- 代理跳转 (-J / -J h1,h2):跳板通道上的嵌套会话 ---------------- */
typedef struct { typedef struct jump_ctx {
ssh_session_t *jump; ssh_session_t *jump; /* 本层 io 数据收发的承载会话 */
uint32_t ch; uint32_t ch;
ssh_session_t *owned_session; /* 随本 ctx 一并释放的下一跳承载会话(非最顶层) */
struct jump_ctx *outer; /* 更外层 io ctx(资源释放链) */
} jump_ctx_t; } jump_ctx_t;
static int jump_read(void *ctx, uint8_t *buf, size_t n) { static int jump_read(void *ctx, uint8_t *buf, size_t n) {
@@ -198,62 +200,125 @@ static int jump_write(void *ctx, const uint8_t *buf, size_t n) {
return ssh_channel_send_data(jc->jump, jc->ch, buf, n) < 0 ? -1 : (int)n; return ssh_channel_send_data(jc->jump, jc->ch, buf, n) < 0 ? -1 : (int)n;
} }
/* 连接目标(可选经跳板)。jump_out 输出跳板会话(非 NULL 时由调用方释放)。 /* 一个跳板实体:[user@]host[:port] */
* 返回已通过握手、未认证的会话。 */ typedef struct {
char user[128];
char host[256];
int port;
} jump_hop_t;
#define PSSH_MAX_JUMPS 8
/* 解析 -J 逗号分隔链: "h1,h2@u:port,..."。返回跳数(0=无)。 */
static int parse_jumps(const char *spec, jump_hop_t *hops, int maxhops) {
if (!spec || !*spec) return 0;
int n = 0;
const char *start = spec;
for (const char *p = spec; ; p++) {
if (*p == ',' || *p == '\0') {
if (n >= maxhops) break;
size_t len = (size_t)(p - start);
char buf[512];
if (len >= sizeof(buf)) len = sizeof(buf) - 1;
memcpy(buf, start, len); buf[len] = '\0';
int port = 22;
char user[128] = "", host[256] = "";
parse_userhost(buf, user, sizeof(user), host, sizeof(host), &port);
if (!host[0]) break;
snprintf(hops[n].user, sizeof(hops[n].user), "%s", user);
snprintf(hops[n].host, sizeof(hops[n].host), "%s", host);
hops[n].port = port;
n++;
if (*p == '\0') break;
start = p + 1;
}
}
return n;
}
/* 连接目标(可选经链式跳板 -J h1,h2,...)。
* jump_out 输出最顶层跳板会话(有真实 socket,供事件循环轮询),
* 由调用方 ssh_session_free 释放。
* 返回已通过握手、未认证的目标会话 s;若 s 经跳板建立,其 io ctx(jump_ctx 链)
* 一并承载各中间/最后跳板会话的释放,调用方 free(ssh_session_io_ctx(s)) 时回收。 */
static ssh_session_t *connect_target(const char *host, int port, static ssh_session_t *connect_target(const char *host, int port,
const char *jump_spec, const char *jump_spec,
const char *user, const char *password, const char *user, const char *password,
const char *keyfile, int hkmode, const char *keyfile, int hkmode,
ssh_session_t **jump_out) { ssh_session_t **jump_out) {
*jump_out = NULL; *jump_out = NULL;
jump_hop_t hops[PSSH_MAX_JUMPS];
int nhops = parse_jumps(jump_spec, hops, PSSH_MAX_JUMPS);
if (nhops > 1) {
/* 多级级联跳板在当前同步 I/O 架构下的应用数据阶段交互不稳定,
* 如实降级为「仅第一跳」的稳定单跳,并明确告知用户。 */
fprintf(stderr, "pssh: warning: chained multi-hop ProxyJump (%d hops) is unsupported; "
"using only the first jump %s@%s:%d\n",
nhops, hops[0].user[0] ? hops[0].user : (user ? user : ""),
hops[0].host, hops[0].port);
nhops = 1;
}
ssh_session_t *s = ssh_session_new(0); ssh_session_t *s = ssh_session_new(0);
if (!s) return NULL; if (!s) return NULL;
ssh_session_set_hostkey_check(s, hkmode); ssh_session_set_hostkey_check(s, hkmode);
ssh_session_set_verbose(s, g_verbose); ssh_session_set_verbose(s, g_verbose);
ssh_session_set_cipher(s, g_cipher); ssh_session_set_cipher(s, g_cipher);
if (jump_spec && *jump_spec) { if (nhops == 0) {
char juser[128], jhost[256];
int jport = 22;
parse_userhost(jump_spec, juser, sizeof(juser), jhost, sizeof(jhost), &jport);
if (!juser[0]) snprintf(juser, sizeof(juser), "%s", user);
fprintf(stderr, "pssh: proxyjump %s@%s:%d\n", juser, jhost, jport);
ssh_session_t *j = ssh_session_new(0);
if (!j) { ssh_session_free(s); return NULL; }
ssh_session_set_hostkey_check(j, hkmode);
ssh_session_set_verbose(j, g_verbose);
ssh_session_set_cipher(j, g_cipher);
if (ssh_client_connect(j, jhost, jport) < 0) {
fprintf(stderr, "pssh: proxyjump: connect %s failed\n", jhost);
ssh_session_free(j); ssh_session_free(s); return NULL;
}
if (!auth_session(j, juser, password, keyfile)) {
fprintf(stderr, "pssh: proxyjump: auth failed\n");
ssh_session_free(j); ssh_session_free(s); return NULL;
}
uint32_t jch;
if (ssh_channel_open_direct(j, &jch, host, (uint32_t)port,
"127.0.0.1", 0) < 0) {
fprintf(stderr, "pssh: proxyjump: open channel to %s:%d failed\n",
host, port);
ssh_session_free(j); ssh_session_free(s); return NULL;
}
jump_ctx_t *jc = (jump_ctx_t *)malloc(sizeof(jump_ctx_t));
if (!jc) { ssh_session_free(j); ssh_session_free(s); return NULL; }
jc->jump = j;
jc->ch = jch;
ssh_session_set_io(s, jc, jump_read, jump_write);
if (ssh_client_connect_io(s, host, port) < 0) {
fprintf(stderr, "pssh: proxyjump: handshake to %s failed\n", host);
free(jc); ssh_session_free(j); ssh_session_free(s); return NULL;
}
*jump_out = j;
} else {
if (ssh_client_connect(s, host, port) < 0) { if (ssh_client_connect(s, host, port) < 0) {
fprintf(stderr, "pssh: connection failed\n"); fprintf(stderr, "pssh: connection failed\n");
ssh_session_free(s); return NULL; ssh_session_free(s); return NULL;
} }
return s;
} }
/* ---------- 单跳代理(jump = hops[0]) ---------- */
char uh[256];
if (hops[0].user[0]) {
size_t ul = strlen(hops[0].user);
if (ul >= sizeof(uh)) ul = sizeof(uh) - 1;
memcpy(uh, hops[0].user, ul); uh[ul] = '\0';
} else {
size_t ul = strlen(user);
if (ul >= sizeof(uh)) ul = sizeof(uh) - 1;
memcpy(uh, user, ul); uh[ul] = '\0';
}
fprintf(stderr, "pssh: proxyjump %s@%s:%d\n", uh, hops[0].host, hops[0].port);
ssh_session_t *j = ssh_session_new(0);
if (!j) { ssh_session_free(s); return NULL; }
ssh_session_set_hostkey_check(j, hkmode);
ssh_session_set_verbose(j, g_verbose);
ssh_session_set_cipher(j, g_cipher);
if (ssh_client_connect(j, hops[0].host, (uint16_t)hops[0].port) < 0) {
fprintf(stderr, "pssh: proxyjump: connect %s failed\n", hops[0].host);
ssh_session_free(j); ssh_session_free(s); return NULL;
}
if (!auth_session(j, uh, password, keyfile)) {
fprintf(stderr, "pssh: proxyjump: auth %s failed\n", hops[0].host);
ssh_session_free(j); ssh_session_free(s); return NULL;
}
uint32_t dich;
if (ssh_channel_open_direct(j, &dich, host, (uint32_t)port,
"127.0.0.1", 0) < 0) {
fprintf(stderr, "pssh: proxyjump: open channel to %s:%d failed\n",
host, port);
ssh_session_free(j); ssh_session_free(s); return NULL;
}
jump_ctx_t *jc = (jump_ctx_t *)malloc(sizeof(jump_ctx_t));
if (!jc) { ssh_session_free(j); ssh_session_free(s); return NULL; }
jc->jump = j;
jc->ch = dich;
jc->owned_session = NULL;
jc->outer = NULL;
ssh_session_set_io(s, jc, jump_read, jump_write);
if (ssh_client_connect_io(s, host, port) < 0) {
fprintf(stderr, "pssh: proxyjump: handshake to %s failed\n", host);
free(jc); ssh_session_free(j); ssh_session_free(s); return NULL;
}
*jump_out = j;
return s; return s;
} }
@@ -269,6 +334,13 @@ int pssh_cmd_ssh(int argc, char **argv) {
int nfwds = 0; int nfwds = 0;
char arg_user[128], arg_host[256]; /* [user@]host[:port] 解析缓冲 */ char arg_user[128], arg_host[256]; /* [user@]host[:port] 解析缓冲 */
/* -W host:port:stdio 转发(不申请 pty/shell,把目标 TCP 隧道桥接到 stdin/stdout)。
* -N:不执行远程命令、不建会话通道,仅做端口转发(需配合 -L/-R/-D)。 */
const char *stdio_target = NULL;
int stdio_port = 0;
int no_command = 0;
char stdio_host[256];
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
const char *a = argv[i]; const char *a = argv[i];
if (strcmp(a, "-V") == 0) { if (strcmp(a, "-V") == 0) {
@@ -312,6 +384,23 @@ int pssh_cmd_ssh(int argc, char **argv) {
if (opt_missing(argc, i, "-J", "<user@host[:port]>") < 0) return 1; if (opt_missing(argc, i, "-J", "<user@host[:port]>") < 0) return 1;
jump_spec = argv[++i]; jump_spec = argv[++i];
} }
else if (strcmp(a, "-W") == 0) {
if (opt_missing(argc, i, "-W", "host:port") < 0) return 1;
const char *spec = argv[++i];
const char *colon = strchr(spec, ':');
if (!colon) {
fprintf(stderr, "pssh: bad -W spec '%s' (host:port)\n", spec);
return 1;
}
size_t hl = (size_t)(colon - spec);
if (hl >= sizeof(stdio_host)) hl = sizeof(stdio_host) - 1;
memcpy(stdio_host, spec, hl); stdio_host[hl] = '\0';
stdio_port = atoi(colon + 1);
if (stdio_host[0] && stdio_port > 0) stdio_target = stdio_host;
}
else if (strcmp(a, "-N") == 0) {
no_command = 1; /* 仅转发,不执行命令/不建会话 */
}
else if (strcmp(a, "-L") == 0) { else if (strcmp(a, "-L") == 0) {
if (opt_missing(argc, i, "-L", "[bind:]port:host:hostport") < 0) return 1; if (opt_missing(argc, i, "-L", "[bind:]port:host:hostport") < 0) return 1;
if (nfwds < 16 && parse_fwd(argv[++i], &fwds[nfwds]) == 0) { if (nfwds < 16 && parse_fwd(argv[++i], &fwds[nfwds]) == 0) {
@@ -393,8 +482,9 @@ int pssh_cmd_ssh(int argc, char **argv) {
fprintf(stderr, "pssh - SSH2 客户端 (平替 ssh)\n" fprintf(stderr, "pssh - SSH2 客户端 (平替 ssh)\n"
"用法: pssh [选项] [user@]host [command]\n" "用法: pssh [选项] [user@]host [command]\n"
"选项: -p <port> -l <user> -pwd <pw> -i <key> -J <user@jump[:port]>\n" "选项: -p <port> -l <user> -pwd <pw> -i <key> -J <user@jump[:port]>\n"
" -L [bind:]port:host:hostport -R [bind:]port:host:hostport\n" " -W host:port -N(仅转发) -L [bind:]port:host:hostport\n"
" -D <port> -o StrictHostKeyChecking=no|accept-new|yes\n"); " -R [bind:]port:host:hostport -D <port>\n"
" -o StrictHostKeyChecking=no|accept-new|yes\n");
return 1; return 1;
} }
if (!user) { if (!user) {
@@ -417,7 +507,10 @@ int pssh_cmd_ssh(int argc, char **argv) {
if (!s) return 1; if (!s) return 1;
if (!auth_session(s, user, password, keyfile)) { if (!auth_session(s, user, password, keyfile)) {
fprintf(stderr, "pssh: auth failed\n"); fprintf(stderr, "pssh: auth failed\n");
if (jump) { ssh_session_free(jump); } if (jump) {
free(ssh_session_io_ctx(s)); /* 释放跳板 io ctx 链(含中间会话) */
ssh_session_free(jump); /* 释放顶层跳板 */
}
ssh_session_free(s); ssh_session_free(s);
return 1; return 1;
} }
@@ -439,12 +532,29 @@ int pssh_cmd_ssh(int argc, char **argv) {
/* ---- 会话通道 ---- */ /* ---- 会话通道 ---- */
uint32_t ch = 0; uint32_t ch = 0;
int interactive = 0; int interactive = 0;
if (cmd) { int stdio_mode = 0; /* -W:目标 TCP 通道桥接到本地 stdin/stdout */
#ifndef _WIN32
int shell_raw = 0; /* 是否已调用 raw_on(普通交互 shell;仅 POSIX) */
#endif
if (stdio_target) {
/* -W host:port:直接对目标开 direct-tcpip 通道,桥接本地 stdio */
if (ssh_channel_open_direct(s, &ch, stdio_target, (uint32_t)stdio_port,
"127.0.0.1", 0) < 0) {
fprintf(stderr, "pssh: -W: open channel to %s:%d failed\n",
stdio_target, stdio_port);
goto done;
}
stdio_mode = 1;
interactive = 1; /* 读 stdin 转发到通道 */
} else if (cmd) {
if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) { if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) {
fprintf(stderr, "pssh: channel open failed\n"); fprintf(stderr, "pssh: channel open failed\n");
goto done; goto done;
} }
ssh_channel_request_exec(s, ch, cmd); ssh_channel_request_exec(s, ch, cmd);
} else if (no_command) {
/* -N:不建会话通道,仅端口转发 */
interactive = 0; /* 事件循环只泵转发,不读 stdin */
} else { } else {
interactive = 1; interactive = 1;
if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) { if (ssh_channel_open(s, &ch, "session", 0, 0) < 0) {
@@ -455,6 +565,7 @@ int pssh_cmd_ssh(int argc, char **argv) {
SetConsoleOutputCP(CP_UTF8); SetConsoleOutputCP(CP_UTF8);
#else #else
raw_on(); raw_on();
shell_raw = 1;
#endif #endif
ssh_channel_request_pty(s, ch, "xterm", 80, 24); ssh_channel_request_pty(s, ch, "xterm", 80, 24);
ssh_channel_request_shell(s, ch); ssh_channel_request_shell(s, ch);
@@ -548,27 +659,35 @@ int pssh_cmd_ssh(int argc, char **argv) {
/* 转发本地事件 */ /* 转发本地事件 */
ssh_fwd_pump(fwd, s, &rfds, NULL); ssh_fwd_pump(fwd, s, &rfds, NULL);
/* 键盘输入(交互) */ /* 键盘输入(交互 / -W stdio 转发) */
if (interactive) { if (interactive) {
#ifdef _WIN32 #ifdef _WIN32
while (KBHIT()) { if (stdio_mode) {
int c = GETCH(); /* -W:低位字节透传,不映射转义键 */
if (c == 0 || c == 0xE0) { while (KBHIT()) {
int sc = GETCH(); uint8_t b = (uint8_t)GETCH();
uint8_t seq[3]; if (ssh_channel_send_data(s, ch, &b, 1) < 0) goto done;
size_t n = 0; }
switch (sc) { } else {
case 72: seq[0]=0x1b; seq[1]='['; seq[2]='A'; n=3; break; while (KBHIT()) {
case 80: seq[0]=0x1b; seq[1]='['; seq[2]='B'; n=3; break; int c = GETCH();
case 75: seq[0]=0x1b; seq[1]='['; seq[2]='D'; n=3; break; if (c == 0 || c == 0xE0) {
case 77: seq[0]=0x1b; seq[1]='['; seq[2]='C'; n=3; break; int sc = GETCH();
default: break; uint8_t seq[3];
size_t n = 0;
switch (sc) {
case 72: seq[0]=0x1b; seq[1]='['; seq[2]='A'; n=3; break;
case 80: seq[0]=0x1b; seq[1]='['; seq[2]='B'; n=3; break;
case 75: seq[0]=0x1b; seq[1]='['; seq[2]='D'; n=3; break;
case 77: seq[0]=0x1b; seq[1]='['; seq[2]='C'; n=3; break;
default: break;
}
if (n) ssh_channel_send_data(s, ch, seq, n);
continue;
} }
if (n) ssh_channel_send_data(s, ch, seq, n); uint8_t b = (uint8_t)c;
continue; if (ssh_channel_send_data(s, ch, &b, 1) < 0) goto done;
} }
uint8_t b = (uint8_t)c;
if (ssh_channel_send_data(s, ch, &b, 1) < 0) goto done;
} }
#else #else
fd_set ifds; fd_set ifds;
@@ -588,7 +707,7 @@ done:
int exit_code = 0; int exit_code = 0;
if (ch) ssh_channel_exit_status(s, ch, &exit_code); if (ch) ssh_channel_exit_status(s, ch, &exit_code);
#ifndef _WIN32 #ifndef _WIN32
if (interactive) raw_off(); if (shell_raw) raw_off();
#endif #endif
ssh_fwd_free(fwd); ssh_fwd_free(fwd);
if (jump) { if (jump) {