From 2fb74a2f67ed65321449f1ddb86e1269fece4a5a Mon Sep 17 00:00:00 2001 From: OpenCode Date: Fri, 11 Sep 2026 16:50:26 +0800 Subject: [PATCH] fix: default mode claim now uses proven minimal recipe; bind52/prelude default off (toxic) Root causes found via ablation on cold Butler: 1. bind52x4 ending with 0000(unbind) -> Butler enters multi-minute AECH-refusing state (recovers spontaneously); now gated behind HOSTDEMO_BIND=1 2. 45s broadcast prelude also toxic (PRE=45 claim FAIL / PRE=0 PASS); gating hypothesis ('Butler needs 13+ announce rounds') falsified; HOSTDEMO_PRE default now 0 3. AECH requires announce144 identity first: skip2 (no ident) FAIL, per-round announce144 + claim -> PASS 4. IDENT102 patch offset bug: writing IP at 0x1C corrupted MAC high bytes (IP already at 0x1A in const) - patch removed 5. Claim wait loop now drains socket buffer and uses 700ms wall clock (was 10x5ms recv which expired on buffered self-broadcasts) --- src/hostdemo.rs | 70 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/hostdemo.rs b/src/hostdemo.rs index 2b4bab8..95815c1 100644 --- a/src/hostdemo.rs +++ b/src/hostdemo.rs @@ -1107,6 +1107,14 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { if src.ip().to_string() == target && n == 47 { reg = true; break; } } } + // CLAIM_BINDFF=1: 认领前先发单个 bind52(ffff), 测试能否解除 Butler 拒答态 (bind 中毒恢复实验) + if std::env::var("CLAIM_BINDFF").unwrap_or_default() == "1" { + let mut b = build_bind52([0xff, 0xff]); + b[0x1A..0x1E].copy_from_slice(&[o2[3], o2[2], o2[1], o2[0]]); + sock.send_to(&b, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(320)); + println!("CLAIM_BINDFF: 已发 bind52(ffff)"); + } let nonce = rng.next(); let b12c = rng.next(); let mut ch = load_template("claim_h112"); @@ -1201,7 +1209,9 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { if mode != "skip2" && mode != "noclaim" { // 冷启动广播前奏 (cold2=38s, SymphoLight 实测 2.5min 成功; 10s 短前奏被拒!) // ★门控假说: Butler 须收到 ~13+ 轮 discovery/announce 才武装认领应答 - let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(45); + // ★2026-09-11 消融证伪: 广播前奏不仅非必需而且有毒 (PRE=45 认领FAIL / PRE=0 PASS) + // → 默认 PRE=0; 复现 SL 行为用 HOSTDEMO_PRE=45 + let pre_secs: u64 = std::env::var("HOSTDEMO_PRE").ok().and_then(|s| s.parse().ok()).unwrap_or(0); // ★宣告帧用 announce_live 模板 + 新鲜时间戳/随机会话 ID (陈旧宣告 = 僵尸主机, 认领被无视) let euce = load_template("euce"); // 540B, 无主机相关字段 let pre = Instant::now(); @@ -1236,21 +1246,21 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { println!(" [dbg] 前奏期收帧 len={} src={}", n, src); } } - let mut ident = IDENT102.to_vec(); - ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]); - // ★sub=07 绑定帧 ×4: ffff→0000→ffff→0000 (间隔 ~0.32s, sym_startup @98.8-99.5 实测) - // 1.8s 后发 ident - for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] { - let mut b = build_bind52(flag); - b[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]); - sock.send_to(&b, &dst).unwrap(); - std::thread::sleep(Duration::from_millis(320)); + // ★bind52×4 实测有毒 (2026-09-11 消融: skip2 认领PASS / 仅加bind×4 即 FAIL) — + // ffff→0000→ffff→0000 以 0000(unbind) 收尾, Butler 进入未绑定态后无视 AECH。 + // 默认不再发送; 复现实验用 HOSTDEMO_BIND=1 开启。 + if std::env::var("HOSTDEMO_BIND").unwrap_or_default() == "1" { + for flag in [[0xff_u8, 0xff_u8], [0x00, 0x00], [0xff, 0xff], [0x00, 0x00]] { + let mut b = build_bind52(flag); + b[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]); + sock.send_to(&b, &dst).unwrap(); + std::thread::sleep(Duration::from_millis(320)); + } + std::thread::sleep(Duration::from_millis(1800)); } - std::thread::sleep(Duration::from_millis(1800)); - let mut ident = IDENT102.to_vec(); - ident[0x1C..0x20].copy_from_slice(&[o[3], o[2], o[1], o[0]]); - sock.send_to(&ident, &dst).unwrap(); - println!("[2] 冷启动前奏完成 ({}s 广播 + 绑定帧×4 + 身份帧)", pre_secs); + // IDENT102 常量帧 0x1A 起已是本机 IP+MAC — 不要再补丁 (旧代码 patch 0x1C 会踩坏 MAC 高2字节!) + sock.send_to(&IDENT102, &dst).unwrap(); + println!("[2] 冷启动前奏完成 ({}s 广播 + 身份帧; bind 已默认跳过=有毒)", pre_secs); } let mut reg_ok = false; for _ in 0..10 { @@ -1269,26 +1279,40 @@ pub fn run(target: &str, bind_ip: &str, secs: u64) { } for round in 0..3 { if mode == "noclaim" { break; } + // ★每轮先发身份帧(announce144)等 47B 注册应答, 再发认领 — skip2 实测: 无身份帧直接 claim 被无视 + let o2a = [o[0], o[1], o[2], o[3]]; + sock.send_to(&load_template("announce144"), &dst).unwrap(); + let wr = Instant::now(); + let mut reg47 = false; + while wr.elapsed() < Duration::from_millis(400) { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + if n == 47 { reg47 = true; break; } + } + } let nonce = rng.next(); - let mut ch = load_template("claim_h112"); - ch[0x1A..0x1E].copy_from_slice(&[o[3], o[2], o[1], o[0]]); - ch[0x88..0x8C].copy_from_slice(&nonce.to_le_bytes()); - ch[0x8C..0x90].copy_from_slice(&b12.to_le_bytes()); + let ch = build_claim_frame(o2a, nonce, b12); + // 排空缓存 (前奏自广播回收), 避免 AERE 到达前读循环耗尽 + while sock.recv_from(&mut buf).is_ok() {} sock.send_to(&ch, &dst).unwrap(); - for _ in 0..10 { - if let Ok((n, src)) = sock.recv_from(&mut buf) { + let w = Instant::now(); + let mut rx_n = 0u32; + let mut rx_lens: Vec = Vec::new(); + while w.elapsed() < Duration::from_millis(700) { + if let Ok((n, _)) = sock.recv_from(&mut buf) { + rx_n += 1; + if rx_lens.len() < 8 { rx_lens.push(n); } let b = &buf[..n]; - println!(" [dbg] AECH等待中收帧 len={} src={} magic={:?}", n, src, &b[0..4.min(n)]); if n > 0xC4 && b.windows(4).any(|w| w == b"AERE") { let resp = g32(b, 0xC0); let want = agios_g(nonce); - println!("[3] AECH 第{}轮: resp=0x{:08x} G=0x{:08x} {}", round + 1, resp, want, + println!("[3] AECH 第{}轮: reg47={} resp=0x{:08x} G=0x{:08x} {}", round + 1, reg47, resp, want, if resp == want { "★PASS" } else { "FAIL" }); if resp == want { claim_ok = true; } break; } } } + if !claim_ok { println!("[3] AECH 第{}轮: reg47={} 无AERE (等700ms内收{}帧 {:?})", round + 1, reg47, rx_n, rx_lens); } } } if !claim_ok { println!("[3] 认领失败 (真机只应答源 123.113 的 AECH), 终止"); return; }