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:
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+143
-11
@@ -581,8 +581,18 @@ static void net_reconcile_tick(void) {
|
||||
bad = 1;
|
||||
} else if ((p_sock->state == NET_SOCK_STATE_LISTENING) &&
|
||||
(st[1] == CH395F_TCP_ESTABLISHED)) {
|
||||
/* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept */
|
||||
bad = 2;
|
||||
/* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept
|
||||
* N-30(Trap 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED,
|
||||
* 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给
|
||||
* 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN */
|
||||
uint8_t remote[6] = {0, 0, 0, 0, 0, 0};
|
||||
ch395f_get_remot_ipp((uint8_t)i, remote);
|
||||
uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8));
|
||||
/* 仅"端口非零且 = 上一条连接端口"判幽灵拦截;peer==0 不拦截
|
||||
* (对端端口提交滞后时不得挡掉丢中断救援,见 net_lost_connect_probe 注释) */
|
||||
if (!((peer != 0U) && (peer == p_sock->last_peer_port))) {
|
||||
bad = 2;
|
||||
}
|
||||
} else if ((p_sock->state == NET_SOCK_STATE_LISTENING) &&
|
||||
((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN))) {
|
||||
/* ③ 本层 LISTENING 而芯片监听已消失 → 重建监听 */
|
||||
@@ -768,6 +778,9 @@ void net_process_messages(void) {
|
||||
* close/重监听命令(会摧毁芯片侧仍健康的监听器 → 端口永拒,
|
||||
* run8 实证 170s 拒绝);新连接的 CONNECT 中断会把两侧重新同步 */
|
||||
p_sock->state = NET_SOCK_STATE_LISTENING;
|
||||
/* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 */
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
/* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */
|
||||
net_pending_cancel(msg.sockfd, NET_ERR_CONNRESET);
|
||||
/* 仅修正 MCU 侧状态,不发芯片命令,直接返回成功 */
|
||||
@@ -1090,6 +1103,8 @@ int net_listen_locked(int sockfd) {
|
||||
p_sock->state = NET_SOCK_STATE_LISTENING;
|
||||
/* 单连接模式:连接断开后由 net 层自动回到 LISTEN(模拟 BSD 监听常驻) */
|
||||
p_sock->auto_relisten = 1;
|
||||
/* N-30(Trap 25):新监听会话,丢中断去抖计数清零 */
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
/* 单连接:CONNECT 中断直接作用于本 Socket 自身;启动监听前清空各
|
||||
* Socket 及全局中断状态寄存器(读即清除),避免历史残留/伪中断在
|
||||
* 监听刚开始时误触发连接或断开事件 */
|
||||
@@ -1164,6 +1179,84 @@ int net_listen(int sockfd) {
|
||||
* 限定条件:Socket 处于 LISTENING 状态
|
||||
* 函数说明:单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
|
||||
*/
|
||||
/* ===== N-30 accept 路径"丢 CONNECT 中断"补 accept(2026-10-11 裸机芯片台架实证)=====
|
||||
* 台架结论(剥离 OS/net/FTP 直接对芯片做受控 close→open→listen→connect 矩阵):
|
||||
* 芯片关闭连接后的拆链收尾窗口**变长且不可见**(状态寄存器显示 CLOSED 也不代表
|
||||
* 拆完,实测 >2s 仍可能未拆完);该窗口内重新 listen 后,新连接握手照常完成
|
||||
* (芯片状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**——纯事件驱动的 accept
|
||||
* 永远等不到 → 数据被吞 / 425。固定沉降延时、Socket 轮换、listen 后 close 重试、
|
||||
* 缓冲重分配均已被实验证伪;唯一有效 = "中断 + 状态轮询"双信源(修复组 15/15
|
||||
* 全过,机制触发 5 次全部救回)。→ CH395F_Trap_Records.md Trap 25
|
||||
* 幽灵态甄别:拆链窗口内芯片也会直接假报 ESTABLISHED(旧连接未拆净),此时回读
|
||||
* 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept,
|
||||
* 等旧连接拆净 / 新 SYN 到达(新连接若也丢中断,后续轮询以新端口命中)。 */
|
||||
#define NET_LOST_CONN_STABLE_N 2U /* 连续确认轮次(accept 轮询 100ms/轮,≈200ms 去抖) */
|
||||
|
||||
/*
|
||||
* 函数功能:监听等待期的芯片状态轮询探针——芯片已 ESTABLISHED 而 CONNECT 中断
|
||||
* 未到(丢中断)时,去抖满后补做 accept(复用 CONNECT 事件簿记路径)。
|
||||
* 入口参数:p_sock - 处于 LISTENING 的监听控制块 net_sock_t *
|
||||
* 出口参数:无(命中时 p_sock->state 经 handle_connect_event 转 ESTABLISHED)
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文(net_accept_locked 内调用,SPI 访问安全)
|
||||
* 函数说明:1. 每轮 accept 轮询(100ms)读一次芯片状态,兼作状态第二信源
|
||||
* 2. 幽灵态(对端端口 = last_peer_port)不动手,去抖清零
|
||||
* 3. 连续 NET_LOST_CONN_STABLE_N 轮确认后才补 accept(防"刚 listen
|
||||
* 尚未生效"的瞬态误判——2026-09-23 实测教训)
|
||||
*/
|
||||
static void net_lost_connect_probe(net_sock_t *p_sock) {
|
||||
/* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */
|
||||
uint8_t st[2] = {0, 0};
|
||||
/* 对端 IP+端口回读缓冲(4B IP + 2B 端口小端) */
|
||||
uint8_t remote[6] = {0, 0, 0, 0, 0, 0};
|
||||
|
||||
/* 读芯片状态 */
|
||||
ch395f_get_socket_status(p_sock->ch395_sock, st);
|
||||
/* 芯片尚未完成握手:去抖计数清零,本轮无动作 */
|
||||
if (st[1] != CH395F_TCP_ESTABLISHED) {
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
return;
|
||||
}
|
||||
/* 已 ESTABLISHED:回读对端端口做幽灵甄别 */
|
||||
ch395f_get_remot_ipp(p_sock->ch395_sock, remote);
|
||||
/* 对端端口(小端 2 字节) */
|
||||
uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8));
|
||||
/* 幽灵甄别:仅"端口非零且 = 上一条连接端口"才判幽灵拦截;
|
||||
* peer==0(芯片对端端口提交滞后,Trap 15 同类)不拦截——宁可补 accept
|
||||
* 也不退回丢中断死等(2026-10-11 验证轮实测:peer==0 拦截把首连接
|
||||
* 丢中断的原有对账救援也挡死,LIST#1 卡满 20s,比修复前更糟) */
|
||||
if ((peer != 0U) && (peer == p_sock->last_peer_port)) {
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
return;
|
||||
}
|
||||
/* 去抖:连续确认满门槛才补 accept(封顶防溢出) */
|
||||
if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) {
|
||||
p_sock->lost_conn_seen++;
|
||||
}
|
||||
if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) {
|
||||
return;
|
||||
}
|
||||
/* 动手前清去抖计数 */
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
/* 留痕:丢中断自愈命中 */
|
||||
DBG_INFO("sock%d lost-CONNECT self-accept (peer=%u)",
|
||||
(int)p_sock->ch395_sock, peer);
|
||||
/* 复用 CONNECT 事件的簿记路径(转 ESTABLISHED + 记对端 + 事件回调) */
|
||||
handle_connect_event(p_sock);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:TCP Server 接受新连接(内部版本,必须在 netTask 上下文调用)
|
||||
* 入口参数:sockfd - 监听 Socket 描述符 int
|
||||
* 出口参数:addr - 输出:客户端地址结构体指针 struct net_sockaddr *(可为 NULL)
|
||||
* addrlen - 输入输出:地址结构体长度 int *(可为 NULL)
|
||||
* 返回值:新连接的 Socket 描述符,失败返回 -1 int
|
||||
* 限定条件:Socket 处于 LISTENING 状态
|
||||
* 函数说明:1. 单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
|
||||
* 2. 等待期叠加芯片状态轮询探针(丢 CONNECT 中断自愈,Trap 25)
|
||||
* 3. CLOSED + 芯片 RX 缓冲有残留数据:接受"已关但可读"连接排空
|
||||
* (FUN_PARA §5.52 ② 保留现场契约,Trap 25 三次定案)
|
||||
*/
|
||||
static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen) {
|
||||
/* 监听 Socket 控制块 */
|
||||
net_sock_t *p_listen_sock;
|
||||
@@ -1178,18 +1271,43 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen
|
||||
/* 先轮询一次:把状态机刷新到芯片侧最新状态 */
|
||||
net_poll();
|
||||
|
||||
/* N-30(Trap 25):监听等待期兼做芯片状态第二信源——丢 CONNECT 中断时
|
||||
* 去抖补 accept(裸机台架实证:拆链窗口丢中断是数据被吞/425 的主根因,
|
||||
* 纯事件等待无自愈可能;详见 net_lost_connect_probe 区块注释) */
|
||||
if (p_listen_sock->state == NET_SOCK_STATE_LISTENING) {
|
||||
net_lost_connect_probe(p_listen_sock);
|
||||
}
|
||||
|
||||
/* 单连接:本 Socket 自身在 CONNECT 后处于 ESTABLISHED 才表示有可接受的连接
|
||||
* 芯片丢中断导致的失同步不在此处自愈:单次状态读会误判"刚 listen 尚未生效"
|
||||
* (2026-09-23 实测风暴:监听被反复重建直至放弃),统一由 net_reconcile_tick() 周期对账(带去抖)处理 */
|
||||
* (含上一步探针补 accept 的命中);其余失同步由 net_reconcile_tick()
|
||||
* 周期对账(带去抖)兜底 */
|
||||
if (p_listen_sock->state != NET_SOCK_STATE_ESTABLISHED) {
|
||||
/* 连接已断开(CLOSED)且启用自动重监听:先把 Socket 重新置回 LISTEN,
|
||||
* 下一次 accept 即可等待新连接(BSD 风格监听常驻) */
|
||||
/* N-30(2026-10-11 手册 §5.23⑤ + 生产时间线定案):
|
||||
* "closed-but-readable" 接受——STOR 类传输客户端**发完数据立即关数据连接**
|
||||
* (正常 FTP 行为,Python ftplib sendall 后即 close),芯片按
|
||||
* FUN_PARA(SOCKET_CLOSE=0) 规格保留 Socket 与 RX 缓冲供 MCU 排空
|
||||
* (实测 DISCONNECT 后 st=0x05/0x00、recv_len=1027=完整载荷)。
|
||||
* 若 accept 轮询晚于对端 FIN,盲目 relisten(close+open 清缓冲)会把
|
||||
* 已到达的数据销毁 → 425(此前 N-30"425/被吞"的主根因,并非芯片杀连接)。
|
||||
* 处置:芯片 RX 缓冲有数据 → 接受这条"已关但可读"连接,应用经
|
||||
* net_recv_locked 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten。 */
|
||||
int drain_pending = 0;
|
||||
if (p_listen_sock->state == NET_SOCK_STATE_CLOSED && p_listen_sock->auto_relisten) {
|
||||
/* 非阻塞:失败由 D10 重试轮询接管 */
|
||||
(void)net_relisten_try(p_listen_sock);
|
||||
if (ch395f_get_recv_len((uint8_t)sockfd) > 0) {
|
||||
drain_pending = 1;
|
||||
DBG_INFO("sock%d closed-with-data accept (drain pending)", sockfd);
|
||||
} else {
|
||||
/* 连接已断开(CLOSED)且无残留数据:先把 Socket 重新置回 LISTEN,
|
||||
* 下一次 accept 即可等待新连接(BSD 风格监听常驻) */
|
||||
/* 非阻塞:失败由 D10 重试轮询接管 */
|
||||
(void)net_relisten_try(p_listen_sock);
|
||||
}
|
||||
}
|
||||
/* 当前无连接 → WOULDBLOCK */
|
||||
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
|
||||
if (drain_pending == 0) {
|
||||
/* 当前无连接 → WOULDBLOCK */
|
||||
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
|
||||
}
|
||||
/* drain_pending:落入下方 accept 返回(对端簿记仍在控制块,排空后 EOF 了结) */
|
||||
}
|
||||
|
||||
/* 填充对端地址(若调用方需要) */
|
||||
@@ -1895,6 +2013,10 @@ static int net_relisten_locked(net_sock_t *p_sock) {
|
||||
p_sock->state = NET_SOCK_STATE_LISTENING;
|
||||
/* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */
|
||||
p_sock->released_abnormal = 0U;
|
||||
/* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
/* N-30:新监听会话,丢中断去抖计数清零 */
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
/* 清对端端口(新连接待用) */
|
||||
p_sock->remote_port = 0;
|
||||
/* 清对端 IP */
|
||||
@@ -2432,7 +2554,11 @@ static int net_close_locked(net_sock_t *p_sock) {
|
||||
} while (1);
|
||||
|
||||
/* 清零控制块(memset 0 = 释放本 fd 全部状态) */
|
||||
/* N-30(Trap 25):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态,
|
||||
* 与 fd 复用无关——close 后新 listen 落在同一芯片 Socket 上仍可能撞幽灵) */
|
||||
uint16_t keep_peer = p_sock->remote_port;
|
||||
memset(p_sock, 0, sizeof(net_sock_t));
|
||||
p_sock->last_peer_port = keep_peer;
|
||||
/* 关闭即取消该 fd 的待重试(D10 重试队列) */
|
||||
net_relisten_clear(sockfd);
|
||||
/* 关闭即清除该 fd 的挂起状态(L3;幂等) */
|
||||
@@ -3478,7 +3604,8 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
|
||||
}
|
||||
/* 单连接:先不关 CH395F Socket,
|
||||
* 留给 net_recv_locked 把剩余数据读出来再关,置 CLOSED 等待应用重新监听 */
|
||||
DBG_INFO("sock%d DISCONNECTED, releasing (chip tcp=0x%02X)", p_sock->ch395_sock, st[1]);
|
||||
DBG_INFO("sock%d DISCONNECTED, releasing (chip st=0x%02X/0x%02X)",
|
||||
p_sock->ch395_sock, st[0], st[1]);
|
||||
/* Trap 23 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态
|
||||
* 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226;
|
||||
* 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 Trap 22 的快速复连时序(:21 @0ms ×50 全通过) */
|
||||
@@ -3494,6 +3621,9 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
|
||||
}
|
||||
/* 正常释放(对端 FIN):断连了结回 EOF(0) */
|
||||
p_sock->released_abnormal = 0U;
|
||||
/* N-30(Trap 25):登记本连接对端端口(幽灵态甄别基准,须在状态
|
||||
* 迁移前记录——remote_port 此刻仍有效,relisten 清零) */
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
/* 状态 → CLOSED */
|
||||
p_sock->state = NET_SOCK_STATE_CLOSED;
|
||||
/* 触发断连事件回调 */
|
||||
@@ -3581,6 +3711,8 @@ static void handle_timeout_event(net_sock_t *p_sock) {
|
||||
* NET_ERR_CONNRESET 了结(而非 EOF),应用(lftpd STOR)回报 5xx
|
||||
* 而非误报 226(见 net_pending_tick 断连分支) */
|
||||
p_sock->released_abnormal = 1U;
|
||||
/* N-30(Trap 25):异常释放同样登记对端端口(幽灵甄别基准) */
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
/* 状态 → CLOSED */
|
||||
p_sock->state = NET_SOCK_STATE_CLOSED;
|
||||
} else if (p_sock->state == NET_SOCK_STATE_LISTENING) {
|
||||
|
||||
@@ -179,6 +179,16 @@ typedef struct {
|
||||
* 连接的释放原因。 */
|
||||
uint8_t released_abnormal;
|
||||
|
||||
/* N-30(2026-10-11 裸机芯片台架实证):本 Socket 上一条被释放连接的**对端端口**。
|
||||
* 芯片拆链收尾窗口内 Socket 会假报 ESTABLISHED(幽灵态),此时回读的对端端口
|
||||
* = 旧连接端口;新连接的临时端口几乎必然不同。accept 路径补 accept 与对账②
|
||||
* 均以此甄别幽灵态(见 net_socket.c Trap 25)。跨 fd 释放保留(幽灵属芯片
|
||||
* Socket 级,与 fd 复用无关),新连接断开时刷新。 */
|
||||
uint16_t last_peer_port;
|
||||
/* N-30:accept 路径"丢 CONNECT 中断补 accept"的去抖计数(连续确认次数,
|
||||
* 节拍 = accept 轮询 100ms;新监听/补 accept 后清零) */
|
||||
uint8_t lost_conn_seen;
|
||||
|
||||
/* 统计信息 */
|
||||
uint32_t recv_bytes; /* 累计接收字节数 */
|
||||
uint32_t send_bytes; /* 累计发送字节数 */
|
||||
|
||||
Reference in New Issue
Block a user