pssh ssh 补全 ProxyJump 相关选项与文档: 新增 -W host:port(stdio/netcat 转发)与 -N(仅端口转发不执行命令); -J 支持逗号分隔多跳语法但对多级级联降级为仅第一跳(同步 I/O 架构下多跳应用数据阶段不稳定,实测验证并如实降级); 修复目标认证失败时跳板 io ctx 未释放的泄漏; 单跳 -J/直接连接回归通过, 构建无警告; STATUS/AGENT/CHANGELOG 同步更新
这个提交包含在:
@@ -59,5 +59,11 @@ STATUS.md 完成度总表 AGENT.md 本文件
|
||||
- `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 解析处使用)
|
||||
|
||||
### 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`。
|
||||
- 规则: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-08(TLS 1.3 完善期)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
- 通道:session(exec/shell 真实进程执行)、direct-tcpip(出站桥接)
|
||||
- **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 keygen` / `keyscan` / `copy-id` — 密钥工具
|
||||
- `pssh agent` / `add` / `ssha` — agent 守护进程与密钥管理
|
||||
|
||||
+183
-64
@@ -172,11 +172,13 @@ static int auth_session(ssh_session_t *s, const char *user,
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* ---------------- 代理跳转 (-J):跳板通道上的嵌套会话 ---------------- */
|
||||
/* ---------------- 代理跳转 (-J / -J h1,h2):跳板通道上的嵌套会话 ---------------- */
|
||||
|
||||
typedef struct {
|
||||
ssh_session_t *jump;
|
||||
typedef struct jump_ctx {
|
||||
ssh_session_t *jump; /* 本层 io 数据收发的承载会话 */
|
||||
uint32_t ch;
|
||||
ssh_session_t *owned_session; /* 随本 ctx 一并释放的下一跳承载会话(非最顶层) */
|
||||
struct jump_ctx *outer; /* 更外层 io ctx(资源释放链) */
|
||||
} jump_ctx_t;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/* 连接目标(可选经跳板)。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,
|
||||
const char *jump_spec,
|
||||
const char *user, const char *password,
|
||||
const char *keyfile, int hkmode,
|
||||
ssh_session_t **jump_out) {
|
||||
*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);
|
||||
if (!s) return NULL;
|
||||
ssh_session_set_hostkey_check(s, hkmode);
|
||||
ssh_session_set_verbose(s, g_verbose);
|
||||
ssh_session_set_cipher(s, g_cipher);
|
||||
|
||||
if (jump_spec && *jump_spec) {
|
||||
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 (nhops == 0) {
|
||||
if (ssh_client_connect(s, host, port) < 0) {
|
||||
fprintf(stderr, "pssh: connection failed\n");
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -269,6 +334,13 @@ int pssh_cmd_ssh(int argc, char **argv) {
|
||||
int nfwds = 0;
|
||||
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++) {
|
||||
const char *a = argv[i];
|
||||
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;
|
||||
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) {
|
||||
if (opt_missing(argc, i, "-L", "[bind:]port:host:hostport") < 0) return 1;
|
||||
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"
|
||||
"用法: pssh [选项] [user@]host [command]\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"
|
||||
" -D <port> -o StrictHostKeyChecking=no|accept-new|yes\n");
|
||||
" -W host:port -N(仅转发) -L [bind:]port:host:hostport\n"
|
||||
" -R [bind:]port:host:hostport -D <port>\n"
|
||||
" -o StrictHostKeyChecking=no|accept-new|yes\n");
|
||||
return 1;
|
||||
}
|
||||
if (!user) {
|
||||
@@ -417,7 +507,10 @@ int pssh_cmd_ssh(int argc, char **argv) {
|
||||
if (!s) return 1;
|
||||
if (!auth_session(s, user, password, keyfile)) {
|
||||
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);
|
||||
return 1;
|
||||
}
|
||||
@@ -439,12 +532,29 @@ int pssh_cmd_ssh(int argc, char **argv) {
|
||||
/* ---- 会话通道 ---- */
|
||||
uint32_t ch = 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) {
|
||||
fprintf(stderr, "pssh: channel open failed\n");
|
||||
goto done;
|
||||
}
|
||||
ssh_channel_request_exec(s, ch, cmd);
|
||||
} else if (no_command) {
|
||||
/* -N:不建会话通道,仅端口转发 */
|
||||
interactive = 0; /* 事件循环只泵转发,不读 stdin */
|
||||
} else {
|
||||
interactive = 1;
|
||||
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);
|
||||
#else
|
||||
raw_on();
|
||||
shell_raw = 1;
|
||||
#endif
|
||||
ssh_channel_request_pty(s, ch, "xterm", 80, 24);
|
||||
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);
|
||||
|
||||
/* 键盘输入(交互) */
|
||||
/* 键盘输入(交互 / -W stdio 转发) */
|
||||
if (interactive) {
|
||||
#ifdef _WIN32
|
||||
while (KBHIT()) {
|
||||
int c = GETCH();
|
||||
if (c == 0 || c == 0xE0) {
|
||||
int sc = GETCH();
|
||||
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 (stdio_mode) {
|
||||
/* -W:低位字节透传,不映射转义键 */
|
||||
while (KBHIT()) {
|
||||
uint8_t b = (uint8_t)GETCH();
|
||||
if (ssh_channel_send_data(s, ch, &b, 1) < 0) goto done;
|
||||
}
|
||||
} else {
|
||||
while (KBHIT()) {
|
||||
int c = GETCH();
|
||||
if (c == 0 || c == 0xE0) {
|
||||
int sc = GETCH();
|
||||
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);
|
||||
continue;
|
||||
uint8_t b = (uint8_t)c;
|
||||
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
|
||||
fd_set ifds;
|
||||
@@ -588,7 +707,7 @@ done:
|
||||
int exit_code = 0;
|
||||
if (ch) ssh_channel_exit_status(s, ch, &exit_code);
|
||||
#ifndef _WIN32
|
||||
if (interactive) raw_off();
|
||||
if (shell_raw) raw_off();
|
||||
#endif
|
||||
ssh_fwd_free(fwd);
|
||||
if (jump) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户