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:
@@ -56,14 +56,13 @@ volatile uint8_t g_spi1_dma_done;
|
||||
* 否则静默失败(CCM 首次扩容实验实证的根因)。页级大块传输(2048)仍走 DMA,
|
||||
* 源/目的全部为文件作用域静态缓冲。 */
|
||||
|
||||
/* DMA 可达内存区间:必须与分散加载文件 RW_IRAM1(0x20000000,128KB,
|
||||
* SRAM1+SRAM2)一致。CCM(0x10000000,64KB)DMA1/DMA2 物理不可达,
|
||||
* RW_CCM 区任何缓冲都不得进 DMA。阈值只能挡住"当前已知的小栈缓冲",
|
||||
* 挡不住"将来新加的 >1024B 栈缓冲";gd5f_dma_buf_ok() 把这条约定
|
||||
* 升级为运行时强制检查(2026-09-21 加固)。若将来新增其它 DMA 可达
|
||||
* 内存(如外部 SDRAM),此处区间必须同步扩展。 */
|
||||
#define GD5F_DMA_RAM_BASE 0x20000000UL
|
||||
#define GD5F_DMA_RAM_END 0x20020000UL
|
||||
/* DMA 可达内存窗口 GD5F_DMA_RAM_BASE/END 与 CCM 窗口常量已上移到 gd5f2gq5ue.h
|
||||
* (N-26,2026-10-12:nand_ftl.c 的 init 期自检也要用),并带编译期守卫
|
||||
* (白名单窗口与 CCM 永不相交、阈值契约)。此处只留语义提醒:
|
||||
* 窗口必须与分散加载 RW_IRAM1(0x20000000,128KB,SRAM1+SRAM2)一致;
|
||||
* CCM(0x10000000,64KB)DMA1/DMA2 物理不可达,RW_CCM 区任何缓冲都不得进 DMA。
|
||||
* 阈值只能挡住"当前已知的小栈缓冲",挡不住"将来新加的 >1024B 栈缓冲";
|
||||
* gd5f_dma_buf_ok() 把这条约定升级为运行时强制检查(2026-09-21 加固)。 */
|
||||
|
||||
/*
|
||||
* 函数功能:SPI1 TX DMA 完成回调(PROGRAM_LOAD 数据发送用)
|
||||
@@ -104,8 +103,7 @@ static int gd5f_private_bbt_find_best(void);
|
||||
static int gd5f_bbt_load(void);
|
||||
static int gd5f_bbt_save(void);
|
||||
|
||||
/* DMA 可达性守卫(见下方实现) */
|
||||
static int gd5f_dma_buf_ok(const void *p_buf, size_t size);
|
||||
/* DMA 可达性守卫:实现见下方(N-26 起为公开 API,声明在 gd5f2gq5ue.h) */
|
||||
|
||||
/* ======================== DMA 可达性守卫 ======================== */
|
||||
|
||||
@@ -120,7 +118,7 @@ static int gd5f_dma_buf_ok(const void *p_buf, size_t size);
|
||||
* 本函数把"栈/堆缓冲禁入 DMA"的约定升级为运行时强制约束:
|
||||
* 起止地址任一越出 SRAM 区间即判不可达。
|
||||
*/
|
||||
static int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
|
||||
int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
|
||||
uint32_t start = (uint32_t)(uintptr_t)p_buf;
|
||||
uint32_t end;
|
||||
|
||||
|
||||
@@ -77,6 +77,35 @@ extern "C" {
|
||||
*/
|
||||
#define GD5F_DMA_THRESHOLD 1024
|
||||
|
||||
/* ==================== DMA 可达内存窗口(N-26,2026-10-12) ====================
|
||||
* DMA1/DMA2 物理不可达 CCM(0x10000000~0x1000FFFF):以 CCM 缓冲做 DMA 源/目的
|
||||
* 会静默不落地(2026-09-20 首轮 CCM 扩容实验实证根因;口径见
|
||||
* docs/内存管理/内存内容分配策略.md §2 与 §4 规则 R2/R3)。
|
||||
* 白名单窗口必须与分散加载 RW_IRAM1(0x20000000,128KB,SRAM1+SRAM2)一致
|
||||
* (MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct);将来新增其它 DMA 可达内存
|
||||
* (如外部 SDRAM)须同步扩展本窗口。
|
||||
* ⚠ 缓冲**自身地址**无法编译期断言:.bss 地址链接期才确定,armcc 的常量表达式
|
||||
* 取不到(__builtin 系在 MicroLIB 下亦不可用,见 Src/main.c Error_Handler 注释)。
|
||||
* 静态缓冲的落位由 scatter(RW_CCM 只列 heap_4.o)+ nand_ftl.c 的 init 期自检
|
||||
* (gd5f_dma_buf_ok)双层保证;动态传入缓冲由每次传输的守卫强制。 */
|
||||
#define GD5F_DMA_RAM_BASE 0x20000000UL
|
||||
#define GD5F_DMA_RAM_END 0x20020000UL
|
||||
#define GD5F_CCM_RAM_BASE 0x10000000UL
|
||||
#define GD5F_CCM_RAM_END 0x10010000UL
|
||||
|
||||
/* 编译期守卫(N-26):AC5 无 _Static_assert(C11 不支持),用项目惯用的
|
||||
* 负数组 typedef(同 App/cfg/cfg_file.c、Drivers/BSP/UI/ui_menu_tab.h)——
|
||||
* 条件不成立则数组尺寸 -1 → 编译错。
|
||||
* ① DMA 白名单窗口与 CCM 窗口**永不得重叠**(有人改上面常量时立即爆炸);
|
||||
* ② 阈值契约(见 GD5F_DMA_THRESHOLD 处注释):≥ 存储路径最大栈缓冲
|
||||
* (dhara meta 132 B = DHARA_META_SIZE 上限)且 < 页大小(页级传输保留 DMA)。 */
|
||||
typedef char gd5f_dma_window_no_ccm_check_t[
|
||||
((GD5F_DMA_RAM_END <= GD5F_CCM_RAM_BASE) || (GD5F_DMA_RAM_BASE >= GD5F_CCM_RAM_END))
|
||||
? 1 : -1];
|
||||
typedef char gd5f_dma_threshold_contract_check_t[
|
||||
((GD5F_DMA_THRESHOLD >= 132U) && (GD5F_DMA_THRESHOLD < GD5F_PAGE_SIZE))
|
||||
? 1 : -1];
|
||||
|
||||
/* 坏块表持久化:保留块池数量(从 NAND 顶部向下挑选出厂好块存储 BBT) */
|
||||
#define GD5F_BBT_POOL_COUNT 4
|
||||
|
||||
@@ -253,6 +282,18 @@ int32_t gd5f_program_exec(uint32_t page_addr);
|
||||
int32_t gd5f_block_erase(uint32_t block_addr);
|
||||
int32_t gd5f_check_ecc(void);
|
||||
|
||||
/*
|
||||
* 函数功能:判定缓冲整段是否位于 DMA 可达内存(GD5F_DMA_RAM_* 白名单窗口)
|
||||
* 入口参数:p_buf - 传输缓冲首地址 const void*
|
||||
* size - 传输字节数 size_t
|
||||
* 返回值:1 - 整段 DMA 可达;0 - 不可达(含 size=0 / 地址回绕 / 越窗)
|
||||
* 限定条件:无
|
||||
* 函数说明:N-26(2026-10-12)由 gd5f2gq5ue.c 私有守卫升级为公开 API——
|
||||
* 驱动内部每次 >阈值 传输前用它(不可达即回落 CPU 轮询),
|
||||
* FTL 层(nand_ftl.c)在 init 期用它自检静态页缓冲的落位。
|
||||
*/
|
||||
int gd5f_dma_buf_ok(const void *p_buf, size_t size);
|
||||
|
||||
/*
|
||||
* 函数功能:NAND 长耗时操作看门狗心跳弱钩子
|
||||
* 入口参数:无
|
||||
|
||||
@@ -228,6 +228,24 @@ DSTATUS disk_initialize(BYTE pdrv) {
|
||||
|
||||
DBG_INFO("Initializing NAND FTL...");
|
||||
|
||||
/* N-26 启动期地址自检(2026-10-12):本文件三个页缓冲是 SPI1 DMA 路径上
|
||||
* 唯一的**静态**缓冲(dhara_map_read/write → dhara_nand_read/prog/copy,
|
||||
* 2048 B > GD5F_DMA_THRESHOLD;s_page_buf 现仅装 dhara meta 132 B 走轮询,
|
||||
* 但它同为页尺寸且交给 dhara 内部使用,一并守住防将来越窗)。
|
||||
* DMA1/DMA2 不可达 CCM——若 scatter 改动/段放置把它们挪进 CCM,DMA 会
|
||||
* **静默不落地**(数据错而传输"成功")。缓冲地址链接期才确定,armcc
|
||||
* 无法编译期断言(见 gd5f2gq5ue.h 的窗口常量注释),故在 init 期自检:
|
||||
* 任一缓冲越出 DMA 白名单窗口 → 上报并拒绝挂载(存储降级、不挂死,
|
||||
* 与 E-31 的"保留卷、上报自检"形态一致)。每次传输的运行期守卫
|
||||
* (gd5f_read_from_cache/gd5f_program_load 内)保留作第二层兜底。 */
|
||||
if ((gd5f_dma_buf_ok(s_cache_buf, FTL_PAGE_SIZE) == 0) ||
|
||||
(gd5f_dma_buf_ok(s_copy_buf, FTL_PAGE_SIZE) == 0) ||
|
||||
(gd5f_dma_buf_ok(s_page_buf, FTL_PAGE_SIZE) == 0)) {
|
||||
DBG_ERROR("FTL page buffer not DMA-reachable (cache=0x%08lX), "
|
||||
"storage refused", (unsigned long)(uintptr_t)s_cache_buf);
|
||||
return STA_NOINIT;
|
||||
}
|
||||
|
||||
s_nand.log2_page_size = 11;
|
||||
s_nand.log2_ppb = 6;
|
||||
/* dhara 可见块数 = 总块数 - 保留块池(BBT 存储区不交给 dhara,避免被擦写) */
|
||||
|
||||
@@ -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; /* 累计发送字节数 */
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 模块名称:FTP 配置([ftp] 段)
|
||||
* 模块功能:RAM 副本 + 工厂默认 + cfg_file 引擎注册(键表 user/pwd_hash/write)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ftp_cfg.h"
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
/* 私有变量定义区 */
|
||||
|
||||
/* 工厂默认(单一来源宏见 ftp_cfg.h;s_cfg 初值与其一致 = load 前也 fail-closed 可用) */
|
||||
static const ftp_cfg_t s_defaults = {
|
||||
FTP_CFG_DEF_USER,
|
||||
FTP_CFG_DEF_HASH,
|
||||
FTP_CFG_DEF_WRITE,
|
||||
};
|
||||
|
||||
/* RAM 副本(引擎原地更新;注册于 net_cfg_load 内 cfg_file_load 之前) */
|
||||
static ftp_cfg_t s_cfg = {
|
||||
FTP_CFG_DEF_USER,
|
||||
FTP_CFG_DEF_HASH,
|
||||
FTP_CFG_DEF_WRITE,
|
||||
};
|
||||
|
||||
/* [ftp] 段键表(ftp_cfg_t 布局内偏移) */
|
||||
static const cfg_key_def_t s_ftp_keys[] = {
|
||||
{ "user", CFG_VAL_STR, offsetof(ftp_cfg_t, user), FTP_CFG_USER_MAX },
|
||||
{ "pwd_hash", CFG_VAL_STR, offsetof(ftp_cfg_t, pwd_hash), FTP_CFG_HASH_MAX },
|
||||
{ "write", CFG_VAL_UINT, offsetof(ftp_cfg_t, write_en), 1u },
|
||||
};
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:大小写不敏感字符串相等判断
|
||||
* 入口参数:p_a - 串 A p_b - 串 B(均可 NULL)
|
||||
* 返回值:1 - 相等;0 - 不等或任一为 NULL int
|
||||
* 限定条件:无
|
||||
*/
|
||||
static int s_str_ci_eq(const char *p_a, const char *p_b) {
|
||||
if ((p_a == NULL) || (p_b == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
for (size_t i = 0u; ; i++) {
|
||||
char a = p_a[i];
|
||||
char b = p_b[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:注册 [ftp] 段到 cfg_file 引擎(须在 cfg_file_load 之前调用)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:幂等(重复调用只注册一次);由 net_cfg_load 统一触发
|
||||
*/
|
||||
void ftp_cfg_register(void) {
|
||||
static uint8_t s_done;
|
||||
cfg_section_reg_t reg;
|
||||
|
||||
if (s_done != 0u) {
|
||||
return;
|
||||
}
|
||||
(void)memset(®, 0, sizeof(reg));
|
||||
reg.name = "ftp";
|
||||
reg.p_data = &s_cfg;
|
||||
reg.p_defaults = &s_defaults;
|
||||
reg.data_size = sizeof(ftp_cfg_t);
|
||||
reg.p_keys = s_ftp_keys;
|
||||
reg.key_count = 3u;
|
||||
reg.p_cb = NULL; /* 无"立即应用"需求:口令在下次 PASS 时生效 */
|
||||
(void)cfg_file_register(®);
|
||||
s_done = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取 [ftp] 段 RAM 副本(load 前后均有效,load 前 = 工厂默认)
|
||||
* 入口参数:无
|
||||
* 返回值:配置指针(恒非 NULL,指向静态存储)
|
||||
* 限定条件:调用方只读
|
||||
*/
|
||||
const ftp_cfg_t *ftp_cfg_get(void) {
|
||||
return &s_cfg;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判断当前口令哈希是否仍为出厂默认(启动告警依据)
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 仍是默认;0 - 已改 int
|
||||
* 限定条件:大小写不敏感比较
|
||||
*/
|
||||
int ftp_cfg_is_default_pwd(void) {
|
||||
return s_str_ci_eq(s_cfg.pwd_hash, FTP_CFG_DEF_HASH);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 模块名称:FTP 配置([ftp] 段)
|
||||
* 模块功能:FTP 服务鉴权配置——RAM 副本 + 工厂默认 + cfg_file 引擎注册
|
||||
* ([ftp] 段:user / pwd_hash / write);消费者(lftpd/lftpd_auth)
|
||||
* 经 ftp_cfg_get() 只读,配置引擎属主仍为 defaultTask
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权)
|
||||
*/
|
||||
|
||||
#ifndef FTP_CFG_H
|
||||
#define FTP_CFG_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define FTP_CFG_USER_MAX 16u /* 用户名缓冲长度(含 NUL) */
|
||||
#define FTP_CFG_HASH_MAX 65u /* SHA-256 hex 摘要 64 字符 + NUL */
|
||||
|
||||
/* 工厂默认:user=admin、口令=admin(pwd_hash = SHA256("adminadmin"))、write=1。
|
||||
* write=1 保持既有维护工作流(FTP 推 config.ini);生产配置应改口令并按需 write=0。
|
||||
* 出厂默认口令在用期间,lftpd_start 每次启动打 INFO 告警锚点(N-19) */
|
||||
#define FTP_CFG_DEF_USER "admin"
|
||||
#define FTP_CFG_DEF_HASH "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892"
|
||||
#define FTP_CFG_DEF_WRITE 1u
|
||||
|
||||
typedef struct {
|
||||
char user[FTP_CFG_USER_MAX]; /* 期望用户名(大小写不敏感) */
|
||||
char pwd_hash[FTP_CFG_HASH_MAX]; /* SHA-256(user‖password) hex(大小写不敏感) */
|
||||
uint8_t write_en; /* 1 = 允许 STOR/DELE;0 = 只读 */
|
||||
} ftp_cfg_t;
|
||||
|
||||
/*
|
||||
* 函数功能:向 cfg_file 引擎注册 [ftp] 段(幂等)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:cfg_file_load 之前调用(net_cfg_load 内统一编排)
|
||||
*/
|
||||
void ftp_cfg_register(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取 [ftp] 段 RAM 副本(只读)
|
||||
* 入口参数:无
|
||||
* 返回值:恒非 NULL(静态常驻,load 前初值 = 工厂默认) const ftp_cfg_t *
|
||||
* 限定条件:无
|
||||
* 函数说明:defaultTask poll 重载瞬间可能读到撕裂值 → 该次登录失败
|
||||
* (fail-closed,重试即恢复;配置变更本就罕见)
|
||||
*/
|
||||
const ftp_cfg_t *ftp_cfg_get(void);
|
||||
|
||||
/*
|
||||
* 函数功能:判断当前 pwd_hash 是否仍为出厂默认口令哈希
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 默认口令在用;0 - 已改 int
|
||||
* 限定条件:无
|
||||
*/
|
||||
int ftp_cfg_is_default_pwd(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* FTP_CFG_H */
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
#include "ftp_cfg.h"
|
||||
|
||||
/* 深调用链模块(net_cfg_load/save 在 defaultTask 启动链与保存链上):
|
||||
* 日志行缓冲收到 128 B(默认 192),本模块最长行渲染后 < 128 → 不截断 */
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
@@ -145,6 +147,8 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
|
||||
*/
|
||||
int net_cfg_load(void) {
|
||||
s_net_register();
|
||||
/* N-19:[ftp] 段一并注册(所有受管段必须先注册、后 load,引擎按注册表解析) */
|
||||
ftp_cfg_register();
|
||||
|
||||
/* 旧档 NCFG 迁移已于 2026-10-10 删除(net_cfg_legacy 移除;理由见文件头 v1.3)。
|
||||
* 若卷上仍残留 /cfg/netcfg.bin:不读、不删、不迁移 → 配置回落工厂默认 */
|
||||
|
||||
Reference in New Issue
Block a user