commit 057436adb6a4eac89cebb86fd9665e00753abe0a Author: Ultradesu Date: Fri Jul 10 12:45:47 2026 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37b794e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/tmp/ +/target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ac2302a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4355 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64", + "http", + "log", + "url", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f359e08ca85e7bd759e1fd933ff2bccd81864c60a8fba0e259c7f822b0924bf" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rand_core 0.10.1", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "data-encoding-macro" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "crypto-common 0.2.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "ed25519" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" +dependencies = [ + "pkcs8", + "serdect", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011170fe4f04665565b4110afef66774fe9ffff278f3eb5b81cc73d26e27d60" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.10.1", + "serde", + "sha2", + "signature", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "federation-net" +version = "0.1.0" +dependencies = [ + "anyhow", + "blake3", + "data-encoding", + "iroh", + "iroh-base", + "iroh-tickets", + "postcard", + "rand 0.9.4", + "serde", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "federation-net-demo" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "federation-net", + "serde", + "tokio", + "tracing-subscriber", +] + +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hickory-net" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "hickory-proto", + "http", + "idna", + "ipnet", + "jni 0.22.4", + "rand 0.10.2", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni 0.22.4", + "once_cell", + "prefix-trie", + "rand 0.10.2", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni 0.22.4", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.2", + "resolv-conf", + "rustls", + "smallvec", + "system-configuration", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "igd-next" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de7238d487a9aff61f81b5ab41c0a841532a115a398b5fa92a2fadd0885e2581" +dependencies = [ + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.10.2", + "tokio", + "url", + "xmltree", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result", + "windows-sys 0.61.2", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +dependencies = [ + "serde", +] + +[[package]] +name = "iroh" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fca9b4b462c343ff88fc0af4096c186f939b602a0bc08723536ef2c31c93971" +dependencies = [ + "backon", + "blake3", + "bytes", + "cfg_aliases", + "ctutils", + "data-encoding", + "derive_more", + "ed25519-dalek", + "futures-util", + "getrandom 0.4.3", + "hickory-resolver", + "http", + "ipnet", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "pin-project", + "portable-atomic", + "portmapper", + "rand 0.10.2", + "reqwest", + "rustc-hash", + "rustls", + "rustls-pki-types", + "serde", + "smallvec", + "strum", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", +] + +[[package]] +name = "iroh-base" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830a582cd54410dc1aa71d4786a82c3297d7b0165accd8b6dbbb3b240b48140d" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "data-encoding-macro", + "derive_more", + "ed25519-dalek", + "getrandom 0.4.3", + "n0-error", + "rand 0.10.2", + "serde", + "url", + "zeroize", +] + +[[package]] +name = "iroh-dns" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516e4eedc38e33ab69a6bd325520332dc3d67b25454e2d590ebb84a25240dd9a" +dependencies = [ + "arc-swap", + "cfg_aliases", + "derive_more", + "hickory-resolver", + "iroh-base", + "n0-error", + "n0-future", + "ndk-context", + "portable-atomic", + "rand 0.10.2", + "rustls", + "simple-dns", + "strum", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "iroh-metrics" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291065721ad7c477b972e581bbc528df031dc8eb5e39fe1ff3300ae5dfb157ef" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae5f0c4405d1fbc9fb16ff422ca40620e93dc36c30ecaba0c2aee3992b7bd48" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "iroh-relay" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8149bb6a57126225a07d6928846d82dcedfd24ea0f863ef7b2eb475e1d726354" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more", + "getrandom 0.4.3", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-dns", + "iroh-metrics", + "lru", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "pin-project", + "postcard", + "rand 0.10.2", + "reqwest", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "vergen-gitcl", + "webpki-roots", + "ws_stream_wasm", +] + +[[package]] +name = "iroh-tickets" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da53233419ca36bf521ed45683b7748366f9b233032891eefc2d70567a84ac54" +dependencies = [ + "data-encoding", + "derive_more", + "iroh-base", + "n0-error", + "postcard", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + +[[package]] +name = "n0-error" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c37e81176a83a77d2514528b91bdafc70ef88aab428f0e1b91aebb8d99888895" +dependencies = [ + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2acd8b070213b0299282f884b4beba4e7b52d624fdcd504a3ad3665390c11e1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc618745ad0b7414b149d0517ad8b5573b2fb4d4e2717add3d2446ce1fdd826" +dependencies = [ + "derive_more", + "n0-error", + "n0-future", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "netdev" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "569dfbdd2efd771b24ec9bb57f956e04d4fbfc72f62b2f11961723f9b3f4b020" +dependencies = [ + "block2", + "dispatch2", + "dlopen2", + "ipnet", + "jni 0.21.1", + "libc", + "mac-addr", + "ndk-context", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "objc2", + "objc2-core-foundation", + "objc2-core-wlan", + "objc2-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.61.2", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2288fcb784eb3defd5fb16f4c4160d5f477de192eac730f43e1d11c24d9a007" +dependencies = [ + "bitflags", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9cbe01741347ef750d743d6690603f5eed8341e679fb51c8e629337aa11976" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more", + "ipnet", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows", + "windows-result", + "wmi", +] + +[[package]] +name = "noq" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bf95190af1bd4a00a10e8255ca0c8ddd9e9a9f5e79151d7a7eb6d56aff5dc89" +dependencies = [ + "bytes", + "cfg_aliases", + "derive_more", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa6c890013591e709a3e45dd53501351b7e27e7ff3c7e9fc3dce43e300e7e9d3" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more", + "enum-assoc", + "getrandom 0.4.3", + "identity-hash", + "lru-slab", + "rand 0.10.2", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3137a52df66c20090a889828d1c655f21f52294cba64e5c4fbb04fc83eee7c8e" +dependencies = [ + "cfg_aliases", + "libc", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-core-wlan" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e34919aba0d701380d911702455038a8a3587467fe0141d6a71501e7ffe48" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-security", + "objc2-security-foundation", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-security-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef76382e9cedd18123099f17638715cc3d81dba3637d4c0d39ab69df2ef345a5" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "papaya" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "serde", +] + +[[package]] +name = "portmapper" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3713e4977408279158444a18c1a01ac9bf2e7eaf1fbfd1a19ac9cd18d90721" +dependencies = [ + "base64", + "bytes", + "derive_more", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "n0-future", + "netwatch", + "num_enum", + "rand 0.10.2", + "serde", + "smallvec", + "socket2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simple-dns" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4" +dependencies = [ + "bitflags", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52efb639344a7c6adb8e62c6f3d2c19c001ff1b79a5041ba1c6ed42e19c6aa5" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.4.3", + "http", + "httparse", + "rand 0.10.2", + "ring", + "rustls-pki-types", + "sha1_smol", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "vergen-lib", +] + +[[package]] +name = "vergen-gitcl" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows", + "windows-core", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ea7131a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[workspace] +resolver = "3" +members = ["crates/federation-net", "apps/federation-net-demo"] + +[workspace.package] +edition = "2024" +license = "MIT OR Apache-2.0" +rust-version = "1.85" + +[workspace.dependencies] +iroh = "1" +iroh-base = "1" +iroh-tickets = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "io-util", "io-std", "signal"] } +serde = { version = "1", features = ["derive"] } +postcard = { version = "1", features = ["alloc"] } +blake3 = "1" +thiserror = "2" +tracing = "0.1" +clap = { version = "4", features = ["derive"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +anyhow = "1" +data-encoding = "2" +rand = "0.9" +tempfile = "3" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5034d58 --- /dev/null +++ b/README.md @@ -0,0 +1,165 @@ +# federation-net + +A reusable Rust library for direct peer-to-peer networking between equal +peers, built on [Iroh](https://iroh.computer). It establishes QUIC +connections that work through NATs (with hole punching and relay assistance), +and exchanges typed, application-defined messages over them. + +The library contains **no domain logic** — no music, video, file libraries or +databases. The application defines its own message type; the engine treats it +as an opaque serde-serializable payload. + +## What the first version does + +* Persistent peer identity (`/identity.key`, created on first start). +* Connection establishment via a shareable string ticket (`fnet...`). +* An application-level handshake that isolates networks and schemas. +* Typed message exchange in both directions over one QUIC connection. +* Network events (connect, disconnect, message, protocol error). +* Graceful shutdown. + +## What it deliberately does not do (yet) + +No global peer discovery, gossip, broadcast overlay, DHT, content search, +file/chunk/streaming transfer, database sync, CRDTs, authorization, ACLs, +HTTP APIs or metrics. The architecture allows adding these later as separate +modules or ALPN protocols. + +## Usage + +Define your own message type — any `serde`-serializable type works, no extra +traits to implement: + +```rust +use federation_net::{NetworkConfig, NetworkEngine, NetworkEvent, NetworkId, SchemaId}; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +enum DemoMessage { + Text { sender: String, body: String }, + Ping { nonce: u64 }, +} + +#[tokio::main] +async fn main() -> federation_net::Result<()> { + let config = NetworkConfig::builder() + .data_dir("./peer-a") + .network_id(NetworkId::from_name("example-network")) + .schema_id(SchemaId::from_name("demo-message-v1")) + .build()?; + + let (engine, mut events) = NetworkEngine::::start(config).await?; + + // Share this string with another peer out of band. + println!("Ticket: {}", engine.ticket().await?); + + while let Some(event) = events.recv().await { + match event { + NetworkEvent::PeerConnected { peer_id, .. } => { + engine + .send(peer_id, &DemoMessage::Ping { nonce: 1 }) + .await?; + } + NetworkEvent::MessageReceived { peer_id, message } => { + println!("{peer_id}: {message:?}"); + } + _ => {} + } + } + engine.shutdown().await +} +``` + +## Running the demo + +Start the first peer: + +```bash +cargo run -p federation-net-demo -- \ + --data-dir ./tmp/peer-a \ + --network-id demo-network \ + --name alice +``` + +It prints its endpoint id and a ticket: + +```text +Endpoint ID: ... +Network ID: ... +Schema ID: ... +Ticket: fnet... +Waiting for peers... +``` + +Start the second peer with that ticket: + +```bash +cargo run -p federation-net-demo -- \ + --data-dir ./tmp/peer-b \ + --network-id demo-network \ + --name bob \ + --connect 'fnet...' +``` + +Type a line and press Enter to send it to all connected peers. Commands: +`/peers` lists connections, `/ticket` prints your ticket, `/ping` sends a +ping, `/quit` (or Ctrl+C) shuts down gracefully. + +If the second peer uses a different `--network-id`, the connection is refused +with `Connection rejected: network id mismatch`; an incompatible schema is +refused with `Connection rejected: schema id mismatch`. + +## NetworkId + +A `NetworkId` identifies one distinct P2P network. It is 32 bytes, derived +deterministically from a name: `BLAKE3("federation-net:network:" + name)`. +The same name always yields the same id. Peers whose network ids differ +refuse to establish an application-level session, even though they share the +same transport protocol — this isolates independent deployments from each +other. + +## SchemaId + +A `SchemaId` identifies the wire format of the domain message type, derived +as `BLAKE3("federation-net:schema:" + name)` (e.g. `music-domain-v1`, +`demo-chat-v1`). Peers on the same network but with different schema ids +reject each other, because they could not decode each other's messages. Any +backwards-incompatible change to your message type requires a new schema +name. + +## Tickets + +A `PeerTicket` is a self-contained invitation string with the `fnet` prefix +(base32-encoded postcard, versioned). It carries the peer's Iroh address +(endpoint id, relay URL and direct addresses) plus the network id, schema id +and protocol version, so incompatibility is detected before any message is +exchanged. Tickets implement `Display`/`FromStr` and round-trip through their +string form. The remote peer's identity is always taken from the +authenticated Iroh connection, never trusted from the ticket payload. + +## Events + +The engine reports what happens on the network through a single bounded +event channel (`NetworkEventReceiver`): + +* `PeerConnected { peer_id, direction }` — a handshake completed + (`Incoming` or `Outgoing`). +* `PeerDisconnected { peer_id, reason }` — a connection closed. +* `MessageReceived { peer_id, message }` — a domain message arrived and was + decoded into your type. +* `ProtocolError { peer_id, error }` — a per-connection error; the engine + itself keeps running. + +Consume events promptly: the channel is bounded and the engine applies +back-pressure instead of buffering without limit. + +## Verification + +```bash +cargo fmt --check +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test --workspace +``` + +The integration tests establish real connections between two engines in one +process (they may use Iroh's public relay/discovery infrastructure), so they +need network access and take a few seconds. diff --git a/apps/federation-net-demo/Cargo.toml b/apps/federation-net-demo/Cargo.toml new file mode 100644 index 0000000..4d4ab0c --- /dev/null +++ b/apps/federation-net-demo/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "federation-net-demo" +version = "0.1.0" +description = "CLI demo for the federation-net P2P engine" +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +federation-net = { path = "../../crates/federation-net" } +tokio = { workspace = true } +serde = { workspace = true } +clap = { workspace = true } +tracing-subscriber = { workspace = true } +anyhow = { workspace = true } diff --git a/apps/federation-net-demo/src/main.rs b/apps/federation-net-demo/src/main.rs new file mode 100644 index 0000000..c7e2a51 --- /dev/null +++ b/apps/federation-net-demo/src/main.rs @@ -0,0 +1,250 @@ +//! CLI demo for the federation-net P2P engine. +//! +//! Starts a peer, prints a shareable ticket and exchanges chat messages with +//! connected peers. Run two instances (optionally on different machines) and +//! pass the first peer's ticket to the second via `--connect`. + +use std::io::Write; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Context; +use clap::Parser; +use federation_net::{ + ConnectionDirection, NetworkConfig, NetworkEngine, NetworkEvent, NetworkId, PeerTicket, + SchemaId, +}; +use tokio::io::{AsyncBufReadExt, BufReader}; + +/// Domain message type of the demo application. +/// +/// The library knows nothing about this type; it is defined entirely here. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +enum DemoMessage { + Text { sender: String, body: String }, + Ping { nonce: u64 }, +} + +/// Schema name of the demo message format. +const DEMO_SCHEMA: &str = "demo-message-v1"; + +#[derive(Debug, Parser)] +#[command( + name = "federation-net-demo", + about = "P2P chat demo for federation-net" +)] +struct Args { + /// Directory for the persistent peer identity. + #[arg(long)] + data_dir: PathBuf, + + /// Human-readable name of the network to join. + #[arg(long)] + network_id: String, + + /// Display name used as the sender of chat messages. + #[arg(long)] + name: String, + + /// Ticket of a peer to connect to on startup. + #[arg(long)] + connect: Option, + + /// Enable verbose logging. + #[arg(long)] + verbose: bool, +} + +fn prompt(name: &str) { + print!("{name}> "); + let _ = std::io::stdout().flush(); +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let args = Args::parse(); + + let filter = if args.verbose { + "federation_net=debug,federation_net_demo=debug,info" + } else { + "warn" + }; + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(filter)), + ) + .with_writer(std::io::stderr) + .init(); + + let config = NetworkConfig::builder() + .data_dir(&args.data_dir) + .network_id(NetworkId::from_name(&args.network_id)) + .schema_id(SchemaId::from_name(DEMO_SCHEMA)) + .build() + .context("invalid configuration")?; + + let (engine, mut events) = NetworkEngine::::start(config) + .await + .context("failed to start the network engine")?; + + println!("Endpoint ID: {}", engine.endpoint_id()); + println!("Network ID: {}", engine.network_id()); + println!("Schema ID: {}", engine.schema_id()); + match engine.ticket().await { + Ok(ticket) => println!("Ticket: {ticket}"), + Err(err) => eprintln!("Could not create a ticket yet: {err}"), + } + + if let Some(ticket) = &args.connect { + let ticket: PeerTicket = ticket.parse().context("invalid ticket")?; + match engine.connect(ticket).await { + Ok(peer_id) => { + println!("Connected to: {peer_id}"); + println!("Type a message and press Enter."); + } + Err(err) => { + println!("Connection rejected: {err}"); + let _ = engine.shutdown().await; + std::process::exit(1); + } + } + } else { + println!("Waiting for peers..."); + } + + let name = args.name.clone(); + let mut stdin = BufReader::new(tokio::io::stdin()).lines(); + prompt(&name); + + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + println!(); + break; + } + line = stdin.next_line() => { + match line { + Ok(Some(line)) => { + if handle_line(&engine, &name, line.trim()).await { + break; + } + prompt(&name); + } + Ok(None) => break, // stdin closed + Err(err) => { + eprintln!("failed to read stdin: {err}"); + break; + } + } + } + event = events.recv() => { + match event { + Some(event) => { + println!(); + print_event(&name, event); + prompt(&name); + } + None => { + println!("Engine stopped."); + break; + } + } + } + } + } + + engine.shutdown().await.context("shutdown failed")?; + println!("Bye."); + // The blocking thread behind `tokio::io::stdin()` keeps the runtime alive + // until stdin closes; the engine is already shut down, so exit directly. + let _ = std::io::stdout().flush(); + std::process::exit(0); +} + +/// Handles one line of user input. Returns `true` when the user asked to quit. +async fn handle_line(engine: &NetworkEngine, name: &str, line: &str) -> bool { + match line { + "" => false, + "/quit" => true, + "/peers" => { + let peers = engine.connected_peers(); + if peers.is_empty() { + println!("No connected peers."); + } else { + for peer in peers { + println!("{peer}"); + } + } + false + } + "/ticket" => { + match engine.ticket().await { + Ok(ticket) => println!("Ticket: {ticket}"), + Err(err) => println!("Could not create a ticket: {err}"), + } + false + } + "/ping" => { + let nonce = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or_default(); + broadcast(engine, DemoMessage::Ping { nonce }).await; + false + } + body => { + broadcast( + engine, + DemoMessage::Text { + sender: name.to_string(), + body: body.to_string(), + }, + ) + .await; + false + } + } +} + +/// Sends a message to every connected peer. +async fn broadcast(engine: &NetworkEngine, message: DemoMessage) { + let peers = engine.connected_peers(); + if peers.is_empty() { + println!("No connected peers."); + return; + } + for peer in peers { + if let Err(err) = engine.send(peer, &message).await { + println!("failed to send to {peer}: {err}"); + } + } +} + +fn print_event(name: &str, event: NetworkEvent) { + match event { + NetworkEvent::PeerConnected { peer_id, direction } => { + let direction = match direction { + ConnectionDirection::Incoming => "incoming", + ConnectionDirection::Outgoing => "outgoing", + }; + println!("Peer connected ({direction}): {peer_id}"); + } + NetworkEvent::PeerDisconnected { peer_id, reason } => match reason { + Some(reason) => println!("Peer disconnected: {peer_id} ({reason})"), + None => println!("Peer disconnected: {peer_id}"), + }, + NetworkEvent::MessageReceived { peer_id, message } => match message { + DemoMessage::Text { body, .. } => { + println!("{name} received from {peer_id}: {body}"); + } + DemoMessage::Ping { nonce } => { + println!("{name} received ping from {peer_id} (nonce {nonce})"); + } + }, + NetworkEvent::ProtocolError { peer_id, error } => match peer_id { + Some(peer_id) => println!("Protocol error with {peer_id}: {error}"), + None => println!("Protocol error: {error}"), + }, + } +} diff --git a/crates/federation-net/Cargo.toml b/crates/federation-net/Cargo.toml new file mode 100644 index 0000000..43fea27 --- /dev/null +++ b/crates/federation-net/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "federation-net" +version = "0.1.0" +description = "Generic peer-to-peer networking engine built on Iroh" +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +iroh = { workspace = true } +iroh-base = { workspace = true } +iroh-tickets = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +postcard = { workspace = true } +blake3 = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } +data-encoding = { workspace = true } +rand = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +tempfile = { workspace = true } +tokio = { workspace = true, features = ["test-util"] } diff --git a/crates/federation-net/src/config.rs b/crates/federation-net/src/config.rs new file mode 100644 index 0000000..10cbf35 --- /dev/null +++ b/crates/federation-net/src/config.rs @@ -0,0 +1,226 @@ +//! Engine configuration. + +use std::path::PathBuf; +use std::time::Duration; + +use crate::error::{NetworkError, Result}; +use crate::protocol::{NetworkId, SchemaId}; + +/// Default maximum size of an encoded domain message (256 KiB). +pub const DEFAULT_MAX_MESSAGE_SIZE: usize = 256 * 1024; +/// Default timeout applied to handshakes and message round-trips. +pub const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(10); +/// Default capacity of the event channel. +pub const DEFAULT_EVENT_CHANNEL_CAPACITY: usize = 1024; +/// Default limit of concurrently processed streams per peer. +pub const DEFAULT_MAX_CONCURRENT_STREAMS_PER_PEER: usize = 64; + +/// Configuration for a [`crate::NetworkEngine`]. +/// +/// Use [`NetworkConfig::builder`] to construct a validated instance. +#[derive(Debug, Clone)] +pub struct NetworkConfig { + /// Directory where the persistent peer identity is stored. + pub data_dir: PathBuf, + /// Identifier of the network this peer participates in. + pub network_id: NetworkId, + /// Identifier of the domain message schema. + pub schema_id: SchemaId, + /// Maximum size in bytes of a single encoded domain message. + pub max_message_size: usize, + /// Timeout applied to handshakes and message round-trips. + pub request_timeout: Duration, + /// Capacity of the bounded event channel. + pub event_channel_capacity: usize, + /// Maximum number of concurrently processed streams per peer. + pub max_concurrent_streams_per_peer: usize, +} + +impl NetworkConfig { + /// Returns a new [`NetworkConfigBuilder`]. + pub fn builder() -> NetworkConfigBuilder { + NetworkConfigBuilder::default() + } +} + +/// Builder for [`NetworkConfig`]. +/// +/// `data_dir`, `network_id` and `schema_id` are required; everything else has +/// sensible defaults. +#[derive(Debug, Default, Clone)] +pub struct NetworkConfigBuilder { + data_dir: Option, + network_id: Option, + schema_id: Option, + max_message_size: Option, + request_timeout: Option, + event_channel_capacity: Option, + max_concurrent_streams_per_peer: Option, +} + +impl NetworkConfigBuilder { + /// Sets the directory where the persistent peer identity is stored. + pub fn data_dir(mut self, dir: impl Into) -> Self { + self.data_dir = Some(dir.into()); + self + } + + /// Sets the network identifier. + pub fn network_id(mut self, network_id: NetworkId) -> Self { + self.network_id = Some(network_id); + self + } + + /// Sets the schema identifier. + pub fn schema_id(mut self, schema_id: SchemaId) -> Self { + self.schema_id = Some(schema_id); + self + } + + /// Sets the maximum size in bytes of a single encoded domain message. + pub fn max_message_size(mut self, size: usize) -> Self { + self.max_message_size = Some(size); + self + } + + /// Sets the timeout applied to handshakes and message round-trips. + pub fn request_timeout(mut self, timeout: Duration) -> Self { + self.request_timeout = Some(timeout); + self + } + + /// Sets the capacity of the bounded event channel. + pub fn event_channel_capacity(mut self, capacity: usize) -> Self { + self.event_channel_capacity = Some(capacity); + self + } + + /// Sets the maximum number of concurrently processed streams per peer. + pub fn max_concurrent_streams_per_peer(mut self, limit: usize) -> Self { + self.max_concurrent_streams_per_peer = Some(limit); + self + } + + /// Validates the configuration and builds a [`NetworkConfig`]. + pub fn build(self) -> Result { + let data_dir = self + .data_dir + .ok_or_else(|| NetworkError::InvalidConfig("data_dir is required".into()))?; + let network_id = self + .network_id + .ok_or_else(|| NetworkError::InvalidConfig("network_id is required".into()))?; + let schema_id = self + .schema_id + .ok_or_else(|| NetworkError::InvalidConfig("schema_id is required".into()))?; + + if data_dir.as_os_str().is_empty() { + return Err(NetworkError::InvalidConfig( + "data_dir must not be empty".into(), + )); + } + + let max_message_size = self.max_message_size.unwrap_or(DEFAULT_MAX_MESSAGE_SIZE); + if max_message_size == 0 { + return Err(NetworkError::InvalidConfig( + "max_message_size must be greater than zero".into(), + )); + } + if max_message_size > u32::MAX as usize { + return Err(NetworkError::InvalidConfig( + "max_message_size must fit into a 32-bit length prefix".into(), + )); + } + + let request_timeout = self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT); + if request_timeout.is_zero() { + return Err(NetworkError::InvalidConfig( + "request_timeout must be greater than zero".into(), + )); + } + + let event_channel_capacity = self + .event_channel_capacity + .unwrap_or(DEFAULT_EVENT_CHANNEL_CAPACITY); + if event_channel_capacity == 0 { + return Err(NetworkError::InvalidConfig( + "event_channel_capacity must be greater than zero".into(), + )); + } + + let max_concurrent_streams_per_peer = self + .max_concurrent_streams_per_peer + .unwrap_or(DEFAULT_MAX_CONCURRENT_STREAMS_PER_PEER); + if max_concurrent_streams_per_peer == 0 { + return Err(NetworkError::InvalidConfig( + "max_concurrent_streams_per_peer must be greater than zero".into(), + )); + } + + Ok(NetworkConfig { + data_dir, + network_id, + schema_id, + max_message_size, + request_timeout, + event_channel_capacity, + max_concurrent_streams_per_peer, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn base_builder() -> NetworkConfigBuilder { + NetworkConfig::builder() + .data_dir("./some-dir") + .network_id(NetworkId::from_name("test-network")) + .schema_id(SchemaId::from_name("test-schema")) + } + + #[test] + fn builder_applies_defaults() { + let config = base_builder().build().expect("valid config"); + assert_eq!(config.max_message_size, DEFAULT_MAX_MESSAGE_SIZE); + assert_eq!(config.request_timeout, DEFAULT_REQUEST_TIMEOUT); + assert_eq!( + config.event_channel_capacity, + DEFAULT_EVENT_CHANNEL_CAPACITY + ); + assert_eq!( + config.max_concurrent_streams_per_peer, + DEFAULT_MAX_CONCURRENT_STREAMS_PER_PEER + ); + } + + #[test] + fn builder_rejects_missing_required_fields() { + assert!(NetworkConfig::builder().build().is_err()); + assert!( + NetworkConfig::builder() + .data_dir("./dir") + .network_id(NetworkId::from_name("n")) + .build() + .is_err() + ); + } + + #[test] + fn builder_rejects_invalid_values() { + assert!(base_builder().max_message_size(0).build().is_err()); + assert!( + base_builder() + .request_timeout(Duration::ZERO) + .build() + .is_err() + ); + assert!(base_builder().event_channel_capacity(0).build().is_err()); + assert!( + base_builder() + .max_concurrent_streams_per_peer(0) + .build() + .is_err() + ); + } +} diff --git a/crates/federation-net/src/engine.rs b/crates/federation-net/src/engine.rs new file mode 100644 index 0000000..e2f7fa9 --- /dev/null +++ b/crates/federation-net/src/engine.rs @@ -0,0 +1,757 @@ +//! The network engine: endpoint lifecycle, connection registry and messaging. + +use std::collections::HashMap; +use std::fmt; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak}; +use std::time::Duration; + +use iroh::endpoint::{Connection, RecvStream, SendStream, VarInt, presets}; +use iroh::protocol::{AcceptError, ProtocolHandler, Router}; +use iroh::{Endpoint, EndpointId}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use tokio::sync::{Semaphore, mpsc}; +use tokio::task::JoinSet; +use tokio::time::timeout; +use tracing::{debug, info, warn}; + +use crate::config::NetworkConfig; +use crate::error::{NetworkError, Result}; +use crate::event::{ConnectionDirection, NetworkEvent, NetworkEventReceiver}; +use crate::identity; +use crate::protocol::{ + ALPN, Handshake, HandshakeAck, HandshakeErrorCode, MAX_HANDSHAKE_FRAME_SIZE, + MessageRejectReason, MessageRequest, MessageResponse, NetworkId, PROTOCOL_VERSION, SchemaId, +}; +use crate::ticket::{PeerTicket, TICKET_VERSION}; +use crate::wire; + +/// Close code for a graceful, error-free close. +const CLOSE_CODE_OK: VarInt = VarInt::from_u32(0); +/// Close code used when the handshake was rejected by either side. +const CLOSE_CODE_HANDSHAKE_REJECTED: VarInt = VarInt::from_u32(1); +/// Close code used when the engine shuts down. +const CLOSE_CODE_SHUTDOWN: VarInt = VarInt::from_u32(2); +/// Close code used when a connection is replaced by a newer one. +const CLOSE_CODE_REPLACED: VarInt = VarInt::from_u32(3); + +/// Extra bytes allowed on top of `max_message_size` for message framing +/// (request id, length varints and enum tags). +const MESSAGE_FRAME_OVERHEAD: usize = 64; +/// Maximum size of an encoded [`MessageResponse`] frame. +const MAX_RESPONSE_FRAME_SIZE: usize = 1024; +/// How long the accepting side keeps a rejected connection open so the +/// rejection ack reaches the initiator before the connection is torn down. +const REJECT_LINGER: Duration = Duration::from_secs(3); +/// How long [`NetworkEngine::shutdown`] waits for background tasks before +/// aborting them. +const SHUTDOWN_TASK_GRACE: Duration = Duration::from_secs(5); + +/// Bounds required of a domain message type. +/// +/// This trait is implemented automatically for every type that satisfies the +/// bounds; applications never implement it by hand. +pub trait Message: Serialize + DeserializeOwned + Send + Sync + 'static {} + +impl Message for T where T: Serialize + DeserializeOwned + Send + Sync + 'static {} + +/// Locks a mutex, recovering the guard if the mutex was poisoned. +/// +/// The engine never panics while holding one of its locks, so poisoning can +/// only originate from a panic in unrelated user code; recovering is safe. +fn lock(mutex: &Mutex) -> MutexGuard<'_, T> { + mutex.lock().unwrap_or_else(PoisonError::into_inner) +} + +/// State kept for every active peer connection. +struct PeerState { + connection: Connection, + direction: ConnectionDirection, + semaphore: Arc, + /// Monotonic registration counter, used so a stale connection's cleanup + /// never removes a newer connection to the same peer. + generation: u64, +} + +/// State shared between the engine handles, the protocol handler and all +/// background tasks. +struct Shared { + config: NetworkConfig, + endpoint: Endpoint, + router: Mutex>, + peers: Mutex>, + events: Mutex>>>, + tasks: Mutex>, + next_generation: AtomicU64, + shutting_down: AtomicBool, +} + +impl Shared { + fn is_shutting_down(&self) -> bool { + self.shutting_down.load(Ordering::SeqCst) + } + + fn ensure_running(&self) -> Result<()> { + if self.is_shutting_down() { + Err(NetworkError::ShuttingDown) + } else { + Ok(()) + } + } + + /// Delivers an event to the application. + /// + /// The channel is bounded; if it is full this awaits until the + /// application consumes events, applying natural back-pressure without + /// unbounded buffering. + async fn emit(&self, event: NetworkEvent) { + let sender = lock(&self.events).clone(); + if let Some(sender) = sender + && sender.send(event).await.is_err() + { + debug!("event receiver dropped; event discarded"); + } + } + + /// Spawns a background task tracked until shutdown. + fn spawn_task(&self, future: F) + where + F: Future + Send + 'static, + { + let mut tasks = lock(&self.tasks); + // Reap already-finished tasks so the set does not grow unboundedly. + while tasks.try_join_next().is_some() {} + tasks.spawn(future); + } + + /// Adds a connection to the registry, replacing (and closing) any previous + /// connection to the same peer. Returns the registration generation. + fn register_connection( + &self, + peer_id: EndpointId, + connection: Connection, + direction: ConnectionDirection, + ) -> u64 { + let generation = self.next_generation.fetch_add(1, Ordering::Relaxed); + let state = PeerState { + connection, + direction, + semaphore: Arc::new(Semaphore::new(self.config.max_concurrent_streams_per_peer)), + generation, + }; + let replaced = lock(&self.peers).insert(peer_id, state); + if let Some(old) = replaced { + debug!(peer = %peer_id, "replacing existing connection"); + old.connection + .close(CLOSE_CODE_REPLACED, b"replaced by a new connection"); + } + generation + } + + /// Removes the connection from the registry and reports the disconnect, + /// unless a newer connection to the same peer took its place. + async fn cleanup_connection( + &self, + peer_id: EndpointId, + generation: u64, + reason: Option, + ) { + let removed = { + let mut peers = lock(&self.peers); + match peers.get(&peer_id) { + Some(state) if state.generation == generation => peers.remove(&peer_id), + _ => None, + } + }; + if let Some(state) = removed { + info!( + peer = %peer_id, + direction = ?state.direction, + reason = reason.as_deref().unwrap_or("unknown"), + "peer disconnected" + ); + self.emit(NetworkEvent::PeerDisconnected { peer_id, reason }) + .await; + } + } + + /// Validates a remote handshake against the local configuration. + fn validate_handshake(&self, handshake: &Handshake) -> Option { + if handshake.protocol_version != PROTOCOL_VERSION { + Some(HandshakeErrorCode::UnsupportedProtocolVersion) + } else if handshake.network_id != self.config.network_id { + Some(HandshakeErrorCode::NetworkMismatch) + } else if handshake.schema_id != self.config.schema_id { + Some(HandshakeErrorCode::SchemaMismatch) + } else { + None + } + } + + /// Drives an established connection: accepts message streams until the + /// connection closes, then cleans up the registry entry. + async fn connection_loop( + self: Arc, + peer_id: EndpointId, + connection: Connection, + generation: u64, + ) { + let semaphore = { + let peers = lock(&self.peers); + match peers.get(&peer_id) { + Some(state) if state.generation == generation => state.semaphore.clone(), + // The connection was already replaced; nothing to drive. + _ => return, + } + }; + loop { + let (send, recv) = match connection.accept_bi().await { + Ok(streams) => streams, + Err(_) => break, + }; + let permit = match semaphore.clone().acquire_owned().await { + Ok(permit) => permit, + Err(_) => break, + }; + let shared = self.clone(); + self.spawn_task(async move { + shared + .handle_message_stream(peer_id, send, recv, permit) + .await; + // The permit is released when the task finishes. + }); + } + let reason = connection.close_reason().map(|err| err.to_string()); + self.cleanup_connection(peer_id, generation, reason).await; + } + + /// Handles a single incoming message stream: decodes the request, hands + /// the message to the application and acknowledges it. + async fn handle_message_stream( + &self, + peer_id: EndpointId, + mut send: SendStream, + mut recv: RecvStream, + _permit: tokio::sync::OwnedSemaphorePermit, + ) { + let request_timeout = self.config.request_timeout; + let max_frame = self.config.max_message_size + MESSAGE_FRAME_OVERHEAD; + let request: MessageRequest = + match timeout(request_timeout, wire::read_frame(&mut recv, max_frame)).await { + Ok(Ok(request)) => request, + Ok(Err(err)) => { + self.report_protocol_error(Some(peer_id), format!("bad message frame: {err}")) + .await; + return; + } + Err(_) => { + self.report_protocol_error( + Some(peer_id), + "timed out reading message frame".to_string(), + ) + .await; + return; + } + }; + let request_id = request.request_id; + let response = if self.is_shutting_down() { + MessageResponse::Rejected { + request_id, + reason: MessageRejectReason::ShuttingDown, + } + } else { + match wire::decode::(&request.payload) { + Ok(message) => { + self.emit(NetworkEvent::MessageReceived { peer_id, message }) + .await; + MessageResponse::Accepted { request_id } + } + Err(err) => { + self.report_protocol_error( + Some(peer_id), + format!("failed to decode domain message: {err}"), + ) + .await; + MessageResponse::Rejected { + request_id, + reason: MessageRejectReason::MalformedPayload, + } + } + } + }; + let write = timeout( + request_timeout, + wire::write_frame(&mut send, &response, MAX_RESPONSE_FRAME_SIZE), + ) + .await; + match write { + Ok(Ok(())) => { + let _ = send.finish(); + } + Ok(Err(err)) => { + debug!(peer = %peer_id, error = %err, "failed to send message response"); + } + Err(_) => { + debug!(peer = %peer_id, "timed out sending message response"); + } + } + } + + async fn report_protocol_error(&self, peer_id: Option, error: String) { + match peer_id { + Some(peer) => warn!(peer = %peer, error = %error, "protocol error"), + None => warn!(error = %error, "protocol error"), + } + self.emit(NetworkEvent::ProtocolError { peer_id, error }) + .await; + } + + /// Handles a freshly accepted incoming connection: performs the handshake + /// and, if accepted, drives the connection until it closes. + async fn handle_incoming(self: &Arc, connection: &Connection) -> Result<()> { + let peer_id = connection.remote_id(); + if self.is_shutting_down() { + connection.close(CLOSE_CODE_SHUTDOWN, b"engine is shutting down"); + return Err(NetworkError::ShuttingDown); + } + debug!(peer = %peer_id, "incoming connection"); + let request_timeout = self.config.request_timeout; + + let (mut send, mut recv) = timeout(request_timeout, connection.accept_bi()) + .await + .map_err(|_| NetworkError::Timeout) + .and_then(|res| { + res.map_err(|err| { + NetworkError::Transport(format!("failed to accept handshake stream: {err}")) + }) + }) + .inspect_err(|_| { + connection.close(CLOSE_CODE_HANDSHAKE_REJECTED, b"handshake failed"); + })?; + + let handshake: Result = timeout( + request_timeout, + wire::read_frame(&mut recv, MAX_HANDSHAKE_FRAME_SIZE), + ) + .await + .map_err(|_| NetworkError::Timeout) + .and_then(|res| res); + + let verdict = match &handshake { + Ok(handshake) => self.validate_handshake(handshake), + Err(_) => Some(HandshakeErrorCode::InvalidHandshake), + }; + + if let Some(code) = verdict { + warn!(peer = %peer_id, code = ?code, "handshake rejected"); + let ack = HandshakeAck { + accepted: false, + error: Some(code), + }; + let sent = timeout( + request_timeout, + wire::write_frame(&mut send, &ack, MAX_HANDSHAKE_FRAME_SIZE), + ) + .await; + if matches!(sent, Ok(Ok(()))) { + let _ = send.finish(); + // Keep the connection open briefly so QUIC delivers the ack; + // the initiator closes as soon as it has read the rejection. + let _ = timeout(REJECT_LINGER, connection.closed()).await; + } + connection.close(CLOSE_CODE_HANDSHAKE_REJECTED, b"handshake rejected"); + return Err(handshake_code_to_error(code)); + } + + let ack = HandshakeAck { + accepted: true, + error: None, + }; + timeout( + request_timeout, + wire::write_frame(&mut send, &ack, MAX_HANDSHAKE_FRAME_SIZE), + ) + .await + .map_err(|_| NetworkError::Timeout) + .and_then(|res| res) + .inspect_err(|_| { + connection.close(CLOSE_CODE_HANDSHAKE_REJECTED, b"handshake failed"); + })?; + let _ = send.finish(); + + let generation = + self.register_connection(peer_id, connection.clone(), ConnectionDirection::Incoming); + info!(peer = %peer_id, "peer connected (incoming)"); + self.emit(NetworkEvent::PeerConnected { + peer_id, + direction: ConnectionDirection::Incoming, + }) + .await; + // Drive the connection inside the router's accept task; it runs for + // as long as the connection lives. + self.clone() + .connection_loop(peer_id, connection.clone(), generation) + .await; + Ok(()) + } + + /// Runs the client side of the handshake on a fresh outgoing connection. + async fn run_client_handshake(&self, connection: &Connection) -> Result<()> { + let (mut send, mut recv) = connection.open_bi().await.map_err(|err| { + NetworkError::Transport(format!("failed to open handshake stream: {err}")) + })?; + let handshake = Handshake { + protocol_version: PROTOCOL_VERSION, + network_id: self.config.network_id, + schema_id: self.config.schema_id, + }; + wire::write_frame(&mut send, &handshake, MAX_HANDSHAKE_FRAME_SIZE).await?; + let _ = send.finish(); + let ack: HandshakeAck = wire::read_frame(&mut recv, MAX_HANDSHAKE_FRAME_SIZE).await?; + if ack.accepted { + Ok(()) + } else { + let code = ack.error.unwrap_or(HandshakeErrorCode::InvalidHandshake); + Err(handshake_code_to_error(code)) + } + } +} + +fn handshake_code_to_error(code: HandshakeErrorCode) -> NetworkError { + match code { + HandshakeErrorCode::UnsupportedProtocolVersion => NetworkError::UnsupportedProtocolVersion, + HandshakeErrorCode::NetworkMismatch => NetworkError::NetworkMismatch, + HandshakeErrorCode::SchemaMismatch => NetworkError::SchemaMismatch, + HandshakeErrorCode::InvalidHandshake => NetworkError::HandshakeRejected, + } +} + +/// Protocol handler registered with the Iroh [`Router`] for [`ALPN`]. +/// +/// Holds only a weak reference to the shared state so the router does not +/// keep the engine alive after all engine handles are dropped. +struct FederationProtocol { + shared: Weak>, +} + +impl fmt::Debug for FederationProtocol { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FederationProtocol").finish_non_exhaustive() + } +} + +impl ProtocolHandler for FederationProtocol { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let Some(shared) = self.shared.upgrade() else { + connection.close(CLOSE_CODE_SHUTDOWN, b"engine is gone"); + return Ok(()); + }; + let peer_id = connection.remote_id(); + if let Err(err) = shared.handle_incoming(&connection).await { + shared + .report_protocol_error(Some(peer_id), format!("incoming connection failed: {err}")) + .await; + } + // Errors of a single connection never bring down the router; the + // connection has already been closed with an appropriate code. + Ok(()) + } +} + +/// A generic peer-to-peer network engine on top of Iroh. +/// +/// `M` is the application-defined domain message type; the engine treats it +/// as an opaque, postcard-serializable payload. The engine is cheaply +/// clonable: all clones share the same endpoint and connection registry. +pub struct NetworkEngine { + shared: Arc>, +} + +impl Clone for NetworkEngine { + fn clone(&self) -> Self { + Self { + shared: self.shared.clone(), + } + } +} + +impl fmt::Debug for NetworkEngine { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NetworkEngine").finish_non_exhaustive() + } +} + +impl NetworkEngine { + /// Starts the engine: loads (or creates) the persistent identity, binds + /// the Iroh endpoint and starts accepting connections. + /// + /// Returns the engine handle together with the single receiver for + /// [`NetworkEvent`]s. + pub async fn start(config: NetworkConfig) -> Result<(Self, NetworkEventReceiver)> { + let secret_key = identity::load_or_create(&config.data_dir).await?; + let endpoint = Endpoint::builder(presets::N0) + .secret_key(secret_key) + .bind() + .await + .map_err(|err| NetworkError::Transport(format!("failed to bind endpoint: {err}")))?; + let endpoint_id = endpoint.id(); + let (sender, receiver) = mpsc::channel(config.event_channel_capacity); + let shared = Arc::new(Shared { + config, + endpoint: endpoint.clone(), + router: Mutex::new(None), + peers: Mutex::new(HashMap::new()), + events: Mutex::new(Some(sender)), + tasks: Mutex::new(JoinSet::new()), + next_generation: AtomicU64::new(0), + shutting_down: AtomicBool::new(false), + }); + let handler = FederationProtocol { + shared: Arc::downgrade(&shared), + }; + let router = Router::builder(endpoint).accept(ALPN, handler).spawn(); + *lock(&shared.router) = Some(router); + info!( + endpoint_id = %endpoint_id, + network_id = %shared.config.network_id, + schema_id = %shared.config.schema_id, + "network engine started" + ); + Ok((Self { shared }, NetworkEventReceiver::new(receiver))) + } + + /// Returns the stable identifier of this peer. + pub fn endpoint_id(&self) -> EndpointId { + self.shared.endpoint.id() + } + + /// Returns the network this engine participates in. + pub fn network_id(&self) -> NetworkId { + self.shared.config.network_id + } + + /// Returns the message schema this engine uses. + pub fn schema_id(&self) -> SchemaId { + self.shared.config.schema_id + } + + /// Creates a shareable [`PeerTicket`] for this peer. + /// + /// Waits (bounded by the configured request timeout) for the endpoint to + /// come online so the ticket contains relay information whenever + /// possible; on an isolated network the ticket falls back to the locally + /// known direct addresses. + pub async fn ticket(&self) -> Result { + self.shared.ensure_running()?; + let endpoint = &self.shared.endpoint; + let has_relay = endpoint.addr().relay_urls().next().is_some(); + if !has_relay { + let _ = timeout(self.shared.config.request_timeout, endpoint.online()).await; + } + let endpoint_addr = endpoint.addr(); + if endpoint_addr.is_empty() { + return Err(NetworkError::Transport( + "endpoint has no reachable addresses yet".to_string(), + )); + } + Ok(PeerTicket { + ticket_version: TICKET_VERSION, + protocol_version: PROTOCOL_VERSION, + network_id: self.shared.config.network_id, + schema_id: self.shared.config.schema_id, + endpoint_addr, + }) + } + + /// Connects to the peer described by `ticket` and performs the handshake. + /// + /// Incompatible tickets (different network, schema or protocol version) + /// are rejected before any connection is attempted. An existing healthy + /// connection to the same peer is reused. + pub async fn connect(&self, ticket: PeerTicket) -> Result { + let shared = &self.shared; + shared.ensure_running()?; + if ticket.ticket_version != TICKET_VERSION { + return Err(NetworkError::InvalidTicket(format!( + "unsupported ticket version {}", + ticket.ticket_version + ))); + } + if ticket.protocol_version != PROTOCOL_VERSION { + return Err(NetworkError::UnsupportedProtocolVersion); + } + if ticket.network_id != shared.config.network_id { + return Err(NetworkError::NetworkMismatch); + } + if ticket.schema_id != shared.config.schema_id { + return Err(NetworkError::SchemaMismatch); + } + let target = ticket.endpoint_id(); + if target == self.endpoint_id() { + return Err(NetworkError::InvalidTicket( + "the ticket points to this peer itself".to_string(), + )); + } + if lock(&shared.peers).contains_key(&target) { + debug!(peer = %target, "reusing existing connection"); + return Ok(target); + } + + let request_timeout = shared.config.request_timeout; + let connection = timeout( + request_timeout, + shared.endpoint.connect(ticket.endpoint_addr, ALPN), + ) + .await + .map_err(|_| NetworkError::Timeout)? + .map_err(|err| NetworkError::Transport(format!("failed to connect: {err}")))?; + // The remote identity comes from the authenticated Iroh connection, + // never from the ticket payload. + let peer_id = connection.remote_id(); + + let handshake = timeout(request_timeout, shared.run_client_handshake(&connection)) + .await + .map_err(|_| NetworkError::Timeout) + .and_then(|res| res); + if let Err(err) = handshake { + connection.close(CLOSE_CODE_HANDSHAKE_REJECTED, b"handshake failed"); + return Err(err); + } + + let generation = + shared.register_connection(peer_id, connection.clone(), ConnectionDirection::Outgoing); + info!(peer = %peer_id, "peer connected (outgoing)"); + shared + .emit(NetworkEvent::PeerConnected { + peer_id, + direction: ConnectionDirection::Outgoing, + }) + .await; + let loop_shared = shared.clone(); + shared.spawn_task(async move { + loop_shared + .connection_loop(peer_id, connection, generation) + .await; + }); + Ok(peer_id) + } + + /// Sends a domain message to a connected peer. + /// + /// Returns only after the peer acknowledged the message. Fails with + /// [`NetworkError::PeerNotConnected`] if there is no active connection to + /// the peer; no implicit dialing or peer lookup is performed. + pub async fn send(&self, peer: EndpointId, message: &M) -> Result<()> { + let shared = &self.shared; + shared.ensure_running()?; + let connection = lock(&shared.peers) + .get(&peer) + .map(|state| state.connection.clone()) + .ok_or(NetworkError::PeerNotConnected(peer))?; + let payload = wire::encode(message, shared.config.max_message_size)?; + let request_id: [u8; 16] = rand::random(); + let request = MessageRequest { + request_id, + payload, + }; + let max_frame = shared.config.max_message_size + MESSAGE_FRAME_OVERHEAD; + timeout(shared.config.request_timeout, async move { + let (mut send, mut recv) = connection.open_bi().await.map_err(|err| { + NetworkError::Transport(format!("failed to open message stream: {err}")) + })?; + wire::write_frame(&mut send, &request, max_frame).await?; + let _ = send.finish(); + let response: MessageResponse = + wire::read_frame(&mut recv, MAX_RESPONSE_FRAME_SIZE).await?; + match response { + MessageResponse::Accepted { request_id: id } if id == request_id => Ok(()), + MessageResponse::Rejected { + request_id: id, + reason, + } if id == request_id => Err(NetworkError::MessageRejected(reason.to_string())), + _ => Err(NetworkError::Transport( + "peer responded to a different request".to_string(), + )), + } + }) + .await + .map_err(|_| NetworkError::Timeout)? + } + + /// Returns the ids of all currently connected peers. + pub fn connected_peers(&self) -> Vec { + lock(&self.shared.peers).keys().copied().collect() + } + + /// Closes the connection to `peer` and removes it from the registry. + pub async fn disconnect(&self, peer: EndpointId) -> Result<()> { + let state = lock(&self.shared.peers) + .remove(&peer) + .ok_or(NetworkError::PeerNotConnected(peer))?; + state + .connection + .close(CLOSE_CODE_OK, b"disconnected by local peer"); + info!(peer = %peer, "peer disconnected (local request)"); + self.shared + .emit(NetworkEvent::PeerDisconnected { + peer_id: peer, + reason: Some("disconnected by local peer".to_string()), + }) + .await; + Ok(()) + } + + /// Shuts the engine down gracefully. + /// + /// Rejects new operations, closes all connections, stops the protocol + /// router, closes the endpoint, waits for background tasks and finally + /// closes the event channel. Calling `shutdown` on several clones of the + /// same engine is safe; only the first call does the work. + pub async fn shutdown(self) -> Result<()> { + let shared = &self.shared; + if shared.shutting_down.swap(true, Ordering::SeqCst) { + return Ok(()); + } + info!("network engine shutting down"); + + // Close all active connections. + let states: Vec = lock(&shared.peers) + .drain() + .map(|(_, state)| state) + .collect(); + for state in &states { + state + .connection + .close(CLOSE_CODE_SHUTDOWN, b"engine is shutting down"); + } + drop(states); + + // Shut down the router; this stops the protocol handler and closes + // the endpoint. + let router = lock(&shared.router).take(); + if let Some(router) = router + && let Err(err) = router.shutdown().await + { + warn!(error = %err, "router shutdown reported an error"); + } + shared.endpoint.close().await; + + // Wait for the remaining background tasks; abort stragglers. + let mut tasks = std::mem::take(&mut *lock(&shared.tasks)); + let drained = timeout(SHUTDOWN_TASK_GRACE, async { + while tasks.join_next().await.is_some() {} + }) + .await; + if drained.is_err() { + warn!("background tasks did not finish in time; aborting them"); + tasks.abort_all(); + } + + // Close the event channel: dropping the last sender makes + // `NetworkEventReceiver::recv` return `None` once drained. + *lock(&shared.events) = None; + info!("network engine shut down"); + Ok(()) + } +} diff --git a/crates/federation-net/src/error.rs b/crates/federation-net/src/error.rs new file mode 100644 index 0000000..9d8ec76 --- /dev/null +++ b/crates/federation-net/src/error.rs @@ -0,0 +1,67 @@ +//! Error types for the federation-net library. + +use iroh::EndpointId; + +/// Convenient result alias used across the library. +pub type Result = std::result::Result; + +/// All errors that can be returned by the public API of this library. +#[derive(Debug, thiserror::Error)] +#[non_exhaustive] +pub enum NetworkError { + /// The provided [`crate::NetworkConfig`] is invalid. + #[error("invalid configuration: {0}")] + InvalidConfig(String), + + /// Loading or persisting the peer identity failed. + #[error("identity error: {0}")] + Identity(String), + + /// A peer ticket could not be parsed or validated. + #[error("invalid ticket: {0}")] + InvalidTicket(String), + + /// The remote peer belongs to a different network. + #[error("network id mismatch")] + NetworkMismatch, + + /// The remote peer uses an incompatible message schema. + #[error("schema id mismatch")] + SchemaMismatch, + + /// The remote peer speaks an unsupported protocol version. + #[error("unsupported protocol version")] + UnsupportedProtocolVersion, + + /// The remote peer rejected the handshake as malformed. + #[error("handshake rejected as invalid by remote peer")] + HandshakeRejected, + + /// No active connection to the given peer exists. + #[error("peer is not connected: {0}")] + PeerNotConnected(EndpointId), + + /// A message exceeds the configured maximum size. + #[error("message exceeds maximum size")] + MessageTooLarge, + + /// The operation did not complete within the configured timeout. + #[error("request timed out")] + Timeout, + + /// Encoding or decoding a wire payload failed. + #[error("serialization error: {0}")] + Serialization(String), + + /// The underlying Iroh transport reported an error. + #[error("transport error: {0}")] + Transport(String), + + /// The remote peer rejected a message. + #[error("message rejected by peer: {0}")] + MessageRejected(String), + + /// The engine is shutting down and no longer accepts operations. + #[error("engine is shutting down")] + ShuttingDown, +} diff --git a/crates/federation-net/src/event.rs b/crates/federation-net/src/event.rs new file mode 100644 index 0000000..22b7674 --- /dev/null +++ b/crates/federation-net/src/event.rs @@ -0,0 +1,79 @@ +//! Network events delivered to the application. + +use iroh::EndpointId; +use tokio::sync::mpsc; + +/// Direction of an established peer connection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConnectionDirection { + /// The remote peer connected to us. + Incoming, + /// We connected to the remote peer. + Outgoing, +} + +/// Events emitted by a [`crate::NetworkEngine`]. +/// +/// `M` is the application-defined domain message type. +#[derive(Debug)] +pub enum NetworkEvent { + /// A peer completed the handshake and is now connected. + PeerConnected { + /// Identifier of the connected peer. + peer_id: EndpointId, + /// Whether the peer connected to us or we connected to it. + direction: ConnectionDirection, + }, + + /// A peer disconnected. + PeerDisconnected { + /// Identifier of the disconnected peer. + peer_id: EndpointId, + /// Human-readable reason, if one is known. + reason: Option, + }, + + /// A domain message was received from a peer. + MessageReceived { + /// Identifier of the sending peer. + peer_id: EndpointId, + /// The decoded domain message. + message: M, + }, + + /// A protocol-level error occurred. + /// + /// Errors of a single connection never bring down the engine; they are + /// reported here instead. + ProtocolError { + /// The peer involved, if known. + peer_id: Option, + /// Human-readable error description. + error: String, + }, +} + +/// Receiving side of the engine's event channel. +/// +/// There is exactly one receiver per engine; it is returned from +/// [`crate::NetworkEngine::start`]. The underlying channel is bounded, so the +/// application should consume events promptly to avoid back-pressuring the +/// engine. +#[derive(Debug)] +pub struct NetworkEventReceiver { + rx: mpsc::Receiver>, +} + +impl NetworkEventReceiver { + pub(crate) fn new(rx: mpsc::Receiver>) -> Self { + Self { rx } + } + + /// Receives the next event. + /// + /// Returns `None` after the engine has shut down and all pending events + /// have been consumed. + pub async fn recv(&mut self) -> Option> { + self.rx.recv().await + } +} diff --git a/crates/federation-net/src/identity.rs b/crates/federation-net/src/identity.rs new file mode 100644 index 0000000..2eae9fb --- /dev/null +++ b/crates/federation-net/src/identity.rs @@ -0,0 +1,154 @@ +//! Persistent peer identity. +//! +//! Each peer owns a long-lived Iroh secret key stored at +//! `/identity.key`. The key is created on first start and loaded on +//! every subsequent start, so the peer's [`iroh::EndpointId`] stays stable +//! across restarts. + +use std::path::Path; + +use iroh::SecretKey; + +use crate::error::{NetworkError, Result}; + +/// File name of the persisted secret key inside the data directory. +pub(crate) const IDENTITY_FILE_NAME: &str = "identity.key"; + +/// Loads the peer identity from `data_dir`, creating a new one if none exists. +/// +/// File I/O runs on a blocking thread so the async runtime is never blocked. +pub(crate) async fn load_or_create(data_dir: &Path) -> Result { + let dir = data_dir.to_path_buf(); + tokio::task::spawn_blocking(move || load_or_create_blocking(&dir)) + .await + .map_err(|err| NetworkError::Identity(format!("identity task panicked: {err}")))? +} + +fn load_or_create_blocking(dir: &Path) -> Result { + std::fs::create_dir_all(dir).map_err(|err| { + NetworkError::Identity(format!( + "failed to create data directory {}: {err}", + dir.display() + )) + })?; + let path = dir.join(IDENTITY_FILE_NAME); + if path.exists() { + load_key(&path) + } else { + let key = SecretKey::generate(); + store_key(dir, &path, &key)?; + Ok(key) + } +} + +fn load_key(path: &Path) -> Result { + let content = std::fs::read_to_string(path).map_err(|err| { + NetworkError::Identity(format!( + "failed to read identity file {}: {err}", + path.display() + )) + })?; + let trimmed = content.trim(); + let bytes = data_encoding::HEXLOWER_PERMISSIVE + .decode(trimmed.as_bytes()) + .map_err(|_| corrupted(path))?; + let bytes: [u8; 32] = bytes.try_into().map_err(|_| corrupted(path))?; + Ok(SecretKey::from_bytes(&bytes)) +} + +fn corrupted(path: &Path) -> NetworkError { + NetworkError::Identity(format!( + "identity file {} is corrupted: expected 64 hex characters; \ + remove the file to generate a new identity", + path.display() + )) +} + +/// Persists the key atomically: write to a temporary file in the same +/// directory, then rename it over the final path. +fn store_key(dir: &Path, path: &Path, key: &SecretKey) -> Result<()> { + use std::io::Write; + + let tmp_path = dir.join(format!("{IDENTITY_FILE_NAME}.tmp")); + let mut options = std::fs::OpenOptions::new(); + options.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let write_result = (|| { + let mut file = options.open(&tmp_path)?; + let mut encoded = data_encoding::HEXLOWER.encode(&key.to_bytes()); + encoded.push('\n'); + file.write_all(encoded.as_bytes())?; + file.sync_all()?; + drop(file); + std::fs::rename(&tmp_path, path) + })(); + if let Err(err) = write_result { + // Best effort removal of the temporary file; the original error wins. + let _ = std::fs::remove_file(&tmp_path); + return Err(NetworkError::Identity(format!( + "failed to persist identity file {}: {err}", + path.display() + ))); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn key_is_created_and_reloaded() { + let dir = tempfile::tempdir().expect("tempdir"); + let first = load_or_create(dir.path()).await.expect("create key"); + let second = load_or_create(dir.path()).await.expect("load key"); + assert_eq!(first.public(), second.public()); + assert_eq!(first.to_bytes(), second.to_bytes()); + } + + #[tokio::test] + async fn existing_key_is_not_overwritten() { + let dir = tempfile::tempdir().expect("tempdir"); + let first = load_or_create(dir.path()).await.expect("create key"); + let path = dir.path().join(IDENTITY_FILE_NAME); + let before = std::fs::read(&path).expect("read identity file"); + let second = load_or_create(dir.path()).await.expect("load key"); + let after = std::fs::read(&path).expect("read identity file"); + assert_eq!(before, after); + assert_eq!(first.public(), second.public()); + } + + #[tokio::test] + async fn corrupted_key_file_is_an_error() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join(IDENTITY_FILE_NAME); + std::fs::write(&path, "definitely not a hex key").expect("write garbage"); + let result = load_or_create(dir.path()).await; + assert!(matches!(result, Err(NetworkError::Identity(_)))); + } + + #[tokio::test] + async fn truncated_key_file_is_an_error() { + let dir = tempfile::tempdir().expect("tempdir"); + let path = dir.path().join(IDENTITY_FILE_NAME); + // Valid hex, but not 32 bytes. + std::fs::write(&path, "deadbeef").expect("write short key"); + let result = load_or_create(dir.path()).await; + assert!(matches!(result, Err(NetworkError::Identity(_)))); + } + + #[cfg(unix)] + #[tokio::test] + async fn key_file_has_restrictive_permissions() { + use std::os::unix::fs::PermissionsExt; + + let dir = tempfile::tempdir().expect("tempdir"); + load_or_create(dir.path()).await.expect("create key"); + let metadata = std::fs::metadata(dir.path().join(IDENTITY_FILE_NAME)).expect("metadata"); + assert_eq!(metadata.permissions().mode() & 0o777, 0o600); + } +} diff --git a/crates/federation-net/src/lib.rs b/crates/federation-net/src/lib.rs new file mode 100644 index 0000000..fee3e3f --- /dev/null +++ b/crates/federation-net/src/lib.rs @@ -0,0 +1,73 @@ +//! # federation-net +//! +//! A generic peer-to-peer networking engine built on [Iroh](https://iroh.computer). +//! +//! The library establishes direct (NAT-traversing, relay-assisted) QUIC +//! connections between peers and exchanges typed, application-defined +//! messages over them. It deliberately contains no domain logic: the +//! application chooses its own message type and the engine treats it as an +//! opaque, serde-serializable payload. +//! +//! ## Core concepts +//! +//! * [`NetworkId`] — isolates independent P2P networks from each other. +//! * [`SchemaId`] — isolates applications with incompatible message schemas. +//! * [`PeerTicket`] — a shareable string invitation used to reach a peer. +//! * [`NetworkEngine`] — the engine itself; [`NetworkEventReceiver`] delivers +//! [`NetworkEvent`]s to the application. +//! +//! ## Example +//! +//! ```no_run +//! use federation_net::{NetworkConfig, NetworkEngine, NetworkId, SchemaId}; +//! +//! #[derive(Debug, serde::Serialize, serde::Deserialize)] +//! enum MyMessage { +//! Hello { from: String }, +//! } +//! +//! # async fn run() -> federation_net::Result<()> { +//! let config = NetworkConfig::builder() +//! .data_dir("./peer-a") +//! .network_id(NetworkId::from_name("example-network")) +//! .schema_id(SchemaId::from_name("my-message-v1")) +//! .build()?; +//! +//! let (engine, mut events) = NetworkEngine::::start(config).await?; +//! let ticket = engine.ticket().await?; +//! println!("share this ticket: {ticket}"); +//! +//! while let Some(event) = events.recv().await { +//! println!("{event:?}"); +//! } +//! engine.shutdown().await?; +//! # Ok(()) +//! # } +//! ``` + +#![warn(missing_docs)] +#![forbid(unsafe_code)] + +mod config; +mod engine; +mod error; +mod event; +mod identity; +mod protocol; +mod ticket; +mod wire; + +pub use config::{ + DEFAULT_EVENT_CHANNEL_CAPACITY, DEFAULT_MAX_CONCURRENT_STREAMS_PER_PEER, + DEFAULT_MAX_MESSAGE_SIZE, DEFAULT_REQUEST_TIMEOUT, NetworkConfig, NetworkConfigBuilder, +}; +pub use engine::{Message, NetworkEngine}; +pub use error::{NetworkError, Result}; +pub use event::{ConnectionDirection, NetworkEvent, NetworkEventReceiver}; +pub use protocol::{ALPN, NetworkId, PROTOCOL_VERSION, SchemaId}; +pub use ticket::{PeerTicket, TICKET_VERSION}; + +// Re-exported Iroh types that appear in the public API. +pub use iroh::{EndpointAddr, EndpointId}; +// Re-exported so applications can use the generic ticket helpers. +pub use iroh_tickets::Ticket; diff --git a/crates/federation-net/src/protocol.rs b/crates/federation-net/src/protocol.rs new file mode 100644 index 0000000..ae8008d --- /dev/null +++ b/crates/federation-net/src/protocol.rs @@ -0,0 +1,215 @@ +//! Protocol constants, network/schema identifiers and internal wire structures. + +use std::fmt; + +use serde::{Deserialize, Serialize}; + +/// ALPN identifier for the federation-net protocol. +/// +/// [`NetworkId`] and [`SchemaId`] are deliberately not part of the ALPN; +/// they are verified during the application-level handshake instead. +pub const ALPN: &[u8] = b"/federation-net/1"; + +/// Version of the application-level wire protocol. +pub const PROTOCOL_VERSION: u16 = 1; + +/// Maximum size of an encoded handshake frame. +/// +/// Handshake frames are tiny; this limit only guards against malicious peers. +pub(crate) const MAX_HANDSHAKE_FRAME_SIZE: usize = 16 * 1024; + +fn hash_with_domain(domain: &str, name: &str) -> [u8; 32] { + let mut hasher = blake3::Hasher::new(); + hasher.update(domain.as_bytes()); + hasher.update(name.as_bytes()); + *hasher.finalize().as_bytes() +} + +fn fmt_id(bytes: &[u8; 32], f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in bytes { + write!(f, "{byte:02x}")?; + } + Ok(()) +} + +/// Identifier of a distinct P2P network. +/// +/// Peers with different network ids refuse to establish an application-level +/// session even though they share the same transport protocol. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct NetworkId([u8; 32]); + +impl NetworkId { + /// Creates a network id from raw bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Derives a network id from a human-readable name. + /// + /// The derivation is deterministic: the same name always yields the same + /// id. Internally this computes `BLAKE3("federation-net:network:" + name)`. + pub fn from_name(name: &str) -> Self { + Self(hash_with_domain("federation-net:network:", name)) + } + + /// Returns the raw bytes of this id. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Display for NetworkId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt_id(&self.0, f) + } +} + +impl fmt::Debug for NetworkId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "NetworkId(")?; + fmt_id(&self.0, f)?; + write!(f, ")") + } +} + +/// Identifier of the domain message schema used on top of the network. +/// +/// Peers on the same network but with different schema ids reject each other, +/// because they would not be able to decode each other's messages. A backwards +/// incompatible change to the domain message type requires a new schema id. +#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct SchemaId([u8; 32]); + +impl SchemaId { + /// Creates a schema id from raw bytes. + pub fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Derives a schema id from a human-readable name. + /// + /// The derivation is deterministic: the same name always yields the same + /// id. Internally this computes `BLAKE3("federation-net:schema:" + name)`. + pub fn from_name(name: &str) -> Self { + Self(hash_with_domain("federation-net:schema:", name)) + } + + /// Returns the raw bytes of this id. + pub fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } +} + +impl fmt::Display for SchemaId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt_id(&self.0, f) + } +} + +impl fmt::Debug for SchemaId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "SchemaId(")?; + fmt_id(&self.0, f)?; + write!(f, ")") + } +} + +/// Handshake sent by the connecting side on the first bidirectional stream. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Handshake { + pub protocol_version: u16, + pub network_id: NetworkId, + pub schema_id: SchemaId, +} + +/// Reply to a [`Handshake`], sent by the accepting side. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct HandshakeAck { + pub accepted: bool, + pub error: Option, +} + +/// Reasons for rejecting a handshake. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum HandshakeErrorCode { + UnsupportedProtocolVersion, + NetworkMismatch, + SchemaMismatch, + InvalidHandshake, +} + +/// A single domain message request, sent on its own bidirectional stream. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct MessageRequest { + pub request_id: [u8; 16], + pub payload: Vec, +} + +/// Reasons for rejecting a [`MessageRequest`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) enum MessageRejectReason { + /// The payload could not be decoded into the domain message type. + MalformedPayload, + /// The receiving peer is shutting down. + ShuttingDown, + /// The receiving peer is overloaded and dropped the message. + Overloaded, +} + +impl fmt::Display for MessageRejectReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MalformedPayload => write!(f, "malformed payload"), + Self::ShuttingDown => write!(f, "peer is shutting down"), + Self::Overloaded => write!(f, "peer is overloaded"), + } + } +} + +/// Reply to a [`MessageRequest`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) enum MessageResponse { + Accepted { + request_id: [u8; 16], + }, + Rejected { + request_id: [u8; 16], + reason: MessageRejectReason, + }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn network_id_from_name_is_deterministic() { + let a = NetworkId::from_name("example-network"); + let b = NetworkId::from_name("example-network"); + assert_eq!(a, b); + } + + #[test] + fn different_network_names_produce_different_ids() { + let a = NetworkId::from_name("network-a"); + let b = NetworkId::from_name("network-b"); + assert_ne!(a, b); + } + + #[test] + fn schema_id_from_name_is_deterministic() { + let a = SchemaId::from_name("demo-message-v1"); + let b = SchemaId::from_name("demo-message-v1"); + assert_eq!(a, b); + assert_ne!(a, SchemaId::from_name("demo-message-v2")); + } + + #[test] + fn network_and_schema_domains_are_separated() { + // The same name hashed under different domain prefixes must differ. + let network = NetworkId::from_name("same-name"); + let schema = SchemaId::from_name("same-name"); + assert_ne!(network.as_bytes(), schema.as_bytes()); + } +} diff --git a/crates/federation-net/src/ticket.rs b/crates/federation-net/src/ticket.rs new file mode 100644 index 0000000..b96093e --- /dev/null +++ b/crates/federation-net/src/ticket.rs @@ -0,0 +1,193 @@ +//! Peer tickets: self-contained connection invitations. + +use std::fmt; +use std::str::FromStr; + +use iroh::{EndpointAddr, EndpointId}; +use iroh_tickets::{ParseError, Ticket}; +use serde::{Deserialize, Serialize}; + +use crate::protocol::{NetworkId, SchemaId}; + +/// Current version of the ticket wire format. +pub const TICKET_VERSION: u16 = 1; + +/// Maximum size of a decoded ticket in bytes. +const MAX_TICKET_BYTES: usize = 8 * 1024; +/// Maximum length of a ticket string accepted by [`PeerTicket::from_str`]. +const MAX_TICKET_STRING_LEN: usize = 16 * 1024; + +/// A shareable invitation to connect to a peer. +/// +/// The ticket contains everything needed to dial the peer over Iroh, plus the +/// network and schema identifiers so incompatibility is detected before any +/// domain message is exchanged. +/// +/// Tickets serialize to a string with the `fnet` prefix via [`fmt::Display`] +/// and parse back via [`FromStr`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PeerTicket { + /// Version of the ticket format itself. + pub ticket_version: u16, + /// Version of the application-level wire protocol the peer speaks. + pub protocol_version: u16, + /// Network the issuing peer participates in. + pub network_id: NetworkId, + /// Message schema the issuing peer uses. + pub schema_id: SchemaId, + /// Iroh address of the issuing peer. + pub endpoint_addr: EndpointAddr, +} + +impl PeerTicket { + /// Returns the endpoint id of the peer this ticket points to. + pub fn endpoint_id(&self) -> EndpointId { + self.endpoint_addr.id + } +} + +/// Versioned body of the ticket; everything after the leading version number. +#[derive(Serialize, Deserialize)] +struct TicketBody { + protocol_version: u16, + network_id: NetworkId, + schema_id: SchemaId, + endpoint_addr: EndpointAddr, +} + +impl Ticket for PeerTicket { + const KIND: &'static str = "fnet"; + + fn encode_bytes(&self) -> Vec { + let body = TicketBody { + protocol_version: self.protocol_version, + network_id: self.network_id, + schema_id: self.schema_id, + endpoint_addr: self.endpoint_addr.clone(), + }; + // Serializing plain owned data into a growable Vec is infallible; + // postcard only errors on unsupported types or fixed-size buffers. + postcard::to_stdvec(&(self.ticket_version, body)) + .expect("postcard serialization of a ticket into a Vec cannot fail") + } + + fn decode_bytes(bytes: &[u8]) -> Result { + if bytes.len() > MAX_TICKET_BYTES { + return Err(ParseError::verification_failed( + "ticket exceeds the maximum allowed size", + )); + } + let (ticket_version, rest) = postcard::take_from_bytes::(bytes)?; + if ticket_version != TICKET_VERSION { + return Err(ParseError::verification_failed( + "unsupported ticket version", + )); + } + let body: TicketBody = postcard::from_bytes(rest)?; + Ok(Self { + ticket_version, + protocol_version: body.protocol_version, + network_id: body.network_id, + schema_id: body.schema_id, + endpoint_addr: body.endpoint_addr, + }) + } +} + +impl fmt::Display for PeerTicket { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&Ticket::encode_string(self)) + } +} + +impl FromStr for PeerTicket { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + if s.len() > MAX_TICKET_STRING_LEN { + return Err(ParseError::verification_failed( + "ticket string exceeds the maximum allowed length", + )); + } + Ticket::decode_string(s) + } +} + +#[cfg(test)] +mod tests { + use std::net::{Ipv4Addr, SocketAddr}; + + use iroh::SecretKey; + + use super::*; + + fn sample_ticket() -> PeerTicket { + let endpoint_id = SecretKey::generate().public(); + let addr = EndpointAddr::new(endpoint_id) + .with_ip_addr(SocketAddr::from((Ipv4Addr::LOCALHOST, 4242))); + PeerTicket { + ticket_version: TICKET_VERSION, + protocol_version: 1, + network_id: NetworkId::from_name("test-network"), + schema_id: SchemaId::from_name("test-schema"), + endpoint_addr: addr, + } + } + + #[test] + fn ticket_string_round_trip() { + let ticket = sample_ticket(); + let encoded = ticket.to_string(); + assert!(encoded.starts_with("fnet")); + let decoded: PeerTicket = encoded.parse().expect("parse ticket"); + assert_eq!(decoded, ticket); + // The canonical string form must be stable across round-trips. + assert_eq!(decoded.to_string(), encoded); + } + + #[test] + fn corrupted_ticket_is_rejected() { + let ticket = sample_ticket(); + let mut encoded = ticket.to_string(); + // Truncate the payload; the result must not parse. + encoded.truncate(encoded.len() - 10); + assert!(encoded.parse::().is_err()); + + // Corrupt the alphabet: '!' is not valid base32. + let corrupted = format!("fnet!{}", &ticket.to_string()[5..]); + assert!(corrupted.parse::().is_err()); + } + + #[test] + fn wrong_prefix_is_rejected() { + let ticket = sample_ticket(); + let encoded = ticket.to_string(); + let renamed = format!("blob{}", &encoded[4..]); + assert!(renamed.parse::().is_err()); + } + + #[test] + fn unknown_ticket_version_is_rejected() { + let ticket = sample_ticket(); + let body = TicketBody { + protocol_version: ticket.protocol_version, + network_id: ticket.network_id, + schema_id: ticket.schema_id, + endpoint_addr: ticket.endpoint_addr.clone(), + }; + let bytes = postcard::to_stdvec(&(99u16, body)).expect("encode"); + let mut encoded = String::from("fnet"); + data_encoding::BASE32_NOPAD.encode_append(&bytes, &mut encoded); + encoded.make_ascii_lowercase(); + assert!(encoded.parse::().is_err()); + } + + #[test] + fn oversized_ticket_is_rejected() { + let bytes = vec![0u8; MAX_TICKET_BYTES + 1]; + assert!(PeerTicket::decode_bytes(&bytes).is_err()); + + let huge = "fnet".to_string() + &"a".repeat(MAX_TICKET_STRING_LEN); + assert!(huge.parse::().is_err()); + } +} diff --git a/crates/federation-net/src/wire.rs b/crates/federation-net/src/wire.rs new file mode 100644 index 0000000..f128f70 --- /dev/null +++ b/crates/federation-net/src/wire.rs @@ -0,0 +1,152 @@ +//! Length-prefixed framing of postcard-encoded values. +//! +//! Every frame on the wire is laid out as: +//! +//! ```text +//! 4 bytes: payload length, unsigned big-endian +//! N bytes: postcard payload +//! ``` + +use serde::Serialize; +use serde::de::DeserializeOwned; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +use crate::error::{NetworkError, Result}; + +/// Encodes a value with postcard, enforcing `max_size` on the encoded bytes. +pub(crate) fn encode(value: &T, max_size: usize) -> Result> { + let bytes = postcard::to_stdvec(value) + .map_err(|err| NetworkError::Serialization(format!("postcard encoding failed: {err}")))?; + if bytes.len() > max_size { + return Err(NetworkError::MessageTooLarge); + } + Ok(bytes) +} + +/// Decodes a postcard-encoded value. +pub(crate) fn decode(bytes: &[u8]) -> Result { + postcard::from_bytes(bytes) + .map_err(|err| NetworkError::Serialization(format!("postcard decoding failed: {err}"))) +} + +/// Writes one length-prefixed frame containing the postcard encoding of +/// `value`. +pub(crate) async fn write_frame(writer: &mut W, value: &T, max_size: usize) -> Result<()> +where + W: AsyncWrite + Unpin, + T: Serialize, +{ + let payload = encode(value, max_size)?; + // `max_size` is validated to fit into u32 by the configuration. + let len = payload.len() as u32; + writer + .write_all(&len.to_be_bytes()) + .await + .map_err(|err| NetworkError::Transport(format!("failed to write frame header: {err}")))?; + writer + .write_all(&payload) + .await + .map_err(|err| NetworkError::Transport(format!("failed to write frame payload: {err}")))?; + Ok(()) +} + +/// Reads one length-prefixed frame and decodes it with postcard. +/// +/// The length prefix is validated against `max_size` before any payload +/// memory is allocated. A frame that exceeds the limit is rejected without +/// reading its payload. +pub(crate) async fn read_frame(reader: &mut R, max_size: usize) -> Result +where + R: AsyncRead + Unpin, + T: DeserializeOwned, +{ + let mut len_bytes = [0u8; 4]; + reader + .read_exact(&mut len_bytes) + .await + .map_err(|err| NetworkError::Transport(format!("failed to read frame header: {err}")))?; + let len = u32::from_be_bytes(len_bytes) as usize; + if len > max_size { + return Err(NetworkError::Transport(format!( + "incoming frame of {len} bytes exceeds limit of {max_size} bytes" + ))); + } + let mut payload = vec![0u8; len]; + reader + .read_exact(&mut payload) + .await + .map_err(|err| NetworkError::Transport(format!("failed to read frame payload: {err}")))?; + decode(&payload) +} + +#[cfg(test)] +mod tests { + use serde::Deserialize; + + use super::*; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct TestValue { + text: String, + number: u64, + } + + #[tokio::test] + async fn frame_round_trip() { + let value = TestValue { + text: "hello".into(), + number: 42, + }; + let mut buf = Vec::new(); + write_frame(&mut buf, &value, 1024).await.expect("write"); + let decoded: TestValue = read_frame(&mut buf.as_slice(), 1024).await.expect("read"); + assert_eq!(decoded, value); + } + + #[tokio::test] + async fn oversized_frame_is_rejected_before_allocation() { + // A header declaring a huge payload with no payload bytes present. + // If the length check happened after allocation, this would try to + // allocate 4 GiB; instead it must fail fast on the limit check. + let mut buf = Vec::new(); + buf.extend_from_slice(&u32::MAX.to_be_bytes()); + let result: Result = read_frame(&mut buf.as_slice(), 1024).await; + assert!(matches!(result, Err(NetworkError::Transport(_)))); + } + + #[tokio::test] + async fn oversized_value_is_rejected_on_write() { + let value = TestValue { + text: "x".repeat(2048), + number: 1, + }; + let mut buf = Vec::new(); + let result = write_frame(&mut buf, &value, 1024).await; + assert!(matches!(result, Err(NetworkError::MessageTooLarge))); + assert!(buf.is_empty()); + } + + #[tokio::test] + async fn malformed_payload_is_rejected() { + // Valid header, but payload bytes that do not decode as TestValue. + let payload = [0xffu8; 8]; + let mut buf = Vec::new(); + buf.extend_from_slice(&(payload.len() as u32).to_be_bytes()); + buf.extend_from_slice(&payload); + let result: Result = read_frame(&mut buf.as_slice(), 1024).await; + assert!(matches!(result, Err(NetworkError::Serialization(_)))); + } + + #[tokio::test] + async fn truncated_stream_is_an_error() { + let value = TestValue { + text: "hello".into(), + number: 42, + }; + let mut buf = Vec::new(); + write_frame(&mut buf, &value, 1024).await.expect("write"); + buf.truncate(buf.len() - 1); + let result: Result = read_frame(&mut buf.as_slice(), 1024).await; + assert!(matches!(result, Err(NetworkError::Transport(_)))); + } +} diff --git a/crates/federation-net/tests/integration.rs b/crates/federation-net/tests/integration.rs new file mode 100644 index 0000000..9ed0151 --- /dev/null +++ b/crates/federation-net/tests/integration.rs @@ -0,0 +1,299 @@ +//! Integration tests: two engines in one Tokio runtime. + +use std::path::Path; +use std::time::Duration; + +use federation_net::{ + ConnectionDirection, EndpointId, NetworkConfig, NetworkEngine, NetworkError, NetworkEvent, + NetworkEventReceiver, NetworkId, PeerTicket, SchemaId, +}; + +/// Hard cap on every test so a regression can never hang CI. +const TEST_TIMEOUT: Duration = Duration::from_secs(120); +/// Timeout used when waiting for a single event. +const EVENT_TIMEOUT: Duration = Duration::from_secs(30); + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +enum TestMessage { + Text { sender: String, body: String }, + Ping { nonce: u64 }, +} + +type Engine = NetworkEngine; +type Events = NetworkEventReceiver; + +/// Serializes the network-facing tests: running many endpoints at once makes +/// relay discovery contend and produces spurious connect timeouts. +static NET_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + +fn config(dir: &Path, network: &str, schema: &str) -> NetworkConfig { + NetworkConfig::builder() + .data_dir(dir) + .network_id(NetworkId::from_name(network)) + .schema_id(SchemaId::from_name(schema)) + .request_timeout(Duration::from_secs(10)) + .build() + .expect("valid test config") +} + +async fn start(dir: &Path, network: &str, schema: &str) -> (Engine, Events) { + NetworkEngine::start(config(dir, network, schema)) + .await + .expect("engine starts") +} + +/// Waits for the next event, panicking on timeout or channel close. +async fn next_event(events: &mut Events) -> NetworkEvent { + tokio::time::timeout(EVENT_TIMEOUT, events.recv()) + .await + .expect("timed out waiting for an event") + .expect("event channel closed unexpectedly") +} + +/// Waits until a `PeerConnected` event for `peer` arrives, skipping unrelated +/// events (e.g. protocol errors from earlier rejected attempts). +async fn wait_connected(events: &mut Events, peer: EndpointId) -> ConnectionDirection { + loop { + if let NetworkEvent::PeerConnected { peer_id, direction } = next_event(events).await + && peer_id == peer + { + return direction; + } + } +} + +async fn wait_disconnected(events: &mut Events, peer: EndpointId) { + loop { + if let NetworkEvent::PeerDisconnected { peer_id, .. } = next_event(events).await + && peer_id == peer + { + return; + } + } +} + +async fn wait_message(events: &mut Events, peer: EndpointId) -> TestMessage { + loop { + if let NetworkEvent::MessageReceived { peer_id, message } = next_event(events).await + && peer_id == peer + { + return message; + } + } +} + +#[tokio::test] +async fn connect_and_exchange_messages() { + let _net = NET_LOCK.lock().await; + tokio::time::timeout(TEST_TIMEOUT, async { + let dir_a = tempfile::tempdir().expect("tempdir"); + let dir_b = tempfile::tempdir().expect("tempdir"); + let (engine_a, mut events_a) = start(dir_a.path(), "test-net", "test-schema-v1").await; + let (engine_b, mut events_b) = start(dir_b.path(), "test-net", "test-schema-v1").await; + + // 1. Peer A creates a ticket. + let ticket = engine_a.ticket().await.expect("ticket"); + assert_eq!(ticket.endpoint_id(), engine_a.endpoint_id()); + + // 2. Peer B connects using the ticket. + let peer_a = engine_b.connect(ticket).await.expect("connect"); + assert_eq!(peer_a, engine_a.endpoint_id()); + + // 3. Both sides observe PeerConnected with the right direction. + let dir_on_b = wait_connected(&mut events_b, engine_a.endpoint_id()).await; + assert_eq!(dir_on_b, ConnectionDirection::Outgoing); + let dir_on_a = wait_connected(&mut events_a, engine_b.endpoint_id()).await; + assert_eq!(dir_on_a, ConnectionDirection::Incoming); + + // 8. connected_peers contains the expected endpoint ids. + assert_eq!(engine_b.connected_peers(), vec![engine_a.endpoint_id()]); + assert_eq!(engine_a.connected_peers(), vec![engine_b.endpoint_id()]); + + // 4-5. B sends a message; A receives the correctly typed object. + let hello = TestMessage::Text { + sender: "bob".into(), + body: "hello alice".into(), + }; + engine_b + .send(engine_a.endpoint_id(), &hello) + .await + .expect("send b -> a"); + let received = wait_message(&mut events_a, engine_b.endpoint_id()).await; + assert_eq!(received, hello); + + // 6-7. A sends back over the same connection; B receives it. + let pong = TestMessage::Ping { nonce: 4242 }; + engine_a + .send(engine_b.endpoint_id(), &pong) + .await + .expect("send a -> b"); + let received = wait_message(&mut events_b, engine_a.endpoint_id()).await; + assert_eq!(received, pong); + + engine_a.shutdown().await.expect("shutdown a"); + engine_b.shutdown().await.expect("shutdown b"); + }) + .await + .expect("test timed out"); +} + +#[tokio::test] +async fn network_mismatch_is_rejected() { + let _net = NET_LOCK.lock().await; + tokio::time::timeout(TEST_TIMEOUT, async { + let dir_a = tempfile::tempdir().expect("tempdir"); + let dir_b = tempfile::tempdir().expect("tempdir"); + let (engine_a, _events_a) = start(dir_a.path(), "network-one", "schema-v1").await; + let (engine_b, _events_b) = start(dir_b.path(), "network-two", "schema-v1").await; + + let ticket = engine_a.ticket().await.expect("ticket"); + + // Local pre-validation: the ticket carries A's network id. + let err = engine_b + .connect(ticket.clone()) + .await + .expect_err("must fail"); + assert!(matches!(err, NetworkError::NetworkMismatch), "got {err:?}"); + + // Remote validation: forge a ticket claiming B's own network id, so + // the local check passes and the remote handshake must reject it. + let forged = PeerTicket { + network_id: engine_b.network_id(), + ..ticket + }; + let err = engine_b.connect(forged).await.expect_err("must fail"); + assert!(matches!(err, NetworkError::NetworkMismatch), "got {err:?}"); + + assert!(engine_a.connected_peers().is_empty()); + assert!(engine_b.connected_peers().is_empty()); + + engine_a.shutdown().await.expect("shutdown a"); + engine_b.shutdown().await.expect("shutdown b"); + }) + .await + .expect("test timed out"); +} + +#[tokio::test] +async fn schema_mismatch_is_rejected() { + let _net = NET_LOCK.lock().await; + tokio::time::timeout(TEST_TIMEOUT, async { + let dir_a = tempfile::tempdir().expect("tempdir"); + let dir_b = tempfile::tempdir().expect("tempdir"); + let (engine_a, _events_a) = start(dir_a.path(), "same-network", "schema-one").await; + let (engine_b, _events_b) = start(dir_b.path(), "same-network", "schema-two").await; + + let ticket = engine_a.ticket().await.expect("ticket"); + + // Local pre-validation. + let err = engine_b + .connect(ticket.clone()) + .await + .expect_err("must fail"); + assert!(matches!(err, NetworkError::SchemaMismatch), "got {err:?}"); + + // Remote validation with a forged schema id. + let forged = PeerTicket { + schema_id: engine_b.schema_id(), + ..ticket + }; + let err = engine_b.connect(forged).await.expect_err("must fail"); + assert!(matches!(err, NetworkError::SchemaMismatch), "got {err:?}"); + + engine_a.shutdown().await.expect("shutdown a"); + engine_b.shutdown().await.expect("shutdown b"); + }) + .await + .expect("test timed out"); +} + +#[tokio::test] +async fn shutdown_disconnects_peers() { + let _net = NET_LOCK.lock().await; + tokio::time::timeout(TEST_TIMEOUT, async { + let dir_a = tempfile::tempdir().expect("tempdir"); + let dir_b = tempfile::tempdir().expect("tempdir"); + let (engine_a, mut events_a) = start(dir_a.path(), "test-net", "schema-v1").await; + let (engine_b, mut events_b) = start(dir_b.path(), "test-net", "schema-v1").await; + + let ticket = engine_a.ticket().await.expect("ticket"); + engine_b.connect(ticket).await.expect("connect"); + wait_connected(&mut events_a, engine_b.endpoint_id()).await; + wait_connected(&mut events_b, engine_a.endpoint_id()).await; + + let id_b = engine_b.endpoint_id(); + engine_b.shutdown().await.expect("shutdown b"); + + // 11. A notices the disconnect and drops the peer from its registry. + wait_disconnected(&mut events_a, id_b).await; + assert!(engine_a.connected_peers().is_empty()); + + // Sending to the gone peer now fails without hanging. + let err = engine_a + .send(id_b, &TestMessage::Ping { nonce: 1 }) + .await + .expect_err("peer is gone"); + assert!( + matches!(err, NetworkError::PeerNotConnected(_)), + "got {err:?}" + ); + + // The event channel of the stopped engine closes after draining. + while events_b.recv().await.is_some() {} + + engine_a.shutdown().await.expect("shutdown a"); + }) + .await + .expect("test timed out"); +} + +#[tokio::test] +async fn identity_persists_across_restarts() { + let _net = NET_LOCK.lock().await; + tokio::time::timeout(TEST_TIMEOUT, async { + let dir = tempfile::tempdir().expect("tempdir"); + let (engine, _events) = start(dir.path(), "test-net", "schema-v1").await; + let first_id = engine.endpoint_id(); + engine.shutdown().await.expect("shutdown"); + + let (engine, _events) = start(dir.path(), "test-net", "schema-v1").await; + let second_id = engine.endpoint_id(); + engine.shutdown().await.expect("shutdown"); + + assert_eq!(first_id, second_id); + }) + .await + .expect("test timed out"); +} + +#[tokio::test] +async fn disconnect_removes_peer() { + let _net = NET_LOCK.lock().await; + tokio::time::timeout(TEST_TIMEOUT, async { + let dir_a = tempfile::tempdir().expect("tempdir"); + let dir_b = tempfile::tempdir().expect("tempdir"); + let (engine_a, mut events_a) = start(dir_a.path(), "test-net", "schema-v1").await; + let (engine_b, mut events_b) = start(dir_b.path(), "test-net", "schema-v1").await; + + let ticket = engine_a.ticket().await.expect("ticket"); + let peer_a = engine_b.connect(ticket).await.expect("connect"); + wait_connected(&mut events_a, engine_b.endpoint_id()).await; + wait_connected(&mut events_b, peer_a).await; + + engine_b.disconnect(peer_a).await.expect("disconnect"); + wait_disconnected(&mut events_b, peer_a).await; + assert!(engine_b.connected_peers().is_empty()); + + // Disconnecting twice reports PeerNotConnected. + let err = engine_b.disconnect(peer_a).await.expect_err("already gone"); + assert!( + matches!(err, NetworkError::PeerNotConnected(_)), + "got {err:?}" + ); + + engine_a.shutdown().await.expect("shutdown a"); + engine_b.shutdown().await.expect("shutdown b"); + }) + .await + .expect("test timed out"); +}