长连接保活 + fail2ban 兼容: pssh ssh -o ServerAliveInterval=N 空闲发 keepalive@openssh.com(ssh_keepalive_send, 服务端回包闭环)保持连接不断; psshd 认证失败日志对齐 OpenSSH 格式(Invalid user / Failed password ... from <IP>)并记录对端IP(ssh_session_set_peer_ip), 便于 fail2ban 默认 sshd filter 封禁; 两端实测通过; STATUS/AGENT/CHANGELOG 更新
这个提交包含在:
@@ -91,4 +91,9 @@ session ticket key 支持持久化到文件:新增 `paze_tls_config_set_session_
|
|||||||
- `pssh ssh` config 读取优先级:`-F <file>` > `~/.ssh/config`(OpenSSH 兼容) > `~/.pssh/config.conf`;支持 `Host/HostName/Port/User/IdentityFile/ProxyJump/StrictHostKeyChecking`。
|
- `pssh ssh` config 读取优先级:`-F <file>` > `~/.ssh/config`(OpenSSH 兼容) > `~/.pssh/config.conf`;支持 `Host/HostName/Port/User/IdentityFile/ProxyJump/StrictHostKeyChecking`。
|
||||||
- 新增 `build/run_ecdsa_selfcheck.ps1`(KAT 基线):对 P-256/384/521 跑 ECDSA sign→verify 自洽,防回归。
|
- 新增 `build/run_ecdsa_selfcheck.ps1`(KAT 基线):对 P-256/384/521 跑 ECDSA sign→verify 自洽,防回归。
|
||||||
|
|
||||||
|
### 8. 长连接保活 + fail2ban 兼容(✅ 已完成 2026-08-14)
|
||||||
|
|
||||||
|
- `pssh ssh -o ServerAliveInterval=N`(默认15s):空闲发 `keepalive@openssh.com` global request(ssh_keepalive_send, fire-and-forget),保持空闲连接不中断;服务端回包形成保活闭环。
|
||||||
|
- `psshd` 认证失败日志对齐 OpenSSH 格式(`Invalid user`/`Failed password ... from <IP>`),并记录对端 IP(`ssh_session_set_peer_ip`),fail2ban 默认 `sshd` filter 可直接匹配封禁。
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+5
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
## [开发主线] — 2026-08-14
|
## [开发主线] — 2026-08-14
|
||||||
|
|
||||||
|
### 新增 —— 长连接保活 + fail2ban 兼容
|
||||||
|
|
||||||
|
- **keepalive 保活**:`pssh ssh` 新增 `-o ServerAliveInterval=N`(默认 15s,0=禁用),空闲时定时发 `keepalive@openssh.com` global request(`ssh_keepalive_send`,fire-and-forget),保持空闲连接不被 NAT/防火墙回收;服务端对 GLOBAL_REQUEST 回包形成保活闭环。
|
||||||
|
- **fail2ban 兼容**:`psshd` 认证失败日志对齐 OpenSSH 格式(`Invalid user X from <IP> port N ssh2` / `Failed password for X from <IP> port N ssh2`),并记录对端 IP(`ssh_session_set_peer_ip`),可用 fail2ban 默认 `sshd` filter 封禁。
|
||||||
|
|
||||||
### 新增 —— SSH config 兼容读取 + ECDSA KAT 基线
|
### 新增 —— SSH config 兼容读取 + ECDSA KAT 基线
|
||||||
|
|
||||||
- `pssh ssh` config 读取优先级:`-F <file>` > `~/.ssh/config`(OpenSSH 兼容) > `~/.pssh/config.conf`;支持 `Host/HostName/Port/User/IdentityFile/ProxyJump/StrictHostKeyChecking`。
|
- `pssh ssh` config 读取优先级:`-F <file>` > `~/.ssh/config`(OpenSSH 兼容) > `~/.pssh/config.conf`;支持 `Host/HostName/Port/User/IdentityFile/ProxyJump/StrictHostKeyChecking`。
|
||||||
|
|||||||
@@ -44,12 +44,12 @@
|
|||||||
- 通道:session(exec/shell 真实进程执行)、direct-tcpip(出站桥接)
|
- 通道:session(exec/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` 端口转发,`-J` 代理跳板(单跳)、`-W host:port` stdio 转发、`-N` 仅转发;config 读取 `-F` / `~/.ssh/config`(OpenSSH 兼容) / `~/.pssh/config.conf`
|
- `pssh ssh` — 远程 shell / 执行命令,`-L`/`-R`/`-D` 端口转发,`-J` 代理跳板(单跳)、`-W host:port` stdio 转发、`-N` 仅转发;config 读取 `-F` / `~/.ssh/config`(OpenSSH 兼容) / `~/.pssh/config.conf`;`-o ServerAliveInterval=N` 空闲心跳保活(keepalive@openssh.com)
|
||||||
|
|
||||||
- `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 守护进程与密钥管理
|
||||||
- `pssh sshd` — 服务端:多用户库 + 分级权限 + publickey 认证 + SFTP 子系统
|
- `pssh sshd` — 服务端:多用户库 + 分级权限 + publickey 认证 + SFTP 子系统;**认证失败日志对齐 OpenSSH 格式**(`Invalid user`/`Failed password ... from <IP>`),可与 fail2ban 默认 `sshd` filter 配合
|
||||||
- 密码套件:chacha20-poly1305@openssh.com、aes128/256-gcm@openssh.com
|
- 密码套件:chacha20-poly1305@openssh.com、aes128/256-gcm@openssh.com
|
||||||
|
|
||||||
## 应用(✅ 可用)
|
## 应用(✅ 可用)
|
||||||
|
|||||||
+15
-1
@@ -9,6 +9,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
@@ -330,6 +331,7 @@ int pssh_cmd_ssh(int argc, char **argv) {
|
|||||||
const char *keyfile = NULL;
|
const char *keyfile = NULL;
|
||||||
int hostkey_mode = 0;
|
int hostkey_mode = 0;
|
||||||
const char *jump_spec = NULL;
|
const char *jump_spec = NULL;
|
||||||
|
int server_alive = 15; /* ServerAliveInterval 秒;0=禁用 keepalive */
|
||||||
fwd_spec_t fwds[16];
|
fwd_spec_t fwds[16];
|
||||||
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] 解析缓冲 */
|
||||||
@@ -428,7 +430,7 @@ int pssh_cmd_ssh(int argc, char **argv) {
|
|||||||
if (opt_missing(argc, i, "-F", "<configfile>") < 0) return 1;
|
if (opt_missing(argc, i, "-F", "<configfile>") < 0) return 1;
|
||||||
user_cfg = argv[++i];
|
user_cfg = argv[++i];
|
||||||
} else if (strcmp(a, "-o") == 0) {
|
} else if (strcmp(a, "-o") == 0) {
|
||||||
if (opt_missing(argc, i, "-o", "StrictHostKeyChecking=no|accept-new|yes") < 0) return 1;
|
if (opt_missing(argc, i, "-o", "StrictHostKeyChecking=no|accept-new|yes|ServerAliveInterval=N") < 0) return 1;
|
||||||
const char *kv = argv[++i];
|
const char *kv = argv[++i];
|
||||||
if (strncmp(kv, "StrictHostKeyChecking=", 22) == 0) {
|
if (strncmp(kv, "StrictHostKeyChecking=", 22) == 0) {
|
||||||
const char *v = kv + 22;
|
const char *v = kv + 22;
|
||||||
@@ -436,6 +438,9 @@ int pssh_cmd_ssh(int argc, char **argv) {
|
|||||||
else if (strcmp(v, "accept-new") == 0) hostkey_mode = 1;
|
else if (strcmp(v, "accept-new") == 0) hostkey_mode = 1;
|
||||||
else hostkey_mode = 0;
|
else hostkey_mode = 0;
|
||||||
}
|
}
|
||||||
|
else if (strncmp(kv, "ServerAliveInterval=", 20) == 0) {
|
||||||
|
server_alive = atoi(kv + 20);
|
||||||
|
}
|
||||||
} else if (a[0] == '-') {
|
} else if (a[0] == '-') {
|
||||||
continue;
|
continue;
|
||||||
} else if (!host) {
|
} else if (!host) {
|
||||||
@@ -587,7 +592,16 @@ int pssh_cmd_ssh(int argc, char **argv) {
|
|||||||
if (sel_sock < 0) goto done;
|
if (sel_sock < 0) goto done;
|
||||||
|
|
||||||
uint8_t rbuf[32768];
|
uint8_t rbuf[32768];
|
||||||
|
time_t last_alive = time(NULL);
|
||||||
for (;;) {
|
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"); break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
fd_set rfds;
|
fd_set rfds;
|
||||||
FD_ZERO(&rfds);
|
FD_ZERO(&rfds);
|
||||||
int maxfd = 0;
|
int maxfd = 0;
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ int pssh_cmd_sshd(int argc, char **argv) {
|
|||||||
if (!s) { closesocket(conn); free(ps); continue; }
|
if (!s) { closesocket(conn); free(ps); continue; }
|
||||||
ssh_session_set_io(s, ps, tcp_read, tcp_write);
|
ssh_session_set_io(s, ps, tcp_read, tcp_write);
|
||||||
ssh_session_set_users(s, users, nusers);
|
ssh_session_set_users(s, users, nusers);
|
||||||
|
ssh_session_set_peer_ip(s, inet_ntoa(client.sin_addr)); /* 供失败日志/fail2ban */
|
||||||
if (verbose) ssh_session_set_verbose(s, 1);
|
if (verbose) ssh_session_set_verbose(s, 1);
|
||||||
|
|
||||||
ssh_server_loop(s, (const uint8_t *)ak_text, ak_len);
|
ssh_server_loop(s, (const uint8_t *)ak_text, ak_len);
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ void ssh_session_free(ssh_session_t *s);
|
|||||||
void ssh_session_set_io(ssh_session_t *s, void *ctx,
|
void ssh_session_set_io(ssh_session_t *s, void *ctx,
|
||||||
int (*read_fn)(void *, uint8_t *, size_t),
|
int (*read_fn)(void *, uint8_t *, size_t),
|
||||||
int (*write_fn)(void *, const uint8_t *, size_t));
|
int (*write_fn)(void *, const uint8_t *, size_t));
|
||||||
|
/* Set peer IP(服务端 accept 后设置,用于认证失败日志的 fail2ban 兼容) */
|
||||||
|
void ssh_session_set_peer_ip(ssh_session_t *s, const char *ip);
|
||||||
|
|
||||||
/* Return the underlying socket handle for event loops:
|
/* Return the underlying socket handle for event loops:
|
||||||
* Windows: SOCKET, POSIX: int fd. -1 if none. */
|
* Windows: SOCKET, POSIX: int fd. -1 if none. */
|
||||||
@@ -83,6 +85,10 @@ int ssh_client_connect(ssh_session_t *s, const char *host, uint16_t port);
|
|||||||
/* 在已通过 ssh_session_set_io 设置的传输上完成握手(代理跳转用) */
|
/* 在已通过 ssh_session_set_io 设置的传输上完成握手(代理跳转用) */
|
||||||
int ssh_client_connect_io(ssh_session_t *s, const char *host, uint16_t port);
|
int ssh_client_connect_io(ssh_session_t *s, const char *host, uint16_t port);
|
||||||
|
|
||||||
|
/* 发送 keepalive(keepalive@openssh.com global request,fire-and-forget)。
|
||||||
|
* 用于保持空闲连接不被 NAT/防火墙回收;对端回复仅表明连接存活。 */
|
||||||
|
int ssh_keepalive_send(ssh_session_t *s);
|
||||||
|
|
||||||
/* Authenticate with password. Returns 0 on success. */
|
/* Authenticate with password. Returns 0 on success. */
|
||||||
int ssh_auth_client_loop(ssh_session_t *s,
|
int ssh_auth_client_loop(ssh_session_t *s,
|
||||||
const char *username,
|
const char *username,
|
||||||
|
|||||||
@@ -226,6 +226,17 @@ int ssh_auth_server_loop(ssh_session_t *s,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 认证失败:回 FAILURE 提示可用方法 */
|
/* 认证失败:回 FAILURE 提示可用方法 */
|
||||||
|
/* OpenSSH 兼容日志(fail2ban sshd filter 匹配):
|
||||||
|
* 用户名不存在 -> "Invalid user X from IP port N ssh2"
|
||||||
|
* 存在但密码错 -> "Failed password for X from IP port N ssh2" */
|
||||||
|
{
|
||||||
|
char unamez[256];
|
||||||
|
size_t un = ulen < sizeof(unamez) - 1 ? ulen : sizeof(unamez) - 1;
|
||||||
|
memcpy(unamez, uname, un); unamez[un] = '\0';
|
||||||
|
fprintf(stderr, "%s %s from %s port %u ssh2\n",
|
||||||
|
u ? "Failed password for" : "Invalid user",
|
||||||
|
u ? unamez : unamez, s->peer_ip, (unsigned)s->port);
|
||||||
|
}
|
||||||
send_auth_failure(s, methods);
|
send_auth_failure(s, methods);
|
||||||
free((void *)payload);
|
free((void *)payload);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ typedef struct {
|
|||||||
/* Config */
|
/* Config */
|
||||||
char username[256];
|
char username[256];
|
||||||
char hostname[256];
|
char hostname[256];
|
||||||
|
char peer_ip[64]; /* 对端 IP(服务端 accept 时设置,供日志/fail2ban) */
|
||||||
uint16_t port;
|
uint16_t port;
|
||||||
int is_server;
|
int is_server;
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
#include "ssh_internal.h"
|
#include "ssh_internal.h"
|
||||||
#include "sftp_server.h"
|
#include "sftp_server.h"
|
||||||
|
|
||||||
|
void ssh_session_set_peer_ip(ssh_session_t *s, const char *ip) {
|
||||||
|
if (!s || !ip) return;
|
||||||
|
size_t n = strlen(ip);
|
||||||
|
if (n >= sizeof(s->peer_ip)) n = sizeof(s->peer_ip) - 1;
|
||||||
|
memcpy(s->peer_ip, ip, n);
|
||||||
|
s->peer_ip[n] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <winsock2.h>
|
#include <winsock2.h>
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -238,3 +238,16 @@ int ssh_transport_kexinit(ssh_session_t *s) {
|
|||||||
free((void *)payload);
|
free((void *)payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 发送 keepalive(OpenSSH 约定的 keepalive@openssh.com global request)。
|
||||||
|
* fire-and-forget:want_reply=1 但不阻塞等待;对端回复(SUCCESS/FAILURE)
|
||||||
|
* 仅作为"连接仍存活"的信号,由上层在事件循环中消费/超时判定。
|
||||||
|
* 返回 0 表示已发出,-1 出错。 */
|
||||||
|
int ssh_keepalive_send(ssh_session_t *s) {
|
||||||
|
if (!s) return -1;
|
||||||
|
uint8_t pkt[512];
|
||||||
|
size_t off = 0;
|
||||||
|
off += ssh_str_write(pkt + off, "keepalive@openssh.com");
|
||||||
|
off += ssh_bool_write(pkt + off, 1);
|
||||||
|
return ssh_packet_send(s, SSH_MSG_GLOBAL_REQUEST, pkt, off) < 0 ? -1 : 0;
|
||||||
|
}
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户