W5波+N-19收口:FTP鉴权落地、Trap25三连定案修复(N-30)、N-20/N-23/N-26

N-19 FTP 鉴权:
- lftpd_auth(USER/PASS 状态机 + 连续5次失败锁60s)+ SHA-256
  (user||pwd) 口令哈希;config.ini [ftp] 段 user/pwd_hash/write,
  出厂 admin/admin 在用有启动告警;lftpd_log.c 删除并入 lftpd_auth
- ftp_cfg 配置读写;台架 ftp_auth_bench.py 13/13

N-30 / Trap 25(CH395F_Trap_Records.md 四次定案全记录):
- 修复1 closed-with-data accept:客户端发完正常 FIN 后芯片按
  FUN_PARA(0x09) §5.52② 保留 Socket+RX 缓冲,旧代码把 CLOSED 一律
  relisten 自毁缓冲 → STOR 必挂 425;改为接受"已关但可读"连接排空
- 修复2 丢 CONNECT 中断探针 net_lost_connect_probe + last_peer_port
  幽灵甄别(拆链窗口握手完成但中断丢失,双信源去抖补 accept)
- 修复3 send_list/send_nlst 批量单发:背靠背 ~60B 小段毒化芯片 TX
  (芯片报 SEND_OK 但对端全丢),整份目录一次性写入
- 证伪存档:固定沉降/Socket 轮换冷却/listen 门控(台架 v7 矩阵)
- 台架工具 test/board/ch395f_reuse_bench.{c,h} + host 脚本存档
  (不注册编译;实验分支 n30-chip-bench 保留)

N-20:Error_Handler 死循环降级(记录+复位路径)
N-23:scripts/check_registration.py 双端注册反向校验(pre-commit)
N-26:GD5F NAND 直 DMA 调用方缓冲窗口断言

验证:BUILD_OK 0/0;ftp_regression 连跑 4 轮 11/11(修复前
LIST#4/#7 100% 挂、STOR#2 曾 100% 425);ftp_auth_bench 13/13;
@test 启动标记 PASS;ci_local L0+L0.5 全过;doc-links 通过。
登记簿 N-30 已销项。
This commit is contained in:
2026-10-11 18:50:24 +08:00
parent eddbedeee8
commit d1e6bb4ba7
75 changed files with 3928 additions and 352 deletions
+132 -9
View File
@@ -16,6 +16,8 @@
#include "private/lftpd_io.h"
#include "net_socket.h"
#include "net_config.h"
#include "lftpd_auth.h"
#include "ftp_cfg.h"
#include "ff.h"
typedef struct {
@@ -62,6 +64,10 @@ static command_t commands[] = {
{ NULL, NULL },
};
/* N-19 FTP 鉴权:全局失败计数/锁定状态(跨会话持久——单连接服务器,全局
* 计数即可;成功清零、锁定期满自动放行,见 lftpd_auth.c) */
static ftp_auth_state_t s_auth_state;
static int send_response(int socket, int code, bool include_code,
bool multiline_start, const char *format, ...)
{
@@ -100,8 +106,8 @@ static int send_response(int socket, int code, bool include_code,
static int send_list(int socket, const char *path)
{
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
void *dp = lftpd_io_opendir(path);
if (dp == NULL) {
@@ -112,17 +118,39 @@ static int send_list(int socket, const char *path)
uint32_t size;
int is_dir;
/* N30-PROBE(临时实验):整份目录一次性发送(验证"背靠背多小段
* 毒化芯片 TX"假设——每行一个 net_send 时 LIST#4 必挂) */
static char batch[1536]; /* static: off-stack */
int blen = 0;
int trunc = 0;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
int n;
if (is_dir) {
send_multiline_response_line(socket, directory_format,
(unsigned long)size, name);
n = snprintf(batch + blen, sizeof(batch) - blen, directory_format,
(unsigned long)size, name);
} else {
send_multiline_response_line(socket, file_format,
(unsigned long)size, name);
n = snprintf(batch + blen, sizeof(batch) - blen, file_format,
(unsigned long)size, name);
}
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
trunc = 1;
break;
}
blen += n;
}
lftpd_io_closedir(dp);
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
if (blen > 0) {
if (lftpd_inet_write(socket, batch, blen) < 0) {
return -1;
}
}
if (trunc) {
return -1;
}
return 0;
}
@@ -137,13 +165,33 @@ static int send_nlst(int socket, const char *path)
uint32_t size;
int is_dir;
/* N30-PROBE(临时实验):同 send_list,一次性批量发送 */
static char batch[1536]; /* static: off-stack */
int blen = 0;
int trunc = 0;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
if (!is_dir) {
send_multiline_response_line(socket, name);
int n = snprintf(batch + blen, sizeof(batch) - blen, "%s\r\n", name);
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
trunc = 1;
break;
}
blen += n;
}
}
lftpd_io_closedir(dp);
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
if (blen > 0) {
if (lftpd_inet_write(socket, batch, blen) < 0) {
return -1;
}
}
if (trunc) {
return -1;
}
return 0;
}
@@ -241,6 +289,14 @@ static int cmd_cwd(lftpd_client_t *client, const char *arg)
static int cmd_dele(lftpd_client_t *client, const char *arg)
{
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
const ftp_cfg_t *cfg = ftp_cfg_get();
if ((cfg == NULL) || (cfg->write_en == 0u)) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
if (arg == NULL || strlen(arg) == 0) {
send_simple_response(client->socket, 550, STATUS_550);
return -1;
@@ -374,7 +430,33 @@ static int cmd_noop(lftpd_client_t *client, const char *arg)
static int cmd_pass(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 230, STATUS_230);
const ftp_cfg_t *cfg = ftp_cfg_get();
ftp_auth_creds_t creds;
ftp_auth_result_t res;
if (cfg == NULL) {
/* fail-closed:配置不可用即拒绝(s_cfg 静态常驻,正常不会走到) */
send_simple_response(client->socket, 530, STATUS_530);
return 0;
}
creds.p_user = cfg->user;
creds.p_pwd_hash = cfg->pwd_hash;
creds.write_en = cfg->write_en;
res = ftp_auth_check(&creds, client->auth_user_ok,
(arg != NULL) ? arg : "",
&s_auth_state, (uint32_t)xTaskGetTickCount());
if (res == FTP_AUTH_OK) {
client->auth_logged_in = 1;
send_simple_response(client->socket, 230, STATUS_230);
return 0;
}
/* 预期事件(爆破/配错口令):用 INFO 而非 ERROR——[ERR] 是 @test Forbidden 标记 */
if (res == FTP_AUTH_LOCKED) {
lftpd_log_info("auth locked after %u failures", (unsigned)FTP_AUTH_FAIL_MAX);
} else {
lftpd_log_info("auth failed for user '%s'", client->auth_user);
}
send_simple_response(client->socket, 530, STATUS_530);
return 0;
}
@@ -500,6 +582,14 @@ static int cmd_size(lftpd_client_t *client, const char *arg)
static int cmd_stor(lftpd_client_t *client, const char *arg)
{
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
const ftp_cfg_t *cfg = ftp_cfg_get();
if ((cfg == NULL) || (cfg->write_en == 0u)) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
@@ -552,7 +642,20 @@ static int cmd_type(lftpd_client_t *client, const char *arg)
static int cmd_user(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 230, STATUS_230);
const ftp_cfg_t *cfg = ftp_cfg_get();
client->auth_logged_in = 0;
client->auth_user[0] = '\0';
client->auth_user_ok = 0;
if ((arg != NULL) && (cfg != NULL)) {
strncpy(client->auth_user, arg, sizeof(client->auth_user) - 1);
client->auth_user[sizeof(client->auth_user) - 1] = '\0';
client->auth_user_ok = (uint8_t)ftp_auth_user_match(cfg->user,
client->auth_user);
}
/* 不论用户名对错一律 331:不泄露"用户是否存在";错用户名在 cmd_pass
* 仍走一次同耗时哈希比较(时间拉平,见 lftpd_auth.h 说明 2) */
send_simple_response(client->socket, 331, STATUS_331);
return 0;
}
@@ -595,6 +698,21 @@ static int handle_control_channel(lftpd_client_t *client)
command_tmp[i] = (char)toupper((int)command_tmp[i]);
}
/* N-19:未鉴权命令门——仅 QUIT/FEAT/SYST/NOOP 放行(客户端兼容需要,
* 且无信息量);其余一律 530。USER/PASS 本身也在放行名单外,
* 但它们在 gate 之前不可能通过 → 单独放行见下(USER/PASS 例外) */
if (client->auth_logged_in == 0) {
if ((strcmp(command_tmp, "USER") != 0) &&
(strcmp(command_tmp, "PASS") != 0) &&
(strcmp(command_tmp, "QUIT") != 0) &&
(strcmp(command_tmp, "FEAT") != 0) &&
(strcmp(command_tmp, "SYST") != 0) &&
(strcmp(command_tmp, "NOOP") != 0)) {
send_simple_response(client->socket, 530, STATUS_530);
continue;
}
}
bool matched = false;
for (int i = 0; commands[i].command; i++) {
if (strcmp(commands[i].command, command_tmp) == 0) {
@@ -640,6 +758,11 @@ int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
lftpd->port = port;
lftpd->server_socket = -1; /* memset 清零后显式置"无监听器"(0 是合法 fd) */
/* N-19:出厂默认口令在用告警(台架锚点;改了 [ftp] pwd_hash 即消失) */
if (ftp_cfg_is_default_pwd() != 0) {
lftpd_log_info("using factory default password");
}
while (true) {
lftpd_task_wd_beat(); /* 看门狗:事件循环每轮喂心跳(ftpTask 主循环节拍点) */
if (lftpd->server_socket < 0) {
+5
View File
@@ -6,6 +6,11 @@ typedef struct {
char directory[256];
int socket;
int data_socket;
/* N-19 FTP 鉴权会话态(每会话 memset 0 = 未登录;跨会话锁定计数在
* lftpd.c 的 s_auth_state,不在此处) */
char auth_user[32]; /* USER 阶段暂存的用户名 */
uint8_t auth_user_ok; /* 1 = 用户名与 [ftp] user 匹配 */
uint8_t auth_logged_in; /* 1 = PASS 校验通过 */
} lftpd_client_t;
typedef struct {
+100
View File
@@ -0,0 +1,100 @@
/*
* 模块名称:lftpd_auth
* 模块功能:FTP 鉴权状态机实现(纯函数,仅依赖 stdint/string 与 sha256)
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;契约由
* test/host/ftp_auth 主机单测钉死)
*/
/* 头文件包含区 */
#include <stddef.h>
#include <string.h>
#include "lftpd_auth.h"
#include "sha256.h"
/* 函数定义区 */
/*
* 函数功能:用户名匹配(大小写不敏感)
* 入口参数:p_expect - 配置用户名 p_user - 客户端 USER 参数
* 返回值:1 - 匹配;0 - 不匹配或任一为 NULL int
* 限定条件:无
*/
int ftp_auth_user_match(const char *p_expect, const char *p_user) {
if ((p_expect == NULL) || (p_user == NULL)) {
return 0;
}
for (size_t i = 0u; ; i++) {
char a = p_expect[i];
char b = p_user[i];
if ((a >= 'A') && (a <= 'Z')) {
a = (char)(a + 32);
}
if ((b >= 'A') && (b <= 'Z')) {
b = (char)(b + 32);
}
if (a != b) {
return 0;
}
if (a == '\0') {
return 1;
}
}
}
/*
* 函数功能:口令校验 + 失败计数 + 限时锁定(鉴权契约核心)
* 入口参数:p_creds - 凭据 user_ok - USER 阶段匹配结果 p_pass - 口令
* p_state - 会话外共享状态 now_tick - 当前 tick(ms)
* 返回值:FTP_AUTH_OK / FAIL / LOCKED(枚举)
* 限定条件:锁定判定先于哈希(锁定期内不做哈希);用户名错也走哈希(耗时拉平);
* tick 有符号差抗 32 位回绕;NULL 入参 fail-closed 返回 FAIL
*/
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds,
uint8_t user_ok, const char *p_pass,
ftp_auth_state_t *p_state, uint32_t now_tick) {
sha256_ctx_t ctx;
uint8_t digest[SHA256_DIGEST_SIZE];
int ok;
if ((p_creds == NULL) || (p_state == NULL)) {
return FTP_AUTH_FAIL; /* fail-closed */
}
if (p_state->fail_count >= FTP_AUTH_FAIL_MAX) {
/* 有符号差:now 在 lock_until 之前(含回绕)= 仍在锁定窗内 */
if ((int32_t)(now_tick - p_state->lock_until_tick) < 0) {
return FTP_AUTH_LOCKED;
}
p_state->fail_count = 0u; /* 锁定期满,重新起算 */
}
/* 盐 = 期望用户名;用户名错也走同一哈希路径(耗时拉平,见头文件说明 2) */
sha256_init(&ctx);
if (p_creds->p_user != NULL) {
sha256_update(&ctx, p_creds->p_user, strlen(p_creds->p_user));
}
if (p_pass != NULL) {
sha256_update(&ctx, p_pass, strlen(p_pass));
}
sha256_final(&ctx, digest);
ok = ((user_ok != 0u) &&
(sha256_hex_eq(digest, p_creds->p_pwd_hash) != 0)) ? 1 : 0;
if (ok != 0) {
p_state->fail_count = 0u;
return FTP_AUTH_OK;
}
p_state->fail_count++;
if (p_state->fail_count >= FTP_AUTH_FAIL_MAX) {
p_state->lock_until_tick = now_tick + FTP_AUTH_LOCK_TICKS;
}
return FTP_AUTH_FAIL;
}
+73
View File
@@ -0,0 +1,73 @@
/*
* 模块名称:lftpd_auth
* 模块功能:FTP 鉴权状态机(纯函数、零平台依赖,主机侧可测)——用户名匹配、
* SHA-256(user‖password) 恒定时间校验、连续失败计数与限时锁定;
* tick 由调用方注入(MCU = xTaskGetTickCount,单测 = 合成值)
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;lftpd.c 为唯一消费者)
*/
#ifndef LFTPD_AUTH_H
#define LFTPD_AUTH_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#define FTP_AUTH_FAIL_MAX 5u /* 连续失败 N 次后锁定 */
#define FTP_AUTH_LOCK_TICKS 60000u /* 锁定时长(tick = 1 ms → 60 s) */
/* 一次校验所需的配置快照(由消费者从 ftp_cfg_t 组装,本模块不认配置引擎) */
typedef struct {
const char *p_user; /* 期望用户名 */
const char *p_pwd_hash; /* 64 字符 hex 摘要 */
uint8_t write_en; /* 透传位:写命令门由 lftpd 侧执行,本模块不判 */
} ftp_auth_creds_t;
/* 鉴权状态(跨会话持久;单连接服务器,全局一份即可) */
typedef struct {
uint8_t fail_count; /* 连续失败计数(成功即清零) */
uint32_t lock_until_tick; /* 锁定截止 tick(fail_count ≥ FAIL_MAX 时有效) */
} ftp_auth_state_t;
typedef enum {
FTP_AUTH_OK = 0, /* 口令正确 */
FTP_AUTH_FAIL, /* 口令/用户名错误(已计入失败计数) */
FTP_AUTH_LOCKED /* 锁定中(本次不校验、不计数) */
} ftp_auth_result_t;
/*
* 函数功能:用户名匹配(大小写不敏感,与配置引擎键名口径一致)
* 入口参数:p_expect - 期望用户名 p_user - 来客输入
* 返回值:1 - 匹配;0 - 不匹配或任一为 NULL int
* 限定条件:无
*/
int ftp_auth_user_match(const char *p_expect, const char *p_user);
/*
* 函数功能:口令校验 + 失败计数 + 限时锁定(一次 PASS 的完整判定)
* 入口参数:p_creds - 配置快照 user_ok - USER 阶段的用户名匹配结果
* p_pass - 来客口令(NULL 按空串) p_state - 鉴权状态
* now_tick - 当前 tick(ms)
* 返回值:ftp_auth_result_t
* 限定条件:p_creds/p_state 非空(NULL 一律 FAIL,fail-closed)
* 函数说明:1. 锁定中直接 LOCKED(不算哈希——锁定状态非机密,无侧信道);
* tick 差用 (int32_t) 有符号比较,天然抗 32 位回绕
* 2. 无论用户名对错都走同一条哈希路径(盐 = 期望用户名),
* 耗时一致 → 不泄露"用户是否存在"
* 3. 成功清零计数;失败累计到 FTP_AUTH_FAIL_MAX 上锁
*/
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds,
uint8_t user_ok, const char *p_pass,
ftp_auth_state_t *p_state, uint32_t now_tick);
#ifdef __cplusplus
}
#endif
#endif /* LFTPD_AUTH_H */
-27
View File
@@ -1,27 +0,0 @@
#include "private/lftpd_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "dbg_log.h"
void lftpd_log_internal(const char* level, const char* format, ...) {
static char buffer[256]; /* static: called at depth in ftpTask, keep off-stack */
static char line[288]; /* static: 组装含前缀的整行,交给控制台单写者 */
va_list args;
va_start(args, format);
int err = vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
if (err >= sizeof(buffer)) {
return;
}
/* 走控制台单写者(App/log/console_log.c):整行一次入缓冲,避免逐字符 fputc 交错 */
int n = snprintf(line, sizeof(line), "%s %s\r\n", level, buffer);
if (n > 0) {
if (n > (int)sizeof(line) - 1) {
n = (int)sizeof(line) - 1;
}
(void)dbg_console_write_line(line, (unsigned)n);
}
}