From 914cf9343d1b88c0849b4c39da692f4218e178ba Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 01:40:01 +0700 Subject: [PATCH 1/8] fix(build): green library + wave report 2026-07-10 main has not built from a clean clone since 2026-07-07: build.rs had 4 type errors and f608dad regenerated gen/rust/*.rs with a broken t27c (let;, return () in -> u8 fns). Reproduced: cargo build fails, 0 tests runnable at HEAD 6850649. Surgical green fix (lib + 101 tests): - build.rs: replace malformed .map_or(0, Option) with and_then chain - gen/rust/wire.rs: restore from last-green 2b28312 (used by src/wire.rs) - src/lib.rs: unwire 9 generated re-export modules with ZERO call sites, pending t27c emitter fix (PR #44) Verified on branch: cargo build --lib OK; cargo test --lib = 101 passed. Binaries (trios_meshd, smoke_m1) remain stale vs the rewritten src API (documented as P0b, not fixed blind). Full audit, heatmap, science, 4-sprint plan, next-steps and 3 cooperation lanes: docs/WAVE_REPORT_2026-07-10.md Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- build.rs | 12 ++- docs/WAVE_REPORT_2026-07-10.md | 143 +++++++++++++++++++++++++++++++++ gen/rust/wire.rs | 10 +-- src/lib.rs | 48 +++++------ 4 files changed, 177 insertions(+), 36 deletions(-) create mode 100644 docs/WAVE_REPORT_2026-07-10.md diff --git a/build.rs b/build.rs index 4aa37b00..ae35427a 100644 --- a/build.rs +++ b/build.rs @@ -24,10 +24,14 @@ fn main() { let gen_path = gen_dir.join(format!("{}.rs", name.to_str().unwrap())); let needs_regen = !gen_path.exists() || { - let spec_time = entry.metadata().map_or(0, |m| m.modified().ok()) - .map_or(0, |t| t.elapsed().map_or(0, |d| d.as_secs())); - let gen_time = std::fs::metadata(&gen_path).map_or(0, |m| m.modified().ok()) - .map_or(0, |t| t.elapsed().map_or(0, |d| d.as_secs())); + let spec_time = entry.metadata().ok() + .and_then(|m| m.modified().ok()) + .and_then(|t| t.elapsed().ok()) + .map_or(0, |d| d.as_secs()); + let gen_time = std::fs::metadata(&gen_path).ok() + .and_then(|m| m.modified().ok()) + .and_then(|t| t.elapsed().ok()) + .map_or(0, |d| d.as_secs()); spec_time < gen_time // spec is newer }; diff --git a/docs/WAVE_REPORT_2026-07-10.md b/docs/WAVE_REPORT_2026-07-10.md new file mode 100644 index 00000000..7f0f72b9 --- /dev/null +++ b/docs/WAVE_REPORT_2026-07-10.md @@ -0,0 +1,143 @@ +# 🌊 Wave Report 2026-07-10 — «Сборка сломана на main» + карта слабых мест + 3 линии сотрудничества + +Автор волны: агентная разведка (6 параллельных читателей кода/доков/спек + веб), синтез и правки — вручную. +Якорь: `phi^2 + phi^-2 = 3`. + +## 0. Преамбула честности (состояние репозитория проверено) + +- HEAD `main` = `6850649` (2026-07-07 20:03 +07). Рабочее дерево чистое на момент клонирования. +- Заземлённые счётчики (проверено `2026-07-10`): `68` спек `specs/*.t27`, `101` блок `#[test]` (все в `src/`, ноль в удалённой `tests/`), `11` файлов `src/*.rs`. +- Каждое «hw»-утверждение помечено `-sim`/projected, если нет он-девайс лога с `RC=0`/PASS. Единственные hw-классы доказательств: `smoke/M1_RESULTS.md`, `docs/W12_M1_HW_RESULTS.md`, `docs/W12_E2E_RF_TEST_RESULTS.md`, `smoke/M2_STEP2_RESULTS.md`. +- Все P0-находки этой волны воспроизведены руками (`cargo build`/`cargo test` на чистом клоне), а не только заявлены агентом. + +## 1. Три метафоры (для не-инженера) + +- Замок и стены: ворота замка (`cargo build`) сейчас не открываются вообще — их заклинило неделю назад при «уборке». Внутренняя стража (101 тест) жива, но снаружи к ней не пройти, пока ворота сломаны. +- Тепловая карта: два ярко-красных очага (сборка не компилируется + сгенерированный код-мусор) и группа оранжевых в крипто-рукопожатии, которое называет себя «стойким к MITM», а его же тест доказывает обратное. +- Что реально нашли в коде: не «мелкая опечатка». Коммит-«уборщик» перегенерировал 59 файлов сломанным кодогенератором и сломал скрипт сборки — с 2026-07-07 репозиторий не собирается ни у кого с чистого клона. + +## 2. Снимок реальности (hw vs -sim) + +| Что | Статус | Доказательство | +|---|---|---| +| M1 X25519+ChaCha20 на ARM (1 плата) | `hw` PASS | `smoke/M1_RESULTS.md:19-24` (P201Mini, armv7l, 2026-07-01) | +| M1 на 3 платах | `hw` 3/3 PASS | `docs/W12_M1_HW_RESULTS.md:7` (2026-07-07) | +| E2E + RF смоук | `hw` 30/30 PASS | `docs/W12_E2E_RF_TEST_RESULTS.md:9` (2026-07-07) | +| M2 step2, daemon bring-up (1 плата) | `hw` 5/5 | `smoke/M2_STEP2_RESULTS.md:4` (2026-07-07) | +| M2 TUN/IP mesh-over-radio (3 платы) | `-sim` / не в main | живёт в незамёрдженном PR #57 | +| M3 iperf3 через 2 хопа, M4 общий uplink, M5 self-heal | `-sim` | issues #12/#13/#14 открыты, порога сходимости в `main` нет | +| Zynq-7020 Mini PL bitstream / AD9361 OTA / GPS-PPS | `auto=false` | issue #8 открыт (плата PL ни разу не прошита) | + +Важно: hw-доказательства W12 получены на коммите `d479107` (W12, ДО вечерней «уборки» 2026-07-07). На текущем HEAD те же бинарники не собираются (см. P0b) — то есть демон, гонявший железо, сегодня из main не воспроизводится. + +## 3. Тепловая карта слабых мест (только НОВОЕ, не дублирует STRENGTHEN.md / архивные волны) + +| # | Слабое место | Severity | Файлы | Фикс (E-id) | +|---|---|---|---|---| +| N1 | `main` НЕ СОБИРАЕТСЯ: `build.rs:27-30` — 4 ошибки типов (`.map_or(0, Option)`) | P0 | `build.rs` | E1.1 | +| N2 | Сгенерированный `gen/rust/*.rs` невалиден: 59 файлов с `let;`/`return ();` в `-> u8`; 9+wire вшиты в lib → 114 ошибок компиляции библиотеки | P0 | `gen/rust/*`, `src/lib.rs`, `src/wire.rs` | E1.2 | +| N2b | Бинарники `trios_meshd`/`smoke_m1` отстали от переписанного API `src/` (`StaticKey::from_seed`, `Delivery::Local`, `Hello::parse/authenticated`, trait `Transport::send/recv` — которых больше нет) | P0 | `src/bin/*.rs` | E1.3 | +| N3 | «Noise-XX» криптографически несостоятелен: только `ee+ss` (нет `es`/`se`, нет transcript-hash); собственный тест `noise_xx_resistant_to_mitm` утверждает, что атакующий РАСШИФРОВЫВАЕТ трафик | P1 | `crypto.rs:132-161,738-781` | E1.4 | +| N4 | MAC HELLO-биконов на вшитом в исходник константном ключе; `verify_mac()`/`is_fresh()` в демоне НЕ вызываются — аутентификация метрики это мёртвый код | P1 | `discovery.rs:13-16,82`, `trios_meshd.rs:182-188,301` | E2.1 | +| N5 | Демон выводит статический ключ узла из ПУБЛИЧНОГО детерминированного сида (`Sha256("…node/"‖id)`) → тривиальная имперсонация/дешифровка любого узла | P1 | `trios_meshd.rs:37-42,120,132` | E2.2 | +| N6 | ETX-метрика управляема атакующим через неаутентифицированный список `heard` (false-metric/blackhole) — класс известен, но теперь с точной привязкой к мёртвому N4 | P1 | `router.rs`, `discovery.rs` | E2.1 | +| N7 | Gateway FETCH порождает неограниченный `thread::spawn` + исходящий TCP на запрос (amplification DoS); в демоне ноль `rate/limit/throttle/bound` | P2 | `trios_meshd.rs:196-210,46-63` | E3.1 | +| N8 | MAC HELLO проверяется НЕ constant-time сравнением массивов (тайминг-сайд-канал); во всём `src/` нет `subtle/ct_eq` | P2 | `discovery.rs` | E3.2 | +| N9 | Флаг роли initiator не проверяется → при мисконфиге катастрофическое повторное использование nonce/keystream | P2 | `crypto.rs` | E3.3 | +| N10 | Мировой-writable `/tmp/mesh.drop` читается каждые 300 мс — любой локальный юзер валит любую mesh-линию | P2 | `daemon.rs` | E3.4 | +| N11 | CI никогда не компилирует сгенерированный код; нет `cargo-audit`/fuzz/`clippy::pedantic`; `spec-drift-guard` проверяет ТЕКСТ, а не собираемость; lefthook pre-push смотрит exit-код `tail`, а не `cargo` → сломанная сборка прошла gate | P1 | `.github/workflows/*`, `lefthook.yml` | E3.5 | +| N12 | Doc-drift: `MERGE_ORDER.md` описывает мёртвый стек PR #11–#17; `AUTONOMOUS.md` таргетит чужой репо `gHashTag/trios-mesh`; README «Key docs» ссылается на файлы, уехавшие в `docs/archive/`; `spec-drift-guard` диффает удалённые `gen/zig`,`gen/c` | P1 | `docs/*`, README | E4.3 | +| N13 | `SOUL.md` Art. I «доки MUST be English» противоречит 7 русским не-архивным докам в репо | P2 | `docs/*`, `SOUL.md` | E4.3 | + +Первопричина N1/N2 (доказана): timeline 2026-07-07, прямые пуши в `main` за ~25 мин — `2b28312` (19:29, зелёный CI) → `f608dad` (19:47, «L7 all-Rust helpers»: переписал `build.rs` и перегенерировал `gen/` СЛОМАННЫМ t27c) → `b0ef52c` (19:53, удалил `tests/`, 141→101) → `6850649` (docs). Последний зелёный коммит — `2b28312` (проверено: `cargo test` = 141 passed). Эмиттер t27c роняет `let`/cast-выражения (корень — в `gHashTag/t27`, уже частично разобран в PR #44). + +## 4. Наука → рецепты (первоисточники, свежесть 12 мес) + +| Рецепт | Слабое место | Ссылка | +|---|---|---| +| Заменить самодельный «Noise-XX» на верифицируемый Noise-IK X25519+ChaCha20 с transcript-hash; формально проверить Tamarin | N3, N5 | [AutoTam 2026, arXiv:2606.19937](https://arxiv.org/abs/2606.19937); [Lightweight Noise-IK eval 2025, arXiv:2512.10135](https://arxiv.org/abs/2512.10135); Noise Protocol Framework spec | +| Trust-score co-design между рукопожатием и метрикой; защита от вранья узлов о метрике после закрытия handshake | N4, N6 | [Hybrid MAC + multi-layer security 2025, arXiv:2510.10236](https://arxiv.org/abs/2510.10236); [Trusted Routing for UAV via MARL 2025, arXiv:2508.00938](https://arxiv.org/abs/2508.00938) | +| Перейти от реактивного ETX к предиктивной оценке качества линка (mobility-aware) | N6 | [Fuzzy bio-inspired FANET routing 2026, arXiv:2606.26124](https://arxiv.org/abs/2606.26124); [ML cluster-routing UAV 2025, arXiv:2510.27121](https://arxiv.org/abs/2510.27121) | +| Детерминированное воспроизведение RF-заявлений через channel/radio эмуляцию (цифровой двойник) до 3-платных прогонов | Снимок реальности (слабейший класс доказательств) | [ACHEM digital-twin RF 2026, arXiv:2604.04742](https://arxiv.org/abs/2604.04742) | +| Позиционировать PHY-потолок Zynq/AD9361 BPSK против опубликованного 1.2 Gbps FPGA-SDR mesh; наметить OFDM/MIMO-апгрейд | Roadmap PHY | [1.2 Gbps always-on mesh 2026, arXiv:2603.06667](https://arxiv.org/abs/2603.06667) | +| Систематизировать proof-of-coverage для трека Proof-of-FPGA (классы атак: GPS-spoof, витнес-сговор) | Трек аттестации | [Taxonomy of Proof-of-Location 2025, arXiv:2508.14230](https://arxiv.org/abs/2508.14230); [Decentralized PoL 2026, arXiv:2603.27883](https://arxiv.org/abs/2603.27883) | + +Канонические якоря без изменений (проверено, не устарели): Babel RFC 8966, ETX (MobiCom 2003), BFD RFC 5880, Noise Protocol Framework. Оговорка честности: научные строки читались на уровне абстрактов (arXiv ID сверены через API, глубже abstract не читалось). + +## 5. Декомпозированный план (4 спринта, каждая задача с измеримым критерием) + +Sprint 1 — вернуть зелёную сборку (все `auto=true`, Rust-only): +- E1.1 `build.rs:27-30` — заменить кривой `.map_or(0, Option)` на `and_then`-цепочку. Крит.: `cargo build` без ошибок build-скрипта. СДЕЛАНО в этой волне. +- E1.2 регенерировать `gen/rust/*` починенным t27c ИЛИ, как интерим, отвязать 9 мёртвых re-export-модулей (0 call-sites). Крит.: `cargo build --lib` зелёный + 101 тест. Интерим СДЕЛАН (см. §7). +- E1.3 согласовать `trios_meshd.rs`/`smoke_m1.rs` с текущим API `src/` (вернуть `StaticKey::from_seed`/эквивалент, `Delivery`-варианты, `Hello::parse/authenticated`, методы `Transport`). Крит.: `cargo build --bins` зелёный. ОТКРЫТО (P0b). + +Sprint 2 — идентичность + целостность метрики: +- E2.1 в RX-пути демона вызвать `verify_mac`+`is_fresh` перед `observe()`, MAC-ключ выводить из сессии, убрать константный fallback. Крит.: тест, где поддельный `heard` отвергается. +- E2.2 грузить статические секреты из провижн-хранилища (не из NodeId) + AllowList `NodeId→PubKey`. Крит.: тест — узел с чужим static-key не устанавливает сессию. + +Sprint 3 — харденинг: +- E3.1 rate-limit + bound на FETCH/gateway. E3.2 constant-time MAC (`subtle`). E3.3 явная проверка роли. E3.4 убрать `/tmp/mesh.drop` из прод-пути. E3.5 CI: `cargo build` сгенерированного кода + `cargo-audit` + `clippy -D warnings` + починить lefthook (проверять exit `cargo`, не `tail`). Крит.: красный CI на любом из этих регрессов. + +Sprint 4 — паритет верификации: +- E4.1 84-format golden cross-check, E4.2 t27 iverilog, E4.3 привести доки к реальности (MERGE_ORDER, AUTONOMOUS→свой репо, README-ссылки, SOUL Art.I). Крит.: `spec-drift-guard` зелёный и диффает существующие бэкенды. + +## 6. Что дальше логично для TRI-NET (прямой ответ на вопрос) + +Порядок жёстко продиктован зависимостями, а не желаниями: + +1. Немедленно (P0): зелёная сборка на `main`. Пока `cargo build` красный, каждый следующий PR стоит на песке, а «hw-доказано» нельзя воспроизвести. E1.1 сделан; остаётся E1.2 (регенерация правильным t27c) и E1.3 (демон). Это разблокирует всё остальное. +2. Затем закрыть t27c-эмиттер (PR #44) — иначе `gen/` будет ломаться при каждой регенерации; это общий корень с экосистемой t27. +3. Затем — реальная безопасность (Sprint 2): текущее рукопожатие и MAC декоративны (N3–N6). До этого любые «secure mesh» заявления в README нельзя держать без `-sim`. +4. Только потом — новые фичи (trios-chat PR #56, M3/M4/M5 demo-gates). Мерджить их поверх несобираемого main — множить долг. +5. Параллельный трек без кода: воспроизводимость RF через цифровой двойник (arXiv:2604.04742) — снимает самый хрупкий класс доказательств до закупки железа. + +Одной фразой: сначала почини ворота (сборка), потом стражу (крипто), потом достраивай башни (фичи). Порядок обратный — стройка на сломанном фундаменте. + +## 7. Что реализовано прямо в этой волне (ветка feat/wave-report-2026-07-10) + +Цель — не просто отчёт, а зелёная библиотека. Три хирургических правки: +- `build.rs` — 4 ошибки типов E0308/E0599 заменены на корректную `and_then`-цепочку. +- `gen/rust/wire.rs` — восстановлен из последнего зелёного `2b28312` (используется `src/wire.rs`, компилируется чисто; 0 `return ();`). +- `src/lib.rs` — 9 сгенерированных re-export-модулей с НУЛЁМ call-sites временно отвязаны (закомментированы) с пометкой вернуть после фикса t27c/PR #44. + +Проверено на чистом дереве ветки: `cargo build --lib` → Finished; `cargo test --lib` → **101 passed; 0 failed**. Границы честности: бинарники (`trios_meshd`, `smoke_m1`) НЕ чинились — они отстали от API `src/` (P0b, E1.3), это отдельная задача, требующая знания целевого API демона, а не слепой правки. Полная зелёная сборка (`--bins`) остаётся открытой. + +## 8. Три линии сотрудничества для Wave-(N+1) + +Каждая самодостаточна, разблокирована и стартует параллельно разными акторами. + +Линия A — «t27c emitter fix» (компиляторный трек): +- Scope: починить Rust-эмиттер t27c (роняет `let`/cast → `let;`, `()`), закрыть PR #44, регенерировать `gen/rust` в tri-net. +- Actor fit: контрибьютор `gHashTag/t27` с опытом кодогена. +- Deliverable: `t27c gen-rust` даёт компилируемый выход; E1.2 закрывается «по-настоящему», 9 модулей ре-вайрятся. +- DEMO: CI-шаг `cargo build gen/` зелёный на свежем клоне. +- Cite: PR #44 (найденная первопричина E0425/dropped-let). +- Effort: M. Risk: средний (общий корень с экосистемой). + +Линия B — «Noise-IK + Tamarin» (крипто-трек): +- Scope: заменить самодельный ee+ss на Noise-IK с transcript-hash и AllowList; формально проверить рукопожатие. +- Actor fit: прикладной криптограф / формальные методы. +- Deliverable: `crypto.rs` с реальными `es/se`, тест MITM теперь ПАДАЕТ у атакующего; `.spthy`-модель. +- DEMO: Tamarin-прогон + переписанный `noise_xx_resistant_to_mitm`. +- Cite: [arXiv:2606.19937](https://arxiv.org/abs/2606.19937), [arXiv:2512.10135](https://arxiv.org/abs/2512.10135), Noise spec. +- Effort: M-L. Risk: низкий (self-contained). + +Линия C — «RF digital-twin reproducibility» (PHY/evidence-трек): +- Scope: channel/radio-эмуляция, чтобы 2/3-платные RF-заявления воспроизводились детерминированно до железа. +- Actor fit: SDR/DSP-инженер с GNURadio/Zynq-опытом. +- Deliverable: эмулятор-стенд + переигранный W12 E2E как `-sim`-двойник рядом с `hw`-логом. +- DEMO: `docs/RF_TWIN_*.md` с байт-в-байт совпадением эмуляции и M1-фрейма. +- Cite: [arXiv:2604.04742](https://arxiv.org/abs/2604.04742). +- Effort: L. Risk: средний (нужна валидация эмулятора). + +## 9. Границы (что эта волна НЕ делает) + +- Не мержит PR (только draft, human-only per `docs/AUTONOMOUS.md`). +- Не прошивает железо, не заказывает оборудование, не трогает `-hw`-пункты. +- Не переписывает `docs/archive/STRENGTHEN.md` — волна ДОБАВЛЯет датированный отчёт, бэклог курирует владелец. +- Не чинит бинарники и не регенерирует `gen/` правильным t27c (нет доверенного t27c в этой сессии; эмиттер сам баганут — Линия A). +- Не подтверждает `hw` там, где нет он-девайс лога. + +Co-Authored-By: Claude Fable 5 + +`phi^2 + phi^-2 = 3` diff --git a/gen/rust/wire.rs b/gen/rust/wire.rs index 99123c7e..404a8a16 100644 --- a/gen/rust/wire.rs +++ b/gen/rust/wire.rs @@ -15,22 +15,22 @@ pub fn frame_kind_valid(k: u8) -> bool { pub fn be_byte(w: u32, i: usize) -> u8 { if (i == 0) { - return (); + return (((w >> 24) & 255) as u8); } else { if (i == 1) { - return (); + return (((w >> 16) & 255) as u8); } else { if (i == 2) { - return (); + return (((w >> 8) & 255) as u8); } else { - return (); + return ((w & 255) as u8); } } } } pub fn u32_be(b0: u8, b1: u8, b2: u8, b3: u8) -> u32 { - return ((((() << 24) | (() << 16)) | (() << 8)) | ()); + return (((((b0 as u32) << 24) | ((b1 as u32) << 16)) | ((b2 as u32) << 8)) | (b3 as u32)); } pub fn header_byte(kind: u8, src: u32, dst: u32, ttl: u8, idx: usize) -> u8 { diff --git a/src/lib.rs b/src/lib.rs index 600b6c12..308c9a0b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,33 +17,27 @@ pub mod gf16; pub mod daemon; pub mod discovery; -// Re-export generated mesh components -#[path = "../gen/rust/mesh_routing.rs"] -pub mod mesh_routing; - -#[path = "../gen/rust/etx.rs"] -pub mod etx; - -#[path = "../gen/rust/adaptive_routing.rs"] -pub mod adaptive_routing; - -#[path = "../gen/rust/multipath_routing.rs"] -pub mod multipath_routing; - -#[path = "../gen/rust/frame_buffer.rs"] -pub mod frame_buffer; - -#[path = "../gen/rust/flow_control.rs"] -pub mod flow_control; - -#[path = "../gen/rust/health_dashboard.rs"] -pub mod health_dashboard; - -#[path = "../gen/rust/anomaly_detector.rs"] -pub mod anomaly_detector; - -#[path = "../gen/rust/quarantine_manager.rs"] -pub mod quarantine_manager; +// Re-export generated mesh components. +// +// TEMPORARILY UNWIRED (2026-07-10, wave-report branch): the t27c Rust emitter +// regressed at commit f608dad and regenerated these 9 modules with invalid Rust +// (dropped `let`/cast statements -> `let;`, `return ();` in `-> u8` fns), which +// broke `cargo build` on every clean clone since 2026-07-07. All 9 have ZERO +// call sites in src/ or the binaries (verified: `grep -rn '::' src`), so +// they are re-export theater — unwiring them restores a green build + the 101 +// hand-written tests without touching any live datapath. RE-WIRE only after the +// t27c emitter fix (PR #44) lands and `gen/rust/*.rs` are regenerated cleanly. +// Tracking: docs/WAVE_REPORT_2026-07-10.md P0. +// +// #[path = "../gen/rust/mesh_routing.rs"] pub mod mesh_routing; +// #[path = "../gen/rust/etx.rs"] pub mod etx; +// #[path = "../gen/rust/adaptive_routing.rs"] pub mod adaptive_routing; +// #[path = "../gen/rust/multipath_routing.rs"] pub mod multipath_routing; +// #[path = "../gen/rust/frame_buffer.rs"] pub mod frame_buffer; +// #[path = "../gen/rust/flow_control.rs"] pub mod flow_control; +// #[path = "../gen/rust/health_dashboard.rs"] pub mod health_dashboard; +// #[path = "../gen/rust/anomaly_detector.rs"] pub mod anomaly_detector; +// #[path = "../gen/rust/quarantine_manager.rs"] pub mod quarantine_manager; // Types used across the crate pub type NodeId = u32; From acbd2c7a441c7aa6a03b84261bff5f551728e0c8 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 01:47:04 +0700 Subject: [PATCH 2/8] fix(bin): restore M1 smoke harness on main via root re-exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 2 of the 2026-07-10 wave. smoke_m1.rs — the harness whose exact output lines are quoted in docs/W12_M1_HW_RESULTS.md — did not compile against the current lib: it uses Handshake/Node/MeshError, which exist in src/crypto.rs and src/daemon.rs but were never re-exported at the crate root (lib.rs had zero `pub use`). So the flagship M1 hw-evidence harness was unbuildable from main. Fix (purely additive, zero logic change): - lib.rs: `pub use crypto::{Handshake, MeshError, Session}; pub use daemon::Node;` Verified: cargo test --lib = 101 passed; cargo build --bin smoke-m1 OK; `cargo run --bin smoke-m1` prints the full M1 PASS sequence (X25519 + ChaCha20-Poly1305, tamper + replay rejected). Ground-truth on-wire size is 44 -> 83 bytes, resolving the doc-drift discrepancy (44->79 doc is stale). Still open: trios_meshd (13 errors) needs the root shadow stubs removed (Delivery struct vs router::Delivery enum; StaticKey/Hello stubs; two Transport traits with different method sets) — an API-canonicalization call best made together with the Sprint-2 crypto hardening (same file). Refs #58, #59. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- src/lib.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 308c9a0b..f92087e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,15 @@ pub mod discovery; // #[path = "../gen/rust/anomaly_detector.rs"] pub mod anomaly_detector; // #[path = "../gen/rust/quarantine_manager.rs"] pub mod quarantine_manager; +// Re-export the real, hand-written module APIs at the crate root so binaries +// and downstream code resolve `trios_mesh::Handshake` etc. to the actual +// implementations. Without these, the M1 smoke harness (src/bin/smoke_m1.rs) +// and the daemon cannot compile even though every type exists in a submodule. +// (Additive: does NOT collide with the legacy root stubs below, which remain +// tracked for removal — see docs/WAVE_REPORT_2026-07-10.md P0b.) +pub use crypto::{Handshake, MeshError, Session}; +pub use daemon::Node; + // Types used across the crate pub type NodeId = u32; From dd0639d09e837d9f26773c3901c3638441f74146 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 02:09:28 +0700 Subject: [PATCH 3/8] fix(lib): remove dead shadow stubs -> full green build (closes P0b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 3 of the 2026-07-10 wave. Completes the P0 build fix end-to-end. lib.rs held empty root "shadow stubs" (struct Delivery without variants, struct Hello without parse/authenticated, struct StaticKey without from_seed, a second Transport trait with send_to/recv_from) that shadowed the real crypto::/discovery::/router::/daemon:: types. These stubs were dead (zero call sites in src/; router.rs and modem.rs already use daemon::Transport), and they were the sole reason trios_meshd could not compile. Fix: remove the 5 dead stubs, re-export the real types: pub use crypto::{Handshake, MeshError, Session, StaticKey}; pub use daemon::{Node, Transport}; pub use discovery::Hello; pub use router::{Delivery, DropReason, MeshRouter}; Verified on a clean branch tree: cargo build --all-targets -> Finished cargo test -> 101 passed; 0 failed cargo run --bin smoke-m1 -> full M1 PASS sequence Both binaries (trios_meshd, smoke-m1) now build. Correction to the report N2b framing: the types did NOT "no longer exist" — they were shadowed. Report updated (heatmap N2b, E1.3, §7). Refs #58, #59. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- docs/WAVE_REPORT_2026-07-10.md | 6 ++-- src/lib.rs | 55 ++++++++-------------------------- 2 files changed, 16 insertions(+), 45 deletions(-) diff --git a/docs/WAVE_REPORT_2026-07-10.md b/docs/WAVE_REPORT_2026-07-10.md index 7f0f72b9..be2d2d5b 100644 --- a/docs/WAVE_REPORT_2026-07-10.md +++ b/docs/WAVE_REPORT_2026-07-10.md @@ -36,7 +36,7 @@ |---|---|---|---|---| | N1 | `main` НЕ СОБИРАЕТСЯ: `build.rs:27-30` — 4 ошибки типов (`.map_or(0, Option)`) | P0 | `build.rs` | E1.1 | | N2 | Сгенерированный `gen/rust/*.rs` невалиден: 59 файлов с `let;`/`return ();` в `-> u8`; 9+wire вшиты в lib → 114 ошибок компиляции библиотеки | P0 | `gen/rust/*`, `src/lib.rs`, `src/wire.rs` | E1.2 | -| N2b | Бинарники `trios_meshd`/`smoke_m1` отстали от переписанного API `src/` (`StaticKey::from_seed`, `Delivery::Local`, `Hello::parse/authenticated`, trait `Transport::send/recv` — которых больше нет) | P0 | `src/bin/*.rs` | E1.3 | +| N2b | ЗАКРЫТО (итерации 2-3). Уточнение первопричины: типы НЕ исчезли — `lib.rs` держал мёртвые shadow-заглушки (`struct Delivery` без вариантов, `Hello` без parse/authenticated, `StaticKey` без from_seed, второй `trait Transport` с send_to/recv_from), затенявшие реальные `crypto::`/`discovery::`/`router::`/`daemon::` типы. Заглушки убраны, реальные типы реэкспортированы. Итог: `cargo build --all-targets` зелёный, оба бинарника собираются, smoke-m1 печатает M1 PASS | P0 | `src/lib.rs` | E1.3 ✓ | | N3 | «Noise-XX» криптографически несостоятелен: только `ee+ss` (нет `es`/`se`, нет transcript-hash); собственный тест `noise_xx_resistant_to_mitm` утверждает, что атакующий РАСШИФРОВЫВАЕТ трафик | P1 | `crypto.rs:132-161,738-781` | E1.4 | | N4 | MAC HELLO-биконов на вшитом в исходник константном ключе; `verify_mac()`/`is_fresh()` в демоне НЕ вызываются — аутентификация метрики это мёртвый код | P1 | `discovery.rs:13-16,82`, `trios_meshd.rs:182-188,301` | E2.1 | | N5 | Демон выводит статический ключ узла из ПУБЛИЧНОГО детерминированного сида (`Sha256("…node/"‖id)`) → тривиальная имперсонация/дешифровка любого узла | P1 | `trios_meshd.rs:37-42,120,132` | E2.2 | @@ -69,7 +69,7 @@ Sprint 1 — вернуть зелёную сборку (все `auto=true`, Rust-only): - E1.1 `build.rs:27-30` — заменить кривой `.map_or(0, Option)` на `and_then`-цепочку. Крит.: `cargo build` без ошибок build-скрипта. СДЕЛАНО в этой волне. - E1.2 регенерировать `gen/rust/*` починенным t27c ИЛИ, как интерим, отвязать 9 мёртвых re-export-модулей (0 call-sites). Крит.: `cargo build --lib` зелёный + 101 тест. Интерим СДЕЛАН (см. §7). -- E1.3 согласовать `trios_meshd.rs`/`smoke_m1.rs` с текущим API `src/` (вернуть `StaticKey::from_seed`/эквивалент, `Delivery`-варианты, `Hello::parse/authenticated`, методы `Transport`). Крит.: `cargo build --bins` зелёный. ОТКРЫТО (P0b). +- E1.3 согласовать бинарники с API `src/`. Крит.: `cargo build --bins` зелёный. СДЕЛАНО (итерации 2-3): реальные типы не исчезали — их затеняли мёртвые заглушки в `lib.rs`; заглушки убраны, типы реэкспортированы. `cargo build --all-targets` зелёный. Sprint 2 — идентичность + целостность метрики: - E2.1 в RX-пути демона вызвать `verify_mac`+`is_fresh` перед `observe()`, MAC-ключ выводить из сессии, убрать константный fallback. Крит.: тест, где поддельный `heard` отвергается. @@ -100,7 +100,7 @@ Sprint 4 — паритет верификации: - `gen/rust/wire.rs` — восстановлен из последнего зелёного `2b28312` (используется `src/wire.rs`, компилируется чисто; 0 `return ();`). - `src/lib.rs` — 9 сгенерированных re-export-модулей с НУЛЁМ call-sites временно отвязаны (закомментированы) с пометкой вернуть после фикса t27c/PR #44. -Проверено на чистом дереве ветки: `cargo build --lib` → Finished; `cargo test --lib` → **101 passed; 0 failed**. Границы честности: бинарники (`trios_meshd`, `smoke_m1`) НЕ чинились — они отстали от API `src/` (P0b, E1.3), это отдельная задача, требующая знания целевого API демона, а не слепой правки. Полная зелёная сборка (`--bins`) остаётся открытой. +Проверено на чистом дереве ветки. Итерация 1: `cargo build --lib` Finished; `cargo test --lib` = 101 passed. Итерация 2: восстановлен M1-харнесс `smoke_m1` (реэкспорт `Handshake`/`MeshError`/`Node`), `cargo run --bin smoke-m1` печатает полный M1 PASS; наземная on-wire = 44→83 байта (разрешает doc-drift 44→79). Итерация 3: убраны мёртвые shadow-заглушки в `lib.rs`, реэкспортированы реальные `crypto`/`discovery`/`router`/`daemon` типы. ИТОГ: **`cargo build --all-targets` зелёный, `cargo test` = 101 passed, оба бинарника собираются**. P0 сборки закрыт end-to-end на ветке. Остаётся глубже: реальная регенерация `gen/` починенным t27c (Линия A / PR #44) — интерим-отвязка 9 модулей держит сборку, но `gen/` всё ещё pipeline-театр без call-sites. ## 8. Три линии сотрудничества для Wave-(N+1) diff --git a/src/lib.rs b/src/lib.rs index f92087e1..75e15238 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,48 +40,19 @@ pub mod discovery; // #[path = "../gen/rust/quarantine_manager.rs"] pub mod quarantine_manager; // Re-export the real, hand-written module APIs at the crate root so binaries -// and downstream code resolve `trios_mesh::Handshake` etc. to the actual -// implementations. Without these, the M1 smoke harness (src/bin/smoke_m1.rs) -// and the daemon cannot compile even though every type exists in a submodule. -// (Additive: does NOT collide with the legacy root stubs below, which remain -// tracked for removal — see docs/WAVE_REPORT_2026-07-10.md P0b.) -pub use crypto::{Handshake, MeshError, Session}; -pub use daemon::Node; +// and downstream code resolve `trios_mesh::Delivery` etc. to the actual +// implementations. Every type below exists in a submodule; the crate root only +// re-exports. Earlier a set of empty root "shadow stubs" (a struct Delivery +// with no variants, a struct Hello with no parse/authenticated, a struct +// StaticKey with no from_seed, and a second Transport trait with send_to/ +// recv_from) shadowed these real types and made the binaries and the M1 smoke +// harness uncompilable even though the real code was fine. Those stubs were dead +// (zero call sites in src/; router.rs and modem.rs already use daemon::Transport) +// and are removed. See docs/WAVE_REPORT_2026-07-10.md P0b. +pub use crypto::{Handshake, MeshError, Session, StaticKey}; +pub use daemon::{Node, Transport}; +pub use discovery::Hello; +pub use router::{Delivery, DropReason, MeshRouter}; // Types used across the crate pub type NodeId = u32; - -/// Delivery confirmation for mesh forwarding. -#[derive(Debug, Clone)] -pub struct Delivery { - pub src: NodeId, - pub dst: NodeId, - pub hops: u8, -} - -/// Hello beacon payload. -#[derive(Debug, Clone)] -pub struct Hello { - pub src: NodeId, - pub seq: u32, - pub neighbors: Vec<(NodeId, u8)>, -} - -/// Static key type for pre-shared-key mesh. -pub struct StaticKey { - pub secret: [u8; 32], -} - -/// Transport abstraction (UDP now, radio later). -pub trait Transport: Send { - fn send_to(&self, data: &[u8], dst: std::net::SocketAddr) -> std::io::Result<()>; - fn recv_from(&self, buf: &mut [u8]) -> std::io::Result<(usize, std::net::SocketAddr)>; -} - -/// Mesh router trait. -pub trait MeshRouter: Send { - fn add_neighbor(&mut self, id: NodeId, addr: std::net::SocketAddr, etx: u8); - fn remove_neighbor(&mut self, id: NodeId); - fn next_hop(&self, dst: NodeId) -> Option<(NodeId, std::net::SocketAddr)>; - fn neighbors(&self) -> Vec<(NodeId, std::net::SocketAddr)>; -} From d9dd055fc556646aa6de964dafa7beaf73745441 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 02:39:16 +0700 Subject: [PATCH 4/8] docs(readme): fix drifted counts and broken doc links (N12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 4 of the 2026-07-10 wave — light doc-drift cleanup, facts only. Corrected against ground truth (verified 2026-07-10): - #[test] blocks 110 -> 101 (tests/ dir removed 2026-07-07); command src tests -> src - Rust source lines 4 463 -> 4 544 - "T27 ported: 1" clarified to "68 in tree / 1 wired into src" (the cited command returns 68, contradicting the 1) - build snippet comment 110 -> 101, все в src/ Relinked moved files (now under docs/archive/): STRENGTHEN.md, WAVE_N3_AUDITABILITY_GAP, W7_WEAK_POINTS_STRUCTURAL. De-linked files not on main (live in unmerged PR #29): WAVE_DEPIN, _recon/DEPIN_COMPETITORS — annotated as such. Added a note that radio/README.md was deleted 2026-07-07; current reproducible RF evidence is docs/W12_E2E_RF_TEST_RESULTS.md (30/30). No broken relative links remain in README. No code touched. Refs #58, #59. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 81c6331e..7d75e4c2 100644 --- a/README.md +++ b/README.md @@ -85,16 +85,18 @@ BitNet-ternary benchmark on returned silicon, publish the raw log. | M1 static binary size (armv7l musleabihf) | 534 604 B | `smoke/M1_RESULTS.md` | | M1 binary sha256 | `e5abc335…7290a` | `smoke/M1_RESULTS.md` | | M1 host tests | 20 unit + 2 integration, RC=0 | `cargo test` | -| Rust `#[test]` blocks in repo | 110 | `grep -rE '^\s*#\[test\]' src tests` | -| Rust source lines | 4 463 | `find src -name '*.rs' \| xargs wc -l` | +| Rust `#[test]` blocks in repo | 101 (all in `src/`; `tests/` dir removed 2026-07-07) | `grep -rE '^\s*#\[test\]' src` | +| Rust source lines | 4 544 | `find src -name '*.rs' \| xargs wc -l` | | AD9361 tune target | LO 5.8 GHz | `radio/README.md` | | AD9361 FFT peak (1 MHz tone, digital loopback) | +0.999 MHz | `radio/README.md` | -| AD9361 SNR over noise floor | 108.6 dB (digital loopback only, not over-the-air) | `radio/README.md`; see [W7 finding #5](docs/W7_WEAK_POINTS_STRUCTURAL.md#находка-5) and [REGULATORY_STATUS](docs/REGULATORY_STATUS.md) | +| AD9361 SNR over noise floor | 108.6 dB (digital loopback only, not over-the-air) | `radio/README.md`; see [W7 finding #5](docs/archive/W7_WEAK_POINTS_STRUCTURAL.md#находка-5) and [REGULATORY_STATUS](docs/REGULATORY_STATUS.md) | | AD9361 tuning range | 70 MHz … 6 GHz | `radio/README.md` | | Sample rate | 30.72 MHz | `radio/README.md` | | Capture length | 65 536 samples | `radio/README.md` | + +> Примечание (2026-07-10): файл `radio/README.md` удалён при уборке 2026-07-07; строки AD9361 выше — исторический digital-loopback снимок. Текущее воспроизводимое RF-доказательство: [`docs/W12_E2E_RF_TEST_RESULTS.md`](docs/W12_E2E_RF_TEST_RESULTS.md) (30/30 PASS). Не over-the-air. | Connected P203 Mini boards | 3 | User confirmation 2026-07-04 | -| T27 spec files ported | 1 (`specs/wire.t27`) | `find specs -name '*.t27'` | +| T27 specs in tree / wired into src | 68 / 1 (`specs/wire.t27`) | `find specs -name '*.t27' \| wc -l` | ### DePIN tokenomics (contract source, `gHashTag/trinity-contracts`, not yet deployed to mainnet) @@ -134,7 +136,7 @@ BitNet-ternary benchmark on returned silicon, publish the raw log. ## Build & test (host) ```bash -cargo test # 20+ unit + 2 integration tests (см. Metrics — 110 test blocks в проекте) +cargo test # 101 #[test] blocks, все в src/ (см. Metrics) cargo run --bin smoke-m1 ``` @@ -223,11 +225,11 @@ Sister-репозитории: [`gHashTag/t27`](https://github.com/gHashTag/t27) ## Key docs - [`docs/LOCAL_FLASH.md`](docs/LOCAL_FLASH.md) — пошаговая локальная прошивка трёх плат. -- [`docs/WAVE_DEPIN_2026-07-04.md`](docs/WAVE_DEPIN_2026-07-04.md) — DePIN whitepaper (четыре плеча, tokenomics, positioning). +- `docs/WAVE_DEPIN_2026-07-04.md` — DePIN whitepaper (в неслитом PR #29, не на `main`). - `docs/COMPETITOR_MATRIX_2026-07-04.md` — 10 MANET-конкурентов × 15 полей (в [PR #28](https://github.com/gHashTag/tri-net/pull/28)). -- [`docs/_recon/DEPIN_COMPETITORS_2026-07-04.md`](docs/_recon/DEPIN_COMPETITORS_2026-07-04.md) — 12 DePIN-сетей × 12 полей. -- [`docs/WAVE_N3_AUDITABILITY_GAP_2026-07-04.md`](docs/WAVE_N3_AUDITABILITY_GAP_2026-07-04.md) — auditability δ paper. -- [`docs/STRENGTHEN.md`](docs/STRENGTHEN.md) — science-driven backlog. +- `docs/_recon/DEPIN_COMPETITORS_2026-07-04.md` — 12 DePIN-сетей × 12 полей (в неслитом PR #29, не на `main`). +- [`docs/archive/WAVE_N3_AUDITABILITY_GAP_2026-07-04.md`](docs/archive/WAVE_N3_AUDITABILITY_GAP_2026-07-04.md) — auditability δ paper. +- [`docs/archive/STRENGTHEN.md`](docs/archive/STRENGTHEN.md) — science-driven backlog. - [`docs/AUTONOMOUS.md`](docs/AUTONOMOUS.md) — human-merge only policy для agent PR's. ## License From a8b750ac1c40c0bbadb02d29854c095cf6be6fb5 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 03:09:29 +0700 Subject: [PATCH 5/8] ci: fix lefthook build-gate pipefail bug + add cargo-audit (N11) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 5 of the 2026-07-10 wave. CI-integrity, mechanical, no design. 1. lefthook pre-push `cargo-build` was a no-op gate: `cargo build | tail -3` makes `$?` the exit code of `tail` (always 0), so a failed build passed the pre-push check. This is precisely how the broken 2026-07-07 build was pushed to main. Fixed to check cargo's exit directly (`if ! cargo build`). 2. Added a `cargo-audit` job (rustsec/audit-check@v2) so dependency advisories gate CI. Separate job — surfaces advisories without blocking build/test. Accuracy note (anti-anchor): ci.yml already ran fmt + clippy -D warnings + build + test, so the recon's "CI has no gates" was imprecise; the real hole was the always-passing lefthook gate + no audit + no branch protection on main (a GitHub setting, not a repo file — flagged for the owner). Not touched: spec-drift-guard.yml still diffs deleted gen/zig + gen/c; that guard is entangled with the t27c emitter fix (Lane A / PR #44) and should be reduced to the rust backend only once t27c regenerates cleanly. Refs #58, #59. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- .github/workflows/ci.yml | 9 +++++++++ lefthook.yml | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2f30b62..75dd871c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,3 +17,12 @@ jobs: - run: cargo clippy --all-targets -- -D warnings - run: cargo build --verbose - run: cargo test --verbose + + audit: + name: cargo-audit (dependency advisories) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/lefthook.yml b/lefthook.yml index 33b66e0c..90b8a200 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -81,8 +81,10 @@ pre-push: cargo-build: run: | - cargo build --release 2>&1 | tail -3 - if [ $? -ne 0 ]; then + # NOTE: `cargo build | tail` makes `$?` the exit code of `tail` (always + # 0), so the old form passed even on a failed build — this is how the + # broken 2026-07-07 build reached main. Check cargo's exit directly. + if ! cargo build --release; then echo "cargo build FAILED" exit 1 fi From 4eedd58205acb733af26947bd6affbaeb8dcb6f3 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 03:39:08 +0700 Subject: [PATCH 6/8] fix(meshd): gate M5 sim-drop hook behind TRIOS_SIM_DROPS (N10) Iteration 6 of the 2026-07-10 wave. Local-DoS hardening, bounded, no crypto. The daemon read a hardcoded world-writable /tmp/mesh.drop on every tick and treated any node id found there as link-failed. On a shared host, any local user could write ids into that file and drop arbitrary mesh links on a running node. Fix (matches the existing TRIOS_WATCH / TRIOS_GATEWAY env-var idiom): the M5 self-heal demo hook is now opt-in via TRIOS_SIM_DROPS, which names an operator-chosen file. Unset (default) => no file is read, drop set is empty. The demo still works: set TRIOS_SIM_DROPS=/path/you/control. Verified: cargo build --all-targets OK; cargo test = 101 passed; no hardcoded /tmp/mesh.drop path remains in code. Refs #58, #59. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- src/bin/trios_meshd.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/bin/trios_meshd.rs b/src/bin/trios_meshd.rs index 879dc58b..2271bb84 100644 --- a/src/bin/trios_meshd.rs +++ b/src/bin/trios_meshd.rs @@ -144,8 +144,13 @@ fn main() { } let router = Arc::new(Mutex::new(router)); let rx = Arc::new(Mutex::new(RxShared::default())); - // Peers whose link is simulated-failed (ids in /tmp/mesh.drop) — for M5 demo. + // Peers whose link is simulated-failed — for the M5 self-heal demo. let dropped: Arc>> = Arc::new(Mutex::new(HashSet::new())); + // M5 demo hook, OFF by default. When TRIOS_SIM_DROPS names a file, node ids + // listed in it are treated as link-failed. Opt-in + operator-chosen path: the + // previous build read a hardcoded world-writable /tmp/mesh.drop every tick, so + // any local user could drop any mesh link on a running node (local DoS). + let sim_drops_path: Option = std::env::var("TRIOS_SIM_DROPS").ok(); let watch: Option = std::env::var("TRIOS_WATCH") .ok() .and_then(|s| s.parse().ok()); @@ -270,9 +275,11 @@ fn main() { v.sort(); v }; - // Refresh the simulated link-failure set from /tmp/mesh.drop (M5 control). - let dset: HashSet = std::fs::read_to_string("/tmp/mesh.drop") - .ok() + // Refresh the simulated link-failure set (M5 control; opt-in via + // TRIOS_SIM_DROPS, off by default — no file is read when unset). + let dset: HashSet = sim_drops_path + .as_deref() + .and_then(|p| std::fs::read_to_string(p).ok()) .map(|s| { s.split_whitespace() .filter_map(|x| x.parse().ok()) From 1d2446de88a22587f8135052f61b4485ecc03e2e Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 04:08:50 +0700 Subject: [PATCH 7/8] fix(meshd): bound gateway FETCH concurrency + connect timeout (N7) Iteration 7 of the 2026-07-10 wave. Amplification-DoS hardening, bounded, no crypto, no new deps. The M4 gateway spawned a thread + an outbound TCP connect for every FETCH_REQ with no bound. A flood of requests (or a Sybil) made the gateway spawn unbounded threads and outbound connections. Fix: - MAX_INFLIGHT_FETCHES = 4, tracked with an AtomicUsize; a request over the cap is refused (logged) instead of spawning. - fetch_public_ip now uses connect_timeout (4s) so a hung/unreachable host cannot pin a fetch slot indefinitely and defeat the cap (it already had a 6s read timeout). Verified: cargo build --all-targets OK; cargo test = 101 passed. Refs #58, #59. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- src/bin/trios_meshd.rs | 47 +++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/bin/trios_meshd.rs b/src/bin/trios_meshd.rs index 2271bb84..61c85bf4 100644 --- a/src/bin/trios_meshd.rs +++ b/src/bin/trios_meshd.rs @@ -19,6 +19,7 @@ use sha2::{Digest, Sha256}; use std::collections::{HashMap, HashSet}; use std::io; use std::net::{SocketAddr, UdpSocket}; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; @@ -32,6 +33,9 @@ const HELLO_TYPE: u8 = 0; const DATA_TYPE: u8 = 1; const FETCH_REQ: u8 = 2; // "fetch the internet for me" (M4 shared uplink) const FETCH_RESP: u8 = 3; // gateway's reply carrying the fetched bytes +/// Max concurrent gateway internet-fetches. Caps the amplification a flood of +/// FETCH_REQ can cause (each spawns a thread + an outbound TCP connect). +const MAX_INFLIGHT_FETCHES: usize = 4; /// Deterministic demo static key from a node id. fn seed_for(id: NodeId) -> [u8; 32] { @@ -45,7 +49,14 @@ fn seed_for(id: NodeId) -> [u8; 32] { /// the node that actually has an uplink; the result travels back over the mesh. fn fetch_public_ip() -> String { use std::io::{Read, Write}; - match std::net::TcpStream::connect("api.ipify.org:80") { + use std::net::ToSocketAddrs; + // Bound connect time too: without a timeout a hung/unreachable host would + // pin a fetch slot indefinitely and defeat MAX_INFLIGHT_FETCHES. + let addr = match "api.ipify.org:80".to_socket_addrs().ok().and_then(|mut a| a.next()) { + Some(a) => a, + None => return "ERR: resolve".to_string(), + }; + match std::net::TcpStream::connect_timeout(&addr, Duration::from_secs(4)) { Ok(mut s) => { let _ = s.set_read_timeout(Some(Duration::from_secs(6))); let _ = @@ -156,6 +167,9 @@ fn main() { .and_then(|s| s.parse().ok()); // M4: this node has a real internet uplink and serves FETCH requests. let gateway = std::env::var("TRIOS_GATEWAY").is_ok(); + // N7: bound concurrent gateway fetches so a FETCH_REQ flood cannot spawn + // unbounded threads + outbound TCP connections (amplification DoS). + let inflight_fetches = Arc::new(AtomicUsize::new(0)); let started = Instant::now(); println!("[meshd] node {me} on {} — peers {peer_ids:?}", cfg.listen); @@ -202,16 +216,29 @@ fn main() { if p.first() == Some(&FETCH_REQ) && gateway && p.len() >= 5 => { let origin = u32::from_le_bytes([p[1], p[2], p[3], p[4]]); - let router = router.clone(); - thread::spawn(move || { - let ip = fetch_public_ip(); - let mut resp = vec![FETCH_RESP]; - resp.extend_from_slice(ip.as_bytes()); - let d = router.lock().unwrap().send_ip(origin, &resp); - println!( - "[meshd] gateway fetched \"{ip}\" -> reply to {origin}: {d:?}" + // N7: refuse the fetch if we are already at the concurrency + // cap, instead of spawning without bound. + if inflight_fetches.fetch_add(1, Ordering::SeqCst) + >= MAX_INFLIGHT_FETCHES + { + inflight_fetches.fetch_sub(1, Ordering::SeqCst); + eprintln!( + "[meshd] FETCH from {origin} dropped: {MAX_INFLIGHT_FETCHES} already in flight" ); - }); + } else { + let router = router.clone(); + let inflight = inflight_fetches.clone(); + thread::spawn(move || { + let ip = fetch_public_ip(); + let mut resp = vec![FETCH_RESP]; + resp.extend_from_slice(ip.as_bytes()); + let d = router.lock().unwrap().send_ip(origin, &resp); + println!( + "[meshd] gateway fetched \"{ip}\" -> reply to {origin}: {d:?}" + ); + inflight.fetch_sub(1, Ordering::SeqCst); + }); + } } // M4: the gateway's reply — internet reached us over the mesh. Delivery::Local(p) if p.first() == Some(&FETCH_RESP) => { From 87b8e6dff5ad892ab2c1f1e550621366c51f26e0 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Fri, 10 Jul 2026 05:56:52 +0700 Subject: [PATCH 8/8] =?UTF-8?q?fix(router):=20loop=20prevention=20?= =?UTF-8?q?=E2=80=94=20split-horizon=20+=20self-origin=20drop=20(N-new-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iteration 8 of the 2026-07-10 wave. Fresh audit of the previously-unexamined routing module found a real P1 forwarding bug. handle_frame selected the next hop with no exclusion of the neighbor the frame arrived from, and no guard against a frame this node originated looping back. With a node pair (A—C) whose only route toward the destination is via each other, a packet ping-pongs A->C->A->C... until TTL is burned, making zero forward progress — TTL was the only backstop. Fix: - drop a frame whose end-to-end src == self.id (looped back to origin) - split-horizon: if the chosen next hop == `from`, drop instead of bouncing - new DropReason::Loop for both (no exhaustive DropReason matches exist, so the added variant is safe) Tests: +2 regression tests (frame_looped_back_to_origin_is_dropped, split_horizon_no_bounce_back_to_sender). Suite 101 -> 103 passed; cargo build --all-targets green. Noted but NOT changed (semantically debatable, would alter max reach by one hop): TTL is checked-then-decremented, so a fully-travelled packet spends its last transmission on a ttl==0 frame the next hop always discards. Left for a decision by the routing owner. Refs #58, #59. Co-Authored-By: Claude Fable 5 phi^2 + phi^-2 = 3 --- src/router.rs | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/router.rs b/src/router.rs index c04b014c..8b8f7741 100644 --- a/src/router.rs +++ b/src/router.rs @@ -52,6 +52,9 @@ pub enum DropReason { SealFailed(MeshError), /// E3.2 — Frame header.src != actual link peer (spoof attempt). SrcSpoof, + /// Loop prevention: the frame was originated by this node (looped back), or + /// its only next hop is the neighbor it just arrived from (split-horizon). + Loop, } /// Outcome of handling one frame. @@ -408,11 +411,21 @@ impl MeshRouter { if hdr.dst == self.id { return Delivery::Local(payload); } + // Loop prevention (TTL is only a backstop). Never re-forward a frame this + // node originated (looped back to us), and never bounce a frame straight + // back to the neighbor it arrived from (split-horizon). Without these, a + // pair of nodes whose only route toward the dst is via each other + // ping-pongs the packet until the hop budget is burned, making zero + // forward progress. + if hdr.src == self.id { + return Delivery::Dropped(DropReason::Loop); + } if hdr.ttl == 0 { return Delivery::Dropped(DropReason::TtlExpired); } let nh = match self.next_hop(hdr.dst) { - Some(n) => n, + Some(n) if n != from => n, + Some(_) => return Delivery::Dropped(DropReason::Loop), None => return Delivery::Dropped(DropReason::NoRoute), }; // Re-seal end-to-end payload under the outgoing link, TTL-1. @@ -576,6 +589,58 @@ mod tests { ); } + #[test] + fn frame_looped_back_to_origin_is_dropped() { + // A frame whose end-to-end src is THIS node has looped back to its + // origin; it must be dropped, never re-forwarded. + let (sa, sb) = sessions(); // A(1) <-> C(3) + let mut a = MeshRouter::new(1, 16); + let mut c = MeshRouter::new(3, 16); + a.add_link(3, sa, Box::new(VecTransport::default())); + c.add_link(1, sb, Box::new(VecTransport::default())); + + // src = 3 (C's own id), dst = B(2), sealed under the A/C link. + let hdr = Header::new(FrameKind::Data, 3, 2, 8).to_bytes(); + let body = { + let link = a.links.get_mut(&3).unwrap(); + link.session.seal(&hdr, b"x").unwrap() + }; + let mut frame = hdr.to_vec(); + frame.extend_from_slice(&body); + assert_eq!(c.handle_frame(1, &frame), Delivery::Dropped(DropReason::Loop)); + } + + #[test] + fn split_horizon_no_bounce_back_to_sender() { + // C's only route toward B(2) resolves back to A(1), the neighbor the + // frame arrived from. Without split-horizon C would re-send it to A and + // the pair would ping-pong until TTL is burned; instead C drops it. + let (sa, sb) = sessions(); // A(1) <-> C(3) + let mut a = MeshRouter::new(1, 16); + let mut c = MeshRouter::new(3, 16); + a.add_link(3, sa, Box::new(VecTransport::default())); + c.add_link(1, sb, Box::new(VecTransport::default())); + + // C only knows neighbor A(1), so its next hop toward any unknown dst is 1. + for _ in 0..4 { + c.observe(1, true, true); + } + assert_eq!(c.next_hop(2), Some(1), "C's only next hop toward B is A"); + + let hdr = Header::new(FrameKind::Data, 1, 2, 8).to_bytes(); + let body = { + let link = a.links.get_mut(&3).unwrap(); + link.session.seal(&hdr, b"x").unwrap() + }; + let mut frame = hdr.to_vec(); + frame.extend_from_slice(&body); + assert_eq!( + c.handle_frame(1, &frame), + Delivery::Dropped(DropReason::Loop), + "split-horizon: never bounce a frame back to the sender" + ); + } + #[test] fn no_route_is_dropped() { let mut a = MeshRouter::new(1, 16);