文件
PazeSSH/apps/ssh/main.c
T

802 行
30 KiB
C

/* pssh ssh —— SSH2 客户端 (平替 ssh)
* 支持:交互 shell / 执行命令 / 端口转发 -L -R -D(SOCKS5) / 代理跳转 -J /
* config 文件 -F / ~/.ssh/config / ~/.pssh/config.conf / 公钥与 agent 认证
*/
#include "paze/ssh.h"
#include "paze/ssh_agent.h"
#include "paze/ssh_config.h"
#include "paze/ssh_tcpip.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#ifdef _WIN32
#include <winsock2.h>
#include <windows.h>
#include <conio.h>
#include <fcntl.h>
#include <io.h>
#define GETCH _getch
#define KBHIT _kbhit
#define STDIN_FD 0
#else
#include <termios.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
static struct termios orig_tio;
static void raw_on(void) {
struct termios t;
tcgetattr(STDIN_FILENO, &orig_tio);
t = orig_tio;
t.c_lflag &= ~(ICANON | ECHO);
t.c_cc[VMIN] = 0;
t.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &t);
}
static void raw_off(void) {
tcsetattr(STDIN_FILENO, TCSANOW, &orig_tio);
}
#define STDIN_FD STDIN_FILENO
#endif
/* 转发规格 */
typedef struct {
int kind; /* SSH_FWD_L / SSH_FWD_R / SSH_FWD_D */
char bind[128];
int bind_port;
char target[256];
int target_port;
} fwd_spec_t;
#define PSSH_VERSION "Beta-0.0.1"
/* -v / -c 选项(跨 connect_target 传递) */
static int g_verbose = 0;
static const char *g_cipher = NULL;
/* -o ReconnectInterval=N:断线自动重连间隔秒(0=禁用)。断线时 pssh_cmd_ssh 返回 -2,
* 由 pssh main 循环重连。 */
static int g_reconnect = 0;
/* 供外层(pssh main)查询自动重连间隔秒 */
int pssh_ssh_reconnect_interval(void) { return g_reconnect; }
/* 选项缺少参数时的用法提示;缺参返回 -1 */
static int opt_missing(int argc, int i, const char *opt, const char *hint) {
if (i + 1 < argc) return 0;
fprintf(stderr, "pssh: option '%s' requires an argument\n"
" %s %s\n", opt, opt, hint);
return -1;
}
/* -L/-R 解析: [bind:]port:host:hostport */
static int parse_fwd(const char *spec, fwd_spec_t *out) {
const char *c1 = strrchr(spec, ':'); /* host:hostport 前的冒号 */
if (!c1) return -1;
const char *c2 = NULL;
for (const char *p = spec; p < c1; p++) if (*p == ':') c2 = p;
if (!c2) return -1;
const char *c3 = NULL;
for (const char *p = spec; p < c2; p++) if (*p == ':') c3 = p;
if (c3) { /* bind:port:host:hostport */
size_t bl = (size_t)(c3 - spec);
if (bl >= sizeof(out->bind)) bl = sizeof(out->bind) - 1;
memcpy(out->bind, spec, bl); out->bind[bl] = '\0';
out->bind_port = atoi(c3 + 1);
} else {
snprintf(out->bind, sizeof(out->bind), "127.0.0.1");
out->bind_port = atoi(spec);
}
size_t hl = (size_t)(c1 - (c2 + 1));
if (hl >= sizeof(out->target)) hl = sizeof(out->target) - 1;
memcpy(out->target, c2 + 1, hl); out->target[hl] = '\0';
out->target_port = atoi(c1 + 1);
return (out->bind_port > 0 && out->target_port > 0) ? 0 : -1;
}
/* [user@]host[:port] 解析 */
static void parse_userhost(const char *spec, char *user, size_t ulen,
char *host, size_t hlen, int *port) {
char buf[512];
snprintf(buf, sizeof(buf), "%s", spec);
char *at = strchr(buf, '@');
char *hs = buf;
if (user && ulen) user[0] = '\0';
if (at) {
*at = '\0';
if (user && ulen) {
size_t sl = strlen(buf);
if (sl >= ulen) sl = ulen - 1;
memcpy(user, buf, sl);
user[sl] = '\0';
}
hs = at + 1;
}
char *colon = strchr(hs, ':');
if (colon) { *colon = '\0'; if (port) *port = atoi(colon + 1); }
{
size_t sl = strlen(hs);
if (sl >= hlen) sl = hlen - 1;
memcpy(host, hs, sl);
host[sl] = '\0';
}
}
/* 交互输入密码(以 * 显示) */
static void input_password(char *pw, size_t n) {
fprintf(stderr, "Password: ");
int idx = 0, ch;
while (idx < (int)n - 1) {
ch = GETCH();
if (ch == '\r' || ch == '\n') break;
if (ch == '\b' || ch == 0x7f) {
if (idx > 0) { idx--; fprintf(stderr, "\b \b"); }
continue;
}
if (ch < 32) continue;
pw[idx++] = (char)ch;
fprintf(stderr, "*");
}
pw[idx] = '\0';
fprintf(stderr, "\n");
}
/* 认证:优先 -i 私钥,否则 agent,最后回退密码。返回 1=成功。 */
static int auth_session(ssh_session_t *s, const char *user,
const char *password, const char *keyfile) {
uint8_t *keydata = NULL; size_t keylen = 0;
if (keyfile) {
FILE *kf = fopen(keyfile, "rb");
if (kf) {
fseek(kf, 0, SEEK_END);
long sz = ftell(kf);
rewind(kf);
if (sz > 0 && sz < 4 * 1024 * 1024) {
keydata = (uint8_t *)malloc((size_t)sz);
if (keydata && fread(keydata, 1, (size_t)sz, kf) != (size_t)sz) {
free(keydata); keydata = NULL;
} else keylen = (size_t)sz;
}
fclose(kf);
}
if (!keydata)
fprintf(stderr, "pssh: warning: cannot read key file %s\n", keyfile);
}
int ok = 0;
if (keyfile) {
ok = ssh_auth_client_loop(s, user, password, keydata, keylen) == 0;
} else {
ssh_agent_t *agent = NULL;
if (ssh_agent_connect(&agent) == 0) {
ok = ssh_auth_client_agent_loop(s, user, password, agent) == 0;
ssh_agent_close(agent);
}
if (!ok)
ok = ssh_auth_client_loop(s, user, password, NULL, 0) == 0;
}
free(keydata);
return ok;
}
/* ---------------- 代理跳转 (-J / -J h1,h2):跳板通道上的嵌套会话 ---------------- */
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) {
jump_ctx_t *jc = (jump_ctx_t *)ctx;
size_t got = 0;
while (got < n) {
size_t avail = n - got;
uint32_t rid;
if (ssh_channel_recv_data(jc->jump, &rid, buf + got, &avail) < 0)
return -1;
got += avail;
/* avail==0:消费了非数据包(如 WINDOW_ADJUST),继续等数据 */
}
return (int)got;
}
static int jump_write(void *ctx, const uint8_t *buf, size_t n) {
jump_ctx_t *jc = (jump_ctx_t *)ctx;
return ssh_channel_send_data(jc->jump, jc->ch, buf, n) < 0 ? -1 : (int)n;
}
/* 一个跳板实体:[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 (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;
}
int pssh_cmd_ssh(int argc, char **argv) {
char *host = NULL;
int port = 22;
const char *user = NULL, *cmd = NULL;
const char *password_arg = NULL;
const char *keyfile = NULL;
int hostkey_mode = 0;
const char *jump_spec = NULL;
int server_alive = 15; /* ServerAliveInterval 秒;0=禁用 keepalive */
fwd_spec_t fwds[16];
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;
int agent_fwd = 0; /* -A:agent 转发(POSIX;Windows agent 为命名管道不支持) */
char stdio_host[256];
int reconnect_flag = 0; /* 断线标记(供自动重连) */
const char *user_cfg = NULL; /* -F <file>:显式指定 config 文件 */
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "-V") == 0) {
fprintf(stderr, "pssh %s\n", PSSH_VERSION);
return 0;
}
else if (strcmp(a, "-v") == 0) { g_verbose = 1; }
else if (strcmp(a, "-c") == 0) {
if (opt_missing(argc, i, "-c",
"<cipher> (chacha20-poly1305@openssh.com|aes128-gcm@openssh.com|aes256-gcm@openssh.com)") < 0) return 1;
const char *c = argv[++i];
if (strcmp(c, "chacha20-poly1305@openssh.com") != 0 &&
strcmp(c, "aes128-gcm@openssh.com") != 0 &&
strcmp(c, "aes256-gcm@openssh.com") != 0) {
fprintf(stderr, "pssh: unsupported cipher '%s' "
"(chacha20-poly1305@openssh.com, aes128-gcm@openssh.com, "
"aes256-gcm@openssh.com)\n", c);
return 1;
}
g_cipher = c;
}
else if (strcmp(a, "-p") == 0) {
if (opt_missing(argc, i, "-p", "<port>") < 0) return 1;
port = atoi(argv[++i]);
}
else if (strncmp(a, "-p", 2) == 0 && strlen(a) > 2 && strncmp(a, "-pwd", 4) != 0) { port = atoi(a + 2); }
else if (strcmp(a, "-pwd") == 0) {
if (opt_missing(argc, i, "-pwd", "<password>") < 0) return 1;
password_arg = argv[++i];
}
else if (strncmp(a, "-pwd=", 5) == 0) { password_arg = a + 5; }
else if (strcmp(a, "-l") == 0) {
if (opt_missing(argc, i, "-l", "<user>") < 0) return 1;
user = argv[++i];
}
else if (strcmp(a, "-i") == 0) {
if (opt_missing(argc, i, "-i", "<keyfile>") < 0) return 1;
keyfile = argv[++i];
}
else if (strcmp(a, "-J") == 0) {
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, "-A") == 0) {
agent_fwd = 1; /* 开启 agent 转发(POSIX) */
}
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) {
fwds[nfwds].kind = SSH_FWD_L;
nfwds++;
}
} else if (strcmp(a, "-R") == 0) {
if (opt_missing(argc, i, "-R", "[bind:]port:host:hostport") < 0) return 1;
if (nfwds < 16 && parse_fwd(argv[++i], &fwds[nfwds]) == 0) {
fwds[nfwds].kind = SSH_FWD_R;
nfwds++;
}
} else if (strcmp(a, "-D") == 0) {
if (opt_missing(argc, i, "-D", "<port>") < 0) return 1;
if (nfwds < 16) {
fwds[nfwds].kind = SSH_FWD_D;
snprintf(fwds[nfwds].bind, sizeof(fwds[nfwds].bind), "127.0.0.1");
fwds[nfwds].bind_port = atoi(argv[++i]);
fwds[nfwds].target[0] = '\0';
fwds[nfwds].target_port = 0;
if (fwds[nfwds].bind_port > 0) nfwds++;
}
} else if (strcmp(a, "-F") == 0) {
if (opt_missing(argc, i, "-F", "<configfile>") < 0) return 1;
user_cfg = argv[++i];
} else if (strcmp(a, "-o") == 0) {
if (opt_missing(argc, i, "-o", "StrictHostKeyChecking=no|accept-new|yes|ServerAliveInterval=N") < 0) return 1;
const char *kv = argv[++i];
if (strncmp(kv, "StrictHostKeyChecking=", 22) == 0) {
const char *v = kv + 22;
if (strcmp(v, "no") == 0 || strcmp(v, "off") == 0) hostkey_mode = 2;
else if (strcmp(v, "accept-new") == 0) hostkey_mode = 1;
else hostkey_mode = 0;
}
else if (strncmp(kv, "ServerAliveInterval=", 20) == 0) {
server_alive = atoi(kv + 20);
}
else if (strncmp(kv, "ReconnectInterval=", 18) == 0) {
g_reconnect = atoi(kv + 18); /* 断线自动重连间隔秒 */
}
} else if (a[0] == '-') {
continue;
} else if (!host) {
/* [user@]host[:port]:端口随主机一起解析(如 root@host:6666) */
int p2 = 0;
parse_userhost(argv[i], arg_user, sizeof(arg_user),
arg_host, sizeof(arg_host), &p2);
if (arg_user[0]) user = arg_user;
if (p2 > 0) port = p2;
host = arg_host;
} else {
cmd = argv[i];
}
}
/* ---- config 文件:优先 -F > ~/.ssh/config(OpenSSH 约定) > ~/.pssh/config.conf ---- */
char cfg_path[1024];
const char *home = getenv("USERPROFILE");
if (!home) home = getenv("HOME");
if (user_cfg) {
snprintf(cfg_path, sizeof(cfg_path), "%s", user_cfg);
} else if (home) {
FILE *probe = NULL;
snprintf(cfg_path, sizeof(cfg_path), "%s/.ssh/config", home);
probe = fopen(cfg_path, "r");
if (probe) fclose(probe);
else snprintf(cfg_path, sizeof(cfg_path), "%s/.pssh/config.conf", home);
} else {
snprintf(cfg_path, sizeof(cfg_path), ".pssh/config.conf");
}
char cfg_host[256];
if (host) {
pssh_config_entry_t *cfgs = NULL;
int ncfg = 0;
if (pssh_config_load(cfg_path, &cfgs, &ncfg) >= 0) {
pssh_config_entry_t m;
memset(&m, 0, sizeof(m));
m.hostkey_mode = -1;
if (pssh_config_match(cfgs, ncfg, host, &m) > 0) {
if (m.hostname[0]) {
snprintf(cfg_host, sizeof(cfg_host), "%s", m.hostname);
host = cfg_host;
}
if (m.user[0] && !user) user = m.user;
if (m.port != 0 && port == 22) port = m.port;
if (m.identity_file[0] && !keyfile) keyfile = m.identity_file;
if (m.proxyjump[0] && !jump_spec) jump_spec = m.proxyjump;
if (m.hostkey_mode != -1) hostkey_mode = m.hostkey_mode;
}
free(cfgs);
}
}
if (!host) {
fprintf(stderr, "pssh - SSH2 客户端 (平替 ssh)\n"
"用法: pssh [选项] [user@]host [command]\n"
"选项: -p <port> -l <user> -pwd <pw> -i <key> -J <user@jump[:port]>\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) {
const char *envu = getenv("USER");
if (!envu) envu = getenv("USERNAME");
user = envu ? envu : "root";
}
char pw_buf[256];
const char *password = password_arg;
if (!password) {
input_password(pw_buf, sizeof(pw_buf));
password = pw_buf;
}
/* ---- 连接(可选经跳板) + 认证 ---- */
ssh_session_t *jump = NULL;
ssh_session_t *s = connect_target(host, port, jump_spec, user, password,
keyfile, hostkey_mode, &jump);
if (!s) return 1;
if (!auth_session(s, user, password, keyfile)) {
fprintf(stderr, "pssh: auth failed\n");
if (jump) {
free(ssh_session_io_ctx(s)); /* 释放跳板 io ctx 链(含中间会话) */
ssh_session_free(jump); /* 释放顶层跳板 */
}
ssh_session_free(s);
return 1;
}
/* ---- 建立转发 ---- */
ssh_fwd_t *fwd = ssh_fwd_new();
for (int i = 0; i < nfwds; i++) {
fwd_spec_t *fs = &fwds[i];
if (fs->kind == SSH_FWD_R) {
uint32_t ap = 0;
ssh_fwd_add_remote(fwd, s, fs->bind, fs->bind_port,
fs->target, fs->target_port, &ap);
} else {
ssh_fwd_add_local(fwd, s, fs->kind, fs->bind, fs->bind_port,
fs->target, fs->target_port);
}
}
/* ---- 会话通道 ---- */
uint32_t ch = 0;
int interactive = 0;
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) {
fprintf(stderr, "pssh: channel open failed\n");
goto done;
}
#ifdef _WIN32
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);
}
/* ---- agent 转发(-A, POSIX):建立 auth-agent@openssh.com 通道 + 本地 agent ---- */
ssh_agent_t *ag = NULL; uint32_t ach = 0; int ag_fd = -1;
if (agent_fwd) {
ag_fd = -1;
if (ssh_agent_connect(&ag) == 0) {
ag_fd = ssh_agent_fd(ag);
if (ag_fd < 0) { /* Windows:命名管道不支持 select,禁用 */
ssh_agent_close(ag); ag = NULL;
fprintf(stderr, "pssh: agent 转发在 Windows 不受支持(skip -A)\n");
goto done;
}
if (ssh_channel_open(s, &ach, "auth-agent@openssh.com", 0, 0) < 0) {
ssh_agent_close(ag); ag = NULL;
fprintf(stderr, "pssh: agent forwarding channel open failed (server may not support)\n");
goto done;
}
} else {
fprintf(stderr, "pssh: cannot connect local agent (ssh-agent not running)\n");
goto done;
}
}
/* ---- 事件循环 ---- */
long sel_sock = jump ? ssh_session_socket(jump) : ssh_session_socket(s);
if (sel_sock < 0) goto done;
uint8_t rbuf[32768];
time_t last_alive = time(NULL);
for (;;) {
/* 定时 keepalive(ServerAliveInterval):保持空闲连接存活 */
if (server_alive > 0) {
time_t now = time(NULL);
if (now - last_alive >= server_alive) {
last_alive = now;
if (ssh_keepalive_send(s) < 0) { fprintf(stderr, "pssh: keepalive send failed\n"); reconnect_flag = 1; break; }
}
}
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = 0;
FD_SET((SOCKET)sel_sock, &rfds);
if ((int)sel_sock > maxfd) maxfd = (int)sel_sock;
if (interactive) {
#ifndef _WIN32
FD_SET(STDIN_FD, &rfds);
if (STDIN_FD > maxfd) maxfd = STDIN_FD;
#endif
}
ssh_fwd_prepare(fwd, &rfds, NULL, &maxfd);
#ifndef _WIN32
if (ag_fd >= 0) { FD_SET(ag_fd, &rfds); if (ag_fd > maxfd) maxfd = ag_fd; }
#endif
struct timeval tv = {0, 100000};
int sret = select(maxfd + 1, &rfds, NULL, NULL, &tv);
if (sret < 0) break;
#ifndef _WIN32
/* agent 转发:本地 agent fd 可读 → 转发到 auth-agent 通道 */
if (ag_fd >= 0 && sret > 0 && FD_ISSET(ag_fd, &rfds)) {
uint8_t abuf[8192];
ssize_t an = read(ag_fd, abuf, sizeof(abuf));
if (an < 0) break;
if (an > 0 && ssh_channel_send_data(s, ach, abuf, (size_t)an) < 0) break;
}
#endif
if (sret > 0 && FD_ISSET((SOCKET)sel_sock, &rfds)) {
/* 先消费服务器主动推送的 forwarded-tcpip 通道打开。
网关同时看内层 pending 与外层缓冲/socket,防止 -J 场景
内层已缓存但外层 socket 空的包被滞留。 */
for (;;) {
if (ssh_session_data_ready(s) <= 0 &&
ssh_session_data_ready(jump ? jump : s) <= 0) break;
uint32_t ach;
char ahost[256];
uint32_t aport;
int ar = ssh_channel_accept_forwarded(s, &ach, ahost,
sizeof(ahost), &aport);
if (ar == 0) {
if (ssh_fwd_remote_connect(fwd, s, ach, ahost, aport) < 0)
ssh_channel_close(s, ach);
continue;
}
break; /* -2=非 OPEN 留给数据循环; <0=错误 */
}
/* 收通道数据并分发 */
for (;;) {
if (ssh_session_data_ready(s) <= 0 &&
ssh_session_data_ready(jump ? jump : s) <= 0) break;
uint32_t rid;
size_t n = sizeof(rbuf);
int r = ssh_channel_recv_data(s, &rid, rbuf, &n);
if (r < 0) {
/* 通道关闭/EOF:主会话通道 → 结束;转发通道 → 清理后继续 */
if (rid == ch) goto done;
ssh_fwd_channel_closed(fwd, rid);
continue;
}
if (n == 0) {
/* 控制包已消费;先取 pending 中可能有的 forwarded-open */
while (ssh_session_data_ready(s) > 0) {
uint32_t ach;
char ahost[256];
uint32_t aport;
int ar = ssh_channel_accept_forwarded(s, &ach, ahost,
sizeof(ahost), &aport);
if (ar == 0) {
if (ssh_fwd_remote_connect(fwd, s, ach, ahost, aport) < 0)
ssh_channel_close(s, ach);
continue;
}
break; /* -2: 非 OPEN 留在 pending; <0: 错误 */
}
/* 内层 pending / 外层缓冲或 socket 还有数据 → 继续收 */
if (ssh_session_data_ready(s) > 0 ||
ssh_session_data_ready(jump ? jump : s) > 0)
continue;
break;
}
if (rid == ch) {
fwrite(rbuf, 1, n, stdout);
fflush(stdout);
#ifndef _WIN32
} else if (ag_fd >= 0 && rid == ach) {
/* agent 通道数据 → 写本地 agent socket */
ssize_t wc = write(ag_fd, rbuf, n);
if (wc < 0) goto done;
#endif
} else {
ssh_fwd_channel_data(fwd, rid, rbuf, n);
}
}
}
/* 转发本地事件 */
ssh_fwd_pump(fwd, s, &rfds, NULL);
/* 键盘输入(交互 / -W stdio 转发) */
if (interactive) {
#ifdef _WIN32
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;
}
uint8_t b = (uint8_t)c;
if (ssh_channel_send_data(s, ch, &b, 1) < 0) goto done;
}
}
#else
fd_set ifds;
FD_ZERO(&ifds);
FD_SET(STDIN_FILENO, &ifds);
struct timeval itv = {0, 0};
if (select(STDIN_FILENO + 1, &ifds, NULL, NULL, &itv) > 0) {
uint8_t kb;
if (read(STDIN_FILENO, &kb, 1) == 1)
if (ssh_channel_send_data(s, ch, &kb, 1) < 0) goto done;
}
#endif
}
}
done:
int exit_code = 0;
if (ch) ssh_channel_exit_status(s, ch, &exit_code);
#ifndef _WIN32
if (shell_raw) raw_off();
#endif
ssh_fwd_free(fwd);
if (ag) { ssh_agent_close(ag); ag = NULL; } /* agent 转发清理 */
if (jump) {
free(ssh_session_io_ctx(s)); /* jump_ctx_t */
ssh_session_free(jump);
}
ssh_session_free(s);
if (reconnect_flag && g_reconnect > 0) return -2; /* 通知上层自动重连 */
return exit_code;
}