From a308e71ca11afa603b1c294be3e933c89deff638 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 14 Jun 2026 02:06:50 +0000 Subject: [PATCH 1/6] Refresh in-semver dependencies for the v0.4.0 release Pull the available point releases that sit within the current version constraints: tun 0.8.11, tokio-socks 0.5.3, socket2 0.6.4, nostr 0.44.3, nostr-relay-pool 0.44.1, ratatui 0.30.1, serde_json 1.0.150, simple-dns 0.11.3, mdns-sd 0.19.2, plus the transitive maintenance crowd. Lockfile only; no Cargo.toml constraint changes. Defer the out-of-semver crypto and identity majors (secp256k1 0.31, sha2 0.11 with hkdf 0.13, bech32 0.12) and the mdns-sd 0.20 bump to a separate coordinated refresh with its own handshake, identity, and LAN validation. --- Cargo.lock | 422 +++++++++++++++++++++++++++++------------------------ 1 file changed, 228 insertions(+), 194 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2524ecc..3afc519 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,6 +98,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + [[package]] name = "arc-swap" version = "1.9.1" @@ -185,9 +194,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -213,7 +222,7 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -223,7 +232,7 @@ dependencies = [ "quote", "regex", "rustc-hash", - "shlex", + "shlex 1.3.0", "syn 2.0.117", ] @@ -255,15 +264,15 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitcoin-io" -version = "0.1.4" +version = "0.1.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +checksum = "11301df0b06f22dea7bb1916403fdd88a371031e495c49b8f96931b28189e175" [[package]] name = "bitcoin_hashes" -version = "0.14.1" +version = "0.14.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" dependencies = [ "bitcoin-io", "hex-conservative", @@ -278,9 +287,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -344,9 +353,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" @@ -366,16 +381,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b43c3f07ab0ef604fa6f595aa46ec2f8a22172c975e186f6f5bf9829a3b72c41" dependencies = [ - "c2rust-bitfields-derive 0.18.0", -] - -[[package]] -name = "c2rust-bitfields" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcee50917f9de1a018e3f4f9a8f2ff3d030a288cffa4b18d9b391e97c12e4cfb" -dependencies = [ - "c2rust-bitfields-derive 0.21.0", + "c2rust-bitfields-derive", ] [[package]] @@ -389,17 +395,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "c2rust-bitfields-derive" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b457277798202ccd365b9c112ebee08ddd57f1033916c8b8ea52f222e5b715d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "cast" version = "0.3.0" @@ -426,12 +421,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.62" +version = "1.2.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" dependencies = [ "find-msvc-tools", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -587,9 +582,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -670,6 +665,12 @@ dependencies = [ "itertools 0.13.0", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -710,7 +711,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "crossterm_winapi", "derive_more", "document-features", @@ -899,9 +900,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "derive_more" @@ -959,9 +957,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -979,9 +977,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "equivalent" @@ -1039,6 +1037,12 @@ dependencies = [ "regex", ] +[[package]] +name = "fast-srgb8" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" + [[package]] name = "fastrand" version = "2.4.1" @@ -1353,9 +1357,14 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -1398,9 +1407,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1544,7 +1553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -1637,13 +1646,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -1712,10 +1720,16 @@ dependencies = [ ] [[package]] -name = "libredox" -version = "0.1.16" +name = "libm" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -1726,7 +1740,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -1758,17 +1772,23 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" [[package]] name = "lru" version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" + +[[package]] +name = "lru" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" dependencies = [ - "hashbrown 0.16.1", + "hashbrown 0.17.1", ] [[package]] @@ -1798,9 +1818,9 @@ dependencies = [ [[package]] name = "mdns-sd" -version = "0.19.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2bb8ce26633738d98ffcef71ec58bff967c6675be50229823c2835f6316e67e" +checksum = "18148fee27e99e76dbf6e137f27727113d31f766e578d1b93a93c3615fca7081" dependencies = [ "fastrand", "flume", @@ -1813,9 +1833,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmem" @@ -1840,9 +1860,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "log", @@ -1871,7 +1891,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be8919612f6028ab4eacbbfe1234a9a43e3722c6e0915e7ff519066991905092" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "log", "netlink-packet-core", @@ -1910,7 +1930,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -1923,7 +1943,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -1932,11 +1952,11 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -1954,9 +1974,9 @@ dependencies = [ [[package]] name = "nostr" -version = "0.44.2" +version = "0.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa5e3b6a278ed061835fe1ee293b71641e6bf8b401cfe4e1834bbf4ef0a34e1" +checksum = "08d8f0fe13526800300a36bf3b7c5f752e62e32ab81c74a8e5caa2865708625a" dependencies = [ "base64", "bech32", @@ -1982,7 +2002,7 @@ version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7462c9d8ae5ef6a28d66a192d399ad2530f1f2130b13186296dbb11bdef5b3d1" dependencies = [ - "lru", + "lru 0.16.4", "nostr", "tokio", ] @@ -1998,15 +2018,15 @@ dependencies = [ [[package]] name = "nostr-relay-pool" -version = "0.44.0" +version = "0.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b1073ccfbaea5549fb914a9d52c68dab2aecda61535e5143dd73e95445a804b" +checksum = "91b2c039df4f96c4bf7dae52a74fd5516ad6dda83a11c0c69dea91b5255a4f37" dependencies = [ "async-utility", "async-wsocket", "atomic-destructor", "hex", - "lru", + "lru 0.16.4", "negentropy", "nostr", "nostr-database", @@ -2040,9 +2060,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -2122,6 +2142,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "palette" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" +dependencies = [ + "approx", + "fast-srgb8", + "libm", + "palette_derive", +] + +[[package]] +name = "palette_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" +dependencies = [ + "by_address", + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "parking" version = "2.2.1" @@ -2281,18 +2325,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf0d9e68100b3a7989b4901972f265cd542e560a3a8a724e1e20322f4d06ce9" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a990e22f43e84855daf260dded30524ef4a9021cc7541c26540500a50b624389" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", @@ -2429,7 +2473,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "procfs-core", "rustix", ] @@ -2440,7 +2484,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "hex", ] @@ -2543,9 +2587,9 @@ checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "ratatui" -version = "0.30.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +checksum = "1695748e3a735b34968c887ceea5a380b43545903868ae8f5b666593100f6b68" dependencies = [ "instability", "ratatui-core", @@ -2553,22 +2597,26 @@ dependencies = [ "ratatui-macros", "ratatui-termwiz", "ratatui-widgets", + "serde", ] [[package]] name = "ratatui-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +checksum = "42d3603f354bba8c595fa47860e60142d7372b7210c27044c6a7d0e1a4336b44" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "compact_str", - "hashbrown 0.16.1", + "critical-section", + "hashbrown 0.17.1", "indoc", "itertools 0.14.0", "kasuari", - "lru", - "strum 0.27.2", + "lru 0.18.0", + "palette", + "serde", + "strum 0.28.0", "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", @@ -2577,9 +2625,9 @@ dependencies = [ [[package]] name = "ratatui-crossterm" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +checksum = "2b2867bedcbd6a690ca4f8672a687b730ec07660c79844517b084311b529980c" dependencies = [ "cfg-if", "crossterm", @@ -2589,9 +2637,9 @@ dependencies = [ [[package]] name = "ratatui-macros" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +checksum = "80fac59720679490d89d200df411faa249be728681adcabed3d047ae72c48f1d" dependencies = [ "ratatui-core", "ratatui-widgets", @@ -2599,9 +2647,9 @@ dependencies = [ [[package]] name = "ratatui-termwiz" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +checksum = "386b8ff8f74ed749509391c56d549761a2fcdb408e1f42e467286bcb7dac8967" dependencies = [ "ratatui-core", "termwiz", @@ -2609,18 +2657,19 @@ dependencies = [ [[package]] name = "ratatui-widgets" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +checksum = "7ef4f17dd7ac3abf5adc2b920a03c61eee4bfe6a88fa5191936895525371d79c" dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.16.1", + "bitflags 2.13.0", + "hashbrown 0.17.1", "indoc", "instability", "itertools 0.14.0", "line-clipping", "ratatui-core", - "strum 0.27.2", + "serde", + "strum 0.28.0", "time", "unicode-segmentation", "unicode-width", @@ -2652,7 +2701,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -2668,9 +2717,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -2691,9 +2740,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "ring" @@ -2734,7 +2783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19ff8788a90655f715f8236507161558a966c28a2845dc54d0f9985f9bbf389b" dependencies = [ "bindgen", - "bitflags 2.11.1", + "bitflags 2.13.0", "ipnetwork", "libc", "log", @@ -2778,7 +2827,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -2936,9 +2985,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -2997,6 +3046,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook" version = "0.3.18" @@ -3030,11 +3085,11 @@ dependencies = [ [[package]] name = "simple-dns" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df350943049174c4ae8ced56c604e28270258faec12a6a48637a7655287c9ce0" +checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -3051,9 +3106,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket-pktinfo" @@ -3068,9 +3123,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3114,11 +3169,11 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ - "strum_macros 0.27.2", + "strum_macros 0.28.0", ] [[package]] @@ -3136,9 +3191,9 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.27.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", @@ -3227,7 +3282,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", "base64", - "bitflags 2.11.1", + "bitflags 2.13.0", "fancy-regex", "filedescriptor", "finl_unicode", @@ -3312,9 +3367,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" dependencies = [ "deranged", "libc", @@ -3327,9 +3382,9 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tinystr" @@ -3405,9 +3460,9 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +checksum = "a7e2948f60dbe26b35f2c7fb74ac2854c1fddded0fe9d7548fcc674a246f7615" dependencies = [ "either", "futures-util", @@ -3518,9 +3573,9 @@ dependencies = [ [[package]] name = "tun" -version = "0.8.7" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ebb3e56bb60c1e6650c9317997862ab05864c358add3cfaa34b855ffae583d0" +checksum = "dd9b35e3ba2b50284437061a8a30097f56473f287f48b3fe7660712cc03e9047" dependencies = [ "bytes", "cfg-if", @@ -3529,7 +3584,7 @@ dependencies = [ "ipnet", "libc", "log", - "nix 0.31.2", + "nix 0.31.3", "thiserror 2.0.18", "tokio", "tokio-util", @@ -3558,9 +3613,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "ucd-trie" @@ -3585,9 +3640,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-truncate" @@ -3667,9 +3722,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "atomic", "getrandom 0.4.2", @@ -3716,9 +3771,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ "wit-bindgen 0.57.1", ] @@ -3734,9 +3789,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" dependencies = [ "cfg-if", "once_cell", @@ -3747,9 +3802,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" dependencies = [ "js-sys", "wasm-bindgen", @@ -3757,9 +3812,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3767,9 +3822,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" dependencies = [ "bumpalo", "proc-macro2", @@ -3780,9 +3835,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" dependencies = [ "unicode-ident", ] @@ -3815,7 +3870,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -3823,9 +3878,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" dependencies = [ "js-sys", "wasm-bindgen", @@ -3970,7 +4025,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "widestring", "windows-sys 0.61.2", ] @@ -3984,15 +4039,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" @@ -4140,23 +4186,13 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winreg" -version = "0.55.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" -dependencies = [ - "cfg-if", - "windows-sys 0.59.0", -] - [[package]] name = "wintun" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da99be64b5aa3de869c16977994314d0759a698d9a73ab0a5b1d52e2282033ae" dependencies = [ - "c2rust-bitfields 0.18.0", + "c2rust-bitfields", "libloading 0.8.9", "log", "thiserror 1.0.69", @@ -4165,18 +4201,16 @@ dependencies = [ [[package]] name = "wintun-bindings" -version = "0.7.34" +version = "0.7.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ae04d34b8569174e849128d2e36538329a27daa79c06ed0375f2c5d6704461" +checksum = "fc4494d02357537af05cf526be7b817a51752b688a78926af57379abd840d911" dependencies = [ "blocking", - "c2rust-bitfields 0.21.0", "futures", "libloading 0.9.0", "log", "thiserror 2.0.18", "windows-sys 0.61.2", - "winreg", ] [[package]] @@ -4243,7 +4277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.13.0", "indexmap", "log", "serde", @@ -4287,9 +4321,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -4310,18 +4344,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -4330,9 +4364,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] @@ -4351,9 +4385,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" From 4e3890a780ff10ab9ac4bc34b57a3e3580ea7ab8 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 14 Jun 2026 02:38:09 +0000 Subject: [PATCH 2/6] ci: cover Debian 13 and Ubuntu 22.04 in the deb-install matrix The GitHub deb-install matrix ran debian12/ubuntu24/ubuntu26, but the local harness (testing/deb-install/test.sh) runs five distros. Add the missing debian13 (trixie) and ubuntu22 legs so the cloud gate covers the same distro set as local CI. Each new leg invokes the existing test.sh scenario, so per-distro behavior is identical to the local run. Update the granularity-only parity notes in ci.yml, ci-local.sh, and check-ci-parity.sh to list the full distro set. --- .github/workflows/ci.yml | 12 +++++++++--- testing/check-ci-parity.sh | 6 +++--- testing/ci-local.sh | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60f0282..f9ec207 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,9 +40,9 @@ env: # # Granularity-only differences (same coverage, different matrix shape — # NOT a divergence): -# deb-install — split here into per-distro legs (debian12/ubuntu24/ -# ubuntu26) for parallelism; local runs all distros in one -# suite. +# deb-install — split here into per-distro legs (debian12/debian13/ +# ubuntu22/ubuntu24/ubuntu26) for parallelism; local runs the +# same distro set in one suite. # dns-resolver — single leg here; runs all scenarios (same as local). # ───────────────────────────────────────────────────────────────────────────── @@ -454,6 +454,12 @@ jobs: - suite: deb-install-debian12 type: deb-install scenario: debian12 + - suite: deb-install-debian13 + type: deb-install + scenario: debian13 + - suite: deb-install-ubuntu22 + type: deb-install + scenario: ubuntu22 - suite: deb-install-ubuntu24 type: deb-install scenario: ubuntu24 diff --git a/testing/check-ci-parity.sh b/testing/check-ci-parity.sh index cb5ac7e..deadc6f 100755 --- a/testing/check-ci-parity.sh +++ b/testing/check-ci-parity.sh @@ -13,9 +13,9 @@ # # Granularity-only differences folded before comparison (same coverage, # different matrix shape — NOT a divergence): -# deb-install — GitHub splits into per-distro legs -# (deb-install-debian12/ubuntu24/ubuntu26); local runs all -# distros in one suite. Folded to "deb-install". +# deb-install — GitHub splits into per-distro legs (deb-install-debian12/ +# debian13/ubuntu22/ubuntu24/ubuntu26); local runs the same +# distro set in one suite. Folded to "deb-install". # chaos-* — GitHub fans each chaos scenario into its own matrix leg # (type: chaos); local runs them all via the one CHAOS_SUITES # path. The individual scenario names also differ cosmetically diff --git a/testing/ci-local.sh b/testing/ci-local.sh index aef519e..d9335d4 100755 --- a/testing/ci-local.sh +++ b/testing/ci-local.sh @@ -50,7 +50,8 @@ # Granularity-only differences (same coverage, different matrix shape — # NOT a divergence): # deb-install — local runs all distros sequentially in one suite; GitHub -# splits into per-distro legs (debian12/ubuntu24/ubuntu26). +# splits into per-distro legs (debian12/debian13/ubuntu22/ +# ubuntu24/ubuntu26 — the same distro set). # dns-resolver — single suite both sides; runs all scenarios. # ───────────────────────────────────────────────────────────────────────────── set -uo pipefail From 3e0d9f572636defdf02b00e303083918f1e1bc79 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 14 Jun 2026 03:39:32 +0000 Subject: [PATCH 3/6] ci: build and lint the AUR package on every CI trigger Run the AUR package through makepkg + namcap on the same triggers as the other package workflows (pushes to master/maint/next, pull requests, tags, and manual dispatch), so a broken PKGBUILD is caught continuously rather than only at release time. The build runs in an Arch container (makepkg/namcap are not on ubuntu-latest) and packages the checked-out tree from a local git-archive tarball, so it works for branch/PR builds and unreleased rc tags that have no published GitHub source archive yet. makepkg runs with --nocheck since the test suite is already covered by ci.yml; this job validates packaging. Publishing to the AUR is unchanged in intent but now gated to a real (non-prerelease) release tag push, plus the existing manual-dispatch republish path for packaging-only pkgrel bumps; it depends on the build job so a package that fails to build or lint is never published. Branch pushes and pull requests build and lint but never publish. The PKGBUILD-patching logic is factored into a shared packaging/aur/patch-pkgbuild.sh used by both jobs; the build path sanitizes the version for makepkg (which forbids '-' in pkgver). --- .github/workflows/aur-publish.yml | 167 +++++++++++++++++++++++------- packaging/aur/patch-pkgbuild.sh | 76 ++++++++++++++ 2 files changed, 208 insertions(+), 35 deletions(-) create mode 100755 packaging/aur/patch-pkgbuild.sh diff --git a/.github/workflows/aur-publish.yml b/.github/workflows/aur-publish.yml index ff8803c..7a259e5 100644 --- a/.github/workflows/aur-publish.yml +++ b/.github/workflows/aur-publish.yml @@ -1,25 +1,151 @@ name: AUR Publish on: + push: + branches: + - master + - maint + - next + tags: + - 'v*' + pull_request: workflow_dispatch: inputs: tag: - description: 'Release tag to publish (e.g. v0.3.0). Defaults to the tag the workflow was dispatched from.' + description: 'Release tag to publish (e.g. v0.4.0). Defaults to the tag the workflow was dispatched from.' required: false default: '' pkgrel: description: 'AUR pkgrel to publish. Use 2+ for packaging-only republishes of an existing tag.' required: false default: '1' - push: - tags: - - 'v*' jobs: + # ─────────────────────────────────────────────────────────────────────────── + # Build + lint the AUR package on every trigger, matching the coverage the + # other package workflows (linux/macos/windows/openwrt) give their artifacts: + # branch pushes, pull requests, tags, and manual dispatch. Uses makepkg + + # namcap in an Arch container (neither tool exists on ubuntu-latest) and builds + # the *checked-out tree* from a local git-archive tarball, so it works for + # branch/PR builds and unreleased rc tags whose GitHub source archive does not + # exist yet. This job never publishes. + # ─────────────────────────────────────────────────────────────────────────── + aur-build: + name: Build and lint fips AUR package + runs-on: ubuntu-latest + container: archlinux:base-devel + + steps: + - name: Install build and lint tooling + run: | + set -euo pipefail + pacman -Sy --noconfirm --needed base-devel namcap git curl + + - uses: actions/checkout@v4 + + - name: Resolve package version + id: ver + env: + INPUT_TAG: ${{ inputs.tag }} + INPUT_PKGREL: ${{ inputs.pkgrel }} + run: | + set -euo pipefail + if [ -n "${INPUT_TAG:-}" ]; then + RAW="${INPUT_TAG#v}" + elif [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then + RAW="${GITHUB_REF_NAME#v}" + else + # Branch push / PR: derive the version from the crate manifest. + RAW=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') + fi + # makepkg forbids '-' in pkgver; map e.g. 0.4.0-rc1 -> 0.4.0rc1, + # 0.4.0-dev -> 0.4.0dev. The build only needs an internally consistent + # pkgver (it matches the git-archive prefix below); this is not the + # value the real publish uses. + VERSION="${RAW//-/}" + PKGREL="${INPUT_PKGREL:-1}" + case "$PKGREL" in + ''|*[!0-9]*|0) echo "pkgrel '$PKGREL' must be a positive integer"; exit 1 ;; + esac + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "pkgrel=${PKGREL}" >> "$GITHUB_OUTPUT" + echo "Resolved AUR pkgver=${VERSION} pkgrel=${PKGREL}" + + - name: Create non-root build user and fix ownership + run: | + set -euo pipefail + # makepkg refuses to run as root; create an unprivileged build user + # with passwordless sudo (needed for pacman dep installs during -s). + useradd -m -s /bin/bash builder + echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder + chmod 0440 /etc/sudoers.d/builder + # The checkout is owned by root; hand it to the build user. + chown -R builder:builder "$GITHUB_WORKSPACE" + + - name: Build a local source tarball of the checkout + env: + VERSION: ${{ steps.ver.outputs.version }} + run: | + set -euo pipefail + # The PKGBUILD source= points at GitHub archive/.tar.gz, which does + # not exist for a branch push, a PR, or an unreleased rc tag and would + # 404. Instead build the checked-out tree by packing it into a local + # tarball whose top-level directory matches what the PKGBUILD expects + # ("fips-/"); patch-pkgbuild.sh repoints source= at it. + TARBALL="packaging/aur/fips-${VERSION}.tar.gz" + git config --global --add safe.directory "$GITHUB_WORKSPACE" + git -C "$GITHUB_WORKSPACE" archive --format=tar.gz \ + --prefix="fips-${VERSION}/" -o "$TARBALL" HEAD + chown builder:builder "$TARBALL" + ls -l "$TARBALL" + + - name: Patch PKGBUILD + env: + TAG: v${{ steps.ver.outputs.version }} + VERSION: ${{ steps.ver.outputs.version }} + PKGREL: ${{ steps.ver.outputs.pkgrel }} + run: | + set -euo pipefail + LOCAL_TARBALL="packaging/aur/fips-${VERSION}.tar.gz" \ + bash packaging/aur/patch-pkgbuild.sh + chown builder:builder packaging/aur/PKGBUILD + + - name: makepkg build and namcap lint (as build user) + run: | + set -euo pipefail + sudo -u builder bash -euo pipefail -c ' + cd packaging/aur + echo "::group::namcap PKGBUILD" + namcap PKGBUILD + echo "::endgroup::" + echo "::group::makepkg build" + # --nocheck: skip the PKGBUILD check() (cargo test --lib); the test + # suite is already covered by ci.yml. This job validates packaging. + makepkg -s --noconfirm --nocheck + echo "::endgroup::" + echo "::group::namcap built package" + for pkg in *.pkg.tar.*; do + echo "namcap $pkg" + namcap "$pkg" + done + echo "::endgroup::" + ' + + # ─────────────────────────────────────────────────────────────────────────── + # Publish to the AUR. Runs only on a real (non-prerelease) release tag push, + # or a manual dispatch (packaging-only republish with explicit tag + pkgrel). + # Branch pushes and pull requests build+lint above but never reach this job. + # Gated on aur-build so a package that fails to build/lint is never published. + # ─────────────────────────────────────────────────────────────────────────── aur-publish-fips: name: Publish fips to AUR + needs: aur-build runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || !contains(github.ref_name, '-') + if: >- + github.event_name == 'workflow_dispatch' + || (github.event_name == 'push' + && startsWith(github.ref, 'refs/tags/v') + && !contains(github.ref_name, '-')) steps: - name: Resolve release tag @@ -59,36 +185,7 @@ jobs: TAG: ${{ steps.tag.outputs.tag }} VERSION: ${{ steps.tag.outputs.version }} PKGREL: ${{ steps.tag.outputs.pkgrel }} - run: | - set -euo pipefail - URL="https://github.com/${GITHUB_REPOSITORY}/archive/${TAG}.tar.gz" - echo "Fetching $URL" - SOURCE_SUM=$(curl -fsSL --retry 3 "$URL" | b2sum | awk '{print $1}') - SYSUSERS_SUM=$(b2sum packaging/aur/fips.sysusers | awk '{print $1}') - TMPFILES_SUM=$(b2sum packaging/aur/fips.tmpfiles | awk '{print $1}') - for v in SOURCE_SUM SYSUSERS_SUM TMPFILES_SUM; do - eval val=\$$v - if [ -z "$val" ]; then echo "$v is empty"; exit 1; fi - done - sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/aur/PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=${PKGREL}/" packaging/aur/PKGBUILD - sed -i "s/^conflicts=.*/conflicts=('fips-git' 'fips-git-debug')/" packaging/aur/PKGBUILD - sed -i "s/^options=.*/options=('!lto' '!debug')/" packaging/aur/PKGBUILD - sed -i "s|^b2sums=('SKIP'.*|b2sums=('${SOURCE_SUM}'|" packaging/aur/PKGBUILD - awk -v s1="$SYSUSERS_SUM" -v s2="$TMPFILES_SUM" ' - /^b2sums=\(/ { in_block=1; count=0 } - in_block { - count++ - if (count == 2) sub(/[a-f0-9]{128}/, s1) - if (count == 3) sub(/[a-f0-9]{128}/, s2) - if ($0 ~ /\)/) in_block=0 - } - { print } - ' packaging/aur/PKGBUILD > packaging/aur/PKGBUILD.new - mv packaging/aur/PKGBUILD.new packaging/aur/PKGBUILD - echo "Patched PKGBUILD:" - grep -E "^(pkgver|pkgrel|conflicts|options)=" packaging/aur/PKGBUILD - awk '/^b2sums=\(/,/\)$/' packaging/aur/PKGBUILD + run: bash packaging/aur/patch-pkgbuild.sh - name: Publish to AUR uses: KSXGitHub/github-actions-deploy-aur@v4.1.2 diff --git a/packaging/aur/patch-pkgbuild.sh b/packaging/aur/patch-pkgbuild.sh new file mode 100755 index 0000000..55b8fe3 --- /dev/null +++ b/packaging/aur/patch-pkgbuild.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Patch packaging/aur/PKGBUILD in place with the release pkgver, pkgrel, +# conflicts, options, and b2sums. +# +# Shared by the AUR publish workflow's real-publish job and its dry-run job +# so the patching logic lives in exactly one place. +# +# Required environment variables: +# TAG - release tag (e.g. v0.4.0 or v0.4.0-rc1) +# VERSION - tag without the leading 'v' (e.g. 0.4.0) +# PKGREL - AUR pkgrel (positive integer) +# +# Source of the tarball b2sum: +# Default: fetch the GitHub source archive for $TAG and hash it. +# Dry-run: set LOCAL_TARBALL to a path; its b2sum is used instead and the +# PKGBUILD source= line is rewritten to point at that local file. +# This lets a dry-run validate an rc tag that has no published +# GitHub release archive yet (the archive URL would 404). +# +# Uses $GITHUB_REPOSITORY for the source URL (owner/repo). + +set -euo pipefail + +: "${TAG:?TAG must be set}" +: "${VERSION:?VERSION must be set}" +: "${PKGREL:?PKGREL must be set}" + +PKGBUILD="packaging/aur/PKGBUILD" + +SYSUSERS_SUM=$(b2sum packaging/aur/fips.sysusers | awk '{print $1}') +TMPFILES_SUM=$(b2sum packaging/aur/fips.tmpfiles | awk '{print $1}') + +if [ -n "${LOCAL_TARBALL:-}" ]; then + # Dry-run path: hash the locally-created tarball of the checkout. + echo "Using local tarball $LOCAL_TARBALL for b2sum (dry-run)" + SOURCE_SUM=$(b2sum "$LOCAL_TARBALL" | awk '{print $1}') +else + URL="https://github.com/${GITHUB_REPOSITORY:?GITHUB_REPOSITORY must be set}/archive/${TAG}.tar.gz" + echo "Fetching $URL" + SOURCE_SUM=$(curl -fsSL --retry 3 "$URL" | b2sum | awk '{print $1}') +fi + +for v in SOURCE_SUM SYSUSERS_SUM TMPFILES_SUM; do + eval val=\$$v + if [ -z "$val" ]; then echo "$v is empty"; exit 1; fi +done + +sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD" +sed -i "s/^pkgrel=.*/pkgrel=${PKGREL}/" "$PKGBUILD" +sed -i "s/^conflicts=.*/conflicts=('fips-git' 'fips-git-debug')/" "$PKGBUILD" +sed -i "s/^options=.*/options=('!lto' '!debug')/" "$PKGBUILD" + +if [ -n "${LOCAL_TARBALL:-}" ]; then + # Repoint the first source entry at the local tarball so makepkg builds the + # checked-out tree instead of fetching the (possibly unpublished) GitHub + # archive. makepkg resolves a bare filename source against $startdir. + LOCAL_BASE=$(basename "$LOCAL_TARBALL") + sed -i "s|^source=(\"\$pkgname-\$pkgver.tar.gz::[^\"]*\"|source=(\"\$pkgname-\$pkgver.tar.gz::${LOCAL_BASE}\"|" "$PKGBUILD" +fi + +sed -i "s|^b2sums=('SKIP'.*|b2sums=('${SOURCE_SUM}'|" "$PKGBUILD" +awk -v s1="$SYSUSERS_SUM" -v s2="$TMPFILES_SUM" ' + /^b2sums=\(/ { in_block=1; count=0 } + in_block { + count++ + if (count == 2) sub(/[a-f0-9]{128}/, s1) + if (count == 3) sub(/[a-f0-9]{128}/, s2) + if ($0 ~ /\)/) in_block=0 + } + { print } +' "$PKGBUILD" > "$PKGBUILD.new" +mv "$PKGBUILD.new" "$PKGBUILD" + +echo "Patched PKGBUILD:" +grep -E "^(pkgver|pkgrel|conflicts|options|source)=" "$PKGBUILD" +awk '/^b2sums=\(/,/\)$/' "$PKGBUILD" From 507086e39dd2b61bf5459ebcde6fac001d524f4f Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 14 Jun 2026 15:14:05 +0000 Subject: [PATCH 4/6] docs: refresh tutorials, how-to, design, reference, and examples for v0.4.0 Pre-cut documentation pass for the 0.4.0 release, verified against current source. Corrections: - fipsctl: stale 'show identities'/'show node' -> 'show status' (host-a-service, run-as-unprivileged-user) - mesh address derivation: first 16 bytes of SHA-256(pubkey) with the leading byte set to 0xfd, not a fixed fd97: prefix (reach-mesh-services, ipv6-adapter-walkthrough) - gateway control socket mode 0660 -> 0770 (troubleshoot-gateway) - Tor example: add advertised_port: 8443 so the published port matches the prose (enable-nostr-discovery) - bloom mesh-size estimate rewritten to the OR-union-of-peer-filters algorithm; plus mtu deep-link, gateway pool wording, and a NAT failure-mode line - examples: delete orphaned nostr-rs-relay config, accept inbound to the local 8443 TCP listener, fix fd::/8 -> fd00::/8 typos, dotless wireguard alias Additions: - new Nym mixnet transport section (fips-transport-layer) and the architecture transport list - new LAN/mDNS discovery section (fips-nostr-discovery) - reference docs: Nym transport, LAN discovery, and new control/stats surfaces; drop ble from the connect transport list --- docs/design/fips-architecture.md | 8 +- docs/design/fips-bloom-filters.md | 26 ++- docs/design/fips-gateway.md | 2 +- docs/design/fips-mtu.md | 2 +- docs/design/fips-nostr-discovery.md | 187 +++++++++++++++++- docs/design/fips-transport-layer.md | 121 +++++++++++- .../port-advertisement-and-nat-traversal.md | 2 +- docs/how-to/enable-nostr-discovery.md | 1 + docs/how-to/run-as-unprivileged-user.md | 2 +- docs/how-to/troubleshoot-gateway.md | 2 +- docs/reference/cli-fipsctl.md | 3 +- docs/reference/configuration.md | 50 +++++ docs/reference/control-socket.md | 3 +- docs/reference/transports.md | 19 ++ docs/tutorials/host-a-service.md | 5 +- docs/tutorials/ipv6-adapter-walkthrough.md | 8 +- docs/tutorials/reach-mesh-services.md | 7 +- examples/k8s-sidecar/README.md | 2 +- examples/sidecar-nostr-relay/README.md | 17 +- examples/sidecar-nostr-relay/entrypoint.sh | 2 + .../sidecar-nostr-relay/relay/config.toml | 49 ----- examples/wireguard-sidecar-macos/fips.yaml | 4 +- 22 files changed, 436 insertions(+), 86 deletions(-) delete mode 100644 examples/sidecar-nostr-relay/relay/config.toml diff --git a/docs/design/fips-architecture.md b/docs/design/fips-architecture.md index 7d909b0..d782813 100644 --- a/docs/design/fips-architecture.md +++ b/docs/design/fips-architecture.md @@ -221,9 +221,11 @@ discovery protocol, and error-recovery integration view live in ## Transport Abstraction FIPS treats the communication medium as a pluggable component. UDP, -TCP, raw Ethernet, Tor, and BLE all implement the same small datagram -interface (send, receive, report MTU) and feed peers into a single FMP -routing layer; radio and serial transports are in the planned set. +TCP, raw Ethernet, Tor, BLE, and Nym all implement the same small +datagram interface (send, receive, report MTU) and feed peers into a +single FMP routing layer; radio and serial transports are in the +planned set. Nym (an outbound-only mixnet transport) and Tor are +privacy-oriented deployment modes rather than failover paths. Multi-transport nodes bridge between networks transparently. The transport-layer specification — including per-transport categories, the trait surface, the connection model, and implementation status — diff --git a/docs/design/fips-bloom-filters.md b/docs/design/fips-bloom-filters.md index bc2e6f1..1e7f2ca 100644 --- a/docs/design/fips-bloom-filters.md +++ b/docs/design/fips-bloom-filters.md @@ -180,7 +180,7 @@ network with no overlap (excluding the node itself at the split point). All peers — including non-tree mesh shortcuts — still **receive** FilterAnnounce messages and **store** received filters locally. These -stored filters are consulted during routing (step 3 of `find_next_hop()`) +stored filters are consulted during routing (step 4 of `find_next_hop()`) for single-hop shortcut discovery. However, mesh peer filters contain only the mesh peer's own tree-propagated information, not transitive entries from the broader network. @@ -339,14 +339,24 @@ positions that folding produces. ## Mesh Size Estimation -Each filter's saturation can be inverted into an estimated entry count +A filter's saturation can be inverted into an estimated entry count via the standard formula `n ≈ -(m/k) · ln(1 − X/m)`, where `m` is the filter size in bits, `k` is the hash count, and `X` is the population -count. Combining the parent's inbound filter with the children's -inbound filters gives an estimate of the whole network: parent + each -child's subtree are disjoint by construction, and adding 1 for the -node itself yields the total. The result is cached on the node and -exposed through the control socket and `fipstop` dashboard. +count. Rather than estimate per-filter and sum, the node first builds +an **OR-union of every connected peer's inbound filter** — all routing +peers, including cross-links, not just the tree parent and children — +inserts its own address into the union, and inverts the cardinality +**once on the resulting union**. Because filter propagation is +split-horizon (each outgoing filter excludes the peer it routes back +to), every routing peer advertises a near-complete "whole mesh minus +my subtree" view, so the union covers the network. OR-ing is +idempotent, so overlapping bits deduplicate instead of over-counting, +and folding in all peers rather than only the tree neighborhood damps +the count flap on a parent switch (the cross-links still carry the +upward coverage) and removes any dependence on tree-declaration cache +freshness. The result is cached on the node and exposed through the +control socket and `fipstop` dashboard. (See `compute_mesh_size()` in +`src/node/mod.rs`.) The estimator refuses to produce a value when any contributing filter is above the antipoison FPR cap (`node.bloom.max_inbound_fpr`, @@ -378,7 +388,7 @@ as described above. | 500ms rate limiting | **Implemented** | | FilterAnnounce gossip (all peers) | **Implemented** | | Filter cardinality logging | **Implemented** | -| Mesh size estimation (parent + children + 1) | **Implemented** | +| Mesh size estimation (OR-union of peer filters) | **Implemented** | | Inbound FPR cap (antipoison) | **Implemented** | | Size class negotiation | Future direction | | Folding support | Future direction | diff --git a/docs/design/fips-gateway.md b/docs/design/fips-gateway.md index bdadccd..a70b0af 100644 --- a/docs/design/fips-gateway.md +++ b/docs/design/fips-gateway.md @@ -218,7 +218,7 @@ involving the DNS proxy or the pool. ### Virtual IP Pool -The pool allocates IPv6 addresses from a configured CIDR (default +The pool allocates IPv6 addresses from a required CIDR (commonly `fd01::/112`). Each address maps to one mesh destination, keyed by `NodeAddr` rather than by hostname — different `.fips` aliases for the same node share a virtual IP. Address 0 (the network-equivalent) diff --git a/docs/design/fips-mtu.md b/docs/design/fips-mtu.md index ea88aab..95ecd6b 100644 --- a/docs/design/fips-mtu.md +++ b/docs/design/fips-mtu.md @@ -251,7 +251,7 @@ would later drop. The adapter integrates with the MTU subsystem rather than owning it. The "why we clamp and what `max_mss` means" lives here in the MTU design; the "how the clamp is implemented at the TUN" lives in the -[IPv6 adapter](fips-ipv6-adapter.md#tcp-mss-clamping) doc. +[IPv6 adapter](fips-ipv6-adapter.md#tun-side-tcp-mss-clamping) doc. ## ICMP Packet Too Big diff --git a/docs/design/fips-nostr-discovery.md b/docs/design/fips-nostr-discovery.md index df65088..43fe572 100644 --- a/docs/design/fips-nostr-discovery.md +++ b/docs/design/fips-nostr-discovery.md @@ -1,4 +1,13 @@ -# FIPS Nostr-Mediated Discovery and NAT Traversal +# FIPS Discovery: Nostr-Mediated and LAN/mDNS + +FIPS nodes have two discovery mechanisms beyond the static `peers[]` +list. The bulk of this document describes **Nostr-mediated discovery**, +which works across the internet using public Nostr relays as a +signaling channel and can punch through UDP NAT. A second, much +simpler mechanism — **LAN/mDNS discovery** — finds peers on the same +local link with no relay, STUN, or NAT traversal at all; it is +described in its own section near the end. The two are independent: a +node can enable either, both, or neither. Nostr-mediated discovery lets FIPS nodes find each other, and if necessary, punch through UDP NAT, using public Nostr relays as the @@ -377,6 +386,182 @@ semaphore and replay-cache layers downstream. advert says "I am npub X at 1.2.3.4:5678" but whose FMP handshake presents a different static key is rejected at the mesh layer. +## LAN/mDNS discovery + +LAN discovery is a separate, link-local discovery mechanism that finds +peers on the same broadcast domain using mDNS / DNS-SD +([RFC 6762](https://www.rfc-editor.org/rfc/rfc6762) / +[RFC 6763](https://www.rfc-editor.org/rfc/rfc6763)). Unlike +Nostr-mediated discovery, it contacts no relay, runs no STUN +observation, and performs no NAT traversal: an endpoint learned from a +LAN advert is by construction routable from the consumer's own link. +The result is sub-second peer pairing on the same LAN. + +It is unrelated to the "LAN candidate" terminology used in the +NAT-traversal sections above (which refers to a host's own +locally-bound address offered as a hole-punch candidate). LAN/mDNS +discovery is a distinct subsystem under `src/discovery/lan/`. + +### Role + +LAN discovery adds two capabilities, both confined to the local link: + +- **Advertising.** The node publishes a `_fips._udp.local.` DNS-SD + service advert carrying its `npub`, its protocol version, and (if + configured) a discovery scope. The advert is multicast on the local + link only; it does not leave the broadcast domain unless the + operator's network bridges mDNS. +- **Browsing.** The node concurrently browses for the same service + type, learns the endpoints of other FIPS nodes on the link, and + initiates a normal FMP link to each newly-seen peer. + +The mDNS service type is `_fips._udp.local.` +(`src/discovery/lan/mod.rs:45`). Per RFC 6763 the `_udp` label denotes +the IP transport used for the advert, not the FIPS upper protocol — +both UDP and TCP FIPS endpoints announce under the same service type +because the link-layer handshake travels over UDP either way. (In +practice LAN discovery dials only over a UDP transport; see the +handshake subsection.) + +### When to use it + +- **You run several FIPS nodes on one LAN** (a lab bench, an office + segment, a home network) and want them to find each other without + hand-maintaining `peers[]` blocks or standing up Nostr discovery. +- **You want the lowest-latency pairing path.** Same-link pairing + completes in well under a second with no relay round-trip. + +Skip it when nodes are not on a shared broadcast domain (mDNS does not +cross routed boundaries), or when you do not want the node to multicast +its identity on the local link. LAN discovery is **opt-in and disabled +by default**, so doing nothing leaves it off. + +### How it works + +The LAN discovery runtime (`src/discovery/lan/mod.rs`) is started +during node initialization when `node.discovery.lan.enabled` is true. +It is independent of Nostr discovery and runs even when Nostr is +disabled (`src/node/lifecycle.rs:1159-1162`). Startup requires an +operational UDP transport: the node advertises the port of its +lowest-`TransportId` operational, non-bootstrap UDP transport, chosen +deterministically so the advertised port is stable across restarts +(`src/node/lifecycle.rs:1169-1180`). If no such port exists, the +runtime returns `NoAdvertisedPort` and LAN discovery does not start +(`src/discovery/lan/mod.rs:156-158`). + +The runtime does two things concurrently: + +1. **Responder.** It registers a DNS-SD service with instance name + `fips-` and a TXT record carrying the keys + below. `mdns-sd`'s address auto-detection appends every non-loopback + interface address, with `127.0.0.1` seeded so same-host peers and + integration tests can still resolve the advert + (`src/discovery/lan/mod.rs:182-203`). +2. **Browser.** A background pump receives `ServiceResolved` events for + the same service type. For each resolved advert it extracts the + `npub` and `scope` TXT values, drops adverts that echo the node's own + npub, drops cross-scope adverts (see scope filtering), drops records + without an `npub`, and surfaces one `LanDiscoveredPeer` per routable + interface address (`src/discovery/lan/mod.rs:212-299`). IPv6 + unicast link-local addresses without an interface scope id are + skipped, since they cannot be dialed unambiguously + (`src/discovery/lan/mod.rs:348-365`). + +The TXT record carries three keys (`src/discovery/lan/mod.rs:47-55`): + +| TXT key | Contents | +| --- | --- | +| `npub` | bech32-encoded npub of the advertising node | +| `scope` | the node's discovery scope, if one is configured (omitted otherwise) | +| `v` | FIPS protocol version (the same `PROTOCOL_VERSION` used by the Nostr advert) | + +Once per node tick, the node drains browser events and acts on them in +`poll_lan_discovery()` (`src/node/lifecycle.rs:907`, called from +`src/node/handlers/rx_loop.rs:266`). For each discovered peer it finds +a UDP transport whose family matches the peer address, parses the +`npub` into a `PeerIdentity`, skips peers it is already connected to or +currently connecting to, and otherwise initiates a connection. + +### Handshake: Noise IK + +LAN-discovered peers are dialed through the standard FMP outbound link +path. `poll_lan_discovery()` calls `initiate_connection()` +(`src/node/lifecycle.rs:380`), which, for connectionless transports +such as UDP, allocates a link and **starts the Noise IK handshake** +(documented at `src/node/lifecycle.rs:373-374`). This is the same +link-layer handshake used by every other FMP connection — IK at the +link layer per the FIPS architecture — not a different pattern for LAN +peers. + +The mDNS advert is **unauthenticated**: anyone on the link can +multicast a TXT claiming any `npub`. Identity is proven end-to-end by +the Noise IK handshake against the observed endpoint. A spoofed advert +carrying another node's npub fails the handshake — the impostor does +not hold the matching static key — and the half-open link is dropped. +The mDNS advert is therefore a routing hint, never an identity +assertion, exactly as a Nostr advert is treated (a successful contact +is not trusted until FMP's Noise IK handshake completes). + +> Note: a stale source doc-comment at `src/node/lifecycle.rs:904-906` +> describes this path as a "Noise XX" handshake. That comment is +> inaccurate — the path uses Noise IK as described above. The comment +> is flagged for a separate source fix and does not reflect actual +> behavior. + +### Scope filtering + +When a discovery scope is configured, the advert carries it in the +`scope` TXT entry and the browser surfaces only peers whose advert +carries a matching scope. Nodes on the same physical LAN but configured +for different mesh networks therefore do not cross-feed each other. + +The scope is resolved by `lan_discovery_scope()` +(`src/node/lifecycle.rs:880-902`): the explicit +`node.discovery.lan.scope`, if non-empty, is used directly. Otherwise +the node falls back to deriving a scope from the Nostr discovery `app` +tag (stripping the `fips-overlay-v1:` prefix when present). This lets +an application keep its public, relay-visible Nostr `app` tag generic +while still isolating LAN discovery per private network, or share one +value across both. A node with no scope on either side surfaces all +adverts it sees on the link. + +### Configuration + +LAN discovery is configured under `node.discovery.lan.*` +(`src/config/node.rs:222-227`, `src/discovery/lan/mod.rs:88-129`): + +| Key | Type | Default | Meaning | +| --- | --- | --- | --- | +| `node.discovery.lan.enabled` | bool | `false` | Master switch. LAN discovery is opt-in; default-off avoids an unexpected per-link identity multicast on upgrade. | +| `node.discovery.lan.service_type` | string | `_fips._udp.local.` | DNS-SD service type. Overridable mainly so integration tests can isolate multiple services on one loopback interface. | +| `node.discovery.lan.scope` | string (optional) | unset | Application/network scope carried in the LAN-only `scope` TXT record. Kept deliberately separate from the public Nostr `app` tag. When unset, the scope falls back to the derived Nostr `app` value. | + +The identity surface published over mDNS (`npub`, version, optional +scope) is a strict subset of what `nostr.advertise` already publishes +publicly, so enabling LAN discovery adds no marginal privacy cost +beyond making the node's presence observable on its own local link. + +### Relationship to Nostr discovery + +The two mechanisms are complementary and independent: + +| | Nostr-mediated | LAN/mDNS | +| --- | --- | --- | +| Reach | Internet-wide, via relays | Same broadcast domain only | +| Signaling channel | Public Nostr relays | mDNS multicast on the local link | +| NAT traversal | STUN + UDP hole-punch for `udp:nat` peers | None — endpoint is link-routable by construction | +| Identity carrier | signed kind 37195 advert (authenticated at publish) | unauthenticated mDNS TXT (routing hint only) | +| Identity proof | FMP Noise IK on the connection | FMP Noise IK on the connection | +| Default | disabled (`nostr.enabled: false`) | disabled (`lan.enabled: false`) | +| Scope key | `app` tag (public) | `scope` TXT (link-local), falls back to `app` | + +Both ultimately converge on the same trust boundary: discovery only +supplies candidate endpoints, and no peer is trusted until FMP's Noise +IK handshake confirms the claimed identity. A node may run both at +once — for example, advertising globally over Nostr while also pairing +instantly with same-LAN peers — with no interaction between the two +beyond the shared scope fallback. + ## See also - [../how-to/enable-nostr-discovery.md](../how-to/enable-nostr-discovery.md) diff --git a/docs/design/fips-transport-layer.md b/docs/design/fips-transport-layer.md index 4e6ace9..18ad41a 100644 --- a/docs/design/fips-transport-layer.md +++ b/docs/design/fips-transport-layer.md @@ -120,6 +120,7 @@ for internet connectivity: | UDP/IP | host:port | 1280–1472 | Unreliable | Primary internet transport | | TCP/IP | host:port | Stream | Reliable | Requires length-prefix framing | | Tor | .onion | Stream | Reliable | High latency, strong anonymity | +| Nym | host:port | Stream | Reliable | Mixnet, outbound-only, strong anonymity | **Shared medium transports** operate over broadcast- or multicast-capable media: @@ -190,6 +191,7 @@ proceed. | --------- | ---------------- | | TCP/IP | TCP three-way handshake | | Tor | Circuit establishment (typically 10–60s, default timeout 120s) | +| Nym | SOCKS5 connect through mixnet (minutes possible, default timeout 300s) | | BLE | L2CAP CoC or GATT connection | | Serial | Physical connection (static) | @@ -599,6 +601,120 @@ SOCKS5-level errors, MTU rejections, accepted/rejected inbound connections, and Tor control-port errors. The full counter table lives in [../reference/transports.md](../reference/transports.md). +## Nym: The Mixnet Transport + +The Nym transport routes FIPS traffic through the Nym mixnet, providing +network-level anonymity via Sphinx packet routing and timing +obfuscation. It uses the "mixnet-as-proxy" pattern: a node connects +outbound through a local `nym-socks5-client` SOCKS5 proxy, which carries +the traffic into the mixnet. The `nym-socks5-client` runs as a separate +process alongside the fips daemon and must be started independently. + +Like Tor, Nym is a privacy-oriented deployment mode chosen for the +anonymity properties of the mixnet, not a failover for other transports. +Like TCP and Tor, it is connection-oriented and reliable; the same +TCP-over-TCP considerations apply, and cost-based parent selection +naturally deprioritizes the high-latency Nym links. + +### Architecture + +The Nym transport is a separate `NymTransport` implementation. It reuses +the FMP header-based stream reader (`tcp/stream.rs`) for packet framing +on the underlying byte stream, and follows the same connection-pool +pattern as the TCP and Tor transports. + +It maintains two pools: a `ConnectingPool` for background SOCKS5 +connection attempts, and an established pool of `NymConnection` entries. +Each `NymConnection` holds a write half, a per-connection receive task, +the configured MTU, and a connection timestamp. + +| Property | Value | +| -------- | ----- | +| Addressing | IP:port or hostname:port | +| Default MTU | 1400 bytes | +| Framing | FMP header-based (shared with TCP) | +| Connection model | Outbound-only, non-blocking connect through SOCKS5 | +| Platform | Cross-platform (requires external nym-socks5-client) | + +### Outbound-Only + +The Nym transport is strictly outbound. It supports no inbound service: +`accept_connections()` returns `false` and `discover()` returns no +peers. A node using the Nym transport can initiate links to remote peers +through the mixnet, but cannot accept inbound connections over Nym. (A +node can still accept inbound links over other transports it runs.) + +### Address Types + +The Nym transport accepts two address formats, parsed into an internal +target address: + +- **IP:port** — a numeric IP and port, sent to the SOCKS5 proxy as a + numeric target. +- **Hostname:port** — the hostname is passed through SOCKS5 so it is + resolved on the exit side rather than locally. + +Both forms are routed through the same SOCKS5 proxy. + +### Connection Establishment + +Connection setup follows the same non-blocking pattern as the TCP and +Tor transports. When FMP needs to reach a peer, the node initiates a +background connect (`connect_async`). The transport spawns a background +tokio task that opens a SOCKS5 connection through the local +`nym-socks5-client`, configures the socket (including TCP keepalive), +splits the stream, and spawns a per-connection receive loop using the +shared FMP stream reader. The call returns immediately while the connect +proceeds in the background. + +SOCKS5 connection setup through the mixnet can take much longer than a +direct TCP connection because each connection traverses multiple mix +nodes with timing obfuscation. Accordingly the connect timeout defaults +to 300 seconds (`connect_timeout_ms`). Non-blocking connect is essential +here — a blocking connect would stall the FMP event loop for the +duration of mixnet setup. As a fallback, `send_async(addr, data)` +performs a connect-on-send if no connection to the address yet exists. + +Each outbound packet is checked against the configured MTU before being +written; an oversized packet is rejected with an MTU-exceeded error +rather than being sent. + +### Startup Readiness + +At startup the transport validates the configured `socks5_addr` and then +probes the SOCKS5 port to wait for `nym-socks5-client` to become ready, +using exponential backoff (starting at 1 second, capped at 10 seconds +between attempts) up to `startup_timeout_secs` (default 120 seconds). If +the proxy does not become reachable within that window, the transport +logs a warning and starts anyway; outbound connections then fail until +the `nym-socks5-client` becomes available. + +### Session Independence + +Same as TCP and Tor: loss of a Nym connection does **not** tear down the +FIPS peer. Noise keys, MMP state, and FSP sessions survive reconnection. + +### Configuration + +The Nym transport block (`transports.nym.*`) has the following fields: + +| Field | Default | Description | +| ----- | ------- | ----------- | +| `socks5_addr` | `127.0.0.1:1080` | Address (host:port) of the local nym-socks5-client SOCKS5 proxy | +| `connect_timeout_ms` | `300000` | Outbound SOCKS5 connect timeout in milliseconds (300s) | +| `mtu` | `1400` | Maximum FIPS packet size for Nym connections, in bytes | +| `startup_timeout_secs` | `120` | Seconds to wait for nym-socks5-client to become ready at startup | + +The Nym transport requires an external `nym-socks5-client`. Named +instances are supported for multiple proxy endpoints. Unknown +configuration keys are rejected. + +### Statistics + +The Nym transport exposes per-instance counters covering successful +send/receive, send/receive errors, connection establishment, SOCKS5-level +errors, connect timeouts, and MTU rejections. + ## Discovery Discovery determines that a FIPS-capable endpoint is reachable at a given @@ -725,7 +841,8 @@ TransportType { } ``` -Predefined types exist for UDP, TCP, Ethernet, WiFi, Tor, and Serial. +Predefined types exist for UDP, TCP, Ethernet, WiFi, Tor, Nym, BLE, and +Serial. ### Congestion Reporting @@ -750,6 +867,7 @@ on all forwarded datagrams. | UDP | `SO_RXQ_OVFL` kernel drop counter | `recvmsg()` ancillary data on every packet | | TCP | Not implemented | Returns `None` (TCP handles congestion internally) | | Tor | Not implemented | Returns `None` (TCP handles congestion internally) | +| Nym | Not implemented | Returns `None` (TCP handles congestion internally) | | Ethernet | Not implemented | Returns `None` | ### Transport Addresses @@ -780,6 +898,7 @@ transitions through `Starting` to `Up` (operational). `stop()` moves to | Ethernet | **Implemented** | AF_PACKET SOCK_DGRAM, EtherType 0x2121, beacon discovery, Linux only | | WiFi | **Implemented** (via Ethernet transport, infrastructure mode) | mac80211 translates 802.11↔802.3; broadcast beacons unreliable through APs | | Tor | **Implemented** | Outbound SOCKS5, inbound via onion service, .onion and clearnet addressing | +| Nym | **Implemented** | Outbound-only SOCKS5 through nym-socks5-client, mixnet anonymity, IP/hostname addressing | | BLE | **Implemented** (Linux/glibc only; experimental) | L2CAP CoC, ATT_MTU negotiation, per-link MTU; musl/macOS/Windows skip | | Radio | Future direction | Constrained MTU (51–222 bytes) | | Serial | Future direction | SLIP/COBS framing, point-to-point | diff --git a/docs/design/port-advertisement-and-nat-traversal.md b/docs/design/port-advertisement-and-nat-traversal.md index 24020eb..757013e 100644 --- a/docs/design/port-advertisement-and-nat-traversal.md +++ b/docs/design/port-advertisement-and-nat-traversal.md @@ -526,7 +526,7 @@ own. | Failure | Symptom | Mitigation | | --- | --- | --- | -| Symmetric NAT (one side) | Punch timeout | Retry with port-prediction heuristics; otherwise fall back to a relay or different transport | +| Symmetric NAT (one side) | Punch timeout | Retry with port-prediction heuristics; otherwise fall back to an application-level relay | | Symmetric NAT (both sides) | Punch timeout | Application-level relay required | | Relay latency > 60 s | Stale reflexive address | Use low-latency relays; consider self-hosted relay | | Relay does not support ephemeral kinds | Signaling events persist | Use NIP-40 expiration + NIP-09 deletion as fallback | diff --git a/docs/how-to/enable-nostr-discovery.md b/docs/how-to/enable-nostr-discovery.md index 2e91117..e31b759 100644 --- a/docs/how-to/enable-nostr-discovery.md +++ b/docs/how-to/enable-nostr-discovery.md @@ -284,6 +284,7 @@ transports: tor: mode: directory socks5_addr: "127.0.0.1:9050" + advertised_port: 8443 directory_service: hostname_file: "/var/lib/tor/fips/hostname" bind_addr: "127.0.0.1:8444" diff --git a/docs/how-to/run-as-unprivileged-user.md b/docs/how-to/run-as-unprivileged-user.md index f6f04a3..ce856b8 100644 --- a/docs/how-to/run-as-unprivileged-user.md +++ b/docs/how-to/run-as-unprivileged-user.md @@ -169,7 +169,7 @@ sudo usermod -aG fips $USER Then: ```sh -fipsctl show node +fipsctl show status ``` ## Caveats diff --git a/docs/how-to/troubleshoot-gateway.md b/docs/how-to/troubleshoot-gateway.md index 0293ec1..7c570df 100644 --- a/docs/how-to/troubleshoot-gateway.md +++ b/docs/how-to/troubleshoot-gateway.md @@ -151,7 +151,7 @@ rather than `flush ruleset`, which destroys every table on the host. Symptom: `nc -U /run/fips/gateway.sock` fails with "Permission denied" or "No such file or directory". -The socket is owned by root with mode `0660` (group `fips`). Either +The socket is owned by root with mode `0770` (group `fips`). Either run `nc` as root (`sudo nc -U ...`) or add your user to the `fips` group and re-login. If the file does not exist at all, the gateway either failed to start (check `journalctl -u fips-gateway`) or diff --git a/docs/reference/cli-fipsctl.md b/docs/reference/cli-fipsctl.md index c80867e..feef6a0 100644 --- a/docs/reference/cli-fipsctl.md +++ b/docs/reference/cli-fipsctl.md @@ -69,6 +69,7 @@ Time-series metrics from the in-process history rings. | Subcommand | Control-socket command | Description | | ---------- | ---------------------- | ----------- | | `stats list` | `show_stats_list` | Enumerate available metrics, their units, and the per-ring retention windows. | +| `stats metrics` | `show_metrics` | Dump current counter values for every protocol metric family (`forwarding`, `discovery`, `tree`, `bloom`, `congestion`, `errors`). | | `stats peers` | `show_stats_peers` | List peers tracked in stats history (active or recently active). | | `stats history [options]` | `show_stats_history` | Fetch a time-series window for one metric. | @@ -104,7 +105,7 @@ Tell the daemon to dial a peer over a specific transport. | -------- | ----------- | | `peer` | npub (bech32) or hostname from `/etc/fips/hosts`. | | `address` | Transport endpoint, e.g. `192.168.1.10:2121`, `[2001:db8::1]:2121`, or a Tor onion. FIPS-mesh ULAs (`fd00::/8`) are rejected for the IP-based transports (udp, tcp, ethernet). | -| `transport` | One of `udp`, `tcp`, `tor`, `ethernet`. | +| `transport` | One of `udp`, `tcp`, `tor`, `nym`, `ethernet`. The named transport must be configured and running. | ### `disconnect ` diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index bf02a26..f8de395 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -235,6 +235,29 @@ addresses for the punch socket port. During punching, compatible private-subnet candidates and reflexive candidates are attempted in parallel; the first successful path wins. +#### LAN Discovery (`node.discovery.lan.*`) + +Peer discovery on the local link via mDNS / DNS-SD (RFC 6762 / RFC +6763). When enabled, the node publishes a `_fips._udp.local.` service +advert carrying its `npub` (and optional scope) and concurrently +browses for the same service type to learn same-broadcast-domain peers. +The result is sub-second peer pairing with no Nostr-relay roundtrip, +STUN observation, or NAT traversal: the observed endpoint is by +construction routable from the consumer's LAN. + +mDNS adverts are unauthenticated, so a LAN advert is treated only as a +routing hint. Identity is still proven end-to-end by the Noise XX +handshake the node initiates against the observed endpoint; a spoofed +advert carrying another peer's npub fails the handshake and is dropped. +LAN discovery requires an active UDP transport (peers dial the +advertised UDP port to begin the handshake). + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `node.discovery.lan.enabled` | bool | `false` | Master switch. Opt-in: enable for sub-second same-LAN pairing. Default-off avoids reintroducing a per-LAN identity broadcast on nodes that have deliberately disabled other discovery channels | +| `node.discovery.lan.service_type` | string | `"_fips._udp.local."` | DNS-SD service type. Primarily an override for integration tests running multiple isolated services on one loopback interface; leave at the default in production | +| `node.discovery.lan.scope` | string | *(none)* | Optional application/network scope carried in a `scope=` TXT entry. Browsers with a scope set only surface peers advertising the same scope, so nodes on the same physical LAN configured for different mesh networks do not cross-feed. Intentionally separate from `node.discovery.nostr.app` so relay-visible adverts can stay generic while LAN discovery is isolated per private network | + ### Spanning Tree (`node.tree.*`) Controls tree construction and parent selection. @@ -576,6 +599,25 @@ HiddenServiceDir /var/lib/tor/fips HiddenServicePort 8443 127.0.0.1:8444 ``` +### Nym (`transports.nym.*`) + +Nym transport routes FIPS traffic through the Nym mixnet for +metadata-resistant anonymity. Outbound-only: connections are made +through a `nym-socks5-client` SOCKS5 proxy that must be running +separately (e.g. as a service running alongside the fips daemon or as a +container). There is no inbound listener — a Nym-only node initiates +outbound links but is not reachable for unsolicited inbound handshakes. + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `transports.nym.socks5_addr` | string | `"127.0.0.1:1080"` | `nym-socks5-client` SOCKS5 proxy address (host:port) | +| `transports.nym.connect_timeout_ms` | u64 | `300000` | Outbound connect timeout in milliseconds. Mixnet SOCKS5 connections traverse 3 mix nodes with timing obfuscation and can take several minutes, so this is generous (300s). | +| `transports.nym.mtu` | u16 | `1400` | Default MTU | +| `transports.nym.startup_timeout_secs` | u64 | `120` | Seconds to wait for `nym-socks5-client` to become ready at startup before giving up | + +**Named instances.** Like other transports, multiple Nym instances can +be configured with named sub-keys for different SOCKS5 proxy endpoints. + ### BLE (`transports.ble.*`) Bluetooth Low Energy transport using L2CAP Connection-Oriented Channels. @@ -889,6 +931,9 @@ node: backoff_base_secs: 0 backoff_max_secs: 0 forward_min_interval_secs: 2 + # lan: # uncomment to enable mDNS LAN discovery + # enabled: true # opt-in, default false + # scope: "my-mesh" # optional per-network scope filter tree: announce_min_interval_ms: 500 parent_hysteresis: 0.2 # cost improvement fraction for parent switch @@ -983,6 +1028,11 @@ transports: # # bind_addr: "127.0.0.1:8443" # # max_inbound_connections: 64 # # advertised_port: 443 # public-facing onion port for Nostr adverts + # nym: # uncomment to enable Nym mixnet transport (outbound-only) + # socks5_addr: "127.0.0.1:1080" # nym-socks5-client SOCKS5 proxy address + # connect_timeout_ms: 300000 # connect timeout (300s for mixnet) + # mtu: 1400 # default MTU + # startup_timeout_secs: 120 # wait for nym-socks5-client to be ready # ble: # uncomment to enable BLE transport (Linux only, requires BlueZ) # adapter: "hci0" # HCI adapter name # psm: 0x0085 # L2CAP PSM (133) diff --git a/docs/reference/control-socket.md b/docs/reference/control-socket.md index f59feb6..68c2007 100644 --- a/docs/reference/control-socket.md +++ b/docs/reference/control-socket.md @@ -115,6 +115,7 @@ table below lists every command currently registered. | `show_identity_cache` | — | `entries[]`, `count`, `max_entries`. Each entry: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `last_seen_ms`, `age_ms`. | | `show_listening_sockets` | — | `fips0_addr`, `firewall_active` (bool — `inet fips` table loaded), `sockets[]`. Each entry: `proto` (`tcp` / `udp`), `local_addr` (`::` or the node's fd00::/8 address), `port`, `pid` (nullable), `process` (nullable), `wildcard_bind` (bool — `local_addr == ::`), `filter` (`accept` / `drop` / `unknown` / `no_firewall`). Linux-only; returns an empty `sockets[]` on other platforms. | | `show_stats_list` | — | `metrics[]` (each with `name`, `unit`, `scope`), `fast_ring_seconds`, `slow_ring_minutes`, `peer_retention_seconds`. | +| `show_metrics` | — | Flat snapshot of every counter family in the metrics registry: `forwarding`, `discovery`, `tree`, `bloom`, `congestion`, `errors`. Each value is that family's counter snapshot object. Counter-only — gauges/histograms that need the live node are excluded. Served off the main loop. | | `show_stats_history` | `metric` (req), `peer` (req for per-peer metrics), `window` (`s` / `m` / `h`, default `10m`), `granularity` (`1s` / `1m`, default `1s`) | A single `Series`: `metric`, `unit`, `granularity_seconds`, `values[]`. | | `show_stats_all_history` | `peer` (optional npub), `window`, `granularity` | `granularity_seconds`, `window_seconds`, `peer`, `series[]` (one per metric). | | `show_stats_peers` | — | `peers[]`, `count`. Each entry: `npub`, `node_addr`, `display_name`, `is_active`, `first_seen_secs_ago`, `last_contact_secs_ago`. | @@ -128,7 +129,7 @@ fixtures. | Command | Required params | Behaviour | | ------- | --------------- | --------- | -| `connect` | `npub` (bech32), `address` (transport endpoint), `transport` (`udp`, `tcp`, `tor`, `ethernet`) | Asks the node to dial the peer over the named transport. Returns the API result on success or an error string on failure. | +| `connect` | `npub` (bech32), `address` (transport endpoint), `transport` (`udp`, `tcp`, `tor`, `nym`, `ethernet`) | Asks the node to dial the peer over the named transport. The named transport must be configured and running. Returns the API result on success or an error string on failure. | | `disconnect` | `npub` (bech32) | Asks the node to drop the link to the named peer. | Both commands run on the daemon's main task and may block briefly diff --git a/docs/reference/transports.md b/docs/reference/transports.md index f241722..114ce16 100644 --- a/docs/reference/transports.md +++ b/docs/reference/transports.md @@ -34,6 +34,8 @@ module. | `connections_rejected` | Rejected inbound connections (limit exceeded) | | `connect_timeouts` | Connection timeout count | | `connect_refused` | Connection refused count | +| `pool_inbound` | Current inbound connections held in the connection pool (gauge) | +| `pool_outbound` | Current outbound connections held in the connection pool (gauge) | ## Ethernet @@ -61,6 +63,23 @@ module. | `connections_accepted` | Accepted inbound connections via onion service | | `connections_rejected` | Rejected inbound connections (limit exceeded) | | `control_errors` | Tor control port errors | +| `pool_inbound` | Current inbound connections held in the connection pool (gauge) | +| `pool_outbound` | Current outbound connections held in the connection pool (gauge) | + +## Nym + +| Counter | Description | +| ------- | ----------- | +| `packets_sent` / `bytes_sent` | Successful sends | +| `packets_recv` / `bytes_recv` | Successful receives | +| `send_errors` / `recv_errors` | Send/receive failures | +| `mtu_exceeded` | Packets rejected for MTU violation | +| `connections_established` | Successful SOCKS5 connections through `nym-socks5-client` | +| `connect_timeouts` | Connection timeout count | +| `socks5_errors` | SOCKS5 protocol errors | + +Nym is outbound-only (no inbound listener), so there are no +`connections_accepted` / `connections_rejected` counters. ## Bluetooth diff --git a/docs/tutorials/host-a-service.md b/docs/tutorials/host-a-service.md index 92e0edd..6f8d16d 100644 --- a/docs/tutorials/host-a-service.md +++ b/docs/tutorials/host-a-service.md @@ -98,11 +98,10 @@ is what you want. Or via the daemon: ```sh -sudo fipsctl show identities +sudo fipsctl show status ``` -The first JSON entry has `local: true` and a `ula` field — that -is your address. +The JSON has an `ipv6_addr` field — that is your address. For the rest of this tutorial we will write the address as ``. Substitute the actual `fd97:...` value diff --git a/docs/tutorials/ipv6-adapter-walkthrough.md b/docs/tutorials/ipv6-adapter-walkthrough.md index 90f88d3..1ba2801 100644 --- a/docs/tutorials/ipv6-adapter-walkthrough.md +++ b/docs/tutorials/ipv6-adapter-walkthrough.md @@ -63,9 +63,11 @@ mesh address: dig npub1qmc3cvfz0yu2hx96nq3gp55zdan2qclealn7xshgr448d3nh6lks7zel98.fips AAAA +short ``` -You should see one AAAA record returning a `fd97:...` address. -The prefix is the FIPS ULA range (`fd00::/8`, with `fd97:...` -covering the address space derived from npubs). +You should see one AAAA record returning an address such as +`fd97:...`. The prefix is the FIPS ULA range (`fd00::/8`): only +the leading `fd` byte is fixed, and everything after it is hash +output derived from the npub, so the digits beyond `fd` vary per +node. The query went through `systemd-resolved` (or your platform equivalent), which routed `.fips` queries to the daemon's local diff --git a/docs/tutorials/reach-mesh-services.md b/docs/tutorials/reach-mesh-services.md index 54c61ce..d313946 100644 --- a/docs/tutorials/reach-mesh-services.md +++ b/docs/tutorials/reach-mesh-services.md @@ -56,8 +56,11 @@ hostname on the public internet. There is no separate the tool takes a hostname, it accepts a `.fips` hostname. > **Where the address comes from.** Every FIPS node's mesh -> address is the SHA-256 of its public key, truncated to the -> bottom 64 bits and prepended with `fd97:`. Names of the form +> address is the first 16 bytes of SHA-256 of its public key, +> with the leading byte replaced by `0xfd` (the `fd00::/8` ULA +> prefix). The remaining bytes are hash output, so an address +> like `fd97:...` is per-node — the `97` is part of the hash, +> not a fixed prefix shared across nodes. Names of the form > `.fips` and any shortname mapped in `/etc/fips/hosts` > are aliases for that address. The daemon's local DNS > responder hands the answer back to your kernel without ever diff --git a/examples/k8s-sidecar/README.md b/examples/k8s-sidecar/README.md index 1277ea0..54c29aa 100644 --- a/examples/k8s-sidecar/README.md +++ b/examples/k8s-sidecar/README.md @@ -39,7 +39,7 @@ fips sidecar: 4. Starts dnsmasq and then `exec`s the FIPS daemon. The app container starts concurrently and immediately sees `lo`, `eth0`, and -`fips0`. DNS for `.fips` names resolves to `fd::/8` addresses via the +`fips0`. DNS for `.fips` names resolves to `fd00::/8` addresses via the dnsmasq → FIPS daemon pipeline. ```text diff --git a/examples/sidecar-nostr-relay/README.md b/examples/sidecar-nostr-relay/README.md index 897eb99..3beafea 100644 --- a/examples/sidecar-nostr-relay/README.md +++ b/examples/sidecar-nostr-relay/README.md @@ -73,13 +73,14 @@ ws://npub1xxxx.fips:80 The sidecar pattern enforces strict network isolation on the app container: -- **No IPv4 access**: iptables blocks all eth0 traffic except FIPS UDP - transport (port 2121) and TCP transport (port 443). The app container +- **No IPv4 access**: iptables blocks all eth0 traffic except the FIPS UDP + transport (port 2121), the local FIPS TCP listener (port 8443), and + outbound TCP to peers' published endpoints (port 443). The app container cannot reach the Docker bridge, the host network, or any IPv4 address. - **No IPv6 on eth0**: ip6tables blocks all IPv6 traffic on eth0. The app container cannot use link-local or any Docker-assigned IPv6 addresses. - **FIPS mesh only**: The only routable network path is through `fips0` - (`fd::/8`). All application traffic traverses the FIPS mesh with + (`fd00::/8`). All application traffic traverses the FIPS mesh with end-to-end encryption. - **Loopback allowed**: `lo` is unrestricted for inter-process communication within the shared namespace. @@ -105,7 +106,7 @@ with the transport layer directly. │ Interfaces: │ │ lo — loopback (unrestricted) │ │ eth0 — Docker bridge (iptables: FIPS only) │ -│ fips0 — FIPS TUN (fd::/8, unrestricted) │ +│ fips0 — FIPS TUN (fd00::/8, unrestricted) │ └───────────────────────────────────────────────────┘ ``` @@ -118,7 +119,8 @@ before launching the FIPS daemon: - ACCEPT on `lo` (both directions) - ACCEPT UDP sport/dport 2121 on `eth0` (FIPS UDP transport) -- ACCEPT TCP dport 443 / sport 443 on `eth0` (FIPS TCP transport) +- ACCEPT TCP dport 443 / sport 443 on `eth0` (outbound to peers' TCP endpoints) +- ACCEPT TCP dport/sport 8443 on `eth0` (local FIPS TCP listener, `FIPS_TCP_BIND`) - DROP everything else on `eth0` **IPv6 rules** (ip6tables): @@ -132,7 +134,7 @@ before launching the FIPS daemon: DNS inside the container is handled by dnsmasq (127.0.0.1:53): - `.fips` queries are forwarded to the FIPS daemon's built-in DNS resolver - (127.0.0.1:5354), which resolves npub-based names to `fd::/8` addresses + (127.0.0.1:5354), which resolves npub-based names to `fd00::/8` addresses - All other queries are forwarded to Docker's embedded DNS (127.0.0.11) The `resolv.conf` mount points the container's resolver at 127.0.0.1, @@ -165,7 +167,8 @@ From the app container: # Ping a mesh node by npub (resolves via .fips DNS): docker exec sidecar-nostr-relay-app-1 ping6 -c3 npub1xxxx.fips -# Fetch a web page from a mesh node over FIPS: +# Fetch a web page from some other mesh node over FIPS +# (:8000 is a stand-in for that node's own service; this relay serves :80): docker exec sidecar-nostr-relay-app-1 curl -6 "http://npub1xxxx.fips:8000/" # Docker bridge is blocked — this should fail: diff --git a/examples/sidecar-nostr-relay/entrypoint.sh b/examples/sidecar-nostr-relay/entrypoint.sh index af40441..5e04cd3 100755 --- a/examples/sidecar-nostr-relay/entrypoint.sh +++ b/examples/sidecar-nostr-relay/entrypoint.sh @@ -70,6 +70,8 @@ iptables -A INPUT -i eth0 -p udp --dport 2121 -j ACCEPT iptables -A INPUT -i eth0 -p udp --sport 2121 -j ACCEPT iptables -A OUTPUT -o eth0 -p tcp --dport 443 -j ACCEPT iptables -A INPUT -i eth0 -p tcp --sport 443 -j ACCEPT +iptables -A INPUT -i eth0 -p tcp --dport "${FIPS_TCP_BIND##*:}" -j ACCEPT +iptables -A OUTPUT -o eth0 -p tcp --sport "${FIPS_TCP_BIND##*:}" -j ACCEPT iptables -A OUTPUT -o eth0 -j DROP iptables -A INPUT -i eth0 -j DROP diff --git a/examples/sidecar-nostr-relay/relay/config.toml b/examples/sidecar-nostr-relay/relay/config.toml deleted file mode 100644 index 10c8a96..0000000 --- a/examples/sidecar-nostr-relay/relay/config.toml +++ /dev/null @@ -1,49 +0,0 @@ -# nostr-rs-relay configuration for FIPS mesh deployment. -# Full reference: https://github.com/scsibug/nostr-rs-relay - -[info] -# Shown in NIP-11 relay information document. -relay_url = "" # Set to ws://[]:8080 after first start -name = "FIPS Nostr Relay" -description = "A Nostr relay accessible over the FIPS mesh network." -pubkey = "" # Optional: your npub (hex) as relay operator -contact = "" # Optional: contact address - -[database] -# SQLite database path inside the container (mapped to relay-data volume). -data_directory = "/usr/src/app/db" - -[network] -# Bind on all interfaces (IPv4 + IPv6) — FIPS delivers traffic via the fips0 -# TUN as IPv6 (fd00::/8). Using 0.0.0.0 with port 8080; the relay also needs -# to accept IPv6 connections so we set port separately and rely on the OS -# dual-stack socket (IPV6_V6ONLY=0). -# nostr-rs-relay address field is just the IP, port is separate. -address = "0.0.0.0" -port = 8080 -ping_interval_seconds = 300 - -[limits] -# Adjust to taste. These are conservative defaults for a personal relay. -messages_per_sec = 10 -subscriptions_per_min = 20 -max_blocking_threads = 4 -max_event_bytes = 131072 # 128 KiB per event -max_ws_message_bytes = 131072 -max_ws_frame_bytes = 131072 -broadcast_buffer = 16384 -event_persist_buffer = 4096 - -[authorization] -# Set to true to require NIP-42 AUTH before accepting events. -# Useful for a private relay — only authenticated users can publish. -nip42_auth = false -nip42_dms = false - -[verified_users] -# NIP-05 verification (optional). -mode = "disabled" - -[pay_to_relay] -# Lightning payments for relay access (optional). -enabled = false \ No newline at end of file diff --git a/examples/wireguard-sidecar-macos/fips.yaml b/examples/wireguard-sidecar-macos/fips.yaml index 98d25fa..b8bdf50 100644 --- a/examples/wireguard-sidecar-macos/fips.yaml +++ b/examples/wireguard-sidecar-macos/fips.yaml @@ -20,7 +20,9 @@ transports: peers: - npub: "npub1zv58cn7v83mxvttl70w5fwjwuclfmntv9cnmv5wmz2nzz88u5urqvdx96n" - alias: "fips.v0l.io" + # alias becomes the peer's .fips hostname (.fips), so it must be a + # plain label with no dots — the connection address below is the real host. + alias: "v0l" addresses: - transport: tcp addr: "fips.v0l.io:8443" From e03a1ac50bdb1f19b723107b07b61a4c9d48116f Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 14 Jun 2026 15:14:05 +0000 Subject: [PATCH 5/6] docs: correct stale doc-comments for LAN handshake and mesh-size poll_lan_discovery's comment said Noise XX, but LAN-discovered peers dial over UDP through initiate_connection, which uses Noise IK (IK at FMP). compute_mesh_size's header comment still described the obsolete sum-of-disjoint-subtrees estimate; the function OR-unions every connected peer's inbound filter plus self and estimates cardinality once (matching the body comment). Comment-only, no behavior change. --- src/node/lifecycle.rs | 4 ++-- src/node/mod.rs | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/node/lifecycle.rs b/src/node/lifecycle.rs index d2ba455..be9a4d5 100644 --- a/src/node/lifecycle.rs +++ b/src/node/lifecycle.rs @@ -901,9 +901,9 @@ impl Node { } } - /// Drain mDNS-discovered peers and initiate Noise XX handshakes. + /// Drain mDNS-discovered peers and initiate Noise IK handshakes. /// The handshake itself is the authentication — a spoofed mDNS advert - /// with someone else's npub fails the XX exchange and is dropped. + /// with someone else's npub fails the IK exchange and is dropped. pub(super) async fn poll_lan_discovery(&mut self) { let Some(runtime) = self.lan_discovery.clone() else { return; diff --git a/src/node/mod.rs b/src/node/mod.rs index 1120b75..e9bb248 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1307,9 +1307,12 @@ impl Node { /// Compute and cache the estimated mesh size from bloom filters. /// - /// Uses the spanning tree partition: parent's filter covers nodes reachable - /// upward, children's filters cover disjoint subtrees downward. The sum - /// of estimated entry counts plus one (self) approximates total network size. + /// Builds an OR-union of self plus every connected peer's inbound filter + /// and estimates its cardinality once. Unioning (rather than summing + /// per-peer counts) deduplicates the overlap between the split-horizon + /// filters, each of which approximates "the whole mesh minus my subtree". + /// See the body for why all routing peers contribute, not just the tree + /// neighborhood. pub(crate) fn compute_mesh_size(&mut self) { let my_addr = *self.tree_state.my_node_addr(); From d3cf1d6f25314a8033498984fc7402764e03d398 Mon Sep 17 00:00:00 2001 From: Johnathan Corgan Date: Sun, 14 Jun 2026 17:23:44 +0000 Subject: [PATCH 6/6] Land v0.4.0 pre-release source content: docs, changelog, packaging Bring the source tree to its finished v0.4.0 content state ahead of the release candidate. Documentation, changelog, release notes, and packaging metadata only; no code or version-string changes. CHANGELOG.md: backfill the operator-visible changes that landed since v0.3.0 (the show_metrics scraper query and fipsctl stats metrics, the discovery dedup-cache-full counter, the off-rx_loop control read surface and its new daemon-resolved fields, the fipstop TUI overhaul, the TCP inbound cap now honoring max_connections, host-map hot-reload, log-noise demotions, and the net bug fixes), topic-grouped rather than replayed per commit; intra-cycle fixes folded into their feature entries; the duplicate Unreleased Fixed section merged into one. Release notes: author docs/releases/release-notes-v0.4.0.md to the operator-upgrade bar and mirror it byte-for-byte into the root RELEASE-NOTES.md. Attribute each feature to its author in Contributors (Nym transport and the mixnet demo to @oleksky, opt-in mDNS LAN discovery to @mmalmi). README.md: add the Nym transport to the support matrix (Linux, macOS, Windows; OpenWrt pending verification), the multi-transport bullet, and the "What works today" list; fold mDNS LAN discovery into the existing Nostr-discovery bullets; refresh the status narrative to v0.4.0 over a global, public test mesh. packaging/common/fips.yaml: add commented Nym transport and mDNS LAN discovery example stanzas with verified field names and defaults. Cargo.toml: add homepage, keywords, and categories crate metadata. docs/reference: update the cli-fips version example to the released form; document the new control-socket output fields and the typed RejectReason families in control-socket.md; sync cli-fipstop.md with the overhauled TUI keybindings. --- CHANGELOG.md | 177 +++-- Cargo.toml | 3 + README.md | 35 +- RELEASE-NOTES.md | 935 +++++++------------------- docs/reference/cli-fips.md | 2 +- docs/reference/cli-fipstop.md | 51 +- docs/reference/control-socket.md | 36 +- docs/releases/release-notes-v0.4.0.md | 279 ++++++++ packaging/common/fips.yaml | 29 + 9 files changed, 774 insertions(+), 773 deletions(-) create mode 100644 docs/releases/release-notes-v0.4.0.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 23dc52d..d3b6137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Fixed - -- The Tor transport now increments its `connect_refused` statistic (the - "Refused" line in fipstop) when a SOCKS5 connection is actively - refused, instead of recording every connect failure as a generic - SOCKS5 error. The counter previously stayed at zero. -- MMP sender metrics now ignore duplicate or regressed receiver reports - before updating RTT, loss, goodput, or ETX. Receiver reports also - suppress timestamp echo when dwell time overflows, so stale reports - cannot inflate SRTT. - ### Added - Nym mixnet transport (`transports.nym`) for outbound peer links @@ -70,6 +59,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 is updated at every pool-insert and receive-loop-exit site, plus on transport stop and on send-failure-driven removal. Surfaces through `TcpStatsSnapshot` and `TorStatsSnapshot` for `show_transports`. +- `fipsctl stats metrics`, backed by a new counter-only `show_metrics` + control query that dumps the atomic metric registry as flat counter + name/value pairs. Serves a Prometheus-style scraper that samples node + counters without contending the receive loop. +- Discovery now counts `LookupRequest`s dropped when the dedup cache is + full. A saturated `recent_requests` cache + (`MAX_RECENT_DISCOVERY_REQUESTS`) previously dropped requests + silently; a new `DiscoveryStats::req_dedup_cache_full` counter (typed + reject reason `DiscoveryReject::ReqDedupCacheFull`) makes the drop + visible through `show_routing`. - [`PR-REVIEW.md`](PR-REVIEW.md) — the 13-criteria PR review checklist the maintainer runs against every incoming PR, published at the repo root so contributors can run the same pass on their own change @@ -118,6 +117,52 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 restores headroom toward the fixed-filter capacity limit without materially weakening the antipoison gate: a saturated or poisoned filter is ~100% FPR and still rejected. +- TCP inbound connection cap now honors `node.limits.max_connections`. + The per-transport TCP inbound accept ceiling was hardwired to 256 and + never read `max_connections`, so raising it was a silent no-op for + inbound TCP. The effective cap now resolves with precedence: explicit + per-transport `max_inbound_connections`, then node-wide + `max_connections`, then the built-in default of 256. Established peers + remain bounded node-wide by `add_connection`. +- The control-socket read surface is now served off the `rx_loop`. + Every pure-read `show_*` query — `show_status`, the `show_stats_*` + family, `show_listening_sockets`, the new `show_metrics`, + `show_tree`/`show_bloom`/`show_cache`/`show_routing`/ + `show_identity_cache`, `show_peers`/`show_sessions`/`show_links`/ + `show_connections`/`show_transports`/`show_mmp`, and `show_acl` — now + renders in the control accept task from ArcSwap-published read + snapshots instead of round-tripping the data-plane receive loop; only + the mutating `connect`/`disconnect` commands still reach the loop. + This removes the head-of-line coupling where a busy or slow `rx_loop` + could time out `fipsctl` and `fipstop` observability (the five-second + query pattern operators saw on loaded nodes). Per-entity snapshots + reuse unchanged rows by pointer, so per-tick publish cost stays + bounded as peer/session count grows. New daemon-resolved fields + surface through the snapshots: effective persistence, root/is-root, + and a per-transport-type peer-count map in `show_status`; per-peer + `effective_depth` in `show_peers`; `root_npub` in `show_tree`; and the + last-sent uptree filter fill ratio and subtree estimate in + `show_bloom`. +- `fipstop` TUI overhaul: reworked rendering, navigation model, and the + control read surface it draws from, surfacing the new daemon-resolved + snapshot fields above. Built on a ratatui `TestBackend` + render-snapshot harness that asserts the text grid and per-cell + style of every `ui::draw_*` against canned `show_*` JSON. +- Static host aliases in `/etc/fips/hosts` now hot-reload on mtime + change instead of only at daemon startup, so `fipsctl`/`fipstop` + display names reflect edits without a restart. The peer ACL and host + map both reload once per node tick through a new lock-free + `Reloadable` snapshot. +- Steady-state log noise reduced on saturated public-mesh nodes. + Routine per-peer connection-lifecycle and capacity-cap events are + demoted from info/warn to debug — FMP K-bit cutover promotion, + connection-promoted-to-active-peer (a redundant duplicate + promotion line removed), peer-restart-detected, peer-removed-and- + cleaned-up, the TCP `max_inbound_connections`-reached rejection, and + the congestion-CE-flag line — so genuinely notable info/warn lines + are no longer drowned out. An exhausted FMP-msg1 / FSP-msg3 rekey + retransmission-budget abort (an expected, self-limiting outcome on + lossy or high-latency links) is likewise demoted from warn to debug. - Sidecar example (`examples/sidecar-nostr-relay`): `udp.mtu` is now overridable via the `FIPS_UDP_MTU` environment variable, defaulting to 1472 (preserving prior behavior). Plumbed through `docker-compose.yml` @@ -219,7 +264,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 medians, Linux x86_64, docker-bridge mesh): A→D 1379→2708 Mbps (1.96×), A→E 1394→2663 Mbps (1.91×), E→A 1406→2624 Mbps (1.87×); RTT +0.11–0.19 ms from the worker queue handoff. Windows - continues on the existing tokio-based send/recv path. + continues on the existing tokio-based send/recv path. Two issues in + the off-rx_loop drain path are resolved as part of the overhaul: the + per-peer drain worker is now detached on `Drop` rather than joined + synchronously (a synchronous join from the runtime thread could wedge + the whole daemon when a peer was removed with an in-flight worker), + and the connected-UDP drain no longer busy-spins on a poll error + (#106). - The Debian package no longer ships `/etc/fips/fips.yaml` as a dpkg conf-file. The default configuration is installed as an example at @@ -256,6 +307,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- The Tor transport now increments its `connect_refused` statistic (the + "Refused" line in fipstop) when a SOCKS5 connection is actively + refused, instead of recording every connect failure as a generic + SOCKS5 error. The counter previously stayed at zero. +- MMP sender metrics now ignore duplicate or regressed receiver reports + before updating RTT, loss, goodput, or ETX. Receiver reports also + suppress timestamp echo when dwell time overflows, so stale reports + cannot inflate SRTT. - Six discovery counters (`req_decode_error`, `req_duplicate`, `req_ttl_exhausted`, `resp_decode_error`, `resp_identity_miss`, `resp_proof_failed`) no longer double-count. Each was incremented both @@ -337,25 +396,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 accounting change. Operators with mixed outbound + inbound deployments no longer see legitimate inbound peers rejected once outbound connections fill the pool past the configured cap. -- `PeerRecvDrain::drop` no longer calls `std::thread::join` on the - worker thread. The drain worker uses `packet_tx.blocking_send(...)` - on a tokio mpsc Sender, which internally parks the worker in - `tokio::block_on` on the same `current_thread` runtime that drives - `rx_loop`. Joining synchronously from inside `remove_active_peer` - (which runs on the runtime thread, the runtime's sole driver) - produced a circular wait: rx_loop blocked in libc futex via - `Thread::join`, the worker unable to observe the stop flag because - the runtime that polls it is the very thread now blocked joining - it, and all other peer-drain workers parked on the same runtime - via `block_on`. Full daemon wedge, fipsctl unresponsive, SIGTERM - ignored. Trigger was peer-removal via the 30-s link-dead-timeout - cleanup path with any in-flight worker, with statistical likelihood - amplified by aggressive multi-npub-from-one-NAT reconnect patterns - but not bounded to them. Fix: detach the std::thread (drop the - `JoinHandle` without joining); the stop flag + self-pipe write - already signal the worker to exit; the kernel-level `libc::poll()` - inside the drain loop sees the wake, checks the flag, exits, and - the OS reclaims the thread state independently. - Outbound connection initiation now honors the `node.limits.max_peers` cap that was previously only checked on inbound msg1 admission. Four paths gated: auto-reconnect retries (`process_pending_retries`), @@ -379,13 +419,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 saturation: ~3.6 cap-denials/s × Msg2 (~104 B + AEAD compute) each. Bigger win is cleaner peer-side semantics — no fake-completed handshake whose subsequent data frames fail decryption on this side. -- Mesh-size estimator (`compute_mesh_size`) no longer double-counts the - parent's bloom cardinality during the transient cache window after a - local parent-switch. Symptom: `fipsctl show status` / fipstop displayed - mesh size nearly-but-not-exactly doubling during tree rebalancing. - Fix: explicit parent-skip at the head of the children loop, making the - disjoint-subtree invariant structural rather than dependent on - `peer_declaration` cache freshness. Per-peer 500 ms rate-limiter and +- The mesh-size estimator (`compute_mesh_size`) no longer over-counts + under filter overlap. It previously summed the per-filter cardinality + of the parent and each child filter, which assumes the filters are + perfectly disjoint; a stale or oversized parent filter or a routing + loop inflated the reported mesh size to several times the true value, + and dropping the parent on a tree rebalance collapsed the upward leg + and flapped the count (the symptom operators saw as the size + nearly-but-not-exactly doubling during rebalancing). The estimator now + computes the cardinality of the OR-union over self plus every + connected peer's inbound filter, dropping the parent/child tree gating + entirely. + OR is idempotent, so any overlap is deduplicated — the result equals + the old sum in the disjoint case, stays correct under overlap, damps + the parent-switch flap, and removes the estimate's dependence on + tree-declaration cache freshness. The per-peer 500 ms rate-limiter and overall recompute cadence are unchanged. - Spanning-tree state distribution is now eventually-consistent. Previously every `send_tree_announce_to_all` call site fired only @@ -412,6 +460,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 so any partition healed by the periodic broadcast at T+60 lands inside the convergence window; `wait_for_full_baseline` early-exits on PASS, so successful reps see no extra wall-clock. +- A single-uplink node stranded out of the tree now re-attaches within + a round-trip instead of waiting for the periodic re-broadcast cadence. + A node with one tree peer has periodic parent re-evaluation disabled, + so a lost one-shot attaching `TreeAnnounce` left it self-rooted and + unreachable until the next periodic re-broadcast + (`reeval_interval_secs` later). Tree-position exchange is now + self-healing on the receive path: when an accepted `TreeAnnounce` + advertises a root strictly worse (higher NodeAddr; election is + smallest-wins) than our own, we echo our current declaration back to + that peer, provoking the better-rooted peer to re-push its real + position immediately. The echo fires only in that one direction and is + bounded by the existing per-peer rate limiter. - `rx_loop` tick-arm stall under convergence-phase mesh pressure is eliminated. Previously, the tick body's per-peer `check_*` @@ -583,6 +643,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 future caller bypasses the outer dispatch check. As a side benefit, narrows a cooldown-poisoning vector previously available to an attacker injecting stale failure events for an active peer. +- A manual `fipsctl disconnect` now notifies the peer so teardown is + symmetric. Previously a manual disconnect tore down only the local + side and sent the peer nothing, so the peer kept its session and never + re-emitted its tree and filter announcements; on reconnect it was + never re-adopted as a child and its bloom filter was never recorded. + The local side now sends the disconnected peer a scoped `Disconnect` + (the same message graceful shutdown sends), so both ends tear down and + re-handshake cleanly on the next connection. +- `fips-gateway` no longer drops long-lived or DNS-cached client + mappings while traffic is still flowing. The virtual-IP pool's TTL + clock advanced only on DNS re-query, never on traffic, and the mapping + TTL is wired equal to the DNS TTL, so an in-use mapping was forced to + drain at TTL and reclaimed at the first zero-conntrack tick — breaking + long-lived, bursty, or DNS-cached clients. The tick now refreshes the + mapping's last-referenced time whenever conntrack reports active + sessions, and recovers a draining mapping to active (with a fresh + grace window) when traffic resumes; only genuinely idle mappings + drain. +- Transport-layer mutex poisoning no longer cascades. Ten + `Mutex::lock().unwrap()` sites across the UDP, BLE, and Ethernet + transports would turn a single panic (poisoning the mutex) into a + cascade of panics on every subsequent lock. Each is replaced with + `lock().unwrap_or_else(|e| e.into_inner())`, recovering the guarded + data with no new dependency and no call-graph change; four + `local_addr.unwrap()` calls on the UDP start/adopt paths get a + provably-safe sentinel fallback. The critical sections are short, + locally-scoped, and not reachable from peer input, so this is + robustness hardening, not a remotely-triggerable fix. +- `fipstop` no longer renders a garbled screen on startup or leaves + stray bytes on quit, most visible over SSH and inside tmux. Startup + forces a full repaint (`terminal.clear()`) before the first draw so + prior alternate-screen contents no longer show through; quit gives the + stdin-poll thread a stop flag and joins it before restoring the + terminal, so post-raw-mode keystrokes or terminal query responses no + longer echo onto the restored screen. +- macOS `.fips` name resolution now works on a fresh install: the + shipped resolver shim points at `::1`, matching the daemon's default + IPv6 DNS listener, instead of `127.0.0.1`. The mismatched shim + (`nameserver 127.0.0.1` while the daemon listens on `::1`) broke + `getaddrinfo` for `.fips` on every macOS install since the resolver + was introduced. ## [0.3.0] - 2026-05-11 diff --git a/Cargo.toml b/Cargo.toml index 4b09f82..80dfa76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,10 @@ description = "A distributed, decentralized network routing protocol for mesh no license = "MIT" authors = ["Johnathan Corgan "] repository = "https://github.com/jmcorgan/fips" +homepage = "https://fips.network" readme = "README.md" +keywords = ["mesh", "p2p", "decentralized", "overlay-network", "nostr"] +categories = ["network-programming", "command-line-utilities", "cryptography"] [dependencies] ratatui = "0.30" diff --git a/README.md b/README.md index 66e69aa..66c3d61 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ same way it would on a local network. - **Self-organizing mesh routing.** Spanning-tree coordinates with bloom-filter-guided discovery; no global routing tables, no flooding. -- **Multi-transport.** UDP, TCP, Ethernet, Tor, and Bluetooth (BLE - L2CAP) ship today; transports compose on a single mesh and a +- **Multi-transport.** UDP, TCP, Ethernet, Tor, Nym, and Bluetooth + (BLE L2CAP) ship today; transports compose on a single mesh and a node may run several at once. - **Two-layer encryption.** Noise IK between peers (hop-by-hop) and Noise XK between mesh endpoints (independent end-to-end), with @@ -55,7 +55,8 @@ same way it would on a local network. - **Nostr-mediated discovery and NAT traversal.** Peers publish endpoint adverts on public Nostr relays, exchange candidates via NIP-59 gift-wrapped offers and answers, and establish direct - paths through NATs using STUN-assisted hole punching. + paths through NATs using STUN-assisted hole punching. On the local + network, mDNS LAN discovery finds peers directly without relays. - **LAN gateway.** Optional `fips-gateway` service folds an entire unmodified LAN into the mesh: outbound (LAN clients reach mesh destinations through a DNS-allocated virtual IPv6 pool and @@ -120,6 +121,7 @@ supported; transport availability varies by platform. | TCP | ✅ | ✅ | ✅ | ✅ | | Ethernet | ✅ | ✅ | ❌ | ✅ | | Tor | ✅ | ✅ | ✅ | ✅ | +| Nym | ✅ | ✅ | ✅ | ❌ | | BLE | ✅ | ❌ | ❌ | ❌ | On Linux, a source build requires `libclang` — the LAN gateway's @@ -136,6 +138,11 @@ gated on a build-script probe — install the dependencies first and the `cargo build` line above picks it up. The OpenWrt ipk omits BLE because libdbus is not available on the target. +Nym (mixnet) transport builds on all desktop platforms. The OpenWrt +❌ is provisional, pending verification of `nym-socks5-client` +availability on the target; it will flip to ✅ only if confirmed +buildable there. + ## Documentation `docs/` is organised by reader purpose: @@ -197,14 +204,14 @@ testing/ Docker-based integration test harnesses + chaos simulation ## Status & roadmap -FIPS is at **v0.3.0**. The core protocol works end-to-end over -UDP, TCP, Ethernet, Tor, and Bluetooth on a small live mesh of -deployed nodes. v0.3.0 is the testing-and-polishing track for -everything accumulated since v0.2.0 on the v0.2.x wire format — -Nostr-mediated peer discovery, UDP NAT traversal, peer ACL, the -DNS-responder fix, packaging hardening, and discovery rate-limit -retuning. New wire-format work is staged on the `next` branch for -the post-v0.3.0 release line. +FIPS is at **v0.4.0**. The core protocol works end-to-end over +UDP, TCP, Ethernet, Tor, Nym, and Bluetooth on a global, public test +mesh of thousands of nodes. v0.4.0 builds on the v0.3.0 testing-and-polishing +track, adding the Nym mixnet transport and mDNS LAN discovery +alongside the existing Nostr-mediated peer discovery, UDP NAT +traversal, peer ACL, and packaging hardening. New wire-format work +continues to be staged on the `next` branch for the subsequent +release line. ### What works today @@ -222,10 +229,10 @@ the post-v0.3.0 release line. estimation. - ECN congestion signaling (hop-by-hop CE relay, IPv6 CE marking, kernel-drop detection). -- UDP, TCP, Ethernet, Tor, and BLE transports (BLE via L2CAP CoC - with per-link MTU negotiation). +- UDP, TCP, Ethernet, Tor, Nym (mixnet), and BLE transports (BLE + via L2CAP CoC with per-link MTU negotiation). - Nostr-mediated overlay endpoint discovery and UDP hole punching - for NAT traversal. + for NAT traversal, plus mDNS LAN discovery for local peers. - LAN gateway (`fips-gateway`) with both outbound (LAN-to-mesh) and inbound (mesh-to-LAN port-forwarding) modes. - Peer ACL: per-npub allow / deny admission control at the link diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index fe533e9..0d62767 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,696 +1,262 @@ -# FIPS v0.3.0 +# FIPS v0.4.0 -**Released**: 2026-05-11 +**Released**: 2026-06-DD (provisional) -v0.3.0 is the testing-and-polishing release on the v0.2.x wire format. -It widens the platform reach of FIPS from Linux-only to Linux, macOS, -Windows, and OpenWrt; adds two large new mesh capabilities (Nostr-mediated -peer discovery with UDP NAT traversal, and the `fips-gateway` LAN bridge); -ships a default-deny security baseline for the mesh interface; introduces -mesh-peer access control; substantially speeds up session-layer crypto and -the Linux receive path; and tightens packaging across every supported -distribution channel. +v0.4.0 is the throughput-and-observability release on the v0.3.x wire +format. It adds two new ways for nodes to find and reach each other (the +Nym mixnet transport and opt-in mDNS LAN discovery), overhauls the data +plane for higher single-node throughput and lower per-packet CPU, moves +the entire operator read surface off the data-plane hot path so +observability stays responsive under load, ships a reworked `fipstop` +TUI, and hardens FMP and FSP rekey to be hitless under packet loss in +both directions. It also folds in the accumulated mesh-convergence, +admission-control, and packaging fixes from the maintenance line. -v0.3.0 is wire-compatible with v0.2.x. Mixed meshes interoperate; there -is no flag-day upgrade. - -v0.3.0 also rolls forward all changes from the v0.2.1 maintenance -release. The sections below cover the cumulative v0.2.0 → v0.3.0 -delta; the per-section intros call out which entries first shipped -in v0.2.1. +v0.4.0 is wire-compatible with v0.3.0. Mixed meshes interoperate; there +is no flag-day upgrade. A deployed v0.3.0 node and an upgraded v0.4.0 +node peer, rekey, and route normally, so you can roll the upgrade out +across a mesh in any order. ## At a glance -- 123 commits since v0.2.0 (109 non-merge), spanning 307 files with - +44,186 / -4,078 lines. -- 10 committers plus 3 issue reporters across feature work, fixes, - packaging, and reviews. -- 5 new GitHub Actions CI workflows (Linux Package, macOS Package, - Windows Package, OpenWrt Package, AUR Publish) plus expanded - integration matrices (gateway, NAT-cone, NAT-symmetric, NAT-LAN, - rekey-accept-off, `.deb` install across Debian 12/13 + Ubuntu - 22/24/26, multi-backend `.fips` DNS resolver across the same five - distros). -- The long-standing systemd-resolved DNS-responder silent-drop is - closed end-to-end. -- Pre-1.0 control-socket JSON schema change for two query fields; - see [Upgrade notes](#upgrade-notes). +- New outbound Nym mixnet transport with a single-container demo and a + new mixnet-relay example. +- Opt-in mDNS / DNS-SD discovery on the local link. +- Data-plane overhaul: off-task encrypt and decrypt worker pools, GSO, + connected-UDP send path, copy-avoidance on receive, batched macOS + receive. +- The full `show_*` read surface now serves off the receive loop, so + `fipsctl` and `fipstop` stay responsive on loaded nodes; a new + counter-only `show_metrics` query enables a Prometheus scraper at no + hot-path cost. +- Reworked `fipstop` TUI on a machine-verified render-snapshot base. +- Rekey is now hitless under loss and reordering in both directions. ## What's new -### Mesh discovery and NAT traversal +### Nym mixnet transport -Previously, two FIPS nodes could only become peers if they had a way -to find each other beforehand: a configured address, a shared LAN -segment, or a Bluetooth radio range. v0.3.0 introduces a Nostr-based -overlay-discovery channel that lets nodes find each other through any -public Nostr relay set, plus a STUN-assisted UDP hole-punching path -that connects peers across most consumer NATs. +FIPS can now peer over the [Nym](https://nymtech.net/) mixnet for +metadata-resistant connectivity. The new `transports.nym` transport +makes outbound connections through a `nym-socks5-client` SOCKS5 proxy +that you run alongside the daemon (for example as a service running +alongside the fips daemon, or as a sidecar container). The transport +waits at startup for the nym-socks5-client to become ready before giving +up. -Each participating node publishes a signed overlay advert as a Nostr -**Kind 37195** parameterized replaceable event. (The kind sits in the -application-defined replaceable range and the digits visually spell -*FIPS*: 7=F, 1=I, 9=P, 5=S.) The advert lists reachable transport -endpoints (UDP, TCP, Tor) and is consumed by other nodes to populate -fallback addresses for `via_nostr` peers. Under `policy: open`, the -advert cache is also dialed for non-configured peers within a budget -cap. +This is a privacy and anonymity deployment mode chosen for its own +properties. It mixes your FIPS traffic into the Nym cover-traffic +network so that link-level observers cannot correlate which mesh peers +are talking. A new `examples/sidecar-nostr-mixnet-relay/` demonstrates a +FIPS-reachable Nostr relay peered across the mixnet end to end, and a +single-container demo ships with the transport. -When both peers are behind NAT, the daemon coordinates a UDP hole -punch using NIP-59 gift-wrap signaling for the offer/answer exchange -and STUN for reflexive address discovery. A candidate-pair punch -planner attempts LAN-private and reflexive paths in parallel; on -success the live socket is handed into the standard FIPS UDP transport -via a bootstrap-handoff API. +Enable it by adding a `transports.nym` instance and pointing it at your +running nym-socks5-client. See the transports reference for the field +set. -Operators turn this on with `node.discovery.nostr.enabled: true` and -the configured relay set. `policy: open` adds best-effort dialing of -non-configured peers seen on the relays. New `peers[].via_nostr` and -per-transport `advertise_on_nostr` / `public` flags control what each -endpoint contributes to the published advert. Cross-field validation -runs at startup to catch mis-configured combinations early. +### mDNS LAN discovery -A Docker NAT lab covering cone, symmetric, and LAN scenarios is wired -into the integration CI matrix. A daemon-side failure-suppression -layer (per-npub cooldown after consecutive failures, ±60s clock-skew -tolerance, rate-limited WARN logs) keeps relay traffic well-mannered -when peers come and go from the open discovery cache. A separate -structural cooldown (`protocol_mismatch_cooldown_secs`, default 24h) -suppresses retraversal when a punched peer turns out to be running an -FMP version this daemon cannot handshake with: the punch completes at -the UDP layer, the rx loop spots the version-mismatched packet, -reverse-maps to the originating npub, and removes the peer from the -next sweep until either side upgrades. +Nodes on a shared local link can now find each other with zero address +configuration. The opt-in `node.discovery.lan` path runs an mDNS / +DNS-SD responder and browser: each node advertises a FIPS service record +on the link and adopts the peers it discovers. This complements the +existing Nostr-mediated overlay discovery for the common case where the +peers are simply on the same LAN. -The auto-connect retry loop pins itself to relay ground truth. Each -retry attempt refetches the cached overlay advert against the -configured `advert_relays` (one filter query, 2s timeout) before -dialing, so a peer whose NAT rebound to a fresh endpoint is recovered -on the next retry rather than looping on a stale cached address. -`NoTransportForType` triggers a fire-and-forget re-fetch that either -replaces or evicts the cache entry. A startup peer-init failure (no -operational transport, all addresses unreachable) now schedules a -retry instead of leaving the peer in a dead state until the daemon is -restarted. Adopted NAT-traversed UDP transports inherit the operator's -primary `[transports.udp]` listener config (MTU, recv/send buffer -sizes) instead of falling back to the 1280 IPv6-minimum default. +Turn it on with `node.discovery.lan.enabled: true`. `service_type` and +`scope` tune the advertised service record and which interfaces +participate. Discovery on the local link needs no relay and no STUN. -### Cross-platform reach +### Data-plane throughput overhaul -FIPS now ships first-class binaries for **Linux, macOS, Windows, and -OpenWrt**. +The receive and send paths were reworked for higher single-node +throughput and lower per-packet CPU, building on the v0.3.0 +crypto-backend swap: -- **macOS** support uses the native `utun` TUN interface, raw - Ethernet via BPF, a `.pkg` installer with a launchd plist and - uninstall script, and an x86_64 cross-compile from arm64 build - hosts. A new CI matrix entry runs build and unit-test jobs on - macOS hosts. -- **Windows** support uses [wintun](https://www.wintun.net/) for the - TUN device, a TCP control socket on `localhost:21210` (replacing - the Unix domain socket Linux and macOS use), Windows Service - lifecycle (`fips.exe --install-service`, `--uninstall-service`, - `--service`), and a ZIP package with PowerShell install/uninstall - scripts. -- **MIPS** atomic-ABI portability lets the daemon build for 32-bit - MIPS targets (`mips`, `mipsel`, MIPS32r2) by routing through - `portable_atomic`. This unblocks OpenWrt deployments on - consumer-grade MIPS routers. -- **OpenWrt** packaging gets a procd init with dnsmasq forwarding, - proxy NDP, RA route advertisements, and IPv6 forwarding sysctls. - The `fips-gateway` is enabled by default in the OpenWrt build. +- **Off-task encrypt and decrypt.** Per-peer encrypt and decrypt now run + on dedicated worker tasks rather than inline on the receive loop, so a + single busy peer no longer serializes the whole node's crypto. +- **GSO and connected-UDP send.** The Linux send path uses generic + segmentation offload and a connected-UDP socket where available, + cutting syscall overhead on bulk flows. +- **Copy-avoidance on receive.** The receive hot path avoids buffer + copies it previously made per packet. +- **Batched macOS receive.** macOS gains a `recvmsg_x` batched receive, + mirroring the Linux `recvmmsg` batching from v0.3.0. +- **Shared immutable-state context and an atomic metric registry.** + Immutable per-node state moved into a single shared context, and + counters live in an atomic metric registry that the new `show_metrics` + query reads without touching the hot path. -### FIPS gateway +These are all internal to the data plane and require no operator action. -The new `fips-gateway` binary lets unmodified LAN hosts reach FIPS -mesh destinations without running the FIPS daemon themselves. Two -flows ship together: +### Observability off the hot path -- **Outbound (LAN -> mesh)**: a virtual-IP pool (default - `fd01::/112`) is allocated on demand from `.fips`-name DNS lookups. - A state-machine lifecycle, conntrack-backed session tracking, proxy - NDP on the LAN interface, and TTL-based reclamation handle the - bookkeeping. A LAN host that resolves `peer.fips` gets a virtual - address it can reach over IP, and the gateway translates the flow - to the mesh. -- **Inbound (mesh -> LAN)**: new `gateway.port_forwards` config - installs prerouting DNAT rules so mesh peers can reach a configured - `host:port` on the gateway's LAN. A LAN-side masquerade is added - automatically when any forwards are configured, so replies flow - back through conntrack. +Every read-only control query now renders from a snapshot published once +per tick into a lock-free `ArcSwap`, served from the control accept task +instead of round-tripping the data-plane receive loop. This covers +`show_status`, `show_stats_*`, `show_peers`, `show_sessions`, +`show_links`, `show_connections`, `show_transports`, `show_mmp`, +`show_tree`, `show_bloom`, `show_cache`, `show_routing`, +`show_identity_cache`, `show_acl`, `show_listening_sockets`, and the new +`show_metrics`. Only the mutating `connect` and `disconnect` commands +still reach the loop. -A dedicated control socket at `/run/fips/gateway.sock` exposes -`show_gateway` and `show_mappings`. `fipstop` adds a Gateway tab with -a pool gauge and mappings table. +The practical effect: on a loaded node where the receive loop was busy, +`fipsctl` and `fipstop` queries previously stalled or timed out (the +five-second query pattern operators saw). They now answer promptly +regardless of data-plane load. Per-entity snapshots reuse unchanged rows +by pointer, so the per-tick publish cost stays bounded as peer and +session counts grow. -The gateway's `dns.listen` source default is now `[::1]:5353`, -matching the canonical deployment model: the gateway sits on a host -already serving DHCP and DNS to a LAN segment (an OpenWrt AP, a Linux -router), port 53 there is taken by the existing resolver, and `.fips` -queries are forwarded to the gateway over loopback. The OpenWrt ipk -previously overrode the prior `[::]:53` source default in its packaged -config; that override is now redundant and has been dropped. -Operators on a host without a pre-existing resolver on port 53 can -opt back into the wildcard bind by setting `dns.listen: "[::]:53"` -explicitly. The new default binds IPv6 loopback only, so forwarders -that reach the gateway over IPv4 loopback need an explicit IPv4 -listen address. +A new **`show_metrics`** query (surfaced as `fipsctl stats metrics`) +returns a counter-only snapshot of every metric family. It is the +enabler for a Prometheus scraper that pulls node counters at no hot-path +cost. -The cold-boot startup race between `fips.service` and -`fips-gateway.service` is handled by a systemd `After=fips.service` -ordering, an `ExecStartPre` poll loop that waits up to 30 seconds for -the `fips0` interface to appear, and a DNS upstream probe in the -gateway itself that retries up to 5 times with 1-second backoff. +### Reworked fipstop TUI -Packaging covers systemd, Debian, AUR, and OpenWrt. The full design -is in [`docs/design/fips-gateway.md`](../design/fips-gateway.md). +`fipstop` gets a rendering, navigation, and read-surface overhaul on a +machine-verified base: a render-snapshot harness asserts the exact text +grid and per-cell style of every view against canned control-socket +output. New daemon-resolved fields surface through the snapshots, +including effective persistence, root and is-root state, a +per-transport-type peer-count map, per-peer effective depth, the root +npub, and the last-sent uptree filter fill ratio with the subtree size +estimate. -### Mesh-interface security baseline +A separate fix clears a garbled-screen problem on startup and stray +bytes on quit, most visible over SSH and inside tmux: startup now forces +a full repaint before the first draw, and quit stops and joins the +stdin-poll thread before restoring the terminal, so post-raw-mode +keystrokes no longer echo onto the restored screen. -The FIPS mesh is a flat layer-3 segment. Every authenticated peer can -route packets to every other peer's `fips0` address. Peer identity is -authenticated end-to-end by the FMP and FSP Noise handshakes, but -identity is not authorization. A service on a mesh host that binds to -a wildcard address is, by default, reachable from every peer in the -mesh. +### Rekey reliability -v0.3.0 ships an opt-in default-deny baseline that closes this gap on -Linux: +FMP and FSP session rekey are now hitless under packet loss and +reordering in both directions: -- **`/etc/fips/fips.nft`** is installed as a documented operator - conffile. It defines a single `inet fips` nftables table with one - chain hooked at `input`, default-denies inbound traffic on - `fips0`, and is a no-op for every other interface. -- **`fips-firewall.service`** loads it. The unit ships **disabled by - default**; activation is an explicit - `systemctl enable --now fips-firewall.service`. -- Per-service allowances live in **`/etc/fips/fips.d/*.nft`** - drop-ins that the baseline includes. +- Inbound frames are authenticated against the pending session before + the K-bit cutover promotes it, so a spoofed or stale frame cannot + derail a rekey in progress. +- Rekey message-1 retransmission is bounded, and the link-dead heartbeat + is rekey-aware so an in-flight rekey is not mistaken for a dead link. +- FSP session rekey holds connectivity across the rekey window under + loss and reordering. +- Dual-initiation races (both peers starting a rekey at once on a + high-latency link) are desynchronized with symmetric jitter so the two + sides converge on one session rather than fighting. +- An exhausted retransmission-budget abort, an expected and self-limiting + outcome on lossy or high-latency links, is logged at debug rather than + warn. -Choosing opt-in keeps the mesh quick to bring up for evaluation while -giving operators a documented, packaged path to lock it down for -production. The full design (threat model, rule layout, conntrack -handling, drop-in mechanism, and the rationale for a conffile rather -than an auto-loaded package side-effect) is in -[`docs/design/fips-security.md`](../design/fips-security.md). - -`fipstop`'s Node tab gains a **"Listening on fips0" panel** that -surfaces the answer to the operational question "what services on -this host are reachable from the mesh, and what does the firewall -currently say about each of them?" The panel lists every IPv6 -listening socket bound to either the wildcard address or this node's -`fd00::/8` address, paired with its classification against the -running `inet fips` baseline chain: `OPEN` (canonical accept rule), -`filt` (falls through to drop), or `filt?` (referenced with matchers -the panel cannot fully decompose, e.g. saddr filters or jumps). When -`fips-firewall.service` is inactive, a yellow banner above the table -reminds the operator that every listener is mesh-exposed; wildcard -binds carry a trailing `*` in the Process column. The classifier is -built on a new `show_listening_sockets` control query (Linux-only), -which is also useful from `fipsctl` for scripting. - -### Peer access control - -Operators can now restrict which mesh peers a node will form direct -links with. Optional `/etc/fips/peers.allow` and `/etc/fips/peers.deny` -files (TCP-Wrappers style) match against npub, hex pubkey, host -alias, or `ALL`. Enforcement runs at three points: - -1. Outbound connect (before dialing). -2. Inbound msg1 (the first FMP handshake message from a new peer). -3. Outbound msg2 (the response). - -Files reload automatically on mtime change; a new `fipsctl acl show` -query reports the effective rule set. A six-node Docker integration -harness (`testing/acl/`) exercises allowlist and denylist patterns -end-to-end. - -**Important scope distinction**: peer ACLs are an FMP-layer -restriction. They control who can establish a *direct link* with this -node. They do **not** control session-layer (FSP) reachability through -the mesh. A node that denies peer X with an ACL can still receive FSP -traffic from X relayed via other peers. - -### Bluetooth Low Energy transport (experimental, Linux) - -A new BLE L2CAP Connection-Oriented Channel transport lets FIPS nodes -peer over Bluetooth Low Energy without any IP infrastructure in -between. The transport handles per-link MTU negotiation, continuous -scan/probe peer discovery with cooldown-based deduplication, -continuous advertising, deterministic NodeAddr cross-probe -tie-breaker, and a configurable connection pool with eviction. - -This transport is **experimental in v0.3.0**. It is implemented and -functional on Linux (BlueZ via `bluer`), but the reliability follow-up -logic (probe cooldown, cross-probe tie-breaker, pubkey timeout, -continuous advertising semantics, probe-promotion, fail-fast send) is -not yet behaviorally tested in CI. Its maturity path is field-driven; -please file issues with field reports. macOS BLE support is in -development as a separate track and is not part of v0.3.0. - -### UDP transport profiles - -The UDP transport gains posture flags organized around deployment -patterns: - -- **Public-facing inbound nodes**: `bind_addr: "0.0.0.0:2121"`, - `accept_connections: true` (default), `public: true` for advert - publication. v0.3.0 adds STUN-based public-IP autodiscovery so - cloud nodes (AWS EIP, GCP, Azure 1:1 NAT) advertise the right - address even when the public IP isn't on a host interface. -- **Ephemeral leaf nodes**: `outbound_only: true` binds an ephemeral - port (`0.0.0.0:0`), refuses inbound msg1, and is never advertised - on Nostr regardless of `advertise_on_nostr`. Use this for client - postures that should connect outbound only, without exposing an - inbound listener on a known port. -- **General-purpose nodes**: `accept_connections: false` mirrors the - Ethernet/BLE knob without changing the bind address. The Node-level - handshake gate carves out msg1 from peers already established on - this transport so rekey continues to work. - -Startup validation now rejects `bind_addr` set to a loopback address -when at least one peer has a non-loopback UDP address, closing a -silent-failure trap from v0.2.0 where Linux's source-address routing -check would drop outbound flows from the loopback-bound socket. - -A new `external_addr` field on `transports.udp.*` and -`transports.tcp.*` lets operators specify the advertise-as address -explicitly. This is useful for UDP as a deterministic alternative to -STUN, and required for TCP on cloud-NAT setups (where binding to the -public IP fails with `EADDRNOTAVAIL` because the IP isn't on a host -interface). - -### `.fips` DNS resolver overhaul - -The IPv6 adapter's `.fips` name resolution has been rebuilt around -the constraints of contemporary systemd-based hosts. The default -`dns.bind_addr` is now `::1` (IPv6 loopback), and a setup script -picks one of five backends in priority order: - -1. systemd-resolved global drop-in - (`/etc/systemd/resolved.conf.d/fips.conf`). -2. systemd dns-delegate (per-link configuration handed off to - systemd-resolved). -3. `resolvectl` per-link configuration. -4. Standalone `dnsmasq`. -5. NetworkManager's dnsmasq plugin. - -Teardown reverses only what setup applied, recorded in a state file -at `/run/fips/dns-backend`. A new `testing/dns-resolver/` harness -exercises every backend across Debian 12, Debian 13, Ubuntu 22.04, -Ubuntu 24.04, and Ubuntu 26.04, so a regression in any of the five -backends shows up in CI rather than in the field. - -This overhaul resolves the long-standing silent-drop case where the -`resolvectl dns fips0 []:5354` target collided with the -daemon's mesh-interface filter on certain systemd-resolved -deployments (typically Ubuntu 22 with systemd 249's interface-scoped -routing). - -### Operator tooling additions - -A handful of additions land in `fipsctl`, `fipstop`, and the daemon's -configuration surface: - -- **`node.log_level`** config field replaces the hardcoded - `RUST_LOG=info` previously baked into systemd units and the - OpenWrt procd init. The daemon now loads config before - initializing tracing so the configured level takes effect. - `RUST_LOG` still overrides when set. -- **`fipsctl show identity-cache`** is a new query that lists every - cached node identity (npub, IPv6 address, display name, LRU age) - alongside the configured cache capacity. -- **`fipsctl show peers / sessions / cache / routing`** are - substantially extended: per-peer security signals (replay - suppression count, consecutive decrypt failures), Noise session - counters, session indices, rekey lifecycle state, handshake resend - counts, K-bit epoch, coords-warmup remaining, drain state, per-peer - retry state, per-target lookup detail (attempt, age, last sent), - and pending TUN packet queue depth. -- **Historical statistics**: in-memory time-series rings on the - daemon (1-second × 3600 fast, 1-minute × 1440 slow) cover per-node - and per-peer metrics. New `show_stats_*` control-socket queries, a - `fipsctl stats list / peers / history` subcommand with Unicode - sparkline rendering, and a `fipstop` Graphs tab with btop-style - sparklines surface them to the operator. - -### Performance - -Two independent perf threads land in v0.3.0: a session-layer crypto -backend swap, and a Linux receive-path overhaul. - -**Session-layer crypto backend.** The ChaCha20-Poly1305 backend used -by every FIPS Noise session (end-to-end FSP traffic and link-layer -FMP traffic alike) has been swapped from RustCrypto's -`chacha20poly1305` crate to `ring 0.17`. ring wraps BoringSSL's -hand-tuned ChaCha20-Poly1305 implementation, which dispatches to NEON -on aarch64 and AVX2 / AVX-512 on x86_64. Typical throughput is in the -3-5 GB/s/core range, versus the ~600-800 MB/s/core RustCrypto soft -path on the same hardware. - -Wire format is unchanged. ChaCha20-Poly1305 is byte-deterministic for -a given `(key, nonce, plaintext, aad)`, so any correct AEAD -implementation produces identical ciphertext. A mixed mesh with some -nodes pre-swap and some post-swap interoperates without protocol -awareness; v0.3.0 can roll out across a mesh in any order. - -Measurements on an aarch64 Apple Silicon docker target: - -- Two-node TCP single-stream: 437 -> 1097 Mbps (about 2.5×). -- Two-node UDP at 1000 Mbit: 599 Mbps with 40% loss -> lossless at - line rate. -- Three-node ping under bulk-traffic load: 7.68 ms avg / 215 ms max - -> 0.72 ms / 3.6 ms max as the relay path stops being crypto-bound. - -No operator-visible action is required; the swap is internal to the -session layer. - -**Linux UDP receive path.** The Linux UDP receive path now uses -`recvmmsg(2)` with a 32-packet batch in place of single-packet -`recvmsg(2)`. A single `readable()` wakeup drains up to 32 datagrams -in one syscall before yielding back to the reactor, eliminating the -per-packet scheduler-hop and futex cost that previously capped -inbound rate at one event per scheduler quantum independent of CPU. -`SO_RXQ_OVFL` is sampled once per batch and surfaced through -`AsyncUdpSocket::recv_batch` so the existing 1Hz transport-congestion -detector continues to feed the per-transport `dropping` flag. macOS -and Windows fall through to the per-packet path; `recvmmsg` is -Linux-specific. - -**Inner rx-loop drain batching.** `Node::run_rx_loop` drains up to -256 additional ready items via `try_recv()` after each -`tokio::select!` await fires on the packet and TUN-outbound branches, -in a tight inner loop before yielding. Previously the select cost a -full scheduler hop and futex per packet, capping throughput at one -event per scheduler quantum with the worker near-idle. `biased` -ordering keeps data-plane branches priority over tick / control / DNS -under sustained load; the 256 cap keeps the worker on a busy stream -between yield points (about 400 KB of contiguous traffic) while still -bounding the inner loop so a flood on one branch cannot starve the -periodic tick or control socket. - -**Eager `pubkey_full` precompute.** `PeerIdentity::pubkey_full()` -precomputes the parity-aware full secp256k1 public key at -construction in `from_pubkey`. Previously the method fell through to -an EC point parse on every call when the full key wasn't passed at -construction (i.e. for every peer constructed from an npub or x-only -key), about 6% of per-packet CPU on the bulk-data send path for a -value that never changed after construction. The same parse already -runs at construction inside `NodeAddr::from_pubkey`, so the cost is -paid once where it would be paid anyway. - -These three changes are a coordinated set: the syscall batching -removes the per-packet kernel cost, the inner-loop drain removes the -per-packet scheduler cost, and the pubkey-cache change removes the -per-packet crypto-derivation cost. Like the AEAD swap, they are all -internal and require no operator action. - -### Examples - -- **macOS WireGuard companion** ([#51](https://github.com/jmcorgan/fips/pull/51)): - run FIPS in a local Docker container and route `.fips` traffic - from the macOS host through a WireGuard tunnel to the container's - `fips0`. Only traffic destined for `fd00::/8` transits the - companion; regular internet traffic continues to use the host - network. Persistent FIPS and WireGuard key material is generated - on first run. - -### Documentation - -- **`docs/design/port-advertisement-and-nat-traversal.md`** - documents how nodes find each other through Nostr relays and the - STUN-assisted UDP hole punch. -- **`docs/design/fips-gateway.md`** documents the gateway's virtual - IP pool, lifecycle, control surface, and packaging. -- **`docs/design/fips-security.md`** documents the mesh-interface - security posture, threat model, default-deny baseline, and drop-in - workflow. -- **`CONTRIBUTING.md`** has been expanded with build prerequisites, - Rust toolchain setup, and first-build steps. - -The `docs/` tree has been reorganized end-to-end into four sections -(*tutorials / how-to / reference / design*) with a new -[`docs/getting-started.md`](../getting-started.md) and per-section -landing pages. Content was reconciled against current source: -protocol-layer details, wire-format diagrams, configuration knobs, -and CLI references were brought back into agreement with the -implementation. See [Documentation pointers](#documentation-pointers) -below for entry points by reader intent. +The net operator takeaway: rekey completes cleanly without dropping +traffic, even on lossy or high-latency links, and the log no longer +cries wolf when a rekey gives up and retries. ## Behavior changes worth flagging -These default-config changes affect every operator on upgrade, even -those with no explicit configuration. Two items below — bloom-filter -fill-ratio validation and TreeAnnounce ancestry validation — first -shipped in v0.2.1 and roll forward into v0.3.0; the rest are -v0.3.0-net-new. +These affect operators on upgrade. -- **Discovery rate-limiting** has been retuned to be less aggressive - at cold start. v0.2.0 used a single-lookup-with-internal-retry - model where a timed-out lookup during bloom-filter propagation - could suppress retries for 30 seconds while none of the reset - triggers fired on a stable post-handshake topology. v0.3.0 - replaces this with a per-attempt timeout sequence - (`node.discovery.attempt_timeouts_secs`, default `[1, 2, 4, 8]`, - 15s total). Each attempt sends a fresh `LookupRequest` with a new - `request_id`, letting successive attempts take different - forwarding paths as the bloom and tree state evolve. Post-failure - suppression is **off by default**; operators with chatty - applications can opt back in via `backoff_base_secs` / - `backoff_max_secs`. -- **MMP report intervals** are retuned for constrained transports. - The steady-state floor moves from 100ms to 1000ms, the ceiling - from 2000ms to 5000ms, with a cold-start phase running 200ms for - the first 5 SRTT samples. This reduces BLE overhead by roughly - 10× while keeping reports well above the EWMA convergence - threshold. Session-layer MMP intervals are unchanged. -- **Bloom filter fill-ratio validation** runs on every inbound - `FilterAnnounce`. Filters whose derived false-positive rate - exceeds `node.bloom.max_inbound_fpr` (default 0.05) are rejected - silently on the wire, logged at WARN, and counted in a new - `bloom.fill_exceeded` counter. A rate-limited WARN also fires - when the local outgoing filter exceeds the cap. -- **TreeAnnounce ancestry validation** is now run before tree-state - mutation, enforcing ancestry-self-match, root-single-entry, - parent-second-entry, and root-is-minimum-NodeAddr. Non-conforming - announces are rejected with a WARN. Mixed v0.2.0 / v0.2.1 / v0.3.0 - meshes may produce WARN log lines on the v0.2.1+ side until all - peers upgrade; behavior is correct, log noise only. -- **Log noise reduction**: 35 info-level log messages have been - demoted to debug (handshake cross-connection mechanics, periodic - MMP telemetry, TUN/transport shutdown, retry scheduling). The - default `RUST_LOG` in systemd units is now `info`, where it - previously ran at `debug`. Operator-visible info output now - focuses on lifecycle events, peer promotions, session - establishment, parent switches, and transport start/stop. +- **Bloom filter antipoison cap raised.** `node.bloom.max_inbound_fpr` + moves from 0.05 to 0.10, accepting filters with a higher derived + false-positive rate before rejecting them. This reduces spurious + filter rejections on larger meshes while keeping the antipoison + protection in place. +- **TCP inbound cap honors `max_connections`.** The TCP inbound accept + ceiling now resolves from explicit per-transport + `max_inbound_connections`, then node-wide + `node.limits.max_connections`, then the built-in default of 256. + Previously the TCP inbound ceiling was hardwired to 256 and ignored + `max_connections`, so raising it had no effect on inbound TCP. +- **Static host aliases hot-reload.** `/etc/fips/hosts` now reloads on + mtime change once per tick rather than only at startup, so display + names in `fipsctl` and `fipstop` reflect edits without a daemon + restart. The peer ACL reloads through the same lock-free snapshot + mechanism. +- **Quieter logs on busy public-mesh nodes.** Routine per-peer + connection-lifecycle and capacity-cap events, no-route session-datagram + drops, and exhausted rekey-budget aborts are demoted to debug, so + genuinely notable info and warn lines are no longer drowned out. +- **More visible drops.** Receive-path silent rejections now flow + through typed reject-reason counters, and discovery counts requests + dropped when the dedup cache is full (`req_dedup_cache_full`, visible + via `show_routing`). Drops that were previously silent are now + countable. +- **Tor connect-refused accounting.** The Tor transport increments its + `connect_refused` statistic (the "Refused" line in `fipstop`) on an + actively-refused SOCKS5 connect, instead of recording every connect + failure as a generic SOCKS5 error. ## Notable bug fixes -These pre-existing v0.2.0 bugs are worth singling out because they -either affected real-world deployments or produced misleading -operator experiences. The CHANGELOG has the exhaustive list; this is -the operator-relevant subset. Four items below first shipped in -v0.2.1 and roll forward into v0.3.0: auto-connect Disconnect-reconnect, -`fipsctl connect` mesh-address rejection, `fd00::/8` routing -protection from Tailscale interception, and bloom-filter routing -greedy-tree fallback. The control-socket path-detection fix landed -in v0.2.1 as well, and the unified resolver below is the v0.3.0 -refactor that builds on it. +The CHANGELOG has the exhaustive list. This is the operator-relevant +subset of fixes for behavior that shipped in v0.3.0. -- **DNS responder silent-drop on systemd-resolved** is fixed: the - responder no longer drops queries on Ubuntu 22 / Debian 13 and - similar deployments where systemd applies interface-scoped - routing. Default bind moves to `::1`; new global drop-in backend - available ([#52](https://github.com/jmcorgan/fips/issues/52), - [#77](https://github.com/jmcorgan/fips/issues/77)). -- **Auto-connect peers reconnect after a graceful Disconnect.** - Previously, a clean upstream shutdown left the auto-connect peer - orphaned; only the link-dead, decrypt-fail, and peer-restart - paths scheduled a reconnect - ([#60](https://github.com/jmcorgan/fips/issues/60), reported by - [@SwapMarket](https://github.com/SwapMarket)). -- **`fipsctl connect` rejects FIPS mesh addresses** (`fd00::/8`) - for `udp`, `tcp`, and `ethernet` transports with a clear error - message, instead of echoing success while the daemon silently - failed the bind with `EAFNOSUPPORT` - ([#61](https://github.com/jmcorgan/fips/issues/61), reported by - [@SwapMarket](https://github.com/SwapMarket)). -- **Default control-socket path resolution unified.** Daemon and - client tools now share a single resolver, eliminating a divergence - where `fipsctl` / `fipstop` could connect to a socket the daemon - never bound (notably on dev runs with `XDG_RUNTIME_DIR` set, or - after a prior packaged install left a root-owned `/run/fips` - behind). Canonical order is - `/run/fips` -> `$XDG_RUNTIME_DIR/fips/` -> `/tmp/fips-`. The - `/run/fips` arm is selected by directory existence; the kernel - enforces actual access at `connect(2)` time, so users not yet in - the `fips` group get a clear `EACCES` rather than a silent path - mismatch and a misleading `No such file` fallback to - `$XDG_RUNTIME_DIR`. `XDG_RUNTIME_DIR` is validated as an existing - directory before being used so stale post-logout values are - treated as missing. The deployed fleet is unaffected: packaged - configs set `node.control.socket_path` explicitly - ([#30](https://github.com/jmcorgan/fips/issues/30), reported by - [@Sebastix](https://github.com/Sebastix)). -- **`fd00::/8` routing protected from Tailscale interception.** The - daemon installs an IPv6 routing-policy rule - (`ip -6 rule to fd00::/8 lookup main priority 5265`) at TUN - setup, so Tailscale's table 52 default route can no longer divert - mesh traffic. -- **TCP-over-FIPS reliability on mixed-MTU paths** is markedly - improved. Four interlocking changes ship together: - `Node::transport_mtu()` is now deterministic across daemon - restarts (min across operational transports rather than - insertion-order-dependent); the TCP MSS clamp at the TUN boundary - reads per-destination path MTU instead of a single global ceiling; - reactive `MtuExceeded` from forwarders is mirrored back into the - TUN-side `path_mtu_lookup` so later flows pick up forward-path - bottlenecks without re-discovery; and the proactive end-to-end - `PathMtuNotification` echoed by the destination is mirrored into - the same TUN-side store. Without that fourth piece, on long-lived - stable paths where the destination's echo had tightened the - session MTU but no transit router had emitted a fresh - `MtuExceeded`, new TCP flows opened in that window were clamped by - the staler discovery-time value. The proactive mirror uses the - same tighter-only semantics as the reactive mirror, so it never - loosens the clamp. The Windows TUN reader receives the same - per-destination plumbing. -- **Bloom filter routing greedy-tree fallback.** `find_next_hop` no - longer returns `NoRoute` when the bloom candidate set is non-empty - but no candidate is strictly closer than the current node; it - falls through to greedy tree routing instead. Previously, this - caused dropped packets in topologies where the tree parent was - closer but not a bloom candidate. -- **`fipstop` graceful tty-init failure.** `ratatui::try_init()` - produces a clean error message instead of a hard crash when - terminal initialization fails (Docker on macOS Sequoia, ttyless - environments). -- **TreeAnnounce ancestry on self-root transitions.** When a node - had no smaller-NodeAddr peer to use as a parent, the spanning-tree - state correctly promoted it to root, but the ancestry advertised - on the next `TreeAnnounce` still referenced its previous parent's - path. Receiving peers rejected the announce as - `invalid ancestry: advertised root X is not the minimum path entry - Y`, blocking mesh transit on any path that needed to traverse the - node. The self-root transition is now detected explicitly in - `TreeState::become_root` and the advertised ancestry rebuilt to - start from self; the MMP receive handler corrects stale ancestry - inherited across reconnect eagerly rather than waiting for the - next observation tick. -- **Spanning-tree internal-path updates** that change only the - internal path between root and leaf (without changing the root or - the depth) now propagate to leaves correctly. Previously, a leaf - could continue routing against a stale internal path until the - parent or depth also changed. +- **Symmetric peer teardown on manual disconnect.** A manual + `fipsctl disconnect` now sends the peer a scoped Disconnect so both + ends tear down and re-handshake cleanly. Previously a manual + disconnect tore down only the local side, leaving the peer with a + stale session that was never re-adopted as a child and whose bloom + filter was never re-recorded. +- **Gateway holds long-lived and DNS-cached mappings.** `fips-gateway` + no longer drops a virtual-IP mapping while traffic is still flowing. + The mapping TTL clock previously advanced only on DNS re-query, so a + busy long-lived or DNS-cached client could have its mapping reclaimed + mid-flow. The tick now refreshes the mapping whenever conntrack reports + active sessions and recovers a draining mapping to active when traffic + resumes; only genuinely idle mappings drain. +- **Accurate mesh-size estimate under filter overlap.** The mesh-size + estimator now estimates the cardinality of the OR-union of self plus + every connected peer's inbound filter, instead of summing per-filter + cardinalities of tree peers. Summing assumed the filters were disjoint, + so a stale or oversized parent filter or a routing loop inflated the + reported mesh size and a tree rebalance flapped the count. OR-union + deduplicates overlap, equals the old result in the disjoint case, and + removes the estimate's dependence on tree-declaration cache freshness. +- **Single-uplink node reattaches within a round-trip.** A node with one + tree peer, which has periodic parent re-evaluation disabled, was left + self-rooted and unreachable if its one-shot attaching TreeAnnounce was + lost, until the next periodic re-broadcast. Tree-position exchange is + now self-healing on the receive path: a node that hears an announce + advertising a strictly worse root echoes its own declaration back, + provoking the better-rooted peer to re-push its real position + immediately. ## Upgrade notes -Operator-actionable items when moving from v0.2.x to v0.3.0: +Operator-actionable items moving from v0.3.0 to v0.4.0: -- **Control socket JSON schema (breaking, pre-1.0).** - - `show_cache` response field `entries` has changed type from a - `u64` count to an array of entry objects. The previous scalar - value is now in a new `count` field. - - `show_routing` response field `pending_lookups` has changed - type from a `u64` count to an array of per-target lookup - objects. - - External tooling parsing these fields as numbers must be - updated. In-tree `fipstop` is adjusted to the new schema. The - control-socket interface remains pre-1.0 and is not covered by - stability guarantees. +- **Wire-compatible, no flag day.** v0.4.0 peers with v0.3.0. Upgrade + nodes in any order. During a rolling upgrade you may see some log lines + on the upgraded side as it interacts with not-yet-upgraded peers; + behavior is correct, log noise only. +- **Bloom antipoison cap default changed.** `node.bloom.max_inbound_fpr` + now defaults to 0.10 (was 0.05). If you set this explicitly, review + whether you still want the old value. +- **New optional config surfaces.** `transports.nym` (outbound Nym + mixnet) and `node.discovery.lan` (mDNS LAN discovery) are both opt-in + and off by default. Adding them is the only way to turn the new paths + on. +- **TCP inbound cap.** If you relied on the old hardwired 256 inbound-TCP + ceiling, note it now honors `max_inbound_connections` then + `node.limits.max_connections` then 256. +- **New observability query.** `fipsctl stats metrics` (the + `show_metrics` control query) returns a counter-only snapshot suitable + for a scraper. -- **Cargo feature flags removed.** `tui`, `ble`, `gateway`, and - `nostr-discovery` are gone. Subsystem inclusion is now driven by - platform `cfg` gates, so plain `cargo build` compiles everything - available on the target without `--features` invocations. - Source-build tooling that passed any of these features should be - updated to omit them. - -- **Discovery rate-limiting defaults changed.** Post-failure - suppression is **off by default** - (`node.discovery.backoff_base_secs: 0`, `backoff_max_secs: 0`). - Operators relying on the prior 30s base / 300s cap behavior must - set those fields explicitly. The per-attempt sequence - (`attempt_timeouts_secs`, default `[1, 2, 4, 8]`) now governs - cold-start lookup behavior. - -- **`.fips` DNS bind address default changed.** The default - `dns.bind_addr` is now `::1`. Operators with explicit overrides - of this field should review them; many existing overrides were - workarounds for the silent-drop bug that this release fixes - properly. - -- **Gateway `dns.listen` source default changed.** The - `fips-gateway` `dns.listen` default is now `[::1]:5353` (was - `[::]:53`), matching the canonical deployment model where a - pre-existing resolver on the host already owns port 53. The - OpenWrt ipk previously overrode this in its packaged config; the - override is now redundant and has been dropped. Operators on a - host without a pre-existing resolver on port 53 can opt back into - the wildcard bind by setting `dns.listen: "[::]:53"` explicitly. - The new default binds IPv6 loopback only, so forwarders that - reach the gateway over IPv4 loopback need an explicit IPv4 listen - address. - -- **systemd unit log level.** The shipped systemd units no longer - hardcode `RUST_LOG=info`; the daemon's effective log level is - driven by `node.log_level` (default `info`). `RUST_LOG`, when - set, still overrides. - -- **UDP transport `bind_addr` validation.** Startup now rejects a - `bind_addr` set to a loopback address when at least one peer has - a non-loopback UDP address. Operators who configured a loopback - UDP bind as a workaround should switch to `outbound_only: true` - for the same effect, plus the correct semantics (kernel-assigned - ephemeral port, refuses inbound, never advertised). - -- **Tor advert port.** If the Tor `HiddenServicePort` virtual port - isn't 443, set `transports.tor.advertised_port` to match. The - default is 443 and matches the conventional virtual-port choice. - -## Documentation pointers - -v0.3.0 ships a `docs/` tree reorganized into four sections -(*tutorials / how-to / reference / design*). A new top-level -[`docs/getting-started.md`](../getting-started.md) and per-section -landing pages anchor the entry points. - -Entry points by reader intent: - -- **New users**: [`docs/getting-started.md`](../getting-started.md) - and [`docs/tutorials/`](../tutorials/) cover guided introductions - for bringing up your first node, joining the test mesh, - advertising a node over Nostr, hosting a service, deploying a - gateway, walking through the IPv6 adapter, and resolving peers - via Nostr. -- **Operators with a specific task**: - [`docs/how-to/`](../how-to/) holds task-driven guides for enabling - Nostr discovery, deploying the gateway, troubleshooting the - gateway, deploying a Tor onion, hosting aliases, persistent - identity, running unprivileged, setting up a Bluetooth peer, - enabling the mesh firewall, tuning UDP buffers, and diagnosing - MTU issues. -- **Reference lookups**: [`docs/reference/`](../reference/) holds - the config field reference, control-socket query reference, the - `fips`, `fipsctl`, `fipstop`, and `fips-gateway` CLI references, - and the protocol diagram set. -- **Architectural background**: [`docs/design/`](../design/) holds - design rationale for FIPS as a whole, FMP and FSP, the spanning - tree, bloom-filter discovery, transports, the IPv6 adapter, the - Nostr discovery layer, and the gateway. -- **Security**: [`docs/design/fips-security.md`](../design/fips-security.md) - documents the mesh-interface security baseline, threat model, and - drop-in workflow. - -## Getting v0.3.0 +## Getting v0.4.0 - **Linux x86_64 / aarch64**: `.deb` and tarball at the - [v0.3.0 release page](https://github.com/jmcorgan/fips/releases/tag/v0.3.0). + [v0.4.0 release page](https://github.com/jmcorgan/fips/releases/tag/v0.4.0). - **Arch Linux**: `fips` from the AUR. -- **macOS**: `.pkg` at the v0.3.0 release page. -- **Windows**: ZIP at the v0.3.0 release page. -- **OpenWrt**: `.ipk` at the v0.3.0 release page. -- **From source**: `cargo build --release` from a checkout of the - v0.3.0 tag. +- **macOS**: `.pkg` at the v0.4.0 release page. +- **Windows**: ZIP at the v0.4.0 release page. +- **OpenWrt**: `.ipk` at the v0.4.0 release page. +- **From source**: `cargo build --release` from a checkout of the v0.4.0 + tag (Rust 1.94.1 per `rust-toolchain.toml`; `libclang-dev` is a + required Linux build prerequisite). The full per-commit changelog lives in [`CHANGELOG.md`](../../CHANGELOG.md). Issues and discussion at @@ -698,67 +264,16 @@ The full per-commit changelog lives in ## Contributors -Thanks to everyone who contributed code, packaging work, bug reports, -or reviews to this release. +Thanks to everyone who contributed code, packaging work, bug reports, or +reviews to this release. -**Code and packaging**: - -- [@jcorgan](https://github.com/jmcorgan): release shepherd, Nostr - discovery / NAT traversal, `fips-gateway`, ACL infrastructure, - packaging, security baseline, BLE follow-ups. -- [@Origami74](https://github.com/Origami74): macOS platform support, - from-source Docker companion build and `fipstop` terminal-init - handling, gateway co-development, OpenWrt BLE-feature build fix, - AUR-workflow follow-ups. -- [@jodobear](https://github.com/jodobear): Linux release-artifact - workflow and target-aware build scripts, CONTRIBUTING.md - expansion, rekey integration-test stabilization. -- [@tidley](https://github.com/tidley): Nostr-mediated overlay - discovery and UDP NAT traversal - ([#53](https://github.com/jmcorgan/fips/pull/53)). -- [@alexxie16](https://github.com/alexxie16): peer ACL enforcement - ([#50](https://github.com/jmcorgan/fips/pull/50)), - macOS WireGuard companion example - ([#51](https://github.com/jmcorgan/fips/pull/51)), - follow-up ([#67](https://github.com/jmcorgan/fips/pull/67)). -- [@osh](https://github.com/osh): diagnostic queries for security - validation and mesh debugging - ([#42](https://github.com/jmcorgan/fips/pull/42)). -- [@OceanSlim](https://github.com/0ceanSlim): Windows platform - support ([#45](https://github.com/jmcorgan/fips/pull/45)). -- [@mmalmi](https://github.com/mmalmi): ring AEAD backend - ([#80](https://github.com/jmcorgan/fips/pull/80)), - hot-path drain batching + recvmmsg + eager pubkey_full - ([#81](https://github.com/jmcorgan/fips/pull/81)), - TreeAnnounce self-root ancestry + overlay-advert retry hygiene - ([#82](https://github.com/jmcorgan/fips/pull/82)), - NAT-traversal MTU inheritance - ([#83](https://github.com/jmcorgan/fips/pull/83)). -- [@dskvr](https://github.com/dskvr): initial Arch Linux AUR - packaging ([#21](https://github.com/jmcorgan/fips/pull/21)) and - the AUR publish workflow. -- [@SatsAndSports](https://github.com/SatsAndSports): rekey - message-1 admit fix on non-accepting transports - ([#49](https://github.com/jmcorgan/fips/pull/49)), - TreeAnnounce semantic validation, gateway test image fix - ([#69](https://github.com/jmcorgan/fips/pull/69)). -- [@andrewheadricke](https://github.com/andrewheadricke): MIPS - atomic-ABI portability via `portable_atomic` - ([#62](https://github.com/jmcorgan/fips/pull/62)). -- [@sh1ftred](https://github.com/sh1ftred): Arch packaging namcap - fixes ([#63](https://github.com/jmcorgan/fips/pull/63)). -- [@oleksky](https://github.com/oleksky): macOS WireGuard companion - collaboration on [#51](https://github.com/jmcorgan/fips/pull/51). - -**Issue reports that drove fixes in this release**: - -- [@deavmi](https://github.com/deavmi): MIPS daemon build support - ([#26](https://github.com/jmcorgan/fips/issues/26)). -- [@Sebastix](https://github.com/Sebastix): fipsctl/fipstop - control-socket path detection - ([#30](https://github.com/jmcorgan/fips/issues/30)). -- [@SwapMarket](https://github.com/SwapMarket): auto-connect - reconnect after graceful disconnect - ([#60](https://github.com/jmcorgan/fips/issues/60)) and - fipsctl mesh-address rejection - ([#61](https://github.com/jmcorgan/fips/issues/61)). +- [@jcorgan](https://github.com/jmcorgan): release shepherd, high-level + design, control read plane, rekey hardening, admission, bug fixes, + testing, packaging, PR coordination, and issue resolution. +- [@mmalmi](https://github.com/mmalmi): opt-in mDNS LAN discovery and + data-plane performance work. +- [@Origami74](https://github.com/Origami74): macOS packaging and + website coordination. +- [@dskvr](https://github.com/dskvr): AUR packaging. +- [@oleksky](https://github.com/oleksky): Nym mixnet transport and the + single-container mixnet demo. diff --git a/docs/reference/cli-fips.md b/docs/reference/cli-fips.md index 40b5844..0d0ff4e 100644 --- a/docs/reference/cli-fips.md +++ b/docs/reference/cli-fips.md @@ -28,7 +28,7 @@ controlled through the standard service control manager. | Flag | Argument | Description | | ---- | -------- | ----------- | | `-c`, `--config` | `FILE` | Use `FILE` as the configuration. Skips the default search paths. | -| `-V` | — | Print the short version (e.g. `0.3.0-dev (rev abcdef1)`). | +| `-V` | — | Print the short version (e.g. `0.4.0 (rev abcdef1)`). | | `--version` | — | Print the long version: short version plus build target triple. | | `-h`, `--help` | — | Print usage and exit. | | `--install-service` | — | (Windows only) Install `fips` as a Windows service. Requires Administrator. | diff --git a/docs/reference/cli-fipstop.md b/docs/reference/cli-fipstop.md index 6b759ef..226a82c 100644 --- a/docs/reference/cli-fipstop.md +++ b/docs/reference/cli-fipstop.md @@ -15,8 +15,11 @@ socket, polls a small set of `show_*` queries on a timer, and renders the state in a tabbed full-screen UI. A separate poll runs against the gateway control socket when the Gateway tab is active. -`fipstop` is read-only — it cannot mutate daemon state. Use -[`fipsctl`](cli-fipsctl.md) for `connect` / `disconnect` and friends. +`fipstop` is almost entirely read-only: the only state-mutating action +it offers is disconnecting a peer (`Del` on a selected Peers row, with +a confirmation prompt — see [Keybindings](#keybindings)). For +`connect` and other mutating commands, use +[`fipsctl`](cli-fipsctl.md). ## Options @@ -86,6 +89,11 @@ empty list and the panel hides. ## Keybindings +Press `?` at any time for an in-app help overlay. The overlay and the +status-bar hint footer both read from a single keybinding registry +keyed by `(tab, mode)`, so the always-visible hints describe exactly +the keys the current context accepts. + ### Global | Key | Action | @@ -94,7 +102,8 @@ empty list and the panel hides. | `Tab` | Next tab. | | `Shift-Tab` | Previous tab. | | `g` | Jump to the Graphs tab. | -| `Esc` | Close detail view (if open). | +| `?` | Toggle the help overlay. | +| `Esc` | Close an open detail view; otherwise deselect the active table row. | ### Table tabs (Peers, Sessions, Transports, Gateway) @@ -102,6 +111,13 @@ empty list and the panel hides. | --- | ------ | | `Up`, `Down` | Move row selection. | | `Enter` | Open detail view for the selected row. | +| `Esc` | Deselect the row (return to the tab's overview state). | + +### Peers tab (extra) + +| Key | Action | +| --- | ------ | +| `Del` | Disconnect the selected peer. Opens a `Y`/`N` confirmation modal first; this is the only state-mutating action in `fipstop`. | ### Transports tab (extra) @@ -112,16 +128,43 @@ empty list and the panel hides. | `e` | Expand all transports. | | `c` | Collapse all transports. | +### Multi-pane scrolling tabs (Tree, Filters, Routing) + +Each lays out stacked panes that scroll independently. + +| Key | Action | +| --- | ------ | +| `f` | Move focus to the next pane. | +| `Up`, `Down` | Scroll the focused pane by one row. | +| `PageUp`, `PageDown` | Scroll the focused pane by ten rows. | +| `Home`, `End` | Jump to the top / bottom of the focused pane. | + +### Performance tab (extra) + +The Performance tab lays out two panes (Link MMP, Session MMP). + +| Key | Action | +| --- | ------ | +| `f` | Move focus between the Link and Session MMP panes. | +| `Up`, `Down` | Scroll the focused pane. | +| `PageUp`, `PageDown` | Scroll the focused pane by ten rows. | +| `Home`, `End` | Jump to the top / bottom of the focused pane. | +| `s` | Cycle the sort column of the focused pane. | +| `Shift-S` | Toggle the sort direction of the focused pane. | + ### Graphs tab (extra) | Key | Action | | --- | ------ | -| `Up`, `Down` | Scroll within the stacked plots. | +| `Up`, `Down` | Scroll the stacked plots; in `MetricByPeer` mode, move the by-peer selection (and follow it when the by-peer detail is open). | | `Right`, `Space` | Next time window. Cycles `1m / 1s` → `10m / 1s` → `1h / 1s` → `24h / 1m`. | | `Left` | Previous time window. | +| `Enter` | In `MetricByPeer` mode, expand the selected peer summary into a full-pane plot. | | `m` | Cycle view mode: `Node` (stacked node metrics) → `MetricByPeer` (one per-peer metric across all peers) → `PeerByMetric` (all per-peer metrics for one peer). | | `n` | Next selector (next per-peer metric in MetricByPeer; next peer in PeerByMetric). | | `Shift-N` | Previous selector. | +| `s` | Cycle the sort column of the by-peer summary list. | +| `Shift-S` | Toggle the sort direction of the by-peer summary list. | ## Exit Codes diff --git a/docs/reference/control-socket.md b/docs/reference/control-socket.md index 68c2007..1d41a3e 100644 --- a/docs/reference/control-socket.md +++ b/docs/reference/control-socket.md @@ -100,22 +100,22 @@ table below lists every command currently registered. | Command | Params | `data` shape (top-level keys) | | ------- | ------ | ----------------------------- | -| `show_status` | — | `version`, `npub`, `node_addr`, `ipv6_addr`, `state`, `is_leaf_only`, `peer_count`, `session_count`, `link_count`, `transport_count`, `connection_count`, `tun_state`, `tun_name`, `effective_ipv6_mtu`, `control_socket`, `pid`, `exe_path`, `uptime_secs`, `estimated_mesh_size`, `forwarding`, `sparklines`. | +| `show_status` | — | `version`, `npub`, `node_addr`, `ipv6_addr`, `state`, `is_leaf_only`, `is_root` (bool — this node is the spanning-tree root), `root` (hex node-addr of the current tree root), `persistent` (bool — identity is persisted, i.e. `persistent` set or an `nsec` configured), `peer_count`, `session_count`, `link_count`, `transport_count`, `connection_count`, `transport_peer_counts` (object mapping transport-type name to its connected-peer count; configured transports appear with `0`), `tun_state`, `tun_name`, `effective_ipv6_mtu`, `control_socket`, `pid`, `exe_path`, `uptime_secs`, `estimated_mesh_size`, `forwarding`, `sparklines`. | | `show_acl` | — | `allow_file`, `deny_file`, `enforcement_active`, `effective_mode`, `default_decision`, `allow_all`, `deny_all`, `allow_file_entries`, `deny_file_entries`, `allow_entries`, `deny_entries`. | -| `show_peers` | — | `peers[]` — per-peer object: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `connectivity`, `link_id`, `direction`, `transport_addr`, `transport_type`, `is_parent`, `is_child`, `tree_depth`, `stats`, `noise`, `current_k_bit`, `mmp`, plus optional `nostr_traversal`, `rekey_in_progress`, `rekey_draining`. | +| `show_peers` | — | `peers[]` — per-peer object: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `connectivity`, `link_id`, `direction`, `transport_addr`, `transport_type`, `is_parent`, `is_child`, `tree_depth`, `effective_depth` (`tree_depth + link_cost` — the metric `evaluate_parent` ranks on; `null` when the peer has no coords, or is unmeasured while another peer has an SRTT sample, per the cold-start gate), `stats`, `noise`, `current_k_bit`, `mmp`, plus optional `nostr_traversal`, `rekey_in_progress`, `rekey_draining`. | | `show_links` | — | `links[]` — `link_id`, `transport_id`, `remote_addr`, `direction`, `state`, `created_at_ms`, `stats`. | -| `show_tree` | — | `my_node_addr`, `root`, `is_root`, `depth`, `my_coords[]`, `parent`, `parent_display_name`, `declaration_sequence`, `declaration_signed`, `peer_tree_count`, `peers[]`, `stats`. | +| `show_tree` | — | `my_node_addr`, `root`, `root_npub` (bech32 npub of the current tree root), `is_root`, `depth`, `my_coords[]`, `parent`, `parent_display_name`, `declaration_sequence`, `declaration_signed`, `peer_tree_count`, `peers[]`, `stats`. | | `show_sessions` | — | `sessions[]` — `remote_addr`, `npub`, `display_name`, `state` (`established`, `initiating`, `awaiting_msg3`, `unknown`), `is_initiator`, `last_activity_ms`, `stats`, optional `mmp`, `current_k_bit`, `is_draining`. | -| `show_bloom` | — | `own_node_addr`, `is_leaf_only`, `sequence`, `leaf_dependent_count`, `leaf_dependents[]`, `peer_filters[]`, `stats`. | +| `show_bloom` | — | `own_node_addr`, `is_leaf_only`, `sequence`, `leaf_dependent_count`, `leaf_dependents[]`, `peer_filters[]`, `uptree_fill_ratio` (fill ratio of the last filter actually sent to the tree parent), `uptree_estimated_count` (cardinality estimate of that uptree filter — this node's whole subtree under split-horizon, not the mesh; both are `null` for a root node or before the first announce), `stats`. | | `show_mmp` | — | `peers[]` (link-layer per peer), `sessions[]` (session-layer per session). Each entry includes loss/RTT/ETX/goodput, smoothed values, trends. | | `show_cache` | — | `count`, `max_entries`, `fill_ratio`, `default_ttl_ms`, `expired`, `avg_age_ms`, `entries[]` — per-destination coords, depth, age, last-used, optional `path_mtu`. | | `show_connections` | — | `connections[]` — pending handshakes: `link_id`, `direction`, `handshake_state`, `started_at_ms`, `idle_ms`, `resend_count`, optional `expected_peer`. | | `show_transports` | — | `transports[]` — `transport_id`, `type`, `state`, `mtu`, `name`, `local_addr`, optional `tor_mode`, `onion_address`, `tor_monitoring`, `stats`. | -| `show_routing` | — | `coord_cache_entries`, `identity_cache_entries`, `pending_lookups[]`, `pending_tun_destinations`, `pending_tun_packets`, `recent_requests`, `retries[]`, `forwarding`, `discovery`, `error_signals`, `congestion`. | +| `show_routing` | — | `coord_cache_entries`, `identity_cache_entries`, `pending_lookups[]`, `pending_tun_destinations`, `pending_tun_packets`, `recent_requests`, `retries[]`, `forwarding`, `discovery` (request/response sub-counters; includes `req_deduplicated` — requests suppressed as recent duplicates — and `req_dedup_cache_full` — requests admitted because the dedup cache was full), `error_signals`, `congestion`. | | `show_identity_cache` | — | `entries[]`, `count`, `max_entries`. Each entry: `node_addr`, `npub`, `display_name`, `ipv6_addr`, `last_seen_ms`, `age_ms`. | | `show_listening_sockets` | — | `fips0_addr`, `firewall_active` (bool — `inet fips` table loaded), `sockets[]`. Each entry: `proto` (`tcp` / `udp`), `local_addr` (`::` or the node's fd00::/8 address), `port`, `pid` (nullable), `process` (nullable), `wildcard_bind` (bool — `local_addr == ::`), `filter` (`accept` / `drop` / `unknown` / `no_firewall`). Linux-only; returns an empty `sockets[]` on other platforms. | | `show_stats_list` | — | `metrics[]` (each with `name`, `unit`, `scope`), `fast_ring_seconds`, `slow_ring_minutes`, `peer_retention_seconds`. | -| `show_metrics` | — | Flat snapshot of every counter family in the metrics registry: `forwarding`, `discovery`, `tree`, `bloom`, `congestion`, `errors`. Each value is that family's counter snapshot object. Counter-only — gauges/histograms that need the live node are excluded. Served off the main loop. | +| `show_metrics` | — | Flat snapshot of every counter family in the metrics registry: `forwarding`, `discovery`, `tree`, `bloom`, `congestion`, `errors`. Each value is that family's counter snapshot object. Counter-only — gauges/histograms that need the live node are excluded. Served off the main loop. Silent-rejection sites classify their reason as a typed `RejectReason` and increment the matching per-family counter exposed here — see [Rejection reasons](#rejection-reasons). | | `show_stats_history` | `metric` (req), `peer` (req for per-peer metrics), `window` (`s` / `m` / `h`, default `10m`), `granularity` (`1s` / `1m`, default `1s`) | A single `Series`: `metric`, `unit`, `granularity_seconds`, `values[]`. | | `show_stats_all_history` | `peer` (optional npub), `window`, `granularity` | `granularity_seconds`, `window_seconds`, `peer`, `series[]` (one per metric). | | `show_stats_peers` | — | `peers[]`, `count`. Each entry: `npub`, `node_addr`, `display_name`, `is_active`, `first_seen_secs_ago`, `last_contact_secs_ago`. | @@ -125,6 +125,30 @@ The schema of each query response is pinned by snapshot tests in `src/control/snapshots/`; intentional schema changes regenerate those fixtures. +### Rejection reasons + +Silent-rejection paths across the node classify why a message was +dropped via a typed `RejectReason` rather than only logging it, so the +*what* of a rejection is visible in the counter snapshots above. The +top-level reason set has eight families, mirroring the protocol-layer / +subsystem split of the metrics: + +- **Tree** — spanning-tree `TreeAnnounce` processing rejections. +- **Bloom** — bloom-filter `FilterAnnounce` processing rejections. +- **Discovery** — discovery request / response processing rejections. +- **Handshake** — Noise handshake state-machine rejections. +- **Session** — FSP session state-machine rejections. +- **Mmp** — MMP link-layer rejections. +- **Forwarding** — forwarding-path rejections (no-route, TTL, MTU). +- **Transport** — transport-layer rejections (admission caps, framing). + +Each rejection increments the corresponding counter in its family's +stats, surfaced through `show_metrics` (the `tree`, `bloom`, +`discovery`, and `forwarding` families carry their own counters; the +`errors` family and the remaining subsystem counters carry the rest). +The full per-family variant list lives in `src/node/reject.rs`; it is +not reproduced here to avoid duplicating the source. + ### Mutating commands | Command | Required params | Behaviour | diff --git a/docs/releases/release-notes-v0.4.0.md b/docs/releases/release-notes-v0.4.0.md new file mode 100644 index 0000000..0d62767 --- /dev/null +++ b/docs/releases/release-notes-v0.4.0.md @@ -0,0 +1,279 @@ +# FIPS v0.4.0 + +**Released**: 2026-06-DD (provisional) + +v0.4.0 is the throughput-and-observability release on the v0.3.x wire +format. It adds two new ways for nodes to find and reach each other (the +Nym mixnet transport and opt-in mDNS LAN discovery), overhauls the data +plane for higher single-node throughput and lower per-packet CPU, moves +the entire operator read surface off the data-plane hot path so +observability stays responsive under load, ships a reworked `fipstop` +TUI, and hardens FMP and FSP rekey to be hitless under packet loss in +both directions. It also folds in the accumulated mesh-convergence, +admission-control, and packaging fixes from the maintenance line. + +v0.4.0 is wire-compatible with v0.3.0. Mixed meshes interoperate; there +is no flag-day upgrade. A deployed v0.3.0 node and an upgraded v0.4.0 +node peer, rekey, and route normally, so you can roll the upgrade out +across a mesh in any order. + +## At a glance + +- New outbound Nym mixnet transport with a single-container demo and a + new mixnet-relay example. +- Opt-in mDNS / DNS-SD discovery on the local link. +- Data-plane overhaul: off-task encrypt and decrypt worker pools, GSO, + connected-UDP send path, copy-avoidance on receive, batched macOS + receive. +- The full `show_*` read surface now serves off the receive loop, so + `fipsctl` and `fipstop` stay responsive on loaded nodes; a new + counter-only `show_metrics` query enables a Prometheus scraper at no + hot-path cost. +- Reworked `fipstop` TUI on a machine-verified render-snapshot base. +- Rekey is now hitless under loss and reordering in both directions. + +## What's new + +### Nym mixnet transport + +FIPS can now peer over the [Nym](https://nymtech.net/) mixnet for +metadata-resistant connectivity. The new `transports.nym` transport +makes outbound connections through a `nym-socks5-client` SOCKS5 proxy +that you run alongside the daemon (for example as a service running +alongside the fips daemon, or as a sidecar container). The transport +waits at startup for the nym-socks5-client to become ready before giving +up. + +This is a privacy and anonymity deployment mode chosen for its own +properties. It mixes your FIPS traffic into the Nym cover-traffic +network so that link-level observers cannot correlate which mesh peers +are talking. A new `examples/sidecar-nostr-mixnet-relay/` demonstrates a +FIPS-reachable Nostr relay peered across the mixnet end to end, and a +single-container demo ships with the transport. + +Enable it by adding a `transports.nym` instance and pointing it at your +running nym-socks5-client. See the transports reference for the field +set. + +### mDNS LAN discovery + +Nodes on a shared local link can now find each other with zero address +configuration. The opt-in `node.discovery.lan` path runs an mDNS / +DNS-SD responder and browser: each node advertises a FIPS service record +on the link and adopts the peers it discovers. This complements the +existing Nostr-mediated overlay discovery for the common case where the +peers are simply on the same LAN. + +Turn it on with `node.discovery.lan.enabled: true`. `service_type` and +`scope` tune the advertised service record and which interfaces +participate. Discovery on the local link needs no relay and no STUN. + +### Data-plane throughput overhaul + +The receive and send paths were reworked for higher single-node +throughput and lower per-packet CPU, building on the v0.3.0 +crypto-backend swap: + +- **Off-task encrypt and decrypt.** Per-peer encrypt and decrypt now run + on dedicated worker tasks rather than inline on the receive loop, so a + single busy peer no longer serializes the whole node's crypto. +- **GSO and connected-UDP send.** The Linux send path uses generic + segmentation offload and a connected-UDP socket where available, + cutting syscall overhead on bulk flows. +- **Copy-avoidance on receive.** The receive hot path avoids buffer + copies it previously made per packet. +- **Batched macOS receive.** macOS gains a `recvmsg_x` batched receive, + mirroring the Linux `recvmmsg` batching from v0.3.0. +- **Shared immutable-state context and an atomic metric registry.** + Immutable per-node state moved into a single shared context, and + counters live in an atomic metric registry that the new `show_metrics` + query reads without touching the hot path. + +These are all internal to the data plane and require no operator action. + +### Observability off the hot path + +Every read-only control query now renders from a snapshot published once +per tick into a lock-free `ArcSwap`, served from the control accept task +instead of round-tripping the data-plane receive loop. This covers +`show_status`, `show_stats_*`, `show_peers`, `show_sessions`, +`show_links`, `show_connections`, `show_transports`, `show_mmp`, +`show_tree`, `show_bloom`, `show_cache`, `show_routing`, +`show_identity_cache`, `show_acl`, `show_listening_sockets`, and the new +`show_metrics`. Only the mutating `connect` and `disconnect` commands +still reach the loop. + +The practical effect: on a loaded node where the receive loop was busy, +`fipsctl` and `fipstop` queries previously stalled or timed out (the +five-second query pattern operators saw). They now answer promptly +regardless of data-plane load. Per-entity snapshots reuse unchanged rows +by pointer, so the per-tick publish cost stays bounded as peer and +session counts grow. + +A new **`show_metrics`** query (surfaced as `fipsctl stats metrics`) +returns a counter-only snapshot of every metric family. It is the +enabler for a Prometheus scraper that pulls node counters at no hot-path +cost. + +### Reworked fipstop TUI + +`fipstop` gets a rendering, navigation, and read-surface overhaul on a +machine-verified base: a render-snapshot harness asserts the exact text +grid and per-cell style of every view against canned control-socket +output. New daemon-resolved fields surface through the snapshots, +including effective persistence, root and is-root state, a +per-transport-type peer-count map, per-peer effective depth, the root +npub, and the last-sent uptree filter fill ratio with the subtree size +estimate. + +A separate fix clears a garbled-screen problem on startup and stray +bytes on quit, most visible over SSH and inside tmux: startup now forces +a full repaint before the first draw, and quit stops and joins the +stdin-poll thread before restoring the terminal, so post-raw-mode +keystrokes no longer echo onto the restored screen. + +### Rekey reliability + +FMP and FSP session rekey are now hitless under packet loss and +reordering in both directions: + +- Inbound frames are authenticated against the pending session before + the K-bit cutover promotes it, so a spoofed or stale frame cannot + derail a rekey in progress. +- Rekey message-1 retransmission is bounded, and the link-dead heartbeat + is rekey-aware so an in-flight rekey is not mistaken for a dead link. +- FSP session rekey holds connectivity across the rekey window under + loss and reordering. +- Dual-initiation races (both peers starting a rekey at once on a + high-latency link) are desynchronized with symmetric jitter so the two + sides converge on one session rather than fighting. +- An exhausted retransmission-budget abort, an expected and self-limiting + outcome on lossy or high-latency links, is logged at debug rather than + warn. + +The net operator takeaway: rekey completes cleanly without dropping +traffic, even on lossy or high-latency links, and the log no longer +cries wolf when a rekey gives up and retries. + +## Behavior changes worth flagging + +These affect operators on upgrade. + +- **Bloom filter antipoison cap raised.** `node.bloom.max_inbound_fpr` + moves from 0.05 to 0.10, accepting filters with a higher derived + false-positive rate before rejecting them. This reduces spurious + filter rejections on larger meshes while keeping the antipoison + protection in place. +- **TCP inbound cap honors `max_connections`.** The TCP inbound accept + ceiling now resolves from explicit per-transport + `max_inbound_connections`, then node-wide + `node.limits.max_connections`, then the built-in default of 256. + Previously the TCP inbound ceiling was hardwired to 256 and ignored + `max_connections`, so raising it had no effect on inbound TCP. +- **Static host aliases hot-reload.** `/etc/fips/hosts` now reloads on + mtime change once per tick rather than only at startup, so display + names in `fipsctl` and `fipstop` reflect edits without a daemon + restart. The peer ACL reloads through the same lock-free snapshot + mechanism. +- **Quieter logs on busy public-mesh nodes.** Routine per-peer + connection-lifecycle and capacity-cap events, no-route session-datagram + drops, and exhausted rekey-budget aborts are demoted to debug, so + genuinely notable info and warn lines are no longer drowned out. +- **More visible drops.** Receive-path silent rejections now flow + through typed reject-reason counters, and discovery counts requests + dropped when the dedup cache is full (`req_dedup_cache_full`, visible + via `show_routing`). Drops that were previously silent are now + countable. +- **Tor connect-refused accounting.** The Tor transport increments its + `connect_refused` statistic (the "Refused" line in `fipstop`) on an + actively-refused SOCKS5 connect, instead of recording every connect + failure as a generic SOCKS5 error. + +## Notable bug fixes + +The CHANGELOG has the exhaustive list. This is the operator-relevant +subset of fixes for behavior that shipped in v0.3.0. + +- **Symmetric peer teardown on manual disconnect.** A manual + `fipsctl disconnect` now sends the peer a scoped Disconnect so both + ends tear down and re-handshake cleanly. Previously a manual + disconnect tore down only the local side, leaving the peer with a + stale session that was never re-adopted as a child and whose bloom + filter was never re-recorded. +- **Gateway holds long-lived and DNS-cached mappings.** `fips-gateway` + no longer drops a virtual-IP mapping while traffic is still flowing. + The mapping TTL clock previously advanced only on DNS re-query, so a + busy long-lived or DNS-cached client could have its mapping reclaimed + mid-flow. The tick now refreshes the mapping whenever conntrack reports + active sessions and recovers a draining mapping to active when traffic + resumes; only genuinely idle mappings drain. +- **Accurate mesh-size estimate under filter overlap.** The mesh-size + estimator now estimates the cardinality of the OR-union of self plus + every connected peer's inbound filter, instead of summing per-filter + cardinalities of tree peers. Summing assumed the filters were disjoint, + so a stale or oversized parent filter or a routing loop inflated the + reported mesh size and a tree rebalance flapped the count. OR-union + deduplicates overlap, equals the old result in the disjoint case, and + removes the estimate's dependence on tree-declaration cache freshness. +- **Single-uplink node reattaches within a round-trip.** A node with one + tree peer, which has periodic parent re-evaluation disabled, was left + self-rooted and unreachable if its one-shot attaching TreeAnnounce was + lost, until the next periodic re-broadcast. Tree-position exchange is + now self-healing on the receive path: a node that hears an announce + advertising a strictly worse root echoes its own declaration back, + provoking the better-rooted peer to re-push its real position + immediately. + +## Upgrade notes + +Operator-actionable items moving from v0.3.0 to v0.4.0: + +- **Wire-compatible, no flag day.** v0.4.0 peers with v0.3.0. Upgrade + nodes in any order. During a rolling upgrade you may see some log lines + on the upgraded side as it interacts with not-yet-upgraded peers; + behavior is correct, log noise only. +- **Bloom antipoison cap default changed.** `node.bloom.max_inbound_fpr` + now defaults to 0.10 (was 0.05). If you set this explicitly, review + whether you still want the old value. +- **New optional config surfaces.** `transports.nym` (outbound Nym + mixnet) and `node.discovery.lan` (mDNS LAN discovery) are both opt-in + and off by default. Adding them is the only way to turn the new paths + on. +- **TCP inbound cap.** If you relied on the old hardwired 256 inbound-TCP + ceiling, note it now honors `max_inbound_connections` then + `node.limits.max_connections` then 256. +- **New observability query.** `fipsctl stats metrics` (the + `show_metrics` control query) returns a counter-only snapshot suitable + for a scraper. + +## Getting v0.4.0 + +- **Linux x86_64 / aarch64**: `.deb` and tarball at the + [v0.4.0 release page](https://github.com/jmcorgan/fips/releases/tag/v0.4.0). +- **Arch Linux**: `fips` from the AUR. +- **macOS**: `.pkg` at the v0.4.0 release page. +- **Windows**: ZIP at the v0.4.0 release page. +- **OpenWrt**: `.ipk` at the v0.4.0 release page. +- **From source**: `cargo build --release` from a checkout of the v0.4.0 + tag (Rust 1.94.1 per `rust-toolchain.toml`; `libclang-dev` is a + required Linux build prerequisite). + +The full per-commit changelog lives in +[`CHANGELOG.md`](../../CHANGELOG.md). Issues and discussion at +[github.com/jmcorgan/fips](https://github.com/jmcorgan/fips). + +## Contributors + +Thanks to everyone who contributed code, packaging work, bug reports, or +reviews to this release. + +- [@jcorgan](https://github.com/jmcorgan): release shepherd, high-level + design, control read plane, rekey hardening, admission, bug fixes, + testing, packaging, PR coordination, and issue resolution. +- [@mmalmi](https://github.com/mmalmi): opt-in mDNS LAN discovery and + data-plane performance work. +- [@Origami74](https://github.com/Origami74): macOS packaging and + website coordination. +- [@dskvr](https://github.com/dskvr): AUR packaging. +- [@oleksky](https://github.com/oleksky): Nym mixnet transport and the + single-container mixnet demo. diff --git a/packaging/common/fips.yaml b/packaging/common/fips.yaml index 3efa23b..a9b2dc8 100644 --- a/packaging/common/fips.yaml +++ b/packaging/common/fips.yaml @@ -33,6 +33,23 @@ node: # - "stun:stun.l.google.com:19302" # - "stun:stun.cloudflare.com:3478" # - "stun:global.stun.twilio.com:3478" + # + # Optional mDNS-based LAN discovery for sub-second same-LAN pairing. + # Opt-in (default false): default-off avoids a per-LAN identity + # broadcast on nodes that have deliberately disabled other discovery + # channels, and avoids any multicast surprise on upgrade. Requires an + # operational UDP transport (the advertised port is the one peers dial). + # lan: + # enabled: false + # # Optional application/network scope carried in the LAN-only TXT + # # record. Browsers that set a scope ignore adverts for other scopes. + # # Kept separate from the Nostr discovery `app` tag so relay-visible + # # adverts can stay generic while LAN discovery stays per-private-network. + # # scope: "lab-floor-3" + # # Advanced: overrides the mDNS service type. Leave unset in normal + # # use — only needed to run multiple isolated services on one + # # interface (e.g. test isolation on loopback). + # # service_type: "_fips._udp.local." tun: enabled: true @@ -86,6 +103,18 @@ transports: # auto_connect: true # accept_connections: true + # Nym transport — outbound-only connections through the Nym mixnet for + # sender/receiver privacy. This is a privacy posture, not a NAT-traversal + # or failover transport: it only dials out (no inbound listener) and is + # chosen for its anonymity properties. Requires a nym-socks5-client + # running separately as its own process; FIPS dials it over SOCKS5. + # nym: + # socks5_addr: "127.0.0.1:1080" # nym-socks5-client SOCKS5 address + # connect_timeout_ms: 300000 # outbound connect timeout (300s); + # # mixnet round-trips can take minutes + # mtu: 1400 # per-connection MTU + # startup_timeout_secs: 120 # wait for nym-socks5-client readiness + # Outbound LAN gateway. Allows non-FIPS hosts on the LAN to reach # mesh destinations via DNS-allocated virtual IPs and kernel NAT. # Requires: IPv6 forwarding enabled, fips daemon running with DNS.