test(net/udp): 主机单测按真机改写 UDP-4/17/22/24,补残包组 UDP-26(#73)

剥头判据改了,测试就得跟着改——但这一条提交里真正值钱的是"为什么会错到":
UDP-4 当时写的是"客户端形态整包都是载荷、源地址取对端记账",UDP-24 的 240 轮
定种子随机长跑也照同一个模型脚本报假件。**随机长跑全绿恰恰不能证明模型对**:
它扫的是我自己写的那套模型,模型错了它一样绿。只有真机那次 48→56 的读数把这件事
钉死。

- UDP-4 改写为"客户端形态同样剥头",源地址来自信息头(脚本里特意让头里的源
  `119.28.220.215` ≠ 记账对端 `119.28.220.214`,这样"取错地方"就必然失败),
  并补 UDP-4h 断言 `addrlen`/family 与服务器形态一字不差——一条读路径就该一个契约。
- UDP-17h/m、UDP-22c、UDP-24 的客户端轮同样按定案重写(先 `reset_recv_len` 再
  宣布长度,假件的收包脚本必须可重排,这是长跑教过的第二条)。
- 改完判据后覆盖率门禁照出 `net_udp_src_from_peer()` 一行没人走了。**没有把基线
  调低**,而是补 UDP-26(9 条)钉住"`<8` 字节残包"那一侧:整包原样交付、一个字
  节都不许剥、源地址退到对端记账;没记账时交出全零这个"我不知道",而不是包里的
  字节——它是记账那本账如今唯一的读者。

全套 **677/677 绿**(改造前 470);`net_udp.c` 回到 237/239 行、14/14 函数,
`net_socket.c` 966/969 行、59/59 函数,全仓 8372/9290(90.1 %)、47/47 套,
`scripts/test_coverage_baseline.json` 与改动前一字未变(即无回退)。
This commit is contained in:
2026-11-04 09:15:52 +08:00
parent b5671e8c02
commit 0d469a835a
+96 -23
View File
@@ -231,9 +231,16 @@ static void tc_udp3_server_strip(void)
reset_all();
}
/* ================= UDP-4 客户端模式:不剥头(改造修的缺陷本体) ================= */
static void tc_udp4_client_no_strip(void)
/* ====== UDP-4 客户端形态:DesIP 改写后**仍然剥头**(真机推翻的假设) ====== */
/*
* 这一组是 2026-11-04 真机回归改写的。设计时按手册 §9.2.4 读过一句"客户端模式不
* 带信息头",就以为运行时把 DesIP 从全 F 改成具体对端会让头消失;板子上 TC-NET-1603
* 实测 48 字节应答可读 56 字节、首字节 0x38 = 信息头的长度字段,证明**加不加头由
* 开 Socket 那一刻的 DesIP 决定,之后再改 DesIP 只改发送去向、不改收包帧格式**。
* 本层 bind 一律以全 F 开 Socket,所以头恒在、恒剥。旧断言(客户端形态整包都是
* 载荷、源地址取对端记账)因此是错的,留着就是把一个真机缺陷写进契约。
*/
static void tc_udp4_client_still_strips(void)
{
struct net_sockaddr_in in;
struct net_sockaddr *sa = (struct net_sockaddr *)&in;
@@ -259,24 +266,26 @@ static void tc_udp4_client_no_strip(void)
CHECK(g_chip.open_calls == 1,
"UDP-4d switching mode does NOT reopen the socket (a reopen would drop buffered replies)");
/* 48 字节 SNTP 应答,前 8 字节故意长得像"信息头"——旧实现会把它们剥掉 */
/* 48 字节 SNTP 应答:芯片此时**仍然**前置 8 字节信息头(真机 TC-NET-1603)。
* 头里写的源是应答真正的来源,未必等于我记账的对端——NAT 后的时间服务器就是这种。 */
(void)memset(stream, 0, sizeof(stream));
for (i = 0; i < 48; i++) {
stream[i] = (uint8_t)(0x24 + i); /* 0x24 = LI=0,VN=4,Mode=3 起手 */
}
ch395f_fake_set_recv_stream(stream, 48U);
ch395f_fake_push_recv_len((uint8_t)fd, 48U);
udp_script_server_datagram((const char *)stream, 48, 51999U, "119.28.220.215");
s_net_socks[fd].readable = 1U;
addrlen = (int)sizeof(in);
(void)memset(&in, 0, sizeof(in));
rc = net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen);
CHECK(rc == 48,
"UDP-4e client mode delivers ALL 48 bytes (the pre-refactor build returned 40 and silently ate the first 8)");
"UDP-4e client form still delivers exactly the 48 payload bytes (the header is stripped: the chip keeps prepending it after a runtime DesIP change)");
CHECK((uint8_t)buf[0] == 0x24U && (uint8_t)buf[7] == 0x2BU,
"UDP-4f the first delivered byte is the reply itself, not the byte after a stripped header");
CHECK(in.sin_addr.s_addr == net_inet_addr("119.28.220.214") &&
net_ntohs(in.sin_port) == 123U,
"UDP-4g source address comes from this socket's peer bookkeeping (client mode has no header to read)");
CHECK(in.sin_addr.s_addr == net_inet_addr("119.28.220.215") &&
net_ntohs(in.sin_port) == 51999U,
"UDP-4g source address comes from the header even in client form - the peer bookkeeping would name the address I sent to, not the address the reply came from");
CHECK(addrlen == (int)sizeof(in) && in.sin_family == NET_AF_INET,
"UDP-4h addrlen/family filled as in server form (one read path, one contract)");
reset_all();
}
@@ -879,7 +888,7 @@ static void tc_udp17_set_peer_and_mode(void)
"UDP-17g the chip's destination registers carry the peer (line order, as the chip wants it)");
CHECK(s_net_socks[fd].remote_ip == net_inet_addr("119.28.220.214") &&
s_net_socks[fd].remote_port == 123U,
"UDP-17h peer bookkeeping is written on the switch, not only on send (client-mode receive has no header to read)");
"UDP-17h peer bookkeeping is written on the switch, not only on send (a destination-less sendto needs it, and it is what the chip's DesIP registers mirror)");
CHECK(g_chip.open_calls == 1,
"UDP-17i switching to client mode does NOT reopen the socket (a reopen would drop the reply already buffered - the whole reason SNTP can use this)");
@@ -1352,13 +1361,10 @@ static void tc_udp24_random_soak(void)
/* 每轮重排长度脚本:假件的收包长度是"按调用序号取"的脚本,240 轮只追加
* 不清空会让序号撞满容量、之后每轮都读到同一个陈旧长度。 */
ch395f_fake_reset_recv_len(0);
if (as_client) {
/* 客户端模式:芯片不再前置信息头,整段就是载荷 */
ch395f_fake_set_recv_stream(payload, (uint16_t)len);
ch395f_fake_push_recv_len(0, (uint16_t)len);
} else {
udp_script_server_datagram((const char *)payload, len, sport, sip);
}
/* 两种形态都按"8 字节信息头 + 载荷"脚本:本层 Socket 恒以全 F 开出,运行时
* 改 DesIP 不改收包帧格式(2026-11-04 真机 TC-NET-1603),所以 CLIENT 轮同样
* 带头、同样剥。形态在这条 soak 里考的是**发送去向**与寄存器状态,不是帧格式。 */
udp_script_server_datagram((const char *)payload, len, sport, sip);
n = net_recvfrom_to(fd, rx, len, 800U, (struct net_sockaddr *)&from, &alen);
if (n != len) {
@@ -1375,8 +1381,9 @@ static void tc_udp24_random_soak(void)
bad_src++;
clean = 0;
} else {
uint32_t want_ip = as_client ? peer.sin_addr.s_addr : net_inet_addr(sip);
uint16_t want_port = as_client ? net_ntohs(peer.sin_port) : sport;
/* 源地址恒取自信息头:记账的对端是"我往哪发",不是"谁在回我"。 */
uint32_t want_ip = net_inet_addr(sip);
uint16_t want_port = sport;
if (from.sin_addr.s_addr != want_ip || net_ntohs(from.sin_port) != want_port) {
if (first_bad[2] < 0) { first_bad[2] = i; }
@@ -1434,10 +1441,10 @@ static void tc_udp24_random_soak(void)
(void)snprintf(nm, sizeof(nm), "UDP-24a soak recv length always equals the chip-announced length (bad=%d, first round=%d, rounds=%d)",
bad_len, first_bad[0], UDP_SOAK_ROUNDS);
CHECK(bad_len == 0, nm);
(void)snprintf(nm, sizeof(nm), "UDP-24b payload bytes neither lose the 8-byte info header nor lose real payload: SERVER strips, CLIENT never strips (bad=%d, first round=%d)",
(void)snprintf(nm, sizeof(nm), "UDP-24b payload bytes neither lose the 8-byte info header nor lose real payload: the header is stripped in BOTH forms, since a runtime DesIP change does not change the chip's frame (bad=%d, first round=%d)",
bad_bytes, first_bad[1]);
CHECK(bad_bytes == 0, nm);
(void)snprintf(nm, sizeof(nm), "UDP-24c source address is right in both forms - header-derived in SERVER, peer-derived in CLIENT (bad=%d, first round=%d)",
(void)snprintf(nm, sizeof(nm), "UDP-24c source address is always header-derived, in both forms (bad=%d, first round=%d)",
bad_src, first_bad[2]);
CHECK(bad_src == 0, nm);
(void)snprintf(nm, sizeof(nm), "UDP-24d set_peer keeps the mode exactly as commanded (bad=%d, first round=%d)",
@@ -1555,6 +1562,71 @@ static void tc_udp25_two_socket_park_isolation(void)
reset_all();
}
/* ===== UDP-26 芯片宣布的长度不足 8 字节:无从判头的残包(剥头分支的另一侧) ===== */
/*
* 真机定案之后,"剥不剥头"只剩一个判据:芯片说这一包有几个字节。>= 8 就剥,
* < 8 就整包交出去、源地址退到对端记账(既有定案 TC-NETSOCK-440o)。UDP-4 把
* >= 8 那一侧钉死了,这一组钉 < 8 那一侧——否则这条兜底路径一行都没人走,
* 而它是唯一还会用 `net_udp_src_from_peer()` 的地方。
*/
static void tc_udp26_runt_no_header(void)
{
struct net_sockaddr_in in;
struct net_sockaddr *sa = (struct net_sockaddr *)&in;
uint8_t runt[5];
char buf[16];
int addrlen;
int fd;
int rc;
int i;
bring_up();
fd = udp_bind(4321);
for (i = 0; i < 5; i++) {
runt[i] = (uint8_t)(0xC0 + i);
}
/* 第一轮:谁都没记账(bind 之后对端是零)——源地址就该是"无从得知" */
ch395f_fake_reset_recv_len((uint8_t)fd);
ch395f_fake_set_recv_stream(runt, 5);
ch395f_fake_push_recv_len((uint8_t)fd, 5);
s_net_socks[fd].readable = 1U;
addrlen = (int)sizeof(in);
(void)memset(&in, 0xAA, sizeof(in)); /* 出参必须被写满,不许留调用方旧值 */
rc = net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen);
CHECK(rc == 5,
"UDP-26a a runt the chip announced as 5 bytes is delivered whole - nothing is stripped from a packet too short to carry the 8-byte header");
CHECK((uint8_t)buf[0] == 0xC0U && (uint8_t)buf[4] == 0xC4U,
"UDP-26b the runt's own bytes are the payload (a strip here would eat the packet)");
CHECK(addrlen == (int)sizeof(in) && in.sin_family == NET_AF_INET,
"UDP-26c the runt still fills the address out-param properly (family + addrlen)");
CHECK(in.sin_addr.s_addr == 0U && net_ntohs(in.sin_port) == 0U,
"UDP-26d with nobody booked the runt's source is the zero peer - an honest \"I don't know\", never bytes from the packet");
CHECK(net_udp_armed(fd) == 0 && net_udp_pending(fd) == 0,
"UDP-26e the runt exit clears the out-param slot like every other exit");
/* 第二轮:记了账再残包——源地址退到记账的那个对端(芯片没告诉我们是谁发的) */
(void)memset(&in, 0, sizeof(in));
in.sin_family = NET_AF_INET;
in.sin_port = net_htons(123U);
in.sin_addr.s_addr = net_inet_addr("119.28.220.214");
CHECK(net_udp_set_peer(fd, &in) == 0, "UDP-26f book a peer before the second runt");
ch395f_fake_reset_recv_len((uint8_t)fd);
ch395f_fake_set_recv_stream(runt, 5);
ch395f_fake_push_recv_len((uint8_t)fd, 5);
s_net_socks[fd].readable = 1U;
addrlen = (int)sizeof(in);
(void)memset(&in, 0xAA, sizeof(in));
rc = net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen);
CHECK(rc == 5, "UDP-26g the booked-peer runt also delivers all 5 bytes");
CHECK(in.sin_addr.s_addr == net_inet_addr("119.28.220.214") &&
net_ntohs(in.sin_port) == 123U,
"UDP-26h an unhedrable runt falls back to the booked peer (TC-NETSOCK-440o's contract, and the only remaining reader of the bookkeeping)");
CHECK(net_udp_armed(fd) == 0 && net_udp_pending(fd) == 0,
"UDP-26i no slot left behind on this exit either");
reset_all();
}
/* ===================== 本分片入口 ===================== */
static void ns_run_s7(void)
@@ -1562,7 +1634,7 @@ static void ns_run_s7(void)
tc_udp1_bind_sequence();
tc_udp2_requires_bind();
tc_udp3_server_strip();
tc_udp4_client_no_strip();
tc_udp4_client_still_strips();
tc_udp5_sendto_dest_switches_mode();
tc_udp6_pending_recv_arrival();
tc_udp7_pending_timeout();
@@ -1584,4 +1656,5 @@ static void ns_run_s7(void)
tc_udp23_recvfrom_direct_ctx();
tc_udp24_random_soak();
tc_udp25_two_socket_park_isolation();
tc_udp26_runt_no_header();
}