From c17a870e1c8396ae22557040288e5f1feaf65e83 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 7 Jul 2025 13:01:21 +0200 Subject: [PATCH 001/164] feat: use quinn multipath --- Cargo.lock | 1222 +++++++++++++++++++++-------------------- Cargo.toml | 5 + iroh-relay/Cargo.toml | 4 +- iroh/Cargo.toml | 8 +- iroh/bench/Cargo.toml | 2 +- 5 files changed, 639 insertions(+), 602 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d23ac3bdd80..cfaec28b630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,9 +27,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aead" @@ -44,15 +44,15 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.12" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom 0.3.3", + "getrandom 0.2.16", "once_cell", "version_check", - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -93,9 +93,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -108,36 +108,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", - "once_cell_polyfill", + "once_cell", "windows-sys 0.59.0", ] @@ -198,7 +198,7 @@ checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", "synstructure", ] @@ -210,7 +210,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -234,7 +234,7 @@ checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -276,9 +276,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" @@ -343,7 +343,7 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -370,9 +370,9 @@ dependencies = [ [[package]] name = "backon" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302eaff5357a264a2c42f127ecb8bac761cf99749fc3dc95677e2743991f99e7" +checksum = "fd0b50b1b78dbadd44ab18b3c794e496f3a139abb9fbc27d9c94c4eebbb96496" dependencies = [ "fastrand", "gloo-timers 0.3.0", @@ -423,9 +423,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.8.0" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" [[package]] name = "bincode" @@ -453,9 +453,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.9.1" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "blake3" @@ -493,9 +499,15 @@ checksum = "387e80962b798815a2b5c4bcfdb6bf626fa922ffe9f74e373103b858738e9f31" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytemuck" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -517,9 +529,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.29" +version = "1.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "8691782945451c1c383942c4874dbe63814f61cb57ef773cda2972682b7bb3c0" dependencies = [ "shlex", ] @@ -532,9 +544,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" @@ -606,9 +618,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.41" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071" dependencies = [ "clap_builder", "clap_derive", @@ -616,9 +628,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2" dependencies = [ "anstream", "anstyle", @@ -628,30 +640,27 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cobs" -version = "0.3.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.12", -] +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" [[package]] name = "color-backtrace" @@ -666,9 +675,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -682,15 +691,15 @@ dependencies = [ [[package]] name = "console" -version = "0.16.0" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", "libc", "once_cell", "unicode-width", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -717,9 +726,9 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "cordyceps" -version = "0.3.4" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +checksum = "a0392f465ceba1713d30708f61c160ebf4dc1cf86bb166039d16b11ad4f3b5b6" dependencies = [ "loom", "tracing", @@ -737,9 +746,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", @@ -762,9 +771,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" dependencies = [ "crc-catalog", ] @@ -853,9 +862,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-common" @@ -926,7 +935,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -983,7 +992,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -1001,16 +1010,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl 1.0.0", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl 2.0.1", + "derive_more-impl", ] [[package]] @@ -1021,19 +1021,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", - "unicode-xid", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.104", + "syn 2.0.101", "unicode-xid", ] @@ -1089,7 +1077,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -1131,9 +1119,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "2.2.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" dependencies = [ "curve25519-dalek", "ed25519", @@ -1177,27 +1165,27 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] name = "enumflags2" -version = "0.7.12" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" -version = "0.7.12" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -1217,12 +1205,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -1231,6 +1219,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +[[package]] +name = "fastbloom" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8" +dependencies = [ + "getrandom 0.3.2", + "rand 0.9.1", + "siphasher", + "wide", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1287,9 +1287,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d7be93788013f265201256d58f04936a8079ad5dc898743aa20525f503b683" +checksum = "1f89bda4c2a21204059a977ed3bfe746677dfd137b83c339e702b0ac91d482aa" dependencies = [ "autocfg", "tokio", @@ -1377,7 +1377,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -1418,16 +1418,15 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" +checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" dependencies = [ - "cc", "cfg-if", "libc", "log", "rustversion", - "windows", + "windows 0.58.0", ] [[package]] @@ -1450,15 +1449,15 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "js-sys", @@ -1669,7 +1668,7 @@ dependencies = [ "futures-sink", "futures-timer", "futures-util", - "getrandom 0.3.3", + "getrandom 0.3.2", "no-std-compat", "nonzero_ext", "parking_lot", @@ -1683,9 +1682,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -1727,9 +1726,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" dependencies = [ "allocator-api2", "equivalent", @@ -1768,9 +1767,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "hex" @@ -1882,9 +1881,9 @@ dependencies = [ [[package]] name = "hmac-sha256" -version = "1.1.12" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +checksum = "4a8575493d277c9092b988c780c94737fb9fd8651a1001e16bee3eccfc1baedb" [[package]] name = "hostname-validator" @@ -1988,10 +1987,11 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ + "futures-util", "http 1.3.1", "hyper", "hyper-util", @@ -2000,28 +2000,24 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.1", + "webpki-roots 0.26.11", ] [[package]] name = "hyper-util" -version = "0.1.15" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" dependencies = [ - "base64", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.3.1", "http-body", "hyper", - "ipnet", "libc", - "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2", "tokio", "tower-service", "tracing", @@ -2039,7 +2035,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.0", ] [[package]] @@ -2053,22 +2049,21 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ "displaydoc", - "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locale_core" -version = "2.0.0" +name = "icu_locid" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" dependencies = [ "displaydoc", "litemap", @@ -2077,11 +2072,31 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" + [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" dependencies = [ "displaydoc", "icu_collections", @@ -2089,54 +2104,67 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" [[package]] name = "icu_properties" -version = "2.0.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" dependencies = [ "displaydoc", "icu_collections", - "icu_locale_core", + "icu_locid_transform", "icu_properties_data", "icu_provider", - "potential_utf", - "zerotrie", + "tinystr", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" [[package]] name = "icu_provider" -version = "2.0.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" dependencies = [ "displaydoc", - "icu_locale_core", + "icu_locid", + "icu_provider_macros", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", - "zerotrie", "zerovec", ] +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "idna" version = "1.0.3" @@ -2150,9 +2178,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" dependencies = [ "icu_normalizer", "icu_properties", @@ -2181,25 +2209,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.3", ] [[package]] name = "indicatif" -version = "0.18.0" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ "console", + "number_prefix", "portable-atomic", "tokio", "unicode-width", - "unit-prefix", "web-time", ] @@ -2224,24 +2252,13 @@ dependencies = [ "web-sys", ] -[[package]] -name = "io-uring" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] - [[package]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.10", + "socket2", "widestring", "windows-sys 0.48.0", "winreg", @@ -2256,19 +2273,9 @@ dependencies = [ "serde", ] -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "iroh" -version = "0.91.0" +version = "0.90.0" dependencies = [ "aead", "axum", @@ -2280,11 +2287,11 @@ dependencies = [ "crypto_box", "data-encoding", "der", - "derive_more 2.0.1", + "derive_more", "ed25519-dalek", "futures-buffered", "futures-util", - "getrandom 0.3.3", + "getrandom 0.3.2", "hickory-resolver", "http 1.3.1", "igd-next", @@ -2320,7 +2327,7 @@ dependencies = [ "smallvec", "snafu", "spki", - "strum 0.27.1", + "strum", "stun-rs", "surge-ping", "swarm-discovery", @@ -2341,11 +2348,11 @@ dependencies = [ [[package]] name = "iroh-base" -version = "0.91.0" +version = "0.90.0" dependencies = [ "curve25519-dalek", "data-encoding", - "derive_more 2.0.1", + "derive_more", "ed25519-dalek", "n0-snafu", "nested_enum_utils", @@ -2362,7 +2369,7 @@ dependencies = [ [[package]] name = "iroh-bench" -version = "0.91.0" +version = "0.90.0" dependencies = [ "bytes", "clap", @@ -2372,8 +2379,9 @@ dependencies = [ "iroh-quinn", "n0-future", "n0-snafu", + "n0-watcher", "rand 0.8.5", - "rcgen 0.14.2", + "rcgen", "rustls", "tokio", "tracing", @@ -2382,7 +2390,7 @@ dependencies = [ [[package]] name = "iroh-dns-server" -version = "0.91.0" +version = "0.90.0" dependencies = [ "async-trait", "axum", @@ -2392,7 +2400,7 @@ dependencies = [ "clap", "criterion", "data-encoding", - "derive_more 2.0.1", + "derive_more", "dirs-next", "governor", "hickory-resolver", @@ -2408,7 +2416,7 @@ dependencies = [ "pkarr", "rand 0.8.5", "rand_chacha 0.3.1", - "rcgen 0.13.2", + "rcgen", "redb", "regex", "rustls", @@ -2416,7 +2424,7 @@ dependencies = [ "serde", "snafu", "struct_iterable", - "strum 0.26.3", + "strum", "tokio", "tokio-rustls", "tokio-rustls-acme", @@ -2461,14 +2469,13 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] name = "iroh-quinn" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde160ebee7aabede6ae887460cd303c8b809054224815addf1469d54a6fcf7" +version = "0.13.0" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#773fceabb27f1e56132198dd960d4bd1493e0ed0" dependencies = [ "bytes", "cfg_aliases", @@ -2477,7 +2484,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2", "thiserror 2.0.12", "tokio", "tracing", @@ -2487,12 +2494,13 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "929d5d8fa77d5c304d3ee7cae9aede31f13908bd049f9de8c7c0094ad6f7c535" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#773fceabb27f1e56132198dd960d4bd1493e0ed0" dependencies = [ "bytes", - "getrandom 0.2.16", - "rand 0.8.5", + "fastbloom", + "getrandom 0.3.2", + "lru-slab", + "rand 0.9.1", "ring", "rustc-hash", "rustls", @@ -2507,21 +2515,20 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" +version = "0.5.12" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#773fceabb27f1e56132198dd960d4bd1493e0ed0" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2", "tracing", "windows-sys 0.59.0", ] [[package]] name = "iroh-relay" -version = "0.91.0" +version = "0.90.0" dependencies = [ "ahash", "blake3", @@ -2531,8 +2538,8 @@ dependencies = [ "crypto_box", "dashmap", "data-encoding", - "derive_more 2.0.1", - "getrandom 0.3.3", + "derive_more", + "getrandom 0.3.2", "governor", "hickory-proto", "hickory-resolver", @@ -2555,7 +2562,7 @@ dependencies = [ "proptest", "rand 0.8.5", "rand_chacha 0.3.1", - "rcgen 0.14.2", + "rcgen", "regex", "reloadable-state", "reqwest", @@ -2571,7 +2578,7 @@ dependencies = [ "sha1", "simdutf8", "snafu", - "strum 0.27.1", + "strum", "time", "tokio", "tokio-rustls", @@ -2660,17 +2667,17 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libredox" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags", + "bitflags 2.9.0", "libc", ] @@ -2688,9 +2695,9 @@ checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.8.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "litrs" @@ -2700,9 +2707,9 @@ checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -2733,7 +2740,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" dependencies = [ - "hashbrown 0.15.4", + "hashbrown 0.15.3", ] [[package]] @@ -2787,9 +2794,9 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -2815,22 +2822,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", ] [[package]] @@ -2859,7 +2866,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" dependencies = [ "cfg_aliases", - "derive_more 1.0.0", + "derive_more", "futures-buffered", "futures-lite", "futures-util", @@ -2888,11 +2895,11 @@ dependencies = [ [[package]] name = "n0-watcher" -version = "0.3.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31462392a10d5ada4b945e840cbec2d5f3fee752b96c4b33eb41414d8f45c2a" +checksum = "f216d4ebc5fcf9548244803cbb93f488a2ae160feba3706cd17040d69cf7a368" dependencies = [ - "derive_more 1.0.0", + "derive_more", "n0-future", "snafu", ] @@ -2911,19 +2918,19 @@ dependencies = [ [[package]] name = "netdev" -version = "0.36.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862209dce034f82a44c95ce2b5183730d616f2a68746b9c1959aa2572e77c0a1" +checksum = "f901362e84cd407be6f8cd9d3a46bccf09136b095792785401ea7d283c79b91d" dependencies = [ "dlopen2", "ipnet", "libc", "netlink-packet-core", - "netlink-packet-route 0.22.0", + "netlink-packet-route 0.17.1", "netlink-sys", "once_cell", "system-configuration", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2939,27 +2946,26 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.22.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" +checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" dependencies = [ "anyhow", - "bitflags", + "bitflags 1.3.2", "byteorder", "libc", - "log", "netlink-packet-core", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" -version = "0.24.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" +checksum = "0800eae8638a299eaa67476e1c6b6692922273e0f7939fd188fc861c837b9cd2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.9.0", "byteorder", "libc", "log", @@ -3008,14 +3014,13 @@ dependencies = [ [[package]] name = "netwatch" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901dbb408894af3df3fc51420ba0c6faf3a7d896077b797c39b7001e2f787bd" +version = "0.6.0" +source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#b7ab98d4ff9cc947f2f084004b4cc2a979bb4d06" dependencies = [ "atomic-waker", "bytes", "cfg_aliases", - "derive_more 2.0.1", + "derive_more", "iroh-quinn-udp", "js-sys", "libc", @@ -3024,20 +3029,20 @@ dependencies = [ "nested_enum_utils", "netdev", "netlink-packet-core", - "netlink-packet-route 0.24.0", + "netlink-packet-route 0.23.0", "netlink-proto", "netlink-sys", "pin-project-lite", "serde", "snafu", - "socket2 0.6.0", + "socket2", "time", "tokio", "tokio-util", "tracing", "web-sys", - "windows", - "windows-result", + "windows 0.59.0", + "windows-result 0.3.2", "wmi", ] @@ -3136,24 +3141,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", - "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -3165,6 +3169,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.36.7" @@ -3193,12 +3203,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - [[package]] name = "oorandom" version = "11.1.5" @@ -3231,9 +3235,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -3241,9 +3245,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", @@ -3291,9 +3295,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.8.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6" dependencies = [ "memchr", "thiserror 2.0.12", @@ -3302,9 +3306,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5" dependencies = [ "pest", "pest_generator", @@ -3312,23 +3316,24 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] name = "pest_meta" -version = "2.8.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0" dependencies = [ + "once_cell", "pest", "sha2", ] @@ -3360,7 +3365,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -3377,9 +3382,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkarr" -version = "3.8.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a50f65a2b97031863fbdff2f085ba832360b4bef3106d1fcff9ab5bf4063fe" +checksum = "e32222ae3d617bf92414db29085f8a959a4515effce916e038e9399a335a0d6d" dependencies = [ "async-compat", "base32", @@ -3463,7 +3468,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -3500,19 +3505,19 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portmapper" -version = "0.8.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f1975debe62a70557e42b9ff9466e4890cf9d3d156d296408a711f1c5f642b" +checksum = "2d82975dc029c00d566f4e0f61f567d31f0297a290cb5416b5580dd8b4b54ade" dependencies = [ "base64", "bytes", - "derive_more 2.0.1", + "derive_more", "futures-lite", "futures-util", "hyper-util", @@ -3522,11 +3527,11 @@ dependencies = [ "nested_enum_utils", "netwatch", "num_enum", - "rand 0.9.1", + "rand 0.8.5", "serde", "smallvec", "snafu", - "socket2 0.6.0", + "socket2", "time", "tokio", "tokio-util", @@ -3537,9 +3542,9 @@ dependencies = [ [[package]] name = "postcard" -version = "1.1.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c1de96e20f51df24ca73cafcc4690e044854d803259db27a00a461cb3b9d17a" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" dependencies = [ "cobs", "embedded-io 0.4.0", @@ -3551,22 +3556,13 @@ dependencies = [ [[package]] name = "postcard-derive" -version = "0.2.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f049d94cb6dda6938cc8a531d2898e7c08d71c6de63d8e67123cca6cdde2cc" +checksum = "0239fa9c1d225d4b7eb69925c25c5e082307a141e470573fbbe3a817ce6a7a37" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", -] - -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", + "syn 1.0.109", ] [[package]] @@ -3581,7 +3577,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.25", ] [[package]] @@ -3658,17 +3654,17 @@ dependencies = [ [[package]] name = "proptest" -version = "1.7.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags", + "bitflags 2.9.0", "lazy_static", "num-traits", - "rand 0.9.1", - "rand_chacha 0.9.0", + "rand 0.8.5", + "rand_chacha 0.3.1", "rand_xorshift", "regex-syntax 0.8.5", "rusty-fork", @@ -3678,15 +3674,15 @@ dependencies = [ [[package]] name = "quanta" -version = "0.12.6" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +checksum = "3bd1fe6824cea6538803de3ff1bc0cf3949024db3d43c9643024bfb33a807c0e" dependencies = [ "crossbeam-utils", "libc", "once_cell", "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", "winapi", ] @@ -3699,9 +3695,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "c3bd15a6f2967aef83887dcb9fec0014580467e33720d073560cf015a5683012" dependencies = [ "bytes", "cfg_aliases", @@ -3710,7 +3706,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2", "thiserror 2.0.12", "tokio", "tracing", @@ -3719,13 +3715,12 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b" dependencies = [ "bytes", - "getrandom 0.3.3", - "lru-slab", + "getrandom 0.3.2", "rand 0.9.1", "ring", "rustc-hash", @@ -3740,14 +3735,14 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2", "tracing", "windows-sys 0.59.0", ] @@ -3773,9 +3768,9 @@ dependencies = [ [[package]] name = "r-efi" -version = "5.3.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" @@ -3833,16 +3828,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.2", ] [[package]] name = "rand_xorshift" -version = "0.4.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.6.4", ] [[package]] @@ -3851,7 +3846,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] @@ -3887,19 +3882,6 @@ dependencies = [ "yasna", ] -[[package]] -name = "rcgen" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49bc8ffa8a832eb1d7c8000337f8b0d2f4f2f5ec3cf4ddc26f125e3ad2451824" -dependencies = [ - "pem", - "ring", - "rustls-pki-types", - "time", - "yasna", -] - [[package]] name = "redb" version = "2.4.0" @@ -3911,11 +3893,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] @@ -3998,9 +3980,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64", "bytes", @@ -4012,12 +3994,16 @@ dependencies = [ "hyper", "hyper-rustls", "hyper-util", + "ipnet", "js-sys", "log", + "mime", + "once_cell", "percent-encoding", "pin-project-lite", "quinn", "rustls", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", @@ -4027,21 +4013,21 @@ dependencies = [ "tokio-rustls", "tokio-util", "tower", - "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.1", + "webpki-roots 0.26.11", + "windows-registry", ] [[package]] name = "resolv-conf" -version = "0.7.4" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3" +checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302" [[package]] name = "ring" @@ -4059,9 +4045,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -4093,7 +4079,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags", + "bitflags 2.9.0", "errno", "libc", "linux-raw-sys", @@ -4102,9 +4088,8 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +version = "0.23.25" +source = "git+https://github.com/n0-computer/rustls?rev=be02113e7837df60953d02c2bdd0f4634fef3a80#be02113e7837df60953d02c2bdd0f4634fef3a80" dependencies = [ "log", "once_cell", @@ -4183,11 +4168,11 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.5.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +checksum = "4937d110d34408e9e5ad30ba0b0ca3b6a8a390f8db3636db60144ac4fa792750" dependencies = [ - "core-foundation 0.10.1", + "core-foundation 0.10.0", "core-foundation-sys", "jni", "log", @@ -4210,9 +4195,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "7149975849f1abb3832b246010ef62ccc80d3a76169517ada7188252b9cfb437" dependencies = [ "ring", "rustls-pki-types", @@ -4221,9 +4206,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "rusty-fork" @@ -4243,6 +4228,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "salsa20" version = "0.10.2" @@ -4288,8 +4282,8 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags", - "core-foundation 0.10.1", + "bitflags 2.9.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -4370,7 +4364,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -4397,9 +4391,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -4508,20 +4502,29 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" -version = "0.4.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "smol_str" @@ -4548,7 +4551,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -4561,16 +4564,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "spin" version = "0.9.8" @@ -4631,7 +4624,7 @@ dependencies = [ "proc-macro2", "quote", "struct_iterable_internal", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -4646,16 +4639,7 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" -dependencies = [ - "strum_macros 0.27.1", + "strum_macros", ] [[package]] @@ -4668,20 +4652,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.104", -] - -[[package]] -name = "strum_macros" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -4724,7 +4695,7 @@ dependencies = [ "parking_lot", "pnet_packet", "rand 0.9.1", - "socket2 0.5.10", + "socket2", "thiserror 1.0.69", "tokio", "tracing", @@ -4739,7 +4710,7 @@ dependencies = [ "acto", "hickory-proto", "rand 0.9.1", - "socket2 0.5.10", + "socket2", "thiserror 2.0.12", "tokio", "tracing", @@ -4758,9 +4729,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -4784,7 +4755,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -4793,7 +4764,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags", + "bitflags 2.9.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4816,12 +4787,12 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.2", "once_cell", "rustix", "windows-sys 0.59.0", @@ -4862,7 +4833,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -4873,16 +4844,17 @@ checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] name = "thread_local" -version = "1.1.9" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", + "once_cell", ] [[package]] @@ -4921,9 +4893,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ "displaydoc", "zerovec", @@ -4956,20 +4928,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", - "socket2 0.5.10", + "socket2", "tokio-macros", "windows-sys 0.52.0", ] @@ -4982,7 +4952,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -5010,7 +4980,7 @@ dependencies = [ "num-bigint", "pem", "proc-macro2", - "rcgen 0.13.2", + "rcgen", "reqwest", "ring", "rustls", @@ -5046,7 +5016,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "hashbrown 0.15.4", + "hashbrown 0.15.3", "pin-project-lite", "tokio", ] @@ -5061,7 +5031,7 @@ dependencies = [ "bytes", "futures-core", "futures-sink", - "getrandom 0.3.3", + "getrandom 0.3.2", "http 1.3.1", "httparse", "rand 0.9.1", @@ -5075,59 +5045,44 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.2" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" dependencies = [ - "indexmap", "serde", "serde_spanned", - "toml_datetime 0.7.0", - "toml_parser", - "toml_writer", - "winnow", + "toml_datetime", + "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" - -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.22.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", - "toml_datetime 0.6.11", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" -dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", "winnow", ] [[package]] -name = "toml_writer" -version = "1.0.2" +name = "toml_write" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" [[package]] name = "tower" @@ -5147,18 +5102,15 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ - "bitflags", + "bitflags 2.9.0", "bytes", - "futures-util", "http 1.3.1", "http-body", - "iri-string", "pin-project-lite", - "tower", "tower-layer", "tower-service", "tracing", @@ -5206,20 +5158,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -5294,7 +5246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -5356,9 +5308,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" @@ -5366,12 +5318,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unit-prefix" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" - [[package]] name = "universal-hash" version = "0.5.1" @@ -5400,6 +5346,12 @@ dependencies = [ "serde", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5414,13 +5366,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.17.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom 0.3.3", - "js-sys", - "wasm-bindgen", + "getrandom 0.3.2", ] [[package]] @@ -5465,9 +5415,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" @@ -5500,7 +5450,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", "wasm-bindgen-shared", ] @@ -5535,7 +5485,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5570,7 +5520,7 @@ checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] @@ -5612,14 +5562,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.1", + "webpki-root-certs 1.0.0", ] [[package]] name = "webpki-root-certs" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e" +checksum = "01a83f7e1a9f8712695c03eabe9ed3fbca0feff0152f33f12593e5a6303cb1a4" dependencies = [ "rustls-pki-types", ] @@ -5630,18 +5580,28 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.1", + "webpki-roots 1.0.0", ] [[package]] name = "webpki-roots" -version = "1.0.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "widestring" version = "1.2.0" @@ -5681,48 +5641,83 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.61.3" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-link", - "windows-numerics", + "windows-core 0.58.0", + "windows-targets 0.52.6", ] [[package]] -name = "windows-collections" -version = "0.2.0" +name = "windows" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" dependencies = [ - "windows-core", + "windows-core 0.59.0", + "windows-targets 0.53.0", ] [[package]] name = "windows-core" -version = "0.61.2" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", ] [[package]] -name = "windows-future" -version = "0.2.1" +name = "windows-core" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" dependencies = [ - "windows-core", + "windows-implement 0.59.0", + "windows-interface 0.59.1", + "windows-result 0.3.2", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link", - "windows-threading", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", +] + +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -5733,7 +5728,18 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -5744,39 +5750,68 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-registry" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.2", + "windows-strings 0.3.1", + "windows-targets 0.53.0", +] [[package]] -name = "windows-numerics" +name = "windows-result" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "windows-core", - "windows-link", + "windows-targets 0.52.6", ] [[package]] name = "windows-result" -version = "0.3.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ "windows-link", ] @@ -5817,15 +5852,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.2", -] - [[package]] name = "windows-targets" version = "0.42.2" @@ -5874,9 +5900,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -5888,15 +5914,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6079,9 +6096,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" dependencies = [ "memchr", ] @@ -6102,35 +6119,41 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags", + "bitflags 2.9.0", ] [[package]] name = "wmi" -version = "0.17.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3de777dce4cbcdc661d5d18e78ce4b46a37adc2bb7c0078a556c7f07bcce2f" +checksum = "7787dacdd8e71cbc104658aade4009300777f9b5fda6a75f19145fedb8a18e71" dependencies = [ "chrono", "futures", "log", "serde", "thiserror 2.0.12", - "windows", - "windows-core", + "windows 0.59.0", + "windows-core 0.59.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" -version = "0.6.1" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "ws_stream_wasm" -version = "0.7.5" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" dependencies = [ "async_io_stream", "futures", @@ -6139,7 +6162,7 @@ dependencies = [ "pharos", "rustc_version", "send_wrapper", - "thiserror 2.0.12", + "thiserror 1.0.69", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6164,9 +6187,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" [[package]] name = "xmltree" @@ -6194,9 +6217,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -6206,13 +6229,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", "synstructure", ] @@ -6224,22 +6247,42 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +dependencies = [ + "zerocopy-derive 0.8.25", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", ] [[package]] @@ -6259,7 +6302,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", "synstructure", ] @@ -6269,22 +6312,11 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - [[package]] name = "zerovec" -version = "0.11.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", @@ -6293,11 +6325,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.104", + "syn 2.0.101", ] diff --git a/Cargo.toml b/Cargo.toml index 593f1d0ec73..d5d37a9b5d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,8 @@ unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)", "cfg(iroh_l [workspace.lints.clippy] unused-async = "warn" + + +[patch.crates-io] +rustls = { git = "https://github.com/n0-computer/rustls", rev = "be02113e7837df60953d02c2bdd0f4634fef3a80" } +netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-multipath" } diff --git a/iroh-relay/Cargo.toml b/iroh-relay/Cargo.toml index 9f7bc43c93b..8424cfeac94 100644 --- a/iroh-relay/Cargo.toml +++ b/iroh-relay/Cargo.toml @@ -42,8 +42,8 @@ postcard = { version = "1", default-features = false, features = [ "use-std", "experimental-derive", ] } -quinn = { package = "iroh-quinn", version = "0.14.0", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", version = "0.13.0" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } rand = "0.8" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index ce02aa4d2d3..713e0ebb67f 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -48,9 +48,9 @@ pin-project = "1" pkarr = { version = "3.7", default-features = false, features = [ "relays", ] } -quinn = { package = "iroh-quinn", version = "0.14.0", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", version = "0.13.0" } -quinn-udp = { package = "iroh-quinn-udp", version = "0.5.7" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } +quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } rand = "0.8" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", @@ -109,7 +109,7 @@ hickory-resolver = "0.25.1" igd-next = { version = "0.16", features = ["aio_tokio"] } netdev = { version = "0.36.0" } portmapper = { version = "0.8", default-features = false } -quinn = { package = "iroh-quinn", version = "0.14.0", default-features = false, features = ["runtime-tokio", "rustls-ring"] } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x", default-features = false, features = ["runtime-tokio", "rustls-ring"] } tokio = { version = "1", features = [ "io-util", "macros", diff --git a/iroh/bench/Cargo.toml b/iroh/bench/Cargo.toml index 8070e6259a2..97c46065a7e 100644 --- a/iroh/bench/Cargo.toml +++ b/iroh/bench/Cargo.toml @@ -12,7 +12,7 @@ iroh = { path = ".." } iroh-metrics = "0.35" n0-future = "0.1.1" n0-snafu = "0.2.0" -quinn = { package = "iroh-quinn", version = "0.14" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } rand = "0.8" rcgen = "0.14" rustls = { version = "0.23", default-features = false, features = ["ring"] } From 7fe570da1d6064ad83eb5ea22aa094dd8a269044 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 7 Jul 2025 15:27:42 +0200 Subject: [PATCH 002/164] update iroh-quinn --- Cargo.lock | 10 +++++----- iroh/src/magicsock.rs | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfaec28b630..aab319073b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2474,8 +2474,8 @@ dependencies = [ [[package]] name = "iroh-quinn" -version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#773fceabb27f1e56132198dd960d4bd1493e0ed0" +version = "0.14.0" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" dependencies = [ "bytes", "cfg_aliases", @@ -2494,7 +2494,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#773fceabb27f1e56132198dd960d4bd1493e0ed0" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" dependencies = [ "bytes", "fastbloom", @@ -2516,7 +2516,7 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#773fceabb27f1e56132198dd960d4bd1493e0ed0" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" dependencies = [ "cfg_aliases", "libc", @@ -5630,7 +5630,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index ba6f50f5073..fb2d0c2eec2 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -2624,10 +2624,12 @@ mod tests { info!("stats: {:#?}", stats); // TODO: ensure panics in this function are reported ok if matches!(loss, ExpectedLoss::AlmostNone) { - assert!( - stats.path.lost_packets < 10, - "[receiver] should not loose many packets", - ); + for (id, path) in &stats.paths { + assert!( + path.lost_packets < 10, + "[receiver] path {id:?} should not loose many packets", + ); + } } info!("close"); @@ -2675,10 +2677,12 @@ mod tests { let stats = conn.stats(); info!("stats: {:#?}", stats); if matches!(loss, ExpectedLoss::AlmostNone) { - assert!( - stats.path.lost_packets < 10, - "[sender] should not loose many packets", - ); + for (id, path) in &stats.paths { + assert!( + path.lost_packets < 10, + "[sender] path {id:?} should not loose many packets", + ); + } } info!("close"); From 2f469ac0e21b931ebe75699832d07638f59c85bd Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Tue, 8 Jul 2025 11:44:22 +0200 Subject: [PATCH 003/164] start opening paths --- iroh/src/endpoint.rs | 11 +++++++ iroh/src/magicsock.rs | 76 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index cc9a1cc1fd6..f8545edb212 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1771,6 +1771,17 @@ impl Future for Connecting { Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Ready(Ok(inner)) => { let conn = Connection { inner }; + + // Grab the remote identity and register this connection + + if let Some(remote) = *this.remote_node_id { + let weak_handle = conn.inner.weak_handle(); + this.ep.msock.register_connection(remote, weak_handle); + } else if let Ok(remote) = conn.remote_node_id() { + let weak_handle = conn.inner.weak_handle(); + this.ep.msock.register_connection(remote, weak_handle); + } + try_send_rtt_msg(&conn, this.ep, *this.remote_node_id); Poll::Ready(Ok(conn)) } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index fb2d0c2eec2..f8454cfc1c8 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -43,7 +43,7 @@ use nested_enum_utils::common_fields; use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; -use quinn::{AsyncUdpSocket, ServerConfig}; +use quinn::{AsyncUdpSocket, ServerConfig, WeakConnectionHandle}; use rand::Rng; use smallvec::SmallVec; use snafu::{ResultExt, Snafu}; @@ -195,6 +195,9 @@ pub(crate) struct MagicSock { ipv6_reported: Arc, /// Tracks the networkmap node entity for each node discovery key. node_map: NodeMap, + /// Tracks existing connections + connection_map: ConnectionMap, + /// Tracks the mapped IP addresses ip_mapped_addrs: IpMappedAddresses, /// Local addresses @@ -221,6 +224,22 @@ pub(crate) struct MagicSock { pub(crate) metrics: EndpointMetrics, } +#[derive(Default, Debug)] +struct ConnectionMap { + map: std::sync::Mutex>>, +} + +impl ConnectionMap { + fn insert(&self, remote: NodeId, handle: WeakConnectionHandle) { + self.map + .lock() + .expect("poisoned") + .entry(remote) + .or_default() + .push(handle); + } +} + #[allow(missing_docs)] #[common_fields({ backtrace: Option, @@ -271,6 +290,10 @@ impl MagicSock { self.local_addrs_watch.clone().get() } + pub(crate) fn register_connection(&self, remote: NodeId, conn: WeakConnectionHandle) { + self.connection_map.insert(remote, conn); + } + #[cfg(not(wasm_browser))] fn ip_bind_addrs(&self) -> &[SocketAddr] { &self.ip_bind_addrs @@ -393,8 +416,45 @@ impl MagicSock { } } if !addr.is_empty() { + // Add addr to the internal NodeMap self.node_map - .add_node_addr(addr, source, &self.metrics.magicsock); + .add_node_addr(addr.clone(), source, &self.metrics.magicsock); + + // Add paths to the existing connections + { + let mut map = self.connection_map.map.lock().expect("poisoned"); + let mut to_delete = Vec::new(); + if let Some(conns) = map.get_mut(&addr.node_id) { + for (i, conn) in conns.into_iter().enumerate() { + if let Some(conn) = conn.upgrade() { + for addr in addr.direct_addresses() { + let conn = conn.clone(); + let addr = *addr; + task::spawn(async move { + if let Err(err) = conn + .open_path(addr, quinn_proto::PathStatus::Available) + .await + { + warn!("failed to open path {:?}", err); + } + }); + } + // TODO: add relay path as mapped addr + } else { + to_delete.push(i); + } + } + // cleanup dead connections + let mut i = 0; + conns.retain(|_| { + let remove = to_delete.contains(&i); + i += 1; + + !remove + }); + } + } + Ok(()) } else if pruned != 0 { Err(EmptyPrunedSnafu { pruned }.build()) @@ -505,8 +565,8 @@ impl MagicSock { let mut active_paths = SmallVec::<[_; 3]>::new(); match MappedAddr::from(transmit.destination) { - MappedAddr::None(dest) => { - error!(%dest, "Cannot convert to a mapped address."); + MappedAddr::None(addr) => { + active_paths.push(transports::Addr::from(addr)); } MappedAddr::NodeId(dest) => { trace!( @@ -523,15 +583,14 @@ impl MagicSock { self.ipv6_reported.load(Ordering::Relaxed), &self.metrics.magicsock, ) { - Some((node_id, udp_addr, relay_url, ping_actions)) => { + Some((node_id, _udp_addr, relay_url, ping_actions)) => { if !ping_actions.is_empty() { self.actor_sender .try_send(ActorMessage::PingActions(ping_actions)) .ok(); } - if let Some(addr) = udp_addr { - active_paths.push(transports::Addr::from(addr)); - } + // NodeId mapped addrs are only used for relays, currently. + // IP based addrs will have been added as individual paths if let Some(url) = relay_url { active_paths.push(transports::Addr::Relay(url, node_id)); } @@ -1298,6 +1357,7 @@ impl Handle { actor_sender: actor_sender.clone(), ipv6_reported, node_map, + connection_map: Default::default(), ip_mapped_addrs: ip_mapped_addrs.clone(), discovery, discovery_user_data: RwLock::new(discovery_user_data), From 870716feba7e9b1fafef2685c0e73d95e311406e Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Tue, 8 Jul 2025 12:15:22 +0200 Subject: [PATCH 004/164] add more paths --- Cargo.lock | 8 ++--- Cargo.toml | 9 +++++ iroh/src/endpoint.rs | 8 ++--- iroh/src/magicsock.rs | 78 +++++++++++++++++++++++++------------------ 4 files changed, 61 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aab319073b6..a29794212ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2475,7 +2475,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" +source = "git+https://github.com//n0-computer/quinn?branch=multipath-misc#0d929df5f69ddc660c8ce81e9c348af7972862db" dependencies = [ "bytes", "cfg_aliases", @@ -2494,7 +2494,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" +source = "git+https://github.com//n0-computer/quinn?branch=multipath-misc#0d929df5f69ddc660c8ce81e9c348af7972862db" dependencies = [ "bytes", "fastbloom", @@ -2516,14 +2516,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" +source = "git+https://github.com//n0-computer/quinn?branch=multipath-misc#0d929df5f69ddc660c8ce81e9c348af7972862db" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d5d37a9b5d5..ac71aecc026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,3 +45,12 @@ unused-async = "warn" [patch.crates-io] rustls = { git = "https://github.com/n0-computer/rustls", rev = "be02113e7837df60953d02c2bdd0f4634fef3a80" } netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-multipath" } + +[patch."https://github.com/n0-computer/quinn"] +# iroh-quinn = { path = "../iroh-quinn/quinn" } +# iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } +# iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } + +iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } +iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } +iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index f8545edb212..3e1a7e90e6b 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -772,15 +772,13 @@ impl Endpoint { client_config }; + // TODO: race available addresses, this is currently only using the relay addr to connect + let dest_addr = mapped_addr.private_socket_addr(); let server_name = &tls::name::encode(node_id); let connect = self .msock .endpoint() - .connect_with( - client_config, - mapped_addr.private_socket_addr(), - server_name, - ) + .connect_with(client_config, dest_addr, server_name) .context(QuinnSnafu)?; Ok(Connecting { diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index f8454cfc1c8..d3f4bfe51bd 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -421,39 +421,7 @@ impl MagicSock { .add_node_addr(addr.clone(), source, &self.metrics.magicsock); // Add paths to the existing connections - { - let mut map = self.connection_map.map.lock().expect("poisoned"); - let mut to_delete = Vec::new(); - if let Some(conns) = map.get_mut(&addr.node_id) { - for (i, conn) in conns.into_iter().enumerate() { - if let Some(conn) = conn.upgrade() { - for addr in addr.direct_addresses() { - let conn = conn.clone(); - let addr = *addr; - task::spawn(async move { - if let Err(err) = conn - .open_path(addr, quinn_proto::PathStatus::Available) - .await - { - warn!("failed to open path {:?}", err); - } - }); - } - // TODO: add relay path as mapped addr - } else { - to_delete.push(i); - } - } - // cleanup dead connections - let mut i = 0; - conns.retain(|_| { - let remove = to_delete.contains(&i); - i += 1; - - !remove - }); - } - } + self.add_paths(addr); Ok(()) } else if pruned != 0 { @@ -463,6 +431,41 @@ impl MagicSock { } } + /// Adds all available addresses in the given `addr` as paths + fn add_paths(&self, addr: NodeAddr) { + let mut map = self.connection_map.map.lock().expect("poisoned"); + let mut to_delete = Vec::new(); + if let Some(conns) = map.get_mut(&addr.node_id) { + for (i, conn) in conns.into_iter().enumerate() { + if let Some(conn) = conn.upgrade() { + for addr in addr.direct_addresses() { + let conn = conn.clone(); + let addr = *addr; + task::spawn(async move { + if let Err(err) = conn + .open_path(addr, quinn_proto::PathStatus::Available) + .await + { + warn!("failed to open path {:?}", err); + } + }); + } + // TODO: add relay path as mapped addr + } else { + to_delete.push(i); + } + } + // cleanup dead connections + let mut i = 0; + conns.retain(|_| { + let remove = to_delete.contains(&i); + i += 1; + + !remove + }); + } + } + /// Stores a new set of direct addresses. /// /// If the direct addresses have changed from the previous set, they are published to @@ -873,9 +876,18 @@ impl MagicSock { return; } } + + // Add new addresses as paths + self.add_paths(NodeAddr { + node_id: sender, + relay_url: None, + direct_addresses: cm.my_numbers.iter().copied().collect(), + }); + let ping_actions = self.node_map .handle_call_me_maybe(sender, cm, &self.metrics.magicsock); + for action in ping_actions { match action { PingAction::SendCallMeMaybe { .. } => { From 346a7c2dafa1ce7eec5537b9125ede6ff53049b6 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Tue, 8 Jul 2025 12:36:22 +0200 Subject: [PATCH 005/164] set keep alive and idle timeouts for new paths --- iroh/src/magicsock.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index d3f4bfe51bd..ccddd91c01c 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -442,11 +442,20 @@ impl MagicSock { let conn = conn.clone(); let addr = *addr; task::spawn(async move { - if let Err(err) = conn + match conn .open_path(addr, quinn_proto::PathStatus::Available) .await { - warn!("failed to open path {:?}", err); + Ok(path) => { + path.set_max_idle_timeout(Some( + ENDPOINTS_FRESH_ENOUGH_DURATION, + )) + .ok(); + path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); + } + Err(err) => { + warn!("failed to open path {:?}", err); + } } }); } From 68b1769dc11e0101c102a88bbeb963fe3efa9cef Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Wed, 9 Jul 2025 17:34:32 +0200 Subject: [PATCH 006/164] insert relay path --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- iroh/src/magicsock.rs | 23 ++++++++++++++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a29794212ff..f8b2e3e6c21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2475,7 +2475,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com//n0-computer/quinn?branch=multipath-misc#0d929df5f69ddc660c8ce81e9c348af7972862db" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#70e28875923db76f8dfbf4f058e682d56e6daea1" dependencies = [ "bytes", "cfg_aliases", @@ -2494,7 +2494,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com//n0-computer/quinn?branch=multipath-misc#0d929df5f69ddc660c8ce81e9c348af7972862db" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#70e28875923db76f8dfbf4f058e682d56e6daea1" dependencies = [ "bytes", "fastbloom", @@ -2516,7 +2516,7 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com//n0-computer/quinn?branch=multipath-misc#0d929df5f69ddc660c8ce81e9c348af7972862db" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#70e28875923db76f8dfbf4f058e682d56e6daea1" dependencies = [ "cfg_aliases", "libc", diff --git a/Cargo.toml b/Cargo.toml index ac71aecc026..edb51ff85b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,6 @@ netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-mu # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } -iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } -iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } -iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } +# iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } +# iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } +# iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index ccddd91c01c..b2e9be26491 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -459,7 +459,28 @@ impl MagicSock { } }); } - // TODO: add relay path as mapped addr + // Insert the relay addr + if let Some(addr) = self.get_mapping_addr(addr.node_id) { + let conn = conn.clone(); + let addr = addr.private_socket_addr(); + task::spawn(async move { + match conn + .open_path(addr, quinn_proto::PathStatus::Available) + .await + { + Ok(path) => { + path.set_max_idle_timeout(Some( + ENDPOINTS_FRESH_ENOUGH_DURATION, + )) + .ok(); + path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); + } + Err(err) => { + warn!("failed to open path {:?}", err); + } + } + }); + } } else { to_delete.push(i); } From 0eb3fde19268ff71988721aef992457c2d6e3f53 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 11 Jul 2025 12:35:28 +0200 Subject: [PATCH 007/164] set relay path as backup --- iroh/src/magicsock.rs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index b2e9be26491..f47bcdc996e 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -464,16 +464,11 @@ impl MagicSock { let conn = conn.clone(); let addr = addr.private_socket_addr(); task::spawn(async move { - match conn - .open_path(addr, quinn_proto::PathStatus::Available) - .await - { + match conn.open_path(addr, quinn_proto::PathStatus::Backup).await { Ok(path) => { - path.set_max_idle_timeout(Some( - ENDPOINTS_FRESH_ENOUGH_DURATION, - )) - .ok(); - path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); + // Keep the relay path open + path.set_max_idle_timeout(None).ok(); + path.set_keep_alive_interval(None).ok(); } Err(err) => { warn!("failed to open path {:?}", err); From 79ec17f4c668bcd61244ec472c57d2b5e0fbad3a Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 11 Jul 2025 13:25:33 +0200 Subject: [PATCH 008/164] start removing ping logic from the node_map --- Cargo.lock | 402 +++++--- iroh/src/magicsock.rs | 129 +-- iroh/src/magicsock/node_map.rs | 117 +-- iroh/src/magicsock/node_map/node_state.rs | 1036 ++++++--------------- iroh/src/magicsock/node_map/path_state.rs | 100 +- iroh/src/magicsock/node_map/udp_paths.rs | 3 +- 6 files changed, 568 insertions(+), 1219 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8b2e3e6c21..c3a6558f70f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -451,12 +451,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.9.0" @@ -691,15 +685,15 @@ dependencies = [ [[package]] name = "console" -version = "0.15.11" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "2e09ced7ebbccb63b4c65413d821f2e00ce54c5ca4514ddc6b3c892fdbcbc69d" dependencies = [ "encode_unicode", "libc", "once_cell", "unicode-width", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1010,7 +1004,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" dependencies = [ - "derive_more-impl", + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl 2.0.1", ] [[package]] @@ -1025,6 +1028,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.101", + "unicode-xid", +] + [[package]] name = "diatomic-waker" version = "0.2.3" @@ -2017,7 +2032,7 @@ dependencies = [ "hyper", "libc", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2035,7 +2050,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.0", + "windows-core 0.59.0", ] [[package]] @@ -2219,15 +2234,15 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.11" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd" dependencies = [ "console", - "number_prefix", "portable-atomic", "tokio", "unicode-width", + "unit-prefix", "web-time", ] @@ -2258,7 +2273,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.10", "widestring", "windows-sys 0.48.0", "winreg", @@ -2275,7 +2290,7 @@ dependencies = [ [[package]] name = "iroh" -version = "0.90.0" +version = "0.91.0" dependencies = [ "aead", "axum", @@ -2287,7 +2302,7 @@ dependencies = [ "crypto_box", "data-encoding", "der", - "derive_more", + "derive_more 2.0.1", "ed25519-dalek", "futures-buffered", "futures-util", @@ -2301,7 +2316,7 @@ dependencies = [ "iroh-metrics", "iroh-quinn", "iroh-quinn-proto", - "iroh-quinn-udp", + "iroh-quinn-udp 0.5.12", "iroh-relay", "n0-future", "n0-snafu", @@ -2327,7 +2342,7 @@ dependencies = [ "smallvec", "snafu", "spki", - "strum", + "strum 0.27.2", "stun-rs", "surge-ping", "swarm-discovery", @@ -2348,11 +2363,11 @@ dependencies = [ [[package]] name = "iroh-base" -version = "0.90.0" +version = "0.91.0" dependencies = [ "curve25519-dalek", "data-encoding", - "derive_more", + "derive_more 2.0.1", "ed25519-dalek", "n0-snafu", "nested_enum_utils", @@ -2369,7 +2384,7 @@ dependencies = [ [[package]] name = "iroh-bench" -version = "0.90.0" +version = "0.91.0" dependencies = [ "bytes", "clap", @@ -2379,9 +2394,8 @@ dependencies = [ "iroh-quinn", "n0-future", "n0-snafu", - "n0-watcher", "rand 0.8.5", - "rcgen", + "rcgen 0.14.3", "rustls", "tokio", "tracing", @@ -2390,7 +2404,7 @@ dependencies = [ [[package]] name = "iroh-dns-server" -version = "0.90.0" +version = "0.91.0" dependencies = [ "async-trait", "axum", @@ -2400,7 +2414,7 @@ dependencies = [ "clap", "criterion", "data-encoding", - "derive_more", + "derive_more 2.0.1", "dirs-next", "governor", "hickory-resolver", @@ -2416,7 +2430,7 @@ dependencies = [ "pkarr", "rand 0.8.5", "rand_chacha 0.3.1", - "rcgen", + "rcgen 0.13.2", "redb", "regex", "rustls", @@ -2424,7 +2438,7 @@ dependencies = [ "serde", "snafu", "struct_iterable", - "strum", + "strum 0.26.3", "tokio", "tokio-rustls", "tokio-rustls-acme", @@ -2475,16 +2489,16 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#70e28875923db76f8dfbf4f058e682d56e6daea1" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" dependencies = [ "bytes", "cfg_aliases", "iroh-quinn-proto", - "iroh-quinn-udp", + "iroh-quinn-udp 0.5.12", "pin-project-lite", "rustc-hash", "rustls", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -2494,7 +2508,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#70e28875923db76f8dfbf4f058e682d56e6daea1" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" dependencies = [ "bytes", "fastbloom", @@ -2513,22 +2527,36 @@ dependencies = [ "web-time", ] +[[package]] +name = "iroh-quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#70e28875923db76f8dfbf4f058e682d56e6daea1" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#140fdf97bfb706b1cac591b9711818c5df8012f4" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "iroh-relay" -version = "0.90.0" +version = "0.91.0" dependencies = [ "ahash", "blake3", @@ -2538,7 +2566,7 @@ dependencies = [ "crypto_box", "dashmap", "data-encoding", - "derive_more", + "derive_more 2.0.1", "getrandom 0.3.2", "governor", "hickory-proto", @@ -2562,7 +2590,7 @@ dependencies = [ "proptest", "rand 0.8.5", "rand_chacha 0.3.1", - "rcgen", + "rcgen 0.14.3", "regex", "reloadable-state", "reqwest", @@ -2578,7 +2606,7 @@ dependencies = [ "sha1", "simdutf8", "snafu", - "strum", + "strum 0.27.2", "time", "tokio", "tokio-rustls", @@ -2677,7 +2705,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.9.0", + "bitflags", "libc", ] @@ -2866,7 +2894,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794" dependencies = [ "cfg_aliases", - "derive_more", + "derive_more 1.0.0", "futures-buffered", "futures-lite", "futures-util", @@ -2895,11 +2923,11 @@ dependencies = [ [[package]] name = "n0-watcher" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f216d4ebc5fcf9548244803cbb93f488a2ae160feba3706cd17040d69cf7a368" +checksum = "c31462392a10d5ada4b945e840cbec2d5f3fee752b96c4b33eb41414d8f45c2a" dependencies = [ - "derive_more", + "derive_more 1.0.0", "n0-future", "snafu", ] @@ -2918,19 +2946,19 @@ dependencies = [ [[package]] name = "netdev" -version = "0.31.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f901362e84cd407be6f8cd9d3a46bccf09136b095792785401ea7d283c79b91d" +checksum = "862209dce034f82a44c95ce2b5183730d616f2a68746b9c1959aa2572e77c0a1" dependencies = [ "dlopen2", "ipnet", "libc", "netlink-packet-core", - "netlink-packet-route 0.17.1", + "netlink-packet-route 0.22.0", "netlink-sys", "once_cell", "system-configuration", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2946,26 +2974,27 @@ dependencies = [ [[package]] name = "netlink-packet-route" -version = "0.17.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053998cea5a306971f88580d0829e90f270f940befd7cf928da179d4187a5a66" +checksum = "fc0e7987b28514adf555dc1f9a5c30dfc3e50750bbaffb1aec41ca7b23dcd8e4" dependencies = [ "anyhow", - "bitflags 1.3.2", + "bitflags", "byteorder", "libc", + "log", "netlink-packet-core", "netlink-packet-utils", ] [[package]] name = "netlink-packet-route" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0800eae8638a299eaa67476e1c6b6692922273e0f7939fd188fc861c837b9cd2" +checksum = "56d83370a96813d7c977f8b63054f1162df6e5784f1c598d689236564fb5a6f2" dependencies = [ "anyhow", - "bitflags 2.9.0", + "bitflags", "byteorder", "libc", "log", @@ -3014,14 +3043,15 @@ dependencies = [ [[package]] name = "netwatch" -version = "0.6.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#b7ab98d4ff9cc947f2f084004b4cc2a979bb4d06" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901dbb408894af3df3fc51420ba0c6faf3a7d896077b797c39b7001e2f787bd" dependencies = [ "atomic-waker", "bytes", "cfg_aliases", - "derive_more", - "iroh-quinn-udp", + "derive_more 2.0.1", + "iroh-quinn-udp 0.5.7", "js-sys", "libc", "n0-future", @@ -3029,20 +3059,20 @@ dependencies = [ "nested_enum_utils", "netdev", "netlink-packet-core", - "netlink-packet-route 0.23.0", + "netlink-packet-route 0.24.0", "netlink-proto", "netlink-sys", "pin-project-lite", "serde", "snafu", - "socket2", + "socket2 0.6.0", "time", "tokio", "tokio-util", "tracing", "web-sys", - "windows 0.59.0", - "windows-result 0.3.2", + "windows 0.61.3", + "windows-result 0.3.4", "wmi", ] @@ -3169,12 +3199,6 @@ dependencies = [ "libc", ] -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - [[package]] name = "object" version = "0.36.7" @@ -3511,13 +3535,13 @@ checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "portmapper" -version = "0.6.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d82975dc029c00d566f4e0f61f567d31f0297a290cb5416b5580dd8b4b54ade" +checksum = "62f1975debe62a70557e42b9ff9466e4890cf9d3d156d296408a711f1c5f642b" dependencies = [ "base64", "bytes", - "derive_more", + "derive_more 2.0.1", "futures-lite", "futures-util", "hyper-util", @@ -3527,11 +3551,11 @@ dependencies = [ "nested_enum_utils", "netwatch", "num_enum", - "rand 0.8.5", + "rand 0.9.1", "serde", "smallvec", "snafu", - "socket2", + "socket2 0.6.0", "time", "tokio", "tokio-util", @@ -3660,7 +3684,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.9.0", + "bitflags", "lazy_static", "num-traits", "rand 0.8.5", @@ -3706,7 +3730,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -3742,7 +3766,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2", + "socket2 0.5.10", "tracing", "windows-sys 0.59.0", ] @@ -3846,7 +3870,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] @@ -3882,6 +3906,19 @@ dependencies = [ "yasna", ] +[[package]] +name = "rcgen" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0068c5b3cab1d4e271e0bb6539c87563c43411cad90b057b15c79958fbeb41f7" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "redb" version = "2.4.0" @@ -3897,7 +3934,7 @@ version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] @@ -4079,7 +4116,7 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -4282,7 +4319,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.9.0", + "bitflags", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -4391,9 +4428,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ "serde", ] @@ -4502,7 +4539,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee851d0e5e7af3721faea1843e8015e820a234f81fda3dea9247e15bac9a86a" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] @@ -4564,6 +4601,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -4639,7 +4686,16 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", ] [[package]] @@ -4655,6 +4711,18 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.101", +] + [[package]] name = "stun-rs" version = "0.1.11" @@ -4695,7 +4763,7 @@ dependencies = [ "parking_lot", "pnet_packet", "rand 0.9.1", - "socket2", + "socket2 0.5.10", "thiserror 1.0.69", "tokio", "tracing", @@ -4710,7 +4778,7 @@ dependencies = [ "acto", "hickory-proto", "rand 0.9.1", - "socket2", + "socket2 0.5.10", "thiserror 2.0.12", "tokio", "tracing", @@ -4764,7 +4832,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4939,7 +5007,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.10", "tokio-macros", "windows-sys 0.52.0", ] @@ -4980,7 +5048,7 @@ dependencies = [ "num-bigint", "pem", "proc-macro2", - "rcgen", + "rcgen 0.13.2", "reqwest", "ring", "rustls", @@ -5045,14 +5113,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.22" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" dependencies = [ + "indexmap", "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.7.0", + "toml_parser", + "toml_writer", + "winnow", ] [[package]] @@ -5060,6 +5131,12 @@ name = "toml_datetime" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ "serde", ] @@ -5071,18 +5148,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" dependencies = [ "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", + "toml_datetime 0.6.9", "winnow", ] [[package]] -name = "toml_write" -version = "0.1.1" +name = "toml_parser" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" [[package]] name = "tower" @@ -5106,7 +5189,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ - "bitflags 2.9.0", + "bitflags", "bytes", "http 1.3.1", "http-body", @@ -5318,6 +5401,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unit-prefix" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" + [[package]] name = "universal-hash" version = "0.5.1" @@ -5630,7 +5719,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -5651,12 +5740,24 @@ dependencies = [ [[package]] name = "windows" -version = "0.59.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.0", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -5680,22 +5781,33 @@ checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" dependencies = [ "windows-implement 0.59.0", "windows-interface 0.59.1", - "windows-result 0.3.2", + "windows-result 0.3.4", "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-targets 0.53.3", ] [[package]] name = "windows-core" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", "windows-link", - "windows-result 0.3.2", - "windows-strings 0.4.0", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", ] [[package]] @@ -5755,9 +5867,19 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] [[package]] name = "windows-registry" @@ -5765,9 +5887,9 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ - "windows-result 0.3.2", + "windows-result 0.3.4", "windows-strings 0.3.1", - "windows-targets 0.53.0", + "windows-targets 0.53.3", ] [[package]] @@ -5781,9 +5903,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -5809,9 +5931,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -5852,6 +5974,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -5900,10 +6031,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -5914,6 +6046,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -6119,22 +6260,22 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags", ] [[package]] name = "wmi" -version = "0.14.5" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7787dacdd8e71cbc104658aade4009300777f9b5fda6a75f19145fedb8a18e71" +checksum = "3d3de777dce4cbcdc661d5d18e78ce4b46a37adc2bb7c0078a556c7f07bcce2f" dependencies = [ "chrono", "futures", "log", "serde", "thiserror 2.0.12", - "windows 0.59.0", - "windows-core 0.59.0", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] @@ -6333,3 +6474,8 @@ dependencies = [ "quote", "syn 2.0.101", ] + +[[patch.unused]] +name = "netwatch" +version = "0.6.0" +source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#b7ab98d4ff9cc947f2f084004b4cc2a979bb4d06" diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index f47bcdc996e..892000b2d3b 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -29,7 +29,6 @@ use std::{ }; use bytes::Bytes; -use data_encoding::HEXLOWER; use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl, SecretKey}; use iroh_relay::RelayMap; use n0_future::{ @@ -59,7 +58,7 @@ use url::Url; use self::transports::IpTransport; use self::{ metrics::Metrics as MagicsockMetrics, - node_map::{NodeMap, PingAction, PingRole, SendPing}, + node_map::{NodeMap, PingAction}, transports::{RelayActorConfig, RelayTransport, Transports, UdpSender}, }; #[cfg(not(wasm_browser))] @@ -876,13 +875,8 @@ impl MagicSock { let _guard = span.enter(); trace!("receive disco message"); match dm { - disco::Message::Ping(ping) => { - self.metrics.magicsock.recv_disco_ping.inc(); - self.handle_ping(ping, sender, src); - } - disco::Message::Pong(pong) => { - self.metrics.magicsock.recv_disco_pong.inc(); - self.node_map.handle_pong(sender, src, pong); + disco::Message::Ping(..) | disco::Message::Pong(..) => { + unreachable!("not used anymore"); } disco::Message::CallMeMaybe(cm) => { self.metrics.magicsock.recv_disco_call_me_maybe.inc(); @@ -909,99 +903,19 @@ impl MagicSock { direct_addresses: cm.my_numbers.iter().copied().collect(), }); - let ping_actions = - self.node_map - .handle_call_me_maybe(sender, cm, &self.metrics.magicsock); - - for action in ping_actions { - match action { - PingAction::SendCallMeMaybe { .. } => { - warn!("Unexpected CallMeMaybe as response of handling a CallMeMaybe"); - } - PingAction::SendPing(ping) => { - self.send_ping_queued(ping); - } - } - } + self.node_map + .handle_call_me_maybe(sender, cm, &self.metrics.magicsock); } } trace!("disco message handled"); } - /// Handle a ping message. - fn handle_ping(&self, dm: disco::Ping, sender: NodeId, src: &transports::Addr) { - // Insert the ping into the node map, and return whether a ping with this tx_id was already - // received. - let addr: SendAddr = src.clone().into(); - let handled = self.node_map.handle_ping(sender, addr.clone(), dm.tx_id); - match handled.role { - PingRole::Duplicate => { - debug!(?src, tx = %HEXLOWER.encode(&dm.tx_id), "received ping: path already confirmed, skip"); - return; - } - PingRole::LikelyHeartbeat => {} - PingRole::NewPath => { - debug!(?src, tx = %HEXLOWER.encode(&dm.tx_id), "received ping: new path"); - } - PingRole::Activate => { - debug!(?src, tx = %HEXLOWER.encode(&dm.tx_id), "received ping: path active"); - } - } - - // Send a pong. - debug!(tx = %HEXLOWER.encode(&dm.tx_id), %addr, dstkey = %sender.fmt_short(), - "sending pong"); - let pong = disco::Message::Pong(disco::Pong { - tx_id: dm.tx_id, - ping_observed_addr: addr.clone(), - }); - event!( - target: "iroh::_events::pong::sent", - Level::DEBUG, - remote_node = %sender.fmt_short(), - dst = ?addr, - txn = ?dm.tx_id, - ); - - if !self.disco.try_send(addr.clone(), sender, pong) { - warn!(%addr, "failed to queue pong"); - } - - if let Some(ping) = handled.needs_ping_back { - debug!( - %addr, - dstkey = %sender.fmt_short(), - "sending direct ping back", - ); - self.send_ping_queued(ping); - } - } - - fn send_ping_queued(&self, ping: SendPing) { - let SendPing { - id, - dst, - dst_node, - tx_id, - purpose, - } = ping; - let msg = disco::Message::Ping(disco::Ping { - tx_id, - node_key: self.public_key, - }); - let sent = self.disco.try_send(dst.clone(), dst_node, msg); - if sent { - let msg_sender = self.actor_sender.clone(); - trace!(%dst, tx = %HEXLOWER.encode(&tx_id), ?purpose, "ping sent (queued)"); - self.node_map - .notify_ping_sent(id, dst, tx_id, purpose, msg_sender); - } else { - warn!(dst = ?dst, tx = %HEXLOWER.encode(&tx_id), ?purpose, "failed to send ping: queues full"); - } - } - /// Send the given ping actions out. - async fn send_ping_actions(&self, sender: &UdpSender, msgs: Vec) -> io::Result<()> { + async fn send_ping_actions( + &self, + _sender: &UdpSender, + msgs: Vec, + ) -> io::Result<()> { for msg in msgs { // Abort sending as soon as we know we are shutting down. if self.is_closing() || self.is_closed() { @@ -1046,25 +960,6 @@ impl MagicSock { } } } - PingAction::SendPing(SendPing { - id, - dst, - dst_node, - tx_id, - purpose, - }) => { - let msg = disco::Message::Ping(disco::Ping { - tx_id, - node_key: self.public_key, - }); - - self.send_disco_message(sender, dst.clone(), dst_node, msg) - .await?; - debug!(%dst, tx = %HEXLOWER.encode(&tx_id), ?purpose, "ping sent"); - let msg_sender = self.actor_sender.clone(); - self.node_map - .notify_ping_sent(id, dst, tx_id, purpose, msg_sender); - } } } Ok(()) @@ -1741,7 +1636,6 @@ impl AsyncUdpSocket for MagicUdpSocket { #[derive(Debug)] enum ActorMessage { PingActions(Vec), - EndpointPingExpired(usize, stun_rs::TransactionId), NetworkChange, ScheduleDirectAddrUpdate(UpdateReason, Option<(NodeId, RelayUrl)>), #[cfg(test)] @@ -2036,9 +1930,6 @@ impl Actor { /// Returns `true` if it was a shutdown. async fn handle_actor_message(&mut self, msg: ActorMessage, sender: &UdpSender) { match msg { - ActorMessage::EndpointPingExpired(id, txid) => { - self.msock.node_map.notify_ping_timeout(id, txid); - } ActorMessage::NetworkChange => { self.network_monitor.network_change().await.ok(); } diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index a67a570c3dd..11b135046e3 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -8,10 +8,9 @@ use std::{ use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; use n0_future::time::Instant; use serde::{Deserialize, Serialize}; -use stun_rs::TransactionId; use tracing::{debug, info, instrument, trace, warn}; -use self::node_state::{NodeState, Options, PingHandled}; +use self::node_state::{NodeState, Options}; use super::{ActorMessage, NodeIdMappedAddr, metrics::Metrics, transports}; use crate::disco::{CallMeMaybe, Pong, SendAddr}; #[cfg(any(test, feature = "test-utils"))] @@ -22,8 +21,8 @@ mod path_state; mod path_validity; mod udp_paths; +pub(super) use node_state::PingAction; pub use node_state::{ConnectionType, ControlMsg, DirectAddrInfo, RemoteInfo}; -pub(super) use node_state::{DiscoPingPurpose, PingAction, PingRole, SendPing}; /// Number of nodes that are inactive for which we keep info about. This limit is enforced /// periodically via [`NodeMap::prune_inactive`]. @@ -68,7 +67,6 @@ pub(super) struct NodeMapInner { /// have for the node. These are all the keys the [`NodeMap`] can use. #[derive(Debug, Clone)] enum NodeStateKey { - Idx(usize), NodeId(NodeId), NodeIdMappedAddr(NodeIdMappedAddr), IpPort(IpPort), @@ -168,35 +166,6 @@ impl NodeMap { .receive_relay(relay_url, src) } - pub(super) fn notify_ping_sent( - &self, - id: usize, - dst: SendAddr, - tx_id: stun_rs::TransactionId, - purpose: DiscoPingPurpose, - msg_sender: tokio::sync::mpsc::Sender, - ) { - if let Some(ep) = self - .inner - .lock() - .expect("poisoned") - .get_mut(NodeStateKey::Idx(id)) - { - ep.ping_sent(dst, tx_id, purpose, msg_sender); - } - } - - pub(super) fn notify_ping_timeout(&self, id: usize, tx_id: stun_rs::TransactionId) { - if let Some(ep) = self - .inner - .lock() - .expect("poisoned") - .get_mut(NodeStateKey::Idx(id)) - { - ep.ping_timeout(tx_id, Instant::now()); - } - } - pub(super) fn get_quic_mapped_addr_for_node_key( &self, node_key: NodeId, @@ -208,38 +177,16 @@ impl NodeMap { .map(|ep| *ep.quic_mapped_addr()) } - /// Insert a received ping into the node map, and return whether a ping with this tx_id was already - /// received. - pub(super) fn handle_ping( - &self, - sender: PublicKey, - src: SendAddr, - tx_id: TransactionId, - ) -> PingHandled { - self.inner - .lock() - .expect("poisoned") - .handle_ping(sender, src, tx_id) - } - - pub(super) fn handle_pong(&self, sender: PublicKey, src: &transports::Addr, pong: Pong) { - self.inner - .lock() - .expect("poisoned") - .handle_pong(sender, src, pong) - } - - #[must_use = "actions must be handled"] pub(super) fn handle_call_me_maybe( &self, sender: PublicKey, cm: CallMeMaybe, metrics: &Metrics, - ) -> Vec { + ) { self.inner .lock() .expect("poisoned") - .handle_call_me_maybe(sender, cm, metrics) + .handle_call_me_maybe(sender, cm, metrics); } #[allow(clippy::type_complexity)] @@ -406,7 +353,6 @@ impl NodeMapInner { fn get_id(&self, id: NodeStateKey) -> Option { match id { - NodeStateKey::Idx(id) => Some(id), NodeStateKey::NodeId(node_key) => self.by_node_key.get(&node_key).copied(), NodeStateKey::NodeIdMappedAddr(addr) => self.by_quic_mapped_addr.get(&addr).copied(), NodeStateKey::IpPort(ipp) => self.by_ip_port.get(&ipp).copied(), @@ -502,25 +448,7 @@ impl NodeMapInner { .map(|ep| ep.conn_type()) } - fn handle_pong(&mut self, sender: NodeId, src: &transports::Addr, pong: Pong) { - if let Some(ns) = self.get_mut(NodeStateKey::NodeId(sender)).as_mut() { - let insert = ns.handle_pong(&pong, src.clone().into()); - if let Some((src, key)) = insert { - self.set_node_key_for_ip_port(src, &key); - } - trace!(?insert, "received pong") - } else { - warn!("received pong: node unknown, ignore") - } - } - - #[must_use = "actions must be handled"] - fn handle_call_me_maybe( - &mut self, - sender: NodeId, - cm: CallMeMaybe, - metrics: &Metrics, - ) -> Vec { + fn handle_call_me_maybe(&mut self, sender: NodeId, cm: CallMeMaybe, metrics: &Metrics) { let ns_id = NodeStateKey::NodeId(sender); if let Some(id) = self.get_id(ns_id.clone()) { for number in &cm.my_numbers { @@ -532,43 +460,13 @@ impl NodeMapInner { None => { debug!("received call-me-maybe: ignore, node is unknown"); metrics.recv_disco_call_me_maybe_bad_disco.inc(); - vec![] } Some(ns) => { debug!(endpoints = ?cm.my_numbers, "received call-me-maybe"); - ns.handle_call_me_maybe(cm) - } - } - } - - fn handle_ping(&mut self, sender: NodeId, src: SendAddr, tx_id: TransactionId) -> PingHandled { - #[cfg(any(test, feature = "test-utils"))] - let path_selection = self.path_selection; - let node_state = self.get_or_insert_with(NodeStateKey::NodeId(sender), || { - debug!("received ping: node unknown, add to node map"); - let source = if src.is_relay() { - Source::Relay - } else { - Source::Udp - }; - Options { - node_id: sender, - relay_url: src.relay_url(), - active: true, - source, - #[cfg(any(test, feature = "test-utils"))] - path_selection, - } - }); - - let handled = node_state.handle_ping(src.clone(), tx_id); - if let SendAddr::Udp(ref addr) = src { - if matches!(handled.role, PingRole::NewPath) { - self.set_node_key_for_ip_port(*addr, &sender); + ns.handle_call_me_maybe(cm); } } - handled } /// Inserts a new node into the [`NodeMap`]. @@ -706,6 +604,7 @@ mod tests { use tracing_test::traced_test; use super::{node_state::MAX_INACTIVE_DIRECT_ADDRESSES, *}; + use crate::disco::SendAddr; impl NodeMap { #[track_caller] @@ -838,7 +737,7 @@ mod tests { let txid = stun_rs::TransactionId::from([i as u8; 12]); // Note that this already invokes .prune_direct_addresses() because these are // new UDP paths. - endpoint.handle_ping(addr, txid); + // endpoint.handle_ping(addr, txid); } info!("Pruning addresses"); diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 5a84f5ae378..26a4a6d63a5 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1,19 +1,13 @@ use std::{ - collections::{BTreeSet, HashMap, btree_map::Entry}, - hash::Hash, + collections::{BTreeSet, HashMap}, net::{IpAddr, SocketAddr}, sync::atomic::AtomicBool, }; -use data_encoding::HEXLOWER; use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; -use n0_future::{ - task::{self, AbortOnDropHandle}, - time::{self, Duration, Instant}, -}; +use n0_future::time::{Duration, Instant}; use n0_watcher::Watchable; use serde::{Deserialize, Serialize}; -use tokio::sync::mpsc; use tracing::{Level, debug, event, info, instrument, trace, warn}; use super::{ @@ -39,9 +33,6 @@ pub(super) const MAX_INACTIVE_DIRECT_ADDRESSES: usize = 20; /// How long since an endpoint path was last alive before it might be pruned. const LAST_ALIVE_PRUNE_DURATION: Duration = Duration::from_secs(120); -/// How long we wait for a pong reply before assuming it's never coming. -const PING_TIMEOUT_DURATION: Duration = Duration::from_secs(5); - /// The latency at or under which we don't try to upgrade to a better path. const GOOD_ENOUGH_LATENCY: Duration = Duration::from_millis(5); @@ -52,46 +43,12 @@ pub(super) const SESSION_ACTIVE_TIMEOUT: Duration = Duration::from_secs(45); /// How often we try to upgrade to a better patheven if we have some non-relay route that works. const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); -/// How long until we send a stayin alive ping -const STAYIN_ALIVE_MIN_ELAPSED: Duration = Duration::from_secs(2); - #[derive(Debug)] pub(in crate::magicsock) enum PingAction { SendCallMeMaybe { relay_url: RelayUrl, dst_node: NodeId, }, - SendPing(SendPing), -} - -#[derive(Debug)] -pub(in crate::magicsock) struct SendPing { - pub id: usize, - pub dst: SendAddr, - pub dst_node: NodeId, - pub tx_id: stun_rs::TransactionId, - pub purpose: DiscoPingPurpose, -} - -/// Indicating an [`NodeState`] has handled a ping. -#[derive(Debug)] -pub struct PingHandled { - /// What this ping did to the [`NodeState`]. - pub role: PingRole, - /// Whether the sender path should also be pinged. - /// - /// This is the case if an [`NodeState`] does not yet have a direct path, i.e. it has no - /// best_addr. In this case we want to ping right back to open the direct path in this - /// direction as well. - pub needs_ping_back: Option, -} - -#[derive(Debug)] -pub enum PingRole { - Duplicate, - NewPath, - LikelyHeartbeat, - Activate, } /// An iroh node, which we can have connections with. @@ -109,14 +66,11 @@ pub(super) struct NodeState { quic_mapped_addr: NodeIdMappedAddr, /// The global identifier for this endpoint. node_id: NodeId, - /// The last time we pinged all endpoints. - last_full_ping: Option, /// The url of relay node that we can relay over to communicate. /// /// The fallback/bootstrap path, if non-zero (non-zero for well-behaved clients). relay_url: Option<(RelayUrl, PathState)>, udp_paths: NodeUdpPaths, - sent_pings: HashMap, /// Last time this node was used. /// /// A node is marked as in use when sending datagrams to them, or when having received @@ -174,7 +128,6 @@ impl NodeState { id, quic_mapped_addr, node_id: options.node_id, - last_full_ping: None, relay_url: options.relay_url.map(|url| { ( url.clone(), @@ -182,7 +135,6 @@ impl NodeState { ) }), udp_paths: NodeUdpPaths::new(), - sent_pings: HashMap::new(), last_used: options.active.then(Instant::now), last_call_me_maybe: None, conn_type: Watchable::new(ConnectionType::None), @@ -211,32 +163,7 @@ impl NodeState { /// Returns info about this node. pub(super) fn info(&self, now: Instant) -> RemoteInfo { let conn_type = self.conn_type.get(); - let latency = match conn_type { - ConnectionType::Direct(addr) => self - .udp_paths - .paths - .get(&addr.into()) - .and_then(|state| state.latency()), - ConnectionType::Relay(ref url) => self - .relay_url - .as_ref() - .filter(|(relay_url, _)| relay_url == url) - .and_then(|(_, state)| state.latency()), - ConnectionType::Mixed(addr, ref url) => { - let addr_latency = self - .udp_paths - .paths - .get(&addr.into()) - .and_then(|state| state.latency()); - let relay_latency = self - .relay_url - .as_ref() - .filter(|(relay_url, _)| relay_url == url) - .and_then(|(_, state)| state.latency()); - addr_latency.min(relay_latency) - } - ConnectionType::None => None, - }; + let latency = None; let addrs = self .udp_paths @@ -396,10 +323,6 @@ impl NodeState { #[instrument("want_call_me_maybe", skip_all)] fn want_call_me_maybe(&self, now: &Instant) -> bool { trace!("full ping: wanted?"); - let Some(last_full_ping) = self.last_full_ping else { - debug!("no previous full ping: need full ping"); - return true; - }; match &self.udp_paths.best { UdpSendAddr::None | UdpSendAddr::Unconfirmed(_) => { debug!("best addr not set: need full ping"); @@ -417,7 +340,7 @@ impl NodeState { .expect("send path not tracked?") .latency() .expect("send_addr marked valid incorrectly"); - if latency > GOOD_ENOUGH_LATENCY && *now - last_full_ping >= UPGRADE_INTERVAL { + if latency > GOOD_ENOUGH_LATENCY { debug!( "full ping interval expired and latency is only {}ms: need full ping", latency.as_millis() @@ -437,131 +360,6 @@ impl NodeState { false } - /// Cleanup the expired ping for the passed in txid. - #[instrument("disco", skip_all, fields(node = %self.node_id.fmt_short()))] - pub(super) fn ping_timeout(&mut self, txid: stun_rs::TransactionId, now: Instant) { - if let Some(sp) = self.sent_pings.remove(&txid) { - debug!(tx = %HEXLOWER.encode(&txid), addr = %sp.to, "pong not received in timeout"); - match sp.to { - SendAddr::Udp(addr) => { - if let Some(path_state) = self.udp_paths.paths.get_mut(&addr.into()) { - path_state.last_ping = None; - let consider_alive = path_state - .last_alive() - .map(|last_alive| last_alive.elapsed() <= PING_TIMEOUT_DURATION) - .unwrap_or(false); - if !consider_alive { - // If there was no sign of life from this path during the time - // which we should have received the pong, clear best addr and - // pong. Both are used to select this path again, but we know - // it's not a usable path now. - path_state.validity = PathValidity::empty(); - self.udp_paths.update_to_best_addr(now); - } - } else { - // If we have no state for the best addr it should have been cleared - // anyway. - self.udp_paths.update_to_best_addr(now); - } - } - SendAddr::Relay(ref url) => { - if let Some((home_relay, relay_state)) = self.relay_url.as_mut() { - if home_relay == url { - // lost connectivity via relay - relay_state.last_ping = None; - } - } - } - } - } - } - - #[must_use = "pings must be handled"] - fn start_ping(&self, dst: SendAddr, purpose: DiscoPingPurpose) -> Option { - #[cfg(any(test, feature = "test-utils"))] - if self.path_selection == PathSelection::RelayOnly && !dst.is_relay() { - // don't attempt any hole punching in relay only mode - warn!("in `RelayOnly` mode, ignoring request to start a hole punching attempt."); - return None; - } - #[cfg(wasm_browser)] - if !dst.is_relay() { - return None; // Similar to `RelayOnly` mode, we don't send UDP pings for hole-punching. - } - - let tx_id = stun_rs::TransactionId::default(); - trace!(tx = %HEXLOWER.encode(&tx_id), %dst, ?purpose, - dst = %self.node_id.fmt_short(), "start ping"); - event!( - target: "iroh::_events::ping::sent", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - ?dst, - txn = ?tx_id, - ?purpose, - ); - Some(SendPing { - id: self.id, - dst, - dst_node: self.node_id, - tx_id, - purpose, - }) - } - - /// Record the fact that a ping has been sent out. - pub(super) fn ping_sent( - &mut self, - to: SendAddr, - tx_id: stun_rs::TransactionId, - purpose: DiscoPingPurpose, - sender: mpsc::Sender, - ) { - trace!(%to, tx = %HEXLOWER.encode(&tx_id), ?purpose, "record ping sent"); - - let now = Instant::now(); - let mut path_found = false; - match to { - SendAddr::Udp(addr) => { - if let Some(st) = self.udp_paths.paths.get_mut(&addr.into()) { - st.last_ping.replace(now); - path_found = true - } - } - SendAddr::Relay(ref url) => { - if let Some((home_relay, relay_state)) = self.relay_url.as_mut() { - if home_relay == url { - relay_state.last_ping.replace(now); - path_found = true - } - } - } - } - if !path_found { - // Shouldn't happen. But don't ping an endpoint that's not active for us. - warn!(%to, ?purpose, "unexpected attempt to ping no longer live path"); - return; - } - - let id = self.id; - let _expiry_task = AbortOnDropHandle::new(task::spawn(async move { - time::sleep(PING_TIMEOUT_DURATION).await; - sender - .send(ActorMessage::EndpointPingExpired(id, tx_id)) - .await - .ok(); - })); - self.sent_pings.insert( - tx_id, - SentPing { - to, - at: now, - purpose, - _expiry_task, - }, - ); - } - /// Send a DISCO call-me-maybe message to the peer. /// /// This takes care of sending the needed pings beforehand. This ensures that we open @@ -588,11 +386,7 @@ impl NodeState { } } } - // We send pings regardless of whether we have a RelayUrl. If we were given any - // direct address paths to contact but no RelayUrl, we still need to send a DISCO - // ping to the direct address paths so that the other node will learn about us and - // accepts the connection. - let mut msgs = self.send_pings(now); + let mut msgs = Vec::new(); if let Some(url) = self.relay_url() { debug!(%url, "queue call-me-maybe"); @@ -608,59 +402,6 @@ impl NodeState { msgs } - /// Send DISCO Pings to all the paths of this node. - /// - /// Any paths to the node which have not been recently pinged will be sent a disco - /// ping. - /// - /// The caller is responsible for sending the messages. - #[must_use = "actions must be handled"] - fn send_pings(&mut self, now: Instant) -> Vec { - // We allocate +1 in case the caller wants to add a call-me-maybe message. - let mut ping_msgs = Vec::with_capacity(self.udp_paths.paths.len() + 1); - - if let Some((url, state)) = self.relay_url.as_ref() { - if state.needs_ping(&now) { - debug!(%url, "relay path needs ping"); - if let Some(msg) = - self.start_ping(SendAddr::Relay(url.clone()), DiscoPingPurpose::Discovery) - { - ping_msgs.push(PingAction::SendPing(msg)) - } - } - } - - #[cfg(any(test, feature = "test-utils"))] - if self.path_selection == PathSelection::RelayOnly { - warn!("in `RelayOnly` mode, ignoring request to respond to a hole punching attempt."); - return ping_msgs; - } - - self.prune_direct_addresses(now); - let mut ping_dsts = String::from("["); - self.udp_paths - .paths - .iter() - .filter_map(|(ipp, state)| state.needs_ping(&now).then_some(*ipp)) - .filter_map(|ipp| { - self.start_ping(SendAddr::Udp(ipp.into()), DiscoPingPurpose::Discovery) - }) - .for_each(|msg| { - use std::fmt::Write; - write!(&mut ping_dsts, " {} ", msg.dst).ok(); - ping_msgs.push(PingAction::SendPing(msg)); - }); - ping_dsts.push(']'); - debug!( - %ping_dsts, - dst = %self.node_id.fmt_short(), - paths = %summarize_node_paths(&self.udp_paths.paths), - "sending pings to node", - ); - self.last_full_ping.replace(now); - ping_msgs - } - pub(super) fn update_from_node_addr( &mut self, new_relay_url: Option<&RelayUrl>, @@ -713,114 +454,6 @@ impl NodeState { debug!(new = ?new_addrs , %paths, "added new direct paths for endpoint"); } - /// Handle a received Disco Ping. - /// - /// - Ensures the paths the ping was received on is a known path for this endpoint. - /// - /// - If there is no best_addr for this endpoint yet, sends a ping itself to try and - /// establish one. - /// - /// This is called once we've already verified that we got a valid discovery message - /// from `self` via ep. - pub(super) fn handle_ping( - &mut self, - path: SendAddr, - tx_id: stun_rs::TransactionId, - ) -> PingHandled { - let now = Instant::now(); - - let role = match path { - SendAddr::Udp(addr) => match self.udp_paths.paths.entry(addr.into()) { - Entry::Occupied(mut occupied) => occupied.get_mut().handle_ping(tx_id, now), - Entry::Vacant(vacant) => { - info!(%addr, "new direct addr for node"); - vacant.insert(PathState::with_ping( - self.node_id, - path.clone(), - tx_id, - Source::Udp, - now, - )); - PingRole::NewPath - } - }, - SendAddr::Relay(ref url) => { - match self.relay_url.as_mut() { - Some((home_url, _state)) if home_url != url => { - // either the node changed relays or we didn't have a relay address for the - // node. In both cases, trust the new confirmed url - info!(%url, "new relay addr for node"); - self.relay_url = Some(( - url.clone(), - PathState::with_ping( - self.node_id, - path.clone(), - tx_id, - Source::Relay, - now, - ), - )); - PingRole::NewPath - } - Some((_home_url, state)) => state.handle_ping(tx_id, now), - None => { - info!(%url, "new relay addr for node"); - self.relay_url = Some(( - url.clone(), - PathState::with_ping( - self.node_id, - path.clone(), - tx_id, - Source::Relay, - now, - ), - )); - PingRole::NewPath - } - } - } - }; - event!( - target: "iroh::_events::ping::recv", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - src = ?path, - txn = ?tx_id, - ?role, - ); - - if matches!(path, SendAddr::Udp(_)) && matches!(role, PingRole::NewPath) { - self.prune_direct_addresses(now); - } - - // if the endpoint does not yet have a best_addr - let needs_ping_back = if matches!(path, SendAddr::Udp(_)) - && matches!( - self.udp_paths.best, - UdpSendAddr::None | UdpSendAddr::Unconfirmed(_) | UdpSendAddr::Outdated(_) - ) { - // We also need to send a ping to make this path available to us as well. This - // is always sent together with a pong. So in the worst case the pong gets lost - // and this ping does not. In that case we ping-pong until both sides have - // received at least one pong. Once both sides have received one pong they both - // have a best_addr and this ping will stop being sent. - self.start_ping(path, DiscoPingPurpose::PingBack) - } else { - None - }; - - debug!( - ?role, - needs_ping_back = ?needs_ping_back.is_some(), - paths = %summarize_node_paths(&self.udp_paths.paths), - "endpoint handled ping", - ); - PingHandled { - role, - needs_ping_back, - } - } - /// Prune inactive paths. /// /// This trims the list of inactive paths for an endpoint. At most @@ -873,105 +506,6 @@ impl NodeState { self.udp_paths.update_to_best_addr(now); } - /// Handles a Pong message (a reply to an earlier ping). - /// - /// It reports the address and key that should be inserted for the endpoint if any. - #[instrument(skip(self))] - pub(super) fn handle_pong( - &mut self, - m: &disco::Pong, - src: SendAddr, - ) -> Option<(SocketAddr, PublicKey)> { - event!( - target: "iroh::_events::pong::recv", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - ?src, - txn = ?m.tx_id, - ); - let is_relay = src.is_relay(); - match self.sent_pings.remove(&m.tx_id) { - None => { - // This is not a pong for a ping we sent. In reality however we probably - // did send this ping but it has timed-out by the time we receive this pong - // so we removed the state already. - debug!(tx = %HEXLOWER.encode(&m.tx_id), "received unknown pong (did it timeout?)"); - None - } - Some(sp) => { - let mut node_map_insert = None; - - let now = Instant::now(); - let latency = now - sp.at; - - debug!( - tx = %HEXLOWER.encode(&m.tx_id), - src = %src, - reported_ping_src = %m.ping_observed_addr, - ping_dst = %sp.to, - is_relay = %src.is_relay(), - latency = %latency.as_millis(), - "received pong", - ); - - match src { - SendAddr::Udp(addr) => { - match self.udp_paths.paths.get_mut(&addr.into()) { - None => { - warn!("ignoring pong: no state for src addr"); - // This is no longer an endpoint we care about. - return node_map_insert; - } - Some(st) => { - node_map_insert = Some((addr, self.node_id)); - st.add_pong_reply(PongReply { - latency, - pong_at: now, - from: src, - pong_src: m.ping_observed_addr.clone(), - }); - } - } - debug!( - paths = %summarize_node_paths(&self.udp_paths.paths), - "handled pong", - ); - } - SendAddr::Relay(ref url) => match self.relay_url.as_mut() { - Some((home_url, state)) if home_url == url => { - state.add_pong_reply(PongReply { - latency, - pong_at: now, - from: src, - pong_src: m.ping_observed_addr.clone(), - }); - } - other => { - // if we are here then we sent this ping, but the url changed - // waiting for the response. It was either set to None or changed to - // another relay. This should either never happen or be extremely - // unlikely. Log and ignore for now - warn!( - stored=?other, - received=?url, - "ignoring pong via relay for different relay from last one", - ); - } - }, - } - - // Promote this pong response to our current best address if it's lower latency. - // TODO(bradfitz): decide how latency vs. preference order affects decision - if let SendAddr::Udp(_to) = sp.to { - debug_assert!(!is_relay, "mismatching relay & udp"); - self.udp_paths.update_to_best_addr(now); - } - - node_map_insert - } - } - } - /// Handles a DISCO CallMeMaybe discovery message. /// /// The contract for use of this message is that the node has already pinged to us via @@ -982,7 +516,7 @@ impl NodeState { /// had any [`IpPort`]s to send pings to and our pings might end up blocked. But at /// least open the firewalls on our side, giving the other side another change of making /// it through when it pings in response. - pub(super) fn handle_call_me_maybe(&mut self, m: disco::CallMeMaybe) -> Vec { + pub(super) fn handle_call_me_maybe(&mut self, m: disco::CallMeMaybe) { let now = Instant::now(); let mut call_me_maybe_ipps = BTreeSet::new(); @@ -1035,7 +569,6 @@ impl NodeState { paths = %summarize_node_paths(&self.udp_paths.paths), "updated endpoint paths from call-me-maybe", ); - self.send_pings(now) } /// Marks this node as having received a UDP payload message. @@ -1114,29 +647,6 @@ impl NodeState { return self.send_call_me_maybe(now, SendCallMeMaybe::Always); } - // Send heartbeat ping to keep the current addr going as long as we need it. - if let Some(udp_addr) = self.udp_paths.best.get_addr() { - let elapsed = self.last_ping(&SendAddr::Udp(udp_addr)).map(|l| now - l); - // Send a ping if the last ping is older than 2 seconds. - let needs_ping = match elapsed { - Some(e) => e >= STAYIN_ALIVE_MIN_ELAPSED, - None => false, - }; - - if needs_ping { - debug!( - dst = %udp_addr, - since_last_ping=?elapsed, - "send stayin alive ping", - ); - if let Some(msg) = - self.start_ping(SendAddr::Udp(udp_addr), DiscoPingPurpose::StayinAlive) - { - return vec![PingAction::SendPing(msg)]; - } - } - } - Vec::new() } @@ -1214,26 +724,6 @@ enum SendCallMeMaybe { IfNoRecent, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(super) struct PongReply { - pub(super) latency: Duration, - /// When we received the pong. - pub(super) pong_at: Instant, - /// The pong's src (usually same as endpoint map key). - pub(super) from: SendAddr, - /// What they reported they heard. - pub(super) pong_src: SendAddr, -} - -#[derive(Debug)] -pub(super) struct SentPing { - pub(super) to: SendAddr, - pub(super) at: Instant, - #[allow(dead_code)] - pub(super) purpose: DiscoPingPurpose, - pub(super) _expiry_task: AbortOnDropHandle<()>, -} - /// The reason why a discovery ping message was sent. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DiscoPingPurpose { @@ -1330,7 +820,7 @@ impl From<(RelayUrl, PathState)> for RelayUrlInfo { RelayUrlInfo { relay_url: value.0, last_alive: value.1.last_alive().map(|i| i.elapsed()), - latency: value.1.latency(), + latency: None, } } } @@ -1442,256 +932,262 @@ mod tests { use super::*; use crate::magicsock::node_map::{NodeMap, NodeMapInner}; - #[test] - fn test_remote_infos() { - let now = Instant::now(); - let elapsed = Duration::from_secs(3); - let later = now + elapsed; - let send_addr: RelayUrl = "https://my-relay.com".parse().unwrap(); - let pong_src = SendAddr::Udp("0.0.0.0:1".parse().unwrap()); - let latency = Duration::from_millis(50); - - let relay_and_state = |node_id: NodeId, url: RelayUrl| { - let relay_state = PathState::with_pong_reply( - node_id, - PongReply { - latency, - pong_at: now, - from: SendAddr::Relay(send_addr.clone()), - pong_src: pong_src.clone(), - }, - ); - Some((url, relay_state)) - }; - - // endpoint with a `best_addr` that has a latency but no relay - let (a_endpoint, a_socket_addr) = { - let key = SecretKey::generate(rand::thread_rng()); - let node_id = key.public(); - let ip_port = IpPort { - ip: Ipv4Addr::UNSPECIFIED.into(), - port: 10, - }; - let endpoint_state = BTreeMap::from([( - ip_port, - PathState::with_pong_reply( - node_id, - PongReply { - latency, - pong_at: now, - from: SendAddr::Udp(ip_port.into()), - pong_src: pong_src.clone(), - }, - ), - )]); - ( - NodeState { - id: 0, - quic_mapped_addr: NodeIdMappedAddr::generate(), - node_id: key.public(), - last_full_ping: None, - relay_url: None, - udp_paths: NodeUdpPaths::from_parts( - endpoint_state, - UdpSendAddr::Valid(ip_port.into()), - ), - sent_pings: HashMap::new(), - last_used: Some(now), - last_call_me_maybe: None, - conn_type: Watchable::new(ConnectionType::Direct(ip_port.into())), - has_been_direct: AtomicBool::new(true), - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection::default(), - }, - ip_port.into(), - ) - }; - // endpoint w/ no best addr but a relay w/ latency - let b_endpoint = { - // let socket_addr = "0.0.0.0:9".parse().unwrap(); - let key = SecretKey::generate(rand::thread_rng()); - NodeState { - id: 1, - quic_mapped_addr: NodeIdMappedAddr::generate(), - node_id: key.public(), - last_full_ping: None, - relay_url: relay_and_state(key.public(), send_addr.clone()), - udp_paths: NodeUdpPaths::new(), - sent_pings: HashMap::new(), - last_used: Some(now), - last_call_me_maybe: None, - conn_type: Watchable::new(ConnectionType::Relay(send_addr.clone())), - has_been_direct: AtomicBool::new(false), - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection::default(), - } - }; - - // endpoint w/ no best addr but a relay w/ no latency - let c_endpoint = { - // let socket_addr = "0.0.0.0:8".parse().unwrap(); - let key = SecretKey::generate(rand::thread_rng()); - NodeState { - id: 2, - quic_mapped_addr: NodeIdMappedAddr::generate(), - node_id: key.public(), - last_full_ping: None, - relay_url: Some(( - send_addr.clone(), - PathState::new( - key.public(), - SendAddr::from(send_addr.clone()), - Source::App, - now, - ), - )), - udp_paths: NodeUdpPaths::new(), - sent_pings: HashMap::new(), - last_used: Some(now), - last_call_me_maybe: None, - conn_type: Watchable::new(ConnectionType::Relay(send_addr.clone())), - has_been_direct: AtomicBool::new(false), - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection::default(), - } - }; - - // endpoint w/ expired best addr and relay w/ latency - let (d_endpoint, d_socket_addr) = { - let socket_addr: SocketAddr = "0.0.0.0:7".parse().unwrap(); - let key = SecretKey::generate(rand::thread_rng()); - let node_id = key.public(); - let endpoint_state = BTreeMap::from([( - IpPort::from(socket_addr), - PathState::with_pong_reply( - node_id, - PongReply { - latency, - pong_at: now, - from: SendAddr::Udp(socket_addr), - pong_src: pong_src.clone(), - }, - ), - )]); - ( - NodeState { - id: 3, - quic_mapped_addr: NodeIdMappedAddr::generate(), - node_id: key.public(), - last_full_ping: None, - relay_url: relay_and_state(key.public(), send_addr.clone()), - udp_paths: NodeUdpPaths::from_parts( - endpoint_state, - UdpSendAddr::Outdated(socket_addr), - ), - sent_pings: HashMap::new(), - last_used: Some(now), - last_call_me_maybe: None, - conn_type: Watchable::new(ConnectionType::Mixed( - socket_addr, - send_addr.clone(), - )), - has_been_direct: AtomicBool::new(false), - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection::default(), - }, - socket_addr, - ) - }; - - let mut expect = Vec::from([ - RemoteInfo { - node_id: a_endpoint.node_id, - relay_url: None, - addrs: Vec::from([DirectAddrInfo { - addr: a_socket_addr, - latency: Some(latency), - last_control: Some((elapsed, ControlMsg::Pong)), - last_payload: None, - last_alive: Some(elapsed), - sources: HashMap::new(), - }]), - conn_type: ConnectionType::Direct(a_socket_addr), - latency: Some(latency), - last_used: Some(elapsed), - }, - RemoteInfo { - node_id: b_endpoint.node_id, - relay_url: Some(RelayUrlInfo { - relay_url: b_endpoint.relay_url.as_ref().unwrap().0.clone(), - last_alive: None, - latency: Some(latency), - }), - addrs: Vec::new(), - conn_type: ConnectionType::Relay(send_addr.clone()), - latency: Some(latency), - last_used: Some(elapsed), - }, - RemoteInfo { - node_id: c_endpoint.node_id, - relay_url: Some(RelayUrlInfo { - relay_url: c_endpoint.relay_url.as_ref().unwrap().0.clone(), - last_alive: None, - latency: None, - }), - addrs: Vec::new(), - conn_type: ConnectionType::Relay(send_addr.clone()), - latency: None, - last_used: Some(elapsed), - }, - RemoteInfo { - node_id: d_endpoint.node_id, - relay_url: Some(RelayUrlInfo { - relay_url: d_endpoint.relay_url.as_ref().unwrap().0.clone(), - last_alive: None, - latency: Some(latency), - }), - addrs: Vec::from([DirectAddrInfo { - addr: d_socket_addr, - latency: Some(latency), - last_control: Some((elapsed, ControlMsg::Pong)), - last_payload: None, - last_alive: Some(elapsed), - sources: HashMap::new(), - }]), - conn_type: ConnectionType::Mixed(d_socket_addr, send_addr.clone()), - latency: Some(Duration::from_millis(50)), - last_used: Some(elapsed), - }, - ]); - - let node_map = NodeMap::from_inner(NodeMapInner { - by_node_key: HashMap::from([ - (a_endpoint.node_id, a_endpoint.id), - (b_endpoint.node_id, b_endpoint.id), - (c_endpoint.node_id, c_endpoint.id), - (d_endpoint.node_id, d_endpoint.id), - ]), - by_ip_port: HashMap::from([ - (a_socket_addr.into(), a_endpoint.id), - (d_socket_addr.into(), d_endpoint.id), - ]), - by_quic_mapped_addr: HashMap::from([ - (a_endpoint.quic_mapped_addr, a_endpoint.id), - (b_endpoint.quic_mapped_addr, b_endpoint.id), - (c_endpoint.quic_mapped_addr, c_endpoint.id), - (d_endpoint.quic_mapped_addr, d_endpoint.id), - ]), - by_id: HashMap::from([ - (a_endpoint.id, a_endpoint), - (b_endpoint.id, b_endpoint), - (c_endpoint.id, c_endpoint), - (d_endpoint.id, d_endpoint), - ]), - next_id: 5, - path_selection: PathSelection::default(), - }); - let mut got = node_map.list_remote_infos(later); - got.sort_by_key(|p| p.node_id); - expect.sort_by_key(|p| p.node_id); - remove_non_deterministic_fields(&mut got); - assert_eq!(expect, got); - } + // #[test] + // fn test_remote_infos() { + // let now = Instant::now(); + // let elapsed = Duration::from_secs(3); + // let later = now + elapsed; + // let send_addr: RelayUrl = "https://my-relay.com".parse().unwrap(); + // let pong_src = SendAddr::Udp("0.0.0.0:1".parse().unwrap()); + // let latency = Duration::from_millis(50); + + // let relay_and_state = |node_id: NodeId, url: RelayUrl| { + // let relay_state = PathState::with_pong_reply( + // node_id, + // PongReply { + // latency, + // pong_at: now, + // from: SendAddr::Relay(send_addr.clone()), + // pong_src: pong_src.clone(), + // }, + // ); + // Some((url, relay_state)) + // }; + + // // endpoint with a `best_addr` that has a latency but no relay + // let (a_endpoint, a_socket_addr) = { + // let key = SecretKey::generate(rand::thread_rng()); + // let node_id = key.public(); + // let ip_port = IpPort { + // ip: Ipv4Addr::UNSPECIFIED.into(), + // port: 10, + // }; + // let endpoint_state = BTreeMap::from([( + // ip_port, + // PathState::with_pong_reply( + // node_id, + // PongReply { + // latency, + // pong_at: now, + // from: SendAddr::Udp(ip_port.into()), + // pong_src: pong_src.clone(), + // }, + // ), + // )]); + // ( + // NodeState { + // id: 0, + // quic_mapped_addr: NodeIdMappedAddr::generate(), + // node_id: key.public(), + // last_full_ping: None, + // relay_url: None, + // udp_paths: NodeUdpPaths::from_parts( + // endpoint_state, + // BestAddr::from_parts( + // ip_port.into(), + // latency, + // now, + // now + Duration::from_secs(100), + // ), + // ), + // sent_pings: HashMap::new(), + // last_used: Some(now), + // last_call_me_maybe: None, + // conn_type: Watchable::new(ConnectionType::Direct(ip_port.into())), + // has_been_direct: true, + // #[cfg(any(test, feature = "test-utils"))] + // path_selection: PathSelection::default(), + // }, + // ip_port.into(), + // ) + // }; + // // endpoint w/ no best addr but a relay w/ latency + // let b_endpoint = { + // // let socket_addr = "0.0.0.0:9".parse().unwrap(); + // let key = SecretKey::generate(rand::thread_rng()); + // NodeState { + // id: 1, + // quic_mapped_addr: NodeIdMappedAddr::generate(), + // node_id: key.public(), + // last_full_ping: None, + // relay_url: relay_and_state(key.public(), send_addr.clone()), + // udp_paths: NodeUdpPaths::new(), + // sent_pings: HashMap::new(), + // last_used: Some(now), + // last_call_me_maybe: None, + // conn_type: Watchable::new(ConnectionType::Relay(send_addr.clone())), + // has_been_direct: false, + // #[cfg(any(test, feature = "test-utils"))] + // path_selection: PathSelection::default(), + // } + // }; + + // // endpoint w/ no best addr but a relay w/ no latency + // let c_endpoint = { + // // let socket_addr = "0.0.0.0:8".parse().unwrap(); + // let key = SecretKey::generate(rand::thread_rng()); + // NodeState { + // id: 2, + // quic_mapped_addr: NodeIdMappedAddr::generate(), + // node_id: key.public(), + // last_full_ping: None, + // relay_url: Some(( + // send_addr.clone(), + // PathState::new( + // key.public(), + // SendAddr::from(send_addr.clone()), + // Source::App, + // now, + // ), + // )), + // udp_paths: NodeUdpPaths::new(), + // sent_pings: HashMap::new(), + // last_used: Some(now), + // last_call_me_maybe: None, + // conn_type: Watchable::new(ConnectionType::Relay(send_addr.clone())), + // has_been_direct: false, + // #[cfg(any(test, feature = "test-utils"))] + // path_selection: PathSelection::default(), + // } + // }; + + // // endpoint w/ expired best addr and relay w/ latency + // let (d_endpoint, d_socket_addr) = { + // let socket_addr: SocketAddr = "0.0.0.0:7".parse().unwrap(); + // let expired = now.checked_sub(Duration::from_secs(100)).unwrap(); + // let key = SecretKey::generate(rand::thread_rng()); + // let node_id = key.public(); + // let endpoint_state = BTreeMap::from([( + // IpPort::from(socket_addr), + // PathState::with_pong_reply( + // node_id, + // PongReply { + // latency, + // pong_at: now, + // from: SendAddr::Udp(socket_addr), + // pong_src: pong_src.clone(), + // }, + // ), + // )]); + // ( + // NodeState { + // id: 3, + // quic_mapped_addr: NodeIdMappedAddr::generate(), + // node_id: key.public(), + // last_full_ping: None, + // relay_url: relay_and_state(key.public(), send_addr.clone()), + // udp_paths: NodeUdpPaths::from_parts( + // endpoint_state, + // BestAddr::from_parts(socket_addr, Duration::from_millis(80), now, expired), + // ), + // sent_pings: HashMap::new(), + // last_used: Some(now), + // last_call_me_maybe: None, + // conn_type: Watchable::new(ConnectionType::Mixed( + // socket_addr, + // send_addr.clone(), + // )), + // has_been_direct: false, + // #[cfg(any(test, feature = "test-utils"))] + // path_selection: PathSelection::default(), + // }, + // socket_addr, + // ) + // }; + + // let mut expect = Vec::from([ + // RemoteInfo { + // node_id: a_endpoint.node_id, + // relay_url: None, + // addrs: Vec::from([DirectAddrInfo { + // addr: a_socket_addr, + // latency: Some(latency), + // last_control: Some((elapsed, ControlMsg::Pong)), + // last_payload: None, + // last_alive: Some(elapsed), + // sources: HashMap::new(), + // }]), + // conn_type: ConnectionType::Direct(a_socket_addr), + // latency: Some(latency), + // last_used: Some(elapsed), + // }, + // RemoteInfo { + // node_id: b_endpoint.node_id, + // relay_url: Some(RelayUrlInfo { + // relay_url: b_endpoint.relay_url.as_ref().unwrap().0.clone(), + // last_alive: None, + // latency: Some(latency), + // }), + // addrs: Vec::new(), + // conn_type: ConnectionType::Relay(send_addr.clone()), + // latency: Some(latency), + // last_used: Some(elapsed), + // }, + // RemoteInfo { + // node_id: c_endpoint.node_id, + // relay_url: Some(RelayUrlInfo { + // relay_url: c_endpoint.relay_url.as_ref().unwrap().0.clone(), + // last_alive: None, + // latency: None, + // }), + // addrs: Vec::new(), + // conn_type: ConnectionType::Relay(send_addr.clone()), + // latency: None, + // last_used: Some(elapsed), + // }, + // RemoteInfo { + // node_id: d_endpoint.node_id, + // relay_url: Some(RelayUrlInfo { + // relay_url: d_endpoint.relay_url.as_ref().unwrap().0.clone(), + // last_alive: None, + // latency: Some(latency), + // }), + // addrs: Vec::from([DirectAddrInfo { + // addr: d_socket_addr, + // latency: Some(latency), + // last_control: Some((elapsed, ControlMsg::Pong)), + // last_payload: None, + // last_alive: Some(elapsed), + // sources: HashMap::new(), + // }]), + // conn_type: ConnectionType::Mixed(d_socket_addr, send_addr.clone()), + // latency: Some(Duration::from_millis(50)), + // last_used: Some(elapsed), + // }, + // ]); + + // let node_map = NodeMap::from_inner(NodeMapInner { + // by_node_key: HashMap::from([ + // (a_endpoint.node_id, a_endpoint.id), + // (b_endpoint.node_id, b_endpoint.id), + // (c_endpoint.node_id, c_endpoint.id), + // (d_endpoint.node_id, d_endpoint.id), + // ]), + // by_ip_port: HashMap::from([ + // (a_socket_addr.into(), a_endpoint.id), + // (d_socket_addr.into(), d_endpoint.id), + // ]), + // by_quic_mapped_addr: HashMap::from([ + // (a_endpoint.quic_mapped_addr, a_endpoint.id), + // (b_endpoint.quic_mapped_addr, b_endpoint.id), + // (c_endpoint.quic_mapped_addr, c_endpoint.id), + // (d_endpoint.quic_mapped_addr, d_endpoint.id), + // ]), + // by_id: HashMap::from([ + // (a_endpoint.id, a_endpoint), + // (b_endpoint.id, b_endpoint), + // (c_endpoint.id, c_endpoint), + // (d_endpoint.id, d_endpoint), + // ]), + // next_id: 5, + // path_selection: PathSelection::default(), + // }); + // let mut got = node_map.list_remote_infos(later); + // got.sort_by_key(|p| p.node_id); + // expect.sort_by_key(|p| p.node_id); + // remove_non_deterministic_fields(&mut got); + // assert_eq!(expect, got); + // } fn remove_non_deterministic_fields(infos: &mut [RemoteInfo]) { for info in infos.iter_mut() { @@ -1724,10 +1220,6 @@ mod tests { .collect(); let call_me_maybe = disco::CallMeMaybe { my_numbers }; - let ping_messages = ep.handle_call_me_maybe(call_me_maybe); - - // We have no relay server and no previous direct addresses, so we should get the same - // number of pings as direct addresses in the call-me-maybe. - assert_eq!(ping_messages.len(), my_numbers_count as usize); + ep.handle_call_me_maybe(call_me_maybe); } } diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index b5047129278..9277d0615ff 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -4,11 +4,10 @@ use std::collections::{BTreeMap, HashMap}; use iroh_base::NodeId; use n0_future::time::{Duration, Instant}; -use tracing::{Level, debug, event}; use super::{ - IpPort, PingRole, Source, - node_state::{ControlMsg, PongReply, SESSION_ACTIVE_TIMEOUT}, + IpPort, Source, + node_state::{ControlMsg, SESSION_ACTIVE_TIMEOUT}, }; use crate::{ disco::SendAddr, @@ -52,6 +51,7 @@ pub(super) struct PathState { /// /// See [`PathValidity`] docs. pub(super) validity: PathValidity, + /// When the last payload data was **received** via this path. /// /// This excludes DISCO messages. @@ -100,54 +100,6 @@ impl PathState { } } - pub(super) fn with_ping( - node_id: NodeId, - path: SendAddr, - tx_id: stun_rs::TransactionId, - source: Source, - now: Instant, - ) -> Self { - let mut new = PathState::new(node_id, path, source, now); - new.handle_ping(tx_id, now); - new - } - - pub(super) fn add_pong_reply(&mut self, r: PongReply) { - if let SendAddr::Udp(ref path) = self.path { - if self.validity.is_empty() { - event!( - target: "iroh::_events::holepunched", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - path = ?path, - direction = "outgoing", - ); - } - } - - self.validity = PathValidity::new(r.pong_at, r.latency); - } - - pub(super) fn receive_payload(&mut self, now: Instant) { - self.last_payload_msg = Some(now); - self.validity - .receive_payload(now, path_validity::Source::QuicPayload); - } - - #[cfg(test)] - pub(super) fn with_pong_reply(node_id: NodeId, r: PongReply) -> Self { - PathState { - node_id, - path: r.from.clone(), - last_ping: None, - last_got_ping: None, - call_me_maybe_time: None, - validity: PathValidity::new(r.pong_at, r.latency), - last_payload_msg: None, - sources: HashMap::new(), - } - } - /// Check whether this path is considered active. /// /// Active means the path has received payload messages within the last @@ -167,6 +119,12 @@ impl PathState { self.last_got_ping.as_ref().map(|(time, _tx_id)| time) } + pub(super) fn receive_payload(&mut self, now: Instant) { + self.last_payload_msg = Some(now); + self.validity + .receive_payload(now, path_validity::Source::QuicPayload); + } + /// Reports the last instant this path was considered alive. /// /// Alive means the path is considered in use by the remote endpoint. Either because we @@ -198,10 +156,6 @@ impl PathState { /// Returns the time elapsed since the last control message, and the type of control message. pub(super) fn last_control_msg(&self, now: Instant) -> Option<(Duration, ControlMsg)> { // get every control message and assign it its kind - let last_pong = self - .validity - .latest_pong() - .map(|pong_at| (pong_at, ControlMsg::Pong)); let last_call_me_maybe = self .call_me_maybe_time .as_ref() @@ -210,9 +164,8 @@ impl PathState { .last_incoming_ping() .map(|ping| (*ping, ControlMsg::Ping)); - last_pong + last_call_me_maybe .into_iter() - .chain(last_call_me_maybe) .chain(last_ping) .max_by_key(|(instant, _kind)| *instant) .map(|(instant, kind)| (now.duration_since(instant), kind)) @@ -240,39 +193,6 @@ impl PathState { } } - pub(super) fn handle_ping(&mut self, tx_id: stun_rs::TransactionId, now: Instant) -> PingRole { - if Some(&tx_id) == self.last_got_ping.as_ref().map(|(_t, tx_id)| tx_id) { - PingRole::Duplicate - } else { - let prev = self.last_got_ping.replace((now, tx_id)); - let heartbeat_deadline = HEARTBEAT_INTERVAL + (HEARTBEAT_INTERVAL / 2); - match prev { - Some((prev_time, _tx)) if now.duration_since(prev_time) <= heartbeat_deadline => { - PingRole::LikelyHeartbeat - } - Some((prev_time, _tx)) => { - debug!( - elapsed = ?now.duration_since(prev_time), - "heartbeat missed, reactivating", - ); - PingRole::Activate - } - None => { - if let SendAddr::Udp(ref addr) = self.path { - event!( - target: "iroh::_events::holepunched", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - path = ?addr, - direction = "incoming", - ); - } - PingRole::Activate - } - } - } - } - pub(super) fn add_source(&mut self, source: Source, now: Instant) { self.sources.insert(source, now); } diff --git a/iroh/src/magicsock/node_map/udp_paths.rs b/iroh/src/magicsock/node_map/udp_paths.rs index 29c43ccc99d..475f865e59b 100644 --- a/iroh/src/magicsock/node_map/udp_paths.rs +++ b/iroh/src/magicsock/node_map/udp_paths.rs @@ -7,7 +7,8 @@ //! [`NodeState`]: super::node_state::NodeState use std::{collections::BTreeMap, net::SocketAddr}; -use n0_future::time::Instant; +use n0_future::time::{Duration, Instant}; +use rand::seq::IteratorRandom; use tracing::{Level, event}; use super::{IpPort, path_state::PathState}; From c4baca831ace378aec27f3a55bb688cde0061802 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 11 Jul 2025 14:01:32 +0200 Subject: [PATCH 009/164] start tracking path events --- Cargo.lock | 45 +++++-------------- Cargo.toml | 6 +-- iroh/src/endpoint.rs | 13 ++++-- iroh/src/magicsock.rs | 24 +++++++++- iroh/src/magicsock/node_map/node_state.rs | 16 ------- iroh/src/magicsock/node_map/path_state.rs | 55 +---------------------- 6 files changed, 47 insertions(+), 112 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3a6558f70f..aeb847038a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2050,7 +2050,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.59.0", + "windows-core 0.61.2", ] [[package]] @@ -2489,7 +2489,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#0dc50edf689ee5c6cf21b4ee5c0fea6af548680d" dependencies = [ "bytes", "cfg_aliases", @@ -2508,7 +2508,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e84f16856ea3ac79ed6c206b258d33a30d87834f" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#0dc50edf689ee5c6cf21b4ee5c0fea6af548680d" dependencies = [ "bytes", "fastbloom", @@ -2544,7 +2544,7 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#140fdf97bfb706b1cac591b9711818c5df8012f4" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#79e3fcc710de68b40fd05be5421048bab658ddf4" dependencies = [ "cfg_aliases", "libc", @@ -5113,9 +5113,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" dependencies = [ "indexmap", "serde", @@ -5773,19 +5773,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" -dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.1", - "windows-result 0.3.4", - "windows-strings 0.3.1", - "windows-targets 0.53.3", -] - [[package]] name = "windows-core" version = "0.61.2" @@ -5821,17 +5808,6 @@ dependencies = [ "syn 2.0.101", ] -[[package]] -name = "windows-implement" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "windows-implement" version = "0.60.0" @@ -5889,7 +5865,7 @@ checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" dependencies = [ "windows-result 0.3.4", "windows-strings 0.3.1", - "windows-targets 0.53.3", + "windows-targets 0.53.2", ] [[package]] @@ -5980,7 +5956,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.2", ] [[package]] @@ -6031,11 +6007,10 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ - "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/Cargo.toml b/Cargo.toml index edb51ff85b4..eb098793da4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,6 @@ netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-mu # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } -# iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } -# iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } -# iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "multipath-misc" } +# iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "flub/quinn-path-events-status" } +# iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "flub/quinn-path-events-status" } +# iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "flub/quinn-path-events-status" } diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 3e1a7e90e6b..9d107511027 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1771,13 +1771,20 @@ impl Future for Connecting { let conn = Connection { inner }; // Grab the remote identity and register this connection - if let Some(remote) = *this.remote_node_id { let weak_handle = conn.inner.weak_handle(); - this.ep.msock.register_connection(remote, weak_handle); + let path_events = conn.inner.path_events(); + this.ep + .msock + .register_connection(remote, weak_handle, path_events); } else if let Ok(remote) = conn.remote_node_id() { let weak_handle = conn.inner.weak_handle(); - this.ep.msock.register_connection(remote, weak_handle); + let path_events = conn.inner.path_events(); + this.ep + .msock + .register_connection(remote, weak_handle, path_events); + } else { + warn!("unable to determine node id for the remote"); } try_send_rtt_msg(&conn, this.ep, *this.remote_node_id); diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 892000b2d3b..3067730de6f 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -43,6 +43,7 @@ use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; use quinn::{AsyncUdpSocket, ServerConfig, WeakConnectionHandle}; +use quinn_proto::PathEvent; use rand::Rng; use smallvec::SmallVec; use snafu::{ResultExt, Snafu}; @@ -289,8 +290,29 @@ impl MagicSock { self.local_addrs_watch.clone().get() } - pub(crate) fn register_connection(&self, remote: NodeId, conn: WeakConnectionHandle) { + pub(crate) fn register_connection( + &self, + remote: NodeId, + conn: WeakConnectionHandle, + mut path_events: tokio::sync::broadcast::Receiver, + ) { self.connection_map.insert(remote, conn); + + // TODO: track task + // TODO: find a good home for this + task::spawn(async move { + loop { + match path_events.recv().await { + Ok(event) => { + info!(remote = %remote, "path event: {:?}", event); + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + warn!("lagged path events"); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, + } + } + }); } #[cfg(not(wasm_browser))] diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 26a4a6d63a5..96162223663 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -549,7 +549,6 @@ impl NodeState { // it's been less than 5 seconds ago. Also clear pongs for direct addresses not // included in the updated set. for (ipp, st) in self.udp_paths.paths.iter_mut() { - st.last_ping = None; if !call_me_maybe_ipps.contains(ipp) { // TODO: This seems like a weird way to signal that the endpoint no longer // thinks it has this IpPort as an available path. @@ -607,21 +606,6 @@ impl NodeState { self.last_used = Some(now); } - pub(super) fn last_ping(&self, addr: &SendAddr) -> Option { - match addr { - SendAddr::Udp(addr) => self - .udp_paths - .paths - .get(&(*addr).into()) - .and_then(|ep| ep.last_ping), - SendAddr::Relay(url) => self - .relay_url - .as_ref() - .filter(|(home_url, _state)| home_url == url) - .and_then(|(_home_url, state)| state.last_ping), - } - } - /// Checks if this `Endpoint` is currently actively being used. pub(super) fn is_active(&self, now: &Instant) -> bool { match self.last_used { diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index 9277d0615ff..ce3121539b0 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -17,12 +17,6 @@ use crate::{ }, }; -/// The minimum time between pings to an endpoint. -/// -/// Except in the case of CallMeMaybe frames resetting the counter, as the first pings -/// likely didn't through the firewall. -const DISCO_PING_INTERVAL: Duration = Duration::from_secs(5); - /// State about a particular path to another [`NodeState`]. /// /// This state is used for both the relay path and any direct UDP paths. @@ -34,13 +28,6 @@ pub(super) struct PathState { node_id: NodeId, /// The path this applies for. path: SendAddr, - /// The last (outgoing) ping time. - pub(super) last_ping: Option, - - /// If non-zero, means that this was an endpoint that we learned about at runtime (from an - /// incoming ping). If so, we keep the time updated and use it to discard old candidates. - // NOTE: tx_id Originally added in tailscale due to . - last_got_ping: Option<(Instant, stun_rs::TransactionId)>, /// The time this endpoint was last advertised via a call-me-maybe DISCO message. pub(super) call_me_maybe_time: Option, @@ -71,8 +58,6 @@ impl PathState { Self { node_id, path, - last_ping: None, - last_got_ping: None, call_me_maybe_time: None, validity: PathValidity::empty(), last_payload_msg: None, @@ -91,8 +76,6 @@ impl PathState { PathState { node_id, path, - last_ping: None, - last_got_ping: None, call_me_maybe_time: None, validity: PathValidity::empty(), last_payload_msg: Some(now), @@ -114,17 +97,11 @@ impl PathState { .unwrap_or(false) } - /// Returns the instant the last incoming ping was received. - pub(super) fn last_incoming_ping(&self) -> Option<&Instant> { - self.last_got_ping.as_ref().map(|(time, _tx_id)| time) - } - pub(super) fn receive_payload(&mut self, now: Instant) { self.last_payload_msg = Some(now); self.validity .receive_payload(now, path_validity::Source::QuicPayload); } - /// Reports the last instant this path was considered alive. /// /// Alive means the path is considered in use by the remote endpoint. Either because we @@ -142,14 +119,12 @@ impl PathState { .into_iter() .chain(self.last_payload_msg) .chain(self.call_me_maybe_time) - .chain(self.last_incoming_ping().cloned()) .max() } /// The last control or DISCO message **about** this path. /// /// This is the most recent instant among: - /// - when last pong was received. /// - when this path was last advertised in a received CallMeMaybe message. /// - when the last ping from them was received. /// @@ -160,13 +135,9 @@ impl PathState { .call_me_maybe_time .as_ref() .map(|call_me| (*call_me, ControlMsg::CallMeMaybe)); - let last_ping = self - .last_incoming_ping() - .map(|ping| (*ping, ControlMsg::Ping)); last_call_me_maybe .into_iter() - .chain(last_ping) .max_by_key(|(instant, _kind)| *instant) .map(|(instant, kind)| (now.duration_since(instant), kind)) } @@ -176,30 +147,11 @@ impl PathState { self.validity.latency() } - pub(super) fn needs_ping(&self, now: &Instant) -> bool { - match self.last_ping { - None => true, - Some(last_ping) => { - let elapsed = now.duration_since(last_ping); - - // TODO: remove! - // This logs "ping is too new" for each send whenever the endpoint does *not* need - // a ping. Pretty sure this is not a useful log, but maybe there was a reason? - // if !needs_ping { - // debug!("ping is too new: {}ms", elapsed.as_millis()); - // } - elapsed > DISCO_PING_INTERVAL - } - } - } - pub(super) fn add_source(&mut self, source: Source, now: Instant) { self.sources.insert(source, now); } pub(super) fn clear(&mut self) { - self.last_ping = None; - self.last_got_ping = None; self.call_me_maybe_time = None; self.validity = PathValidity::empty(); } @@ -212,12 +164,7 @@ impl PathState { if let Some(pong_at) = self.validity.latest_pong() { write!(w, "pong-received({:?} ago) ", pong_at.elapsed())?; } - if let Some(when) = self.last_incoming_ping() { - write!(w, "ping-received({:?} ago) ", when.elapsed())?; - } - if let Some(ref when) = self.last_ping { - write!(w, "ping-sent({:?} ago) ", when.elapsed())?; - } + if let Some(last_source) = self.sources.iter().max_by_key(|&(_, instant)| instant) { write!( w, From 549adee0491610dc2ed74a1c27b09a5d2fa87eea Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Sat, 12 Jul 2025 15:42:27 +0200 Subject: [PATCH 010/164] start figuring out more details --- iroh/src/endpoint.rs | 46 +++++++++----- iroh/src/magicsock.rs | 112 ++++++++++++++++++++++++--------- iroh/src/magicsock/node_map.rs | 9 +++ 3 files changed, 125 insertions(+), 42 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 9d107511027..760673ad12f 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -735,14 +735,13 @@ impl Endpoint { self.add_node_addr(node_addr.clone())?; } let node_id = node_addr.node_id; - let direct_addresses = node_addr.direct_addresses.clone(); let relay_url = node_addr.relay_url.clone(); // Get the mapped IPv6 address from the magic socket. Quinn will connect to this // address. Start discovery for this node if it's enabled and we have no valid or // verified address information for this node. Dropping the discovery cancels any // still running task. - let (mapped_addr, _discovery_drop_guard) = self + let (mapped_addr, direct_addresses, _discovery_drop_guard) = self .get_mapping_addr_and_maybe_start_discovery(node_addr) .await .context(NoAddressSnafu)?; @@ -773,7 +772,11 @@ impl Endpoint { }; // TODO: race available addresses, this is currently only using the relay addr to connect - let dest_addr = mapped_addr.private_socket_addr(); + let dest_addr = if relay_url.is_none() && !direct_addresses.is_empty() { + direct_addresses[0] + } else { + mapped_addr.private_socket_addr() + }; let server_name = &tls::name::encode(node_id); let connect = self .msock @@ -781,10 +784,14 @@ impl Endpoint { .connect_with(client_config, dest_addr, server_name) .context(QuinnSnafu)?; + let mut paths = direct_addresses; + paths.push(mapped_addr.private_socket_addr()); + Ok(Connecting { inner: connect, ep: self.clone(), remote_node_id: Some(node_id), + paths, _discovery_drop_guard, }) } @@ -1379,18 +1386,20 @@ impl Endpoint { async fn get_mapping_addr_and_maybe_start_discovery( &self, node_addr: NodeAddr, - ) -> Result<(NodeIdMappedAddr, Option), GetMappingAddressError> { + ) -> Result<(NodeIdMappedAddr, Vec, Option), GetMappingAddressError> + { let node_id = node_addr.node_id; // Only return a mapped addr if we have some way of dialing this node, in other // words, we have either a relay URL or at least one direct address. let addr = if self.msock.has_send_address(node_id) { - self.msock.get_mapping_addr(node_id) + let maddr = self.msock.get_mapping_addr(node_id); + maddr.map(|maddr| (maddr, self.msock.get_direct_addrs(node_id))) } else { None }; match addr { - Some(addr) => { + Some((maddr, direct)) => { // We have some way of dialing this node, but that doesn't actually mean // we can actually connect to any of these addresses. // Therefore, we will invoke the discovery service if we haven't received from the @@ -1402,7 +1411,7 @@ impl Endpoint { let discovery = DiscoveryTask::maybe_start_after_delay(self, node_id, delay) .ok() .flatten(); - Ok((addr, discovery)) + Ok((maddr, direct, discovery)) } None => { @@ -1417,7 +1426,8 @@ impl Endpoint { .await .context(get_mapping_address_error::DiscoverSnafu)?; if let Some(addr) = self.msock.get_mapping_addr(node_id) { - Ok((addr, Some(discovery))) + let direct = self.msock.get_direct_addrs(node_id); + Ok((addr, direct, Some(discovery))) } else { Err(get_mapping_address_error::NoAddressSnafu.build()) } @@ -1646,6 +1656,8 @@ pub struct Connecting { inner: quinn::Connecting, ep: Endpoint, remote_node_id: Option, + /// Additional paths to open once a connection is created + paths: Vec, /// We run discovery as long as we haven't established a connection yet. #[debug("Option")] _discovery_drop_guard: Option, @@ -1774,15 +1786,21 @@ impl Future for Connecting { if let Some(remote) = *this.remote_node_id { let weak_handle = conn.inner.weak_handle(); let path_events = conn.inner.path_events(); - this.ep - .msock - .register_connection(remote, weak_handle, path_events); + this.ep.msock.register_connection( + remote, + weak_handle, + path_events, + this.paths.clone(), + ); } else if let Ok(remote) = conn.remote_node_id() { let weak_handle = conn.inner.weak_handle(); let path_events = conn.inner.path_events(); - this.ep - .msock - .register_connection(remote, weak_handle, path_events); + this.ep.msock.register_connection( + remote, + weak_handle, + path_events, + this.paths.clone(), + ); } else { warn!("unable to determine node id for the remote"); } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 3067730de6f..af9f8501ee9 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -295,8 +295,24 @@ impl MagicSock { remote: NodeId, conn: WeakConnectionHandle, mut path_events: tokio::sync::broadcast::Receiver, + paths: Vec, ) { self.connection_map.insert(remote, conn); + task::spawn(async move { + let conn = conn.clone(); + for addr in paths { + match conn.open_path(addr, quinn_proto::PathStatus::Backup).await { + Ok(path) => { + path.set_max_idle_timeout(Some(ENDPOINTS_FRESH_ENOUGH_DURATION)) + .ok(); + path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); + } + Err(err) => { + warn!("failed to open path {:?}", err); + } + } + } + }); // TODO: track task // TODO: find a good home for this @@ -422,6 +438,10 @@ impl MagicSock { self.node_map.get_quic_mapped_addr_for_node_key(node_id) } + pub(crate) fn get_direct_addrs(&self, node_id: NodeId) -> Vec { + self.node_map.get_direct_addrs(node_id) + } + /// Add addresses for a node to the magic socket's addresbook. #[instrument(skip_all)] pub fn add_node_addr( @@ -1055,6 +1075,40 @@ impl MagicSock { } } +/// Definies the translation of addresses in quinn land vs iroh land. +/// +/// This is necessary, because quinn can only reason about `SocketAddr`s. +#[derive(Clone, Debug)] +pub(crate) enum MultipathMappedAddr { + /// Used for the initial connection. + /// - Only used for sending + /// - This means send on all known paths/transports + Mixed(NodeIdMappedAddr), + /// Relay based transport address + Relay(IpMappedAddr), // TODO: RelayMappedAddr? + /// IP based transport address + #[cfg(not(wasm_browser))] + Ip(SocketAddr), +} + +impl From for MultipathMappedAddr { + fn from(value: SocketAddr) -> Self { + match value.ip() { + IpAddr::V4(_) => Self::Ip(value), + IpAddr::V6(addr) => { + if let Ok(node_id_mapped_addr) = NodeIdMappedAddr::try_from(addr) { + return Self::Mixed(node_id_mapped_addr); + } + #[cfg(not(wasm_browser))] + if let Ok(ip_mapped_addr) = IpMappedAddr::try_from(addr) { + return Self::Relay(ip_mapped_addr); + } + MappedAddr::Self(value) + } + } + } +} + #[derive(Clone, Debug)] enum MappedAddr { NodeId(NodeIdMappedAddr), @@ -3188,17 +3242,18 @@ mod tests { let _accept_task = AbortOnDropHandle::new(accept_task); // Add an empty entry in the NodeMap of ep_1 - msock_1.node_map.add_node_addr( - NodeAddr { - node_id: node_id_2, - relay_url: None, - direct_addresses: Default::default(), - }, - Source::NamedApp { - name: "test".into(), - }, - &msock_1.metrics.magicsock, - ); + msock_1 + .add_node_addr( + NodeAddr { + node_id: node_id_2, + relay_url: None, + direct_addresses: Default::default(), + }, + Source::NamedApp { + name: "test".into(), + }, + ) + .unwrap(); let addr_2 = msock_1.get_mapping_addr(node_id_2).unwrap(); // Set a low max_idle_timeout so quinn gives up on this quickly and our test does @@ -3225,23 +3280,24 @@ mod tests { info!("first connect timed out as expected"); // Provide correct addressing information - msock_1.node_map.add_node_addr( - NodeAddr { - node_id: node_id_2, - relay_url: None, - direct_addresses: msock_2 - .direct_addresses() - .initialized() - .await - .into_iter() - .map(|x| x.addr) - .collect(), - }, - Source::NamedApp { - name: "test".into(), - }, - &msock_1.metrics.magicsock, - ); + msock_1 + .add_node_addr( + NodeAddr { + node_id: node_id_2, + relay_url: None, + direct_addresses: msock_2 + .direct_addresses() + .initialized() + .await + .into_iter() + .map(|x| x.addr) + .collect(), + }, + Source::NamedApp { + name: "test".into(), + }, + ) + .unwrap(); // We can now connect tokio::time::timeout(Duration::from_secs(10), async move { diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 11b135046e3..4eb8e3df716 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -177,6 +177,15 @@ impl NodeMap { .map(|ep| *ep.quic_mapped_addr()) } + pub(super) fn get_direct_addrs(&self, node_key: NodeId) -> Vec { + self.inner + .lock() + .expect("poisoned") + .get(NodeStateKey::NodeId(node_key)) + .map(|ep| ep.direct_addresses().map(Into::into).collect()) + .unwrap_or_default() + } + pub(super) fn handle_call_me_maybe( &self, sender: PublicKey, From 9ef5765db433a149c10fe84fc1d069e4f1cf3d96 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 14 Jul 2025 12:27:01 +0200 Subject: [PATCH 011/164] wip --- iroh/src/endpoint.rs | 60 ++++++------------- iroh/src/magicsock.rs | 74 +++++++----------------- iroh/src/magicsock/relay_mapped_addrs.rs | 54 +++++++++++++++++ iroh/src/magicsock/transports/relay.rs | 2 +- iroh/src/net_report/ip_mapped_addrs.rs | 2 +- 5 files changed, 96 insertions(+), 96 deletions(-) create mode 100644 iroh/src/magicsock/relay_mapped_addrs.rs diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 760673ad12f..fdde0720611 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -34,9 +34,8 @@ use url::Url; #[cfg(wasm_browser)] use crate::discovery::pkarr::PkarrResolver; -#[cfg(not(wasm_browser))] -use crate::{discovery::dns::DnsDiscovery, dns::DnsResolver}; use crate::{ + RelayProtocol, discovery::{ ConcurrentDiscovery, Discovery, DiscoveryContext, DiscoveryError, DiscoveryItem, DiscoverySubscribers, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, IntoDiscoveryError, @@ -47,6 +46,8 @@ use crate::{ net_report::Report, tls, }; +#[cfg(not(wasm_browser))] +use crate::{discovery::dns::DnsDiscovery, dns::DnsResolver}; mod rtt_actor; @@ -735,13 +736,12 @@ impl Endpoint { self.add_node_addr(node_addr.clone())?; } let node_id = node_addr.node_id; - let relay_url = node_addr.relay_url.clone(); // Get the mapped IPv6 address from the magic socket. Quinn will connect to this // address. Start discovery for this node if it's enabled and we have no valid or // verified address information for this node. Dropping the discovery cancels any // still running task. - let (mapped_addr, direct_addresses, _discovery_drop_guard) = self + let (mapped_addr, _discovery_drop_guard) = self .get_mapping_addr_and_maybe_start_discovery(node_addr) .await .context(NoAddressSnafu)?; @@ -753,12 +753,7 @@ impl Endpoint { // Start connecting via quinn. This will time out after 10 seconds if no reachable // address is available. - debug!( - ?mapped_addr, - ?direct_addresses, - ?relay_url, - "Attempting connection..." - ); + debug!(?mapped_addr, "Attempting connection..."); let client_config = { let mut alpn_protocols = vec![alpn.to_vec()]; alpn_protocols.extend(options.additional_alpns); @@ -771,12 +766,7 @@ impl Endpoint { client_config }; - // TODO: race available addresses, this is currently only using the relay addr to connect - let dest_addr = if relay_url.is_none() && !direct_addresses.is_empty() { - direct_addresses[0] - } else { - mapped_addr.private_socket_addr() - }; + let dest_addr = mapped_addr.private_socket_addr(); let server_name = &tls::name::encode(node_id); let connect = self .msock @@ -784,14 +774,10 @@ impl Endpoint { .connect_with(client_config, dest_addr, server_name) .context(QuinnSnafu)?; - let mut paths = direct_addresses; - paths.push(mapped_addr.private_socket_addr()); - Ok(Connecting { inner: connect, ep: self.clone(), remote_node_id: Some(node_id), - paths, _discovery_drop_guard, }) } @@ -1386,20 +1372,18 @@ impl Endpoint { async fn get_mapping_addr_and_maybe_start_discovery( &self, node_addr: NodeAddr, - ) -> Result<(NodeIdMappedAddr, Vec, Option), GetMappingAddressError> - { + ) -> Result<(NodeIdMappedAddr, Option), GetMappingAddressError> { let node_id = node_addr.node_id; // Only return a mapped addr if we have some way of dialing this node, in other // words, we have either a relay URL or at least one direct address. let addr = if self.msock.has_send_address(node_id) { - let maddr = self.msock.get_mapping_addr(node_id); - maddr.map(|maddr| (maddr, self.msock.get_direct_addrs(node_id))) + self.msock.get_mapping_addr(node_id) } else { None }; match addr { - Some((maddr, direct)) => { + Some(maddr) => { // We have some way of dialing this node, but that doesn't actually mean // we can actually connect to any of these addresses. // Therefore, we will invoke the discovery service if we haven't received from the @@ -1411,7 +1395,7 @@ impl Endpoint { let discovery = DiscoveryTask::maybe_start_after_delay(self, node_id, delay) .ok() .flatten(); - Ok((maddr, direct, discovery)) + Ok((maddr, discovery)) } None => { @@ -1426,8 +1410,7 @@ impl Endpoint { .await .context(get_mapping_address_error::DiscoverSnafu)?; if let Some(addr) = self.msock.get_mapping_addr(node_id) { - let direct = self.msock.get_direct_addrs(node_id); - Ok((addr, direct, Some(discovery))) + Ok((addr, Some(discovery))) } else { Err(get_mapping_address_error::NoAddressSnafu.build()) } @@ -1656,8 +1639,6 @@ pub struct Connecting { inner: quinn::Connecting, ep: Endpoint, remote_node_id: Option, - /// Additional paths to open once a connection is created - paths: Vec, /// We run discovery as long as we haven't established a connection yet. #[debug("Option")] _discovery_drop_guard: Option, @@ -1786,21 +1767,15 @@ impl Future for Connecting { if let Some(remote) = *this.remote_node_id { let weak_handle = conn.inner.weak_handle(); let path_events = conn.inner.path_events(); - this.ep.msock.register_connection( - remote, - weak_handle, - path_events, - this.paths.clone(), - ); + this.ep + .msock + .register_connection(remote, weak_handle, path_events); } else if let Ok(remote) = conn.remote_node_id() { let weak_handle = conn.inner.weak_handle(); let path_events = conn.inner.path_events(); - this.ep.msock.register_connection( - remote, - weak_handle, - path_events, - this.paths.clone(), - ); + this.ep + .msock + .register_connection(remote, weak_handle, path_events); } else { warn!("unable to determine node id for the remote"); } @@ -2287,6 +2262,7 @@ mod tests { }; use iroh_base::{NodeAddr, NodeId, SecretKey}; + use iroh_relay::http::Protocol; use n0_future::{StreamExt, task::AbortOnDropHandle}; use n0_snafu::{Error, Result, ResultExt}; use n0_watcher::Watcher; diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index af9f8501ee9..0c5ab9c3534 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -45,6 +45,7 @@ use netwatch::{UdpSocket, ip::LocalAddresses}; use quinn::{AsyncUdpSocket, ServerConfig, WeakConnectionHandle}; use quinn_proto::PathEvent; use rand::Rng; +use relay_mapped_addrs::RelayMappedAddresses; use smallvec::SmallVec; use snafu::{ResultExt, Snafu}; use tokio::sync::{Mutex as AsyncMutex, mpsc}; @@ -79,6 +80,7 @@ use crate::{ mod metrics; mod node_map; +mod relay_mapped_addrs; pub(crate) mod transports; @@ -200,6 +202,8 @@ pub(crate) struct MagicSock { /// Tracks the mapped IP addresses ip_mapped_addrs: IpMappedAddresses, + /// Tracks the mapped IP addresses + relay_mapped_addrs: RelayMappedAddresses, /// Local addresses local_addrs_watch: LocalAddrsWatch, /// Currently bound IP addresses of all sockets @@ -295,25 +299,10 @@ impl MagicSock { remote: NodeId, conn: WeakConnectionHandle, mut path_events: tokio::sync::broadcast::Receiver, - paths: Vec, ) { self.connection_map.insert(remote, conn); - task::spawn(async move { - let conn = conn.clone(); - for addr in paths { - match conn.open_path(addr, quinn_proto::PathStatus::Backup).await { - Ok(path) => { - path.set_max_idle_timeout(Some(ENDPOINTS_FRESH_ENOUGH_DURATION)) - .ok(); - path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); - } - Err(err) => { - warn!("failed to open path {:?}", err); - } - } - } - }); + // TODO: open additional paths // TODO: track task // TODO: find a good home for this task::spawn(async move { @@ -461,6 +450,11 @@ impl MagicSock { self.node_map .add_node_addr(addr.clone(), source, &self.metrics.magicsock); + if let Some(url) = addr.relay_url() { + self.relay_mapped_addrs + .get_or_register(url.clone(), addr.node_id); + } + // Add paths to the existing connections self.add_paths(addr); @@ -633,11 +627,8 @@ impl MagicSock { let mut active_paths = SmallVec::<[_; 3]>::new(); - match MappedAddr::from(transmit.destination) { - MappedAddr::None(addr) => { - active_paths.push(transports::Addr::from(addr)); - } - MappedAddr::NodeId(dest) => { + match MultipathMappedAddr::from(transmit.destination) { + MultipathMappedAddr::Mixed(dest) => { trace!( dst = %dest, src = ?transmit.src_ip, @@ -670,7 +661,10 @@ impl MagicSock { } } #[cfg(not(wasm_browser))] - MappedAddr::Ip(dest) => { + MultipathMappedAddr::Ip(addr) => { + active_paths.push(transports::Addr::Ip(addr)); + } + MultipathMappedAddr::Relay(dest) => { trace!( dst = %dest, src = ?transmit.src_ip, @@ -680,9 +674,9 @@ impl MagicSock { // Check if this is a known IpMappedAddr, and if so, send over UDP // Get the socket addr - match self.ip_mapped_addrs.get_ip_addr(&dest) { - Some(addr) => { - active_paths.push(transports::Addr::from(addr)); + match self.relay_mapped_addrs.get_url(&dest) { + Some((relay, node_id)) => { + active_paths.push(transports::Addr::Relay(relay, node_id)); } None => { error!(%dest, "unknown mapped address"); @@ -1103,33 +1097,7 @@ impl From for MultipathMappedAddr { if let Ok(ip_mapped_addr) = IpMappedAddr::try_from(addr) { return Self::Relay(ip_mapped_addr); } - MappedAddr::Self(value) - } - } - } -} - -#[derive(Clone, Debug)] -enum MappedAddr { - NodeId(NodeIdMappedAddr), - #[cfg(not(wasm_browser))] - Ip(IpMappedAddr), - None(SocketAddr), -} - -impl From for MappedAddr { - fn from(value: SocketAddr) -> Self { - match value.ip() { - IpAddr::V4(_) => MappedAddr::None(value), - IpAddr::V6(addr) => { - if let Ok(node_id_mapped_addr) = NodeIdMappedAddr::try_from(addr) { - return MappedAddr::NodeId(node_id_mapped_addr); - } - #[cfg(not(wasm_browser))] - if let Ok(ip_mapped_addr) = IpMappedAddr::try_from(addr) { - return MappedAddr::Ip(ip_mapped_addr); - } - MappedAddr::None(value) + Self::Ip(value) } } } @@ -1318,6 +1286,7 @@ impl Handle { bind_ip(addr_v4, addr_v6, &metrics).context(BindSocketsSnafu)?; let ip_mapped_addrs = IpMappedAddresses::default(); + let relay_mapped_addrs = RelayMappedAddresses::default(); let (actor_sender, actor_receiver) = mpsc::channel(256); @@ -1367,6 +1336,7 @@ impl Handle { node_map, connection_map: Default::default(), ip_mapped_addrs: ip_mapped_addrs.clone(), + relay_mapped_addrs, discovery, discovery_user_data: RwLock::new(discovery_user_data), direct_addrs: Default::default(), diff --git a/iroh/src/magicsock/relay_mapped_addrs.rs b/iroh/src/magicsock/relay_mapped_addrs.rs new file mode 100644 index 00000000000..32c8b866220 --- /dev/null +++ b/iroh/src/magicsock/relay_mapped_addrs.rs @@ -0,0 +1,54 @@ +use std::{collections::BTreeMap, sync::Arc}; + +use iroh_base::{NodeId, RelayUrl}; +use snafu::Snafu; + +use crate::net_report::IpMappedAddr; + +/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] +#[derive(Debug, Snafu)] +#[snafu(display("Failed to convert"))] +pub struct RelayMappedAddrError; + +/// A Map of [`RelayMappedAddresses`] to [`SocketAddr`]. +#[derive(Debug, Clone, Default)] +pub(crate) struct RelayMappedAddresses(Arc>); + +#[derive(Debug, Default)] +pub(super) struct Inner { + by_mapped_addr: BTreeMap, + by_url: BTreeMap<(RelayUrl, NodeId), IpMappedAddr>, +} + +impl RelayMappedAddresses { + /// Adds a [`RelayUrl`] to the map and returns the generated [`IpMappedAddr`]. + /// + /// If this [`RelayUrl`] already exists in the map, it returns its + /// associated [`IpMappedAddr`]. + /// + /// Otherwise a new [`IpMappedAddr`] is generated for it and returned. + pub(super) fn get_or_register(&self, relay: RelayUrl, node: NodeId) -> IpMappedAddr { + let mut inner = self.0.lock().expect("poisoned"); + if let Some(mapped_addr) = inner.by_url.get(&(relay.clone(), node)) { + return *mapped_addr; + } + let ip_mapped_addr = IpMappedAddr::generate(); + inner + .by_mapped_addr + .insert(ip_mapped_addr, (relay.clone(), node)); + inner.by_url.insert((relay, node), ip_mapped_addr); + ip_mapped_addr + } + + /// Returns the [`IpMappedAddr`] for the given [`RelayUrl`]. + pub(crate) fn get_mapped_addr(&self, relay: RelayUrl, node: NodeId) -> Option { + let inner = self.0.lock().expect("poisoned"); + inner.by_url.get(&(relay, node)).copied() + } + + /// Returns the [`RelayUrl`] for the given [`IpMappedAddr`]. + pub(crate) fn get_url(&self, mapped_addr: &IpMappedAddr) -> Option<(RelayUrl, NodeId)> { + let inner = self.0.lock().expect("poisoned"); + inner.by_mapped_addr.get(mapped_addr).cloned() + } +} diff --git a/iroh/src/magicsock/transports/relay.rs b/iroh/src/magicsock/transports/relay.rs index 295266ec559..33760fdcde3 100644 --- a/iroh/src/magicsock/transports/relay.rs +++ b/iroh/src/magicsock/transports/relay.rs @@ -126,7 +126,7 @@ impl RelayTransport { .segment_size .map_or(dm.datagrams.contents.len(), |s| u16::from(s) as usize); meta_out.ecn = None; - meta_out.dst_ip = None; // TODO: insert the relay url for this relay + meta_out.dst_ip = None; *addr = (dm.url, dm.src).into(); num_msgs += 1; diff --git a/iroh/src/net_report/ip_mapped_addrs.rs b/iroh/src/net_report/ip_mapped_addrs.rs index be7da1867ae..564134e7be9 100644 --- a/iroh/src/net_report/ip_mapped_addrs.rs +++ b/iroh/src/net_report/ip_mapped_addrs.rs @@ -38,7 +38,7 @@ impl IpMappedAddr { /// /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) /// which is recognised by iroh as an IP mapped address. - pub(super) fn generate() -> Self { + pub(crate) fn generate() -> Self { let mut addr = [0u8; 16]; addr[0] = Self::ADDR_PREFIXL; addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID); From 75d55252c64e4749b756b1c17f510bd5a48edf19 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 18 Jul 2025 17:21:25 +0200 Subject: [PATCH 012/164] get some stuff to work again --- iroh/src/endpoint.rs | 1 - iroh/src/magicsock.rs | 20 ++++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index fdde0720611..856ca358104 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2262,7 +2262,6 @@ mod tests { }; use iroh_base::{NodeAddr, NodeId, SecretKey}; - use iroh_relay::http::Protocol; use n0_future::{StreamExt, task::AbortOnDropHandle}; use n0_snafu::{Error, Result, ResultExt}; use n0_watcher::Watcher; diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 0c5ab9c3534..cf034703259 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -643,17 +643,20 @@ impl MagicSock { self.ipv6_reported.load(Ordering::Relaxed), &self.metrics.magicsock, ) { - Some((node_id, _udp_addr, relay_url, ping_actions)) => { + Some((node_id, udp_addr, relay_url, ping_actions)) => { if !ping_actions.is_empty() { self.actor_sender .try_send(ActorMessage::PingActions(ping_actions)) .ok(); } - // NodeId mapped addrs are only used for relays, currently. - // IP based addrs will have been added as individual paths + // Mixed will send all available addrs + if let Some(url) = relay_url { active_paths.push(transports::Addr::Relay(url, node_id)); } + if let Some(addr) = udp_addr { + active_paths.push(transports::Addr::Ip(addr)); + } } None => { error!(%dest, "no NodeState for mapped address"); @@ -812,15 +815,16 @@ impl MagicSock { quic_packets_total += quic_datagram_count; quinn_meta.addr = ip_mapped_addr.private_socket_addr(); } else { - warn!( + trace!( src = %addr, count = %quic_datagram_count, len = quinn_meta.len, - "UDP recv quic packets: no node state found, skipping", + "UDP recv quic packets: no node state found", ); - // If we have no node state for the from addr, set len to 0 to make - // quinn skip the buf completely. - quinn_meta.len = 0; + + // TODO: register in node map + quic_packets_total += quic_datagram_count; + quinn_meta.addr = *addr; } } Some((node_id, quic_mapped_addr)) => { From 4f78898cdf7040636f82b16aa9b837f10db21a12 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 21 Jul 2025 12:14:27 +0200 Subject: [PATCH 013/164] remove ip_mapped_addresses --- iroh/src/magicsock.rs | 44 ++------ iroh/src/magicsock/relay_mapped_addrs.rs | 83 +++++++++++++- iroh/src/net_report.rs | 33 ++---- iroh/src/net_report/ip_mapped_addrs.rs | 134 ----------------------- iroh/src/net_report/reportgen.rs | 19 +--- 5 files changed, 105 insertions(+), 208 deletions(-) delete mode 100644 iroh/src/net_report/ip_mapped_addrs.rs diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index cf034703259..515be0ce9c6 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -45,7 +45,7 @@ use netwatch::{UdpSocket, ip::LocalAddresses}; use quinn::{AsyncUdpSocket, ServerConfig, WeakConnectionHandle}; use quinn_proto::PathEvent; use rand::Rng; -use relay_mapped_addrs::RelayMappedAddresses; +use relay_mapped_addrs::{IpMappedAddr, RelayMappedAddresses}; use smallvec::SmallVec; use snafu::{ResultExt, Snafu}; use tokio::sync::{Mutex as AsyncMutex, mpsc}; @@ -68,14 +68,14 @@ use crate::dns::DnsResolver; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; #[cfg(not(wasm_browser))] -use crate::net_report::{IpMappedAddr, QuicConfig}; +use crate::net_report::QuicConfig; use crate::{ defaults::timeouts::NET_REPORT_TIMEOUT, disco::{self, SendAddr}, discovery::{Discovery, DiscoveryItem, DiscoverySubscribers, NodeData, UserData}, key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, metrics::EndpointMetrics, - net_report::{self, IfStateDetails, IpMappedAddresses, Report}, + net_report::{self, IfStateDetails, Report}, }; mod metrics; @@ -200,8 +200,6 @@ pub(crate) struct MagicSock { /// Tracks existing connections connection_map: ConnectionMap, - /// Tracks the mapped IP addresses - ip_mapped_addrs: IpMappedAddresses, /// Tracks the mapped IP addresses relay_mapped_addrs: RelayMappedAddresses, /// Local addresses @@ -802,30 +800,15 @@ impl MagicSock { // Update the NodeMap and remap RecvMeta to the NodeIdMappedAddr. match self.node_map.receive_udp(*addr) { None => { - // Check if this address is mapped to an IpMappedAddr - if let Some(ip_mapped_addr) = - self.ip_mapped_addrs.get_mapped_addr(addr) - { - trace!( - src = %addr, - count = %quic_datagram_count, - len = quinn_meta.len, - "UDP recv QUIC address discovery packets", - ); - quic_packets_total += quic_datagram_count; - quinn_meta.addr = ip_mapped_addr.private_socket_addr(); - } else { - trace!( - src = %addr, - count = %quic_datagram_count, - len = quinn_meta.len, - "UDP recv quic packets: no node state found", - ); - - // TODO: register in node map - quic_packets_total += quic_datagram_count; - quinn_meta.addr = *addr; - } + trace!( + src = %addr, + count = %quic_datagram_count, + len = quinn_meta.len, + "UDP recv quic packets", + ); + + quic_packets_total += quic_datagram_count; + quinn_meta.addr = *addr; } Some((node_id, quic_mapped_addr)) => { trace!( @@ -1289,7 +1272,6 @@ impl Handle { let (ip_transports, port_mapper) = bind_ip(addr_v4, addr_v6, &metrics).context(BindSocketsSnafu)?; - let ip_mapped_addrs = IpMappedAddresses::default(); let relay_mapped_addrs = RelayMappedAddresses::default(); let (actor_sender, actor_receiver) = mpsc::channel(256); @@ -1339,7 +1321,6 @@ impl Handle { ipv6_reported, node_map, connection_map: Default::default(), - ip_mapped_addrs: ip_mapped_addrs.clone(), relay_mapped_addrs, discovery, discovery_user_data: RwLock::new(discovery_user_data), @@ -1412,7 +1393,6 @@ impl Handle { #[cfg(not(wasm_browser))] dns_resolver, #[cfg(not(wasm_browser))] - Some(ip_mapped_addrs), relay_map.clone(), net_report_config, metrics.net_report.clone(), diff --git a/iroh/src/magicsock/relay_mapped_addrs.rs b/iroh/src/magicsock/relay_mapped_addrs.rs index 32c8b866220..1ac1eaabacc 100644 --- a/iroh/src/magicsock/relay_mapped_addrs.rs +++ b/iroh/src/magicsock/relay_mapped_addrs.rs @@ -1,9 +1,88 @@ -use std::{collections::BTreeMap, sync::Arc}; +use std::{ + collections::BTreeMap, + net::{IpAddr, Ipv6Addr, SocketAddr}, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, +}; use iroh_base::{NodeId, RelayUrl}; use snafu::Snafu; -use crate::net_report::IpMappedAddr; +/// Can occur when converting a [`SocketAddr`] to an [`IpMappedAddr`] +#[derive(Debug, Snafu)] +#[snafu(display("Failed to convert"))] +pub struct IpMappedAddrError; + +/// A map fake Ipv6 address with an actual IP address. +/// +/// It is essentially a lookup key for an IP that iroh's magicsocket knows about. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub(crate) struct IpMappedAddr(Ipv6Addr); + +/// Counter to always generate unique addresses for [`IpMappedAddr`]. +static IP_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); + +impl IpMappedAddr { + /// The Prefix/L of our Unique Local Addresses. + const ADDR_PREFIXL: u8 = 0xfd; + /// The Global ID used in our Unique Local Addresses. + const ADDR_GLOBAL_ID: [u8; 5] = [21, 7, 10, 81, 11]; + /// The Subnet ID used in our Unique Local Addresses. + const ADDR_SUBNET: [u8; 2] = [0, 1]; + + /// The dummy port used for all mapped addresses. + const MAPPED_ADDR_PORT: u16 = 12345; + + /// Generates a globally unique fake UDP address. + /// + /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) + /// which is recognised by iroh as an IP mapped address. + pub(crate) fn generate() -> Self { + let mut addr = [0u8; 16]; + addr[0] = Self::ADDR_PREFIXL; + addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID); + addr[6..8].copy_from_slice(&Self::ADDR_SUBNET); + + let counter = IP_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); + addr[8..16].copy_from_slice(&counter.to_be_bytes()); + + Self(Ipv6Addr::from(addr)) + } + + /// Returns a consistent [`SocketAddr`] for the [`IpMappedAddr`]. + /// + /// This does not have a routable IP address. + /// + /// This uses a made-up, but fixed port number. The [IpMappedAddresses`] map this is + /// made for creates a unique [`IpMappedAddr`] for each IP+port and thus does not use + /// the port to map back to the original [`SocketAddr`]. + pub(crate) fn private_socket_addr(&self) -> SocketAddr { + SocketAddr::new(IpAddr::from(self.0), Self::MAPPED_ADDR_PORT) + } +} + +impl TryFrom for IpMappedAddr { + type Error = IpMappedAddrError; + + fn try_from(value: Ipv6Addr) -> std::result::Result { + let octets = value.octets(); + if octets[0] == Self::ADDR_PREFIXL + && octets[1..6] == Self::ADDR_GLOBAL_ID + && octets[6..8] == Self::ADDR_SUBNET + { + return Ok(Self(value)); + } + Err(IpMappedAddrError) + } +} + +impl std::fmt::Display for IpMappedAddr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "IpMappedAddr({})", self.0) + } +} /// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] #[derive(Debug, Snafu)] diff --git a/iroh/src/net_report.rs b/iroh/src/net_report.rs index e3ab4f5bb45..78b1c28c5e3 100644 --- a/iroh/src/net_report.rs +++ b/iroh/src/net_report.rs @@ -46,7 +46,6 @@ use self::reportgen::QadProbeReport; use self::reportgen::{ProbeFinished, ProbeReport}; mod defaults; -mod ip_mapped_addrs; mod metrics; mod probes; mod report; @@ -73,8 +72,6 @@ pub(crate) mod portmapper { } } -pub(crate) use ip_mapped_addrs::{IpMappedAddr, IpMappedAddresses}; - pub(crate) use self::reportgen::IfStateDetails; #[cfg(not(wasm_browser))] use self::reportgen::SocketState; @@ -215,7 +212,6 @@ impl Client { /// Creates a new net_report client. pub(crate) fn new( #[cfg(not(wasm_browser))] dns_resolver: DnsResolver, - #[cfg(not(wasm_browser))] ip_mapped_addrs: Option, relay_map: RelayMap, opts: Options, metrics: Arc, @@ -233,7 +229,6 @@ impl Client { let socket_state = SocketState { quic_client, dns_resolver, - ip_mapped_addrs, }; Client { @@ -438,7 +433,6 @@ impl Client { for relay_node in self.relay_map.nodes().take(MAX_RELAYS) { if if_state.have_v4 { debug!(?relay_node.url, "v4 QAD probe"); - let ip_mapped_addrs = self.socket_state.ip_mapped_addrs.clone(); let relay_node = relay_node.clone(); let dns_resolver = self.socket_state.dns_resolver.clone(); let quic_client = quic_client.clone(); @@ -448,7 +442,7 @@ impl Client { .child_token() .run_until_cancelled_owned(time::timeout( PROBES_TIMEOUT, - run_probe_v4(ip_mapped_addrs, relay_node, quic_client, dns_resolver), + run_probe_v4(relay_node, quic_client, dns_resolver), )) .instrument(info_span!("QAD-IPv4", %relay_url)), ); @@ -456,7 +450,6 @@ impl Client { if if_state.have_v6 { debug!(?relay_node.url, "v6 QAD probe"); - let ip_mapped_addrs = self.socket_state.ip_mapped_addrs.clone(); let relay_node = relay_node.clone(); let dns_resolver = self.socket_state.dns_resolver.clone(); let quic_client = quic_client.clone(); @@ -466,7 +459,7 @@ impl Client { .child_token() .run_until_cancelled_owned(time::timeout( PROBES_TIMEOUT, - run_probe_v6(ip_mapped_addrs, relay_node, quic_client, dns_resolver), + run_probe_v6(relay_node, quic_client, dns_resolver), )) .instrument(info_span!("QAD-IPv6", %relay_url)), ); @@ -682,20 +675,17 @@ impl Client { #[cfg(not(wasm_browser))] async fn run_probe_v4( - ip_mapped_addrs: Option, relay_node: Arc, quic_client: QuicClient, dns_resolver: DnsResolver, ) -> n0_snafu::Result<(QadProbeReport, QadConn)> { use n0_snafu::ResultExt; - let relay_addr_orig = reportgen::get_relay_addr_ipv4(&dns_resolver, &relay_node).await?; - let relay_addr = - reportgen::maybe_to_mapped_addr(ip_mapped_addrs.as_ref(), relay_addr_orig.into()); + let relay_addr = reportgen::get_relay_addr_ipv4(&dns_resolver, &relay_node).await?; - debug!(?relay_addr_orig, ?relay_addr, "relay addr v4"); + debug!(?relay_addr, "relay addr v4"); let host = relay_node.url.host_str().context("missing host url")?; - let conn = quic_client.create_conn(relay_addr, host).await?; + let conn = quic_client.create_conn(relay_addr.into(), host).await?; let mut receiver = conn.observed_external_addr(); // wait for an addr @@ -750,19 +740,16 @@ async fn run_probe_v4( #[cfg(not(wasm_browser))] async fn run_probe_v6( - ip_mapped_addrs: Option, relay_node: Arc, quic_client: QuicClient, dns_resolver: DnsResolver, ) -> n0_snafu::Result<(QadProbeReport, QadConn)> { use n0_snafu::ResultExt; - let relay_addr_orig = reportgen::get_relay_addr_ipv6(&dns_resolver, &relay_node).await?; - let relay_addr = - reportgen::maybe_to_mapped_addr(ip_mapped_addrs.as_ref(), relay_addr_orig.into()); + let relay_addr = reportgen::get_relay_addr_ipv6(&dns_resolver, &relay_node).await?; - debug!(?relay_addr_orig, ?relay_addr, "relay addr v6"); + debug!(?relay_addr, "relay addr v6"); let host = relay_node.url.host_str().context("missing host url")?; - let conn = quic_client.create_conn(relay_addr, host).await?; + let conn = quic_client.create_conn(relay_addr.into(), host).await?; let mut receiver = conn.observed_external_addr(); // wait for an addr @@ -885,7 +872,6 @@ mod tests { .insecure_skip_relay_cert_verify(true); let mut client = Client::new( resolver.clone(), - None, relay_map.clone(), opts.clone(), Default::default(), @@ -1086,8 +1072,7 @@ mod tests { println!("test: {}", tt.name); let relay_map = RelayMap::empty(); let opts = Options::default(); - let mut client = - Client::new(resolver.clone(), None, relay_map, opts, Default::default()); + let mut client = Client::new(resolver.clone(), relay_map, opts, Default::default()); for s in &mut tt.steps { // trigger the timer tokio::time::advance(Duration::from_secs(s.after)).await; diff --git a/iroh/src/net_report/ip_mapped_addrs.rs b/iroh/src/net_report/ip_mapped_addrs.rs deleted file mode 100644 index 564134e7be9..00000000000 --- a/iroh/src/net_report/ip_mapped_addrs.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::{ - collections::BTreeMap, - net::{IpAddr, Ipv6Addr, SocketAddr}, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, -}; - -use snafu::Snafu; - -/// Can occur when converting a [`SocketAddr`] to an [`IpMappedAddr`] -#[derive(Debug, Snafu)] -#[snafu(display("Failed to convert"))] -pub struct IpMappedAddrError; - -/// A map fake Ipv6 address with an actual IP address. -/// -/// It is essentially a lookup key for an IP that iroh's magicsocket knows about. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub(crate) struct IpMappedAddr(Ipv6Addr); - -/// Counter to always generate unique addresses for [`IpMappedAddr`]. -static IP_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); - -impl IpMappedAddr { - /// The Prefix/L of our Unique Local Addresses. - const ADDR_PREFIXL: u8 = 0xfd; - /// The Global ID used in our Unique Local Addresses. - const ADDR_GLOBAL_ID: [u8; 5] = [21, 7, 10, 81, 11]; - /// The Subnet ID used in our Unique Local Addresses. - const ADDR_SUBNET: [u8; 2] = [0, 1]; - - /// The dummy port used for all mapped addresses. - const MAPPED_ADDR_PORT: u16 = 12345; - - /// Generates a globally unique fake UDP address. - /// - /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) - /// which is recognised by iroh as an IP mapped address. - pub(crate) fn generate() -> Self { - let mut addr = [0u8; 16]; - addr[0] = Self::ADDR_PREFIXL; - addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID); - addr[6..8].copy_from_slice(&Self::ADDR_SUBNET); - - let counter = IP_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); - addr[8..16].copy_from_slice(&counter.to_be_bytes()); - - Self(Ipv6Addr::from(addr)) - } - - /// Returns a consistent [`SocketAddr`] for the [`IpMappedAddr`]. - /// - /// This does not have a routable IP address. - /// - /// This uses a made-up, but fixed port number. The [IpMappedAddresses`] map this is - /// made for creates a unique [`IpMappedAddr`] for each IP+port and thus does not use - /// the port to map back to the original [`SocketAddr`]. - pub(crate) fn private_socket_addr(&self) -> SocketAddr { - SocketAddr::new(IpAddr::from(self.0), Self::MAPPED_ADDR_PORT) - } -} - -impl TryFrom for IpMappedAddr { - type Error = IpMappedAddrError; - - fn try_from(value: Ipv6Addr) -> std::result::Result { - let octets = value.octets(); - if octets[0] == Self::ADDR_PREFIXL - && octets[1..6] == Self::ADDR_GLOBAL_ID - && octets[6..8] == Self::ADDR_SUBNET - { - return Ok(Self(value)); - } - Err(IpMappedAddrError) - } -} - -impl std::fmt::Display for IpMappedAddr { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "IpMappedAddr({})", self.0) - } -} - -/// A Map of [`IpMappedAddresses`] to [`SocketAddr`]. -// TODO(ramfox): before this is ready to be used beyond QAD, we should add -// mechanisms for keeping track of "aliveness" and pruning address, as we do -// with the `NodeMap` -#[derive(Debug, Clone, Default)] -pub(crate) struct IpMappedAddresses(Arc>); - -#[derive(Debug, Default)] -pub(super) struct Inner { - by_mapped_addr: BTreeMap, - /// Because [`std::net::SocketAddrV6`] contains extra fields besides the IP - /// address and port (ie, flow_info and scope_id), the a [`std::net::SocketAddrV6`] - /// with the same IP addr and port might Hash to something different. - /// So to get a hashable key for the map, we are using `(IpAddr, u6)`. - by_ip_port: BTreeMap<(IpAddr, u16), IpMappedAddr>, -} - -impl IpMappedAddresses { - /// Adds a [`SocketAddr`] to the map and returns the generated [`IpMappedAddr`]. - /// - /// If this [`SocketAddr`] already exists in the map, it returns its - /// associated [`IpMappedAddr`]. - /// - /// Otherwise a new [`IpMappedAddr`] is generated for it and returned. - pub(super) fn get_or_register(&self, socket_addr: SocketAddr) -> IpMappedAddr { - let ip_port = (socket_addr.ip(), socket_addr.port()); - let mut inner = self.0.lock().expect("poisoned"); - if let Some(mapped_addr) = inner.by_ip_port.get(&ip_port) { - return *mapped_addr; - } - let ip_mapped_addr = IpMappedAddr::generate(); - inner.by_mapped_addr.insert(ip_mapped_addr, socket_addr); - inner.by_ip_port.insert(ip_port, ip_mapped_addr); - ip_mapped_addr - } - - /// Returns the [`IpMappedAddr`] for the given [`SocketAddr`]. - pub(crate) fn get_mapped_addr(&self, socket_addr: &SocketAddr) -> Option { - let ip_port = (socket_addr.ip(), socket_addr.port()); - let inner = self.0.lock().expect("poisoned"); - inner.by_ip_port.get(&ip_port).copied() - } - - /// Returns the [`SocketAddr`] for the given [`IpMappedAddr`]. - pub(crate) fn get_ip_addr(&self, mapped_addr: &IpMappedAddr) -> Option { - let inner = self.0.lock().expect("poisoned"); - inner.by_mapped_addr.get(mapped_addr).copied() - } -} diff --git a/iroh/src/net_report/reportgen.rs b/iroh/src/net_report/reportgen.rs index 4c26bbea32d..2829b9ab8f1 100644 --- a/iroh/src/net_report/reportgen.rs +++ b/iroh/src/net_report/reportgen.rs @@ -45,6 +45,8 @@ use tokio_util::sync::CancellationToken; use tracing::{Instrument, debug, debug_span, error, info_span, trace, warn}; use url::Host; +#[cfg(not(wasm_browser))] +use super::defaults::timeouts::DNS_TIMEOUT; #[cfg(wasm_browser)] use super::portmapper; // We stub the library use super::{ @@ -52,8 +54,6 @@ use super::{ probes::{Probe, ProbePlan}, }; #[cfg(not(wasm_browser))] -use super::{defaults::timeouts::DNS_TIMEOUT, ip_mapped_addrs::IpMappedAddresses}; -#[cfg(not(wasm_browser))] use crate::discovery::dns::DNS_STAGGERING_MS; use crate::net_report::defaults::timeouts::{ CAPTIVE_PORTAL_DELAY, CAPTIVE_PORTAL_TIMEOUT, OVERALL_REPORT_TIMEOUT, PROBES_TIMEOUT, @@ -105,8 +105,6 @@ pub(crate) struct SocketState { pub(crate) quic_client: Option, /// The DNS resolver to use for probes that need to resolve DNS records. pub(crate) dns_resolver: DnsResolver, - /// Optional [`IpMappedAddresses`] used to enable QAD in iroh - pub(crate) ip_mapped_addrs: Option, } impl Client { @@ -518,17 +516,6 @@ impl Probe { } } -#[cfg(not(wasm_browser))] -pub(super) fn maybe_to_mapped_addr( - ip_mapped_addrs: Option<&IpMappedAddresses>, - addr: SocketAddr, -) -> SocketAddr { - if let Some(ip_mapped_addrs) = ip_mapped_addrs { - return ip_mapped_addrs.get_or_register(addr).private_socket_addr(); - } - addr -} - #[cfg(not(wasm_browser))] #[derive(Debug, Snafu)] #[snafu(module)] @@ -874,7 +861,7 @@ mod tests { let quic_client = iroh_relay::quic::QuicClient::new(ep.clone(), client_config); let dns_resolver = DnsResolver::default(); - let (report, conn) = super::super::run_probe_v4(None, relay, quic_client, dns_resolver) + let (report, conn) = super::super::run_probe_v4(relay, quic_client, dns_resolver) .await .unwrap(); From 130710fb43c8eca93b4db8165e71ae6a1d745d3e Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 21 Jul 2025 12:38:16 +0200 Subject: [PATCH 014/164] use correct relay addr on recv --- iroh/src/magicsock.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 515be0ce9c6..4729873c26d 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -825,8 +825,11 @@ impl MagicSock { } transports::Addr::Relay(src_url, src_node) => { // Relay - let quic_mapped_addr = self.node_map.receive_relay(src_url, *src_node); - quinn_meta.addr = quic_mapped_addr.private_socket_addr(); + let _quic_mapped_addr = self.node_map.receive_relay(src_url, *src_node); + let mapped_addr = self + .relay_mapped_addrs + .get_or_register(src_url.clone(), *src_node); + quinn_meta.addr = mapped_addr.private_socket_addr(); } } } else { From dba89df89ad29443d2c30d41fd84887fc8b45eb8 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 21 Jul 2025 12:45:42 +0200 Subject: [PATCH 015/164] ensure connection registration --- iroh/src/endpoint.rs | 45 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 856ca358104..22c0df41eaf 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1623,7 +1623,7 @@ impl Future for IncomingFuture { Poll::Pending => Poll::Pending, Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Ready(Ok(inner)) => { - let conn = Connection { inner }; + let conn = Connection::new(inner, None, &this.ep); try_send_rtt_msg(&conn, this.ep, None); Poll::Ready(Ok(conn)) } @@ -1711,11 +1711,12 @@ impl Connecting { pub fn into_0rtt(self) -> Result<(Connection, ZeroRttAccepted), Self> { match self.inner.into_0rtt() { Ok((inner, zrtt_accepted)) => { - let conn = Connection { inner }; + let conn = Connection::new(inner, self.remote_node_id, &self.ep); let zrtt_accepted = ZeroRttAccepted { inner: zrtt_accepted, _discovery_drop_guard: self._discovery_drop_guard, }; + // This call is why `self.remote_node_id` was introduced. // When we `Connecting::into_0rtt`, then we don't yet have `handshake_data` // in our `Connection`, thus `try_send_rtt_msg` won't be able to pick up @@ -1761,24 +1762,7 @@ impl Future for Connecting { Poll::Pending => Poll::Pending, Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Ready(Ok(inner)) => { - let conn = Connection { inner }; - - // Grab the remote identity and register this connection - if let Some(remote) = *this.remote_node_id { - let weak_handle = conn.inner.weak_handle(); - let path_events = conn.inner.path_events(); - this.ep - .msock - .register_connection(remote, weak_handle, path_events); - } else if let Ok(remote) = conn.remote_node_id() { - let weak_handle = conn.inner.weak_handle(); - let path_events = conn.inner.path_events(); - this.ep - .msock - .register_connection(remote, weak_handle, path_events); - } else { - warn!("unable to determine node id for the remote"); - } + let conn = Connection::new(inner, *this.remote_node_id, &this.ep); try_send_rtt_msg(&conn, this.ep, *this.remote_node_id); Poll::Ready(Ok(conn)) @@ -1838,6 +1822,27 @@ pub struct RemoteNodeIdError { } impl Connection { + fn new(inner: quinn::Connection, remote_id: Option, ep: &Endpoint) -> Self { + let conn = Connection { inner }; + + // Grab the remote identity and register this connection + if let Some(remote) = remote_id { + let weak_handle = conn.inner.weak_handle(); + let path_events = conn.inner.path_events(); + ep.msock + .register_connection(remote, weak_handle, path_events); + } else if let Ok(remote) = conn.remote_node_id() { + let weak_handle = conn.inner.weak_handle(); + let path_events = conn.inner.path_events(); + ep.msock + .register_connection(remote, weak_handle, path_events); + } else { + warn!("unable to determine node id for the remote"); + } + + conn + } + /// Initiates a new outgoing unidirectional stream. /// /// Streams are cheap and instantaneous to open unless blocked by flow control. As a From aab083d5b0316153b532dffb2a2f19e9b0d5afe6 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 21 Jul 2025 12:50:42 +0200 Subject: [PATCH 016/164] remove rtt_actor, this is now done inside quinn on a per path basis --- iroh/src/endpoint.rs | 84 +++++----------- iroh/src/endpoint/rtt_actor.rs | 171 --------------------------------- 2 files changed, 24 insertions(+), 231 deletions(-) delete mode 100644 iroh/src/endpoint/rtt_actor.rs diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 22c0df41eaf..15ab21abda9 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -28,28 +28,6 @@ use n0_future::{Stream, time::Duration}; use n0_watcher::Watcher; use nested_enum_utils::common_fields; use pin_project::pin_project; -use snafu::{ResultExt, Snafu, ensure}; -use tracing::{debug, instrument, trace, warn}; -use url::Url; - -#[cfg(wasm_browser)] -use crate::discovery::pkarr::PkarrResolver; -use crate::{ - RelayProtocol, - discovery::{ - ConcurrentDiscovery, Discovery, DiscoveryContext, DiscoveryError, DiscoveryItem, - DiscoverySubscribers, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, IntoDiscoveryError, - Lagged, UserData, pkarr::PkarrPublisher, - }, - magicsock::{self, Handle, NodeIdMappedAddr, OwnAddressSnafu}, - metrics::EndpointMetrics, - net_report::Report, - tls, -}; -#[cfg(not(wasm_browser))] -use crate::{discovery::dns::DnsDiscovery, dns::DnsResolver}; - -mod rtt_actor; // Missing still: SendDatagram and ConnectionClose::frame_type's Type. pub use quinn::{ @@ -67,12 +45,29 @@ pub use quinn_proto::{ ServerConfig as CryptoServerConfig, UnsupportedVersion, }, }; +use snafu::{ResultExt, Snafu, ensure}; +use tracing::{debug, instrument, trace, warn}; +use url::Url; -use self::rtt_actor::RttMessage; pub use super::magicsock::{ AddNodeAddrError, ConnectionType, ControlMsg, DirectAddr, DirectAddrInfo, DirectAddrType, RemoteInfo, Source, }; +#[cfg(wasm_browser)] +use crate::discovery::pkarr::PkarrResolver; +#[cfg(not(wasm_browser))] +use crate::{discovery::dns::DnsDiscovery, dns::DnsResolver}; +use crate::{ + discovery::{ + ConcurrentDiscovery, Discovery, DiscoveryContext, DiscoveryError, DiscoveryItem, + DiscoverySubscribers, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, IntoDiscoveryError, + Lagged, UserData, pkarr::PkarrPublisher, + }, + magicsock::{self, Handle, NodeIdMappedAddr, OwnAddressSnafu}, + metrics::EndpointMetrics, + net_report::Report, + tls, +}; /// The delay to fall back to discovery when direct addresses fail. /// @@ -527,8 +522,6 @@ impl StaticConfig { pub struct Endpoint { /// Handle to the magicsocket/actor msock: Handle, - /// Handle to the actor that resets the quinn RTT estimator - rtt_actor: Arc, /// Configuration structs for quinn, holds the transport config, certificate setup, secret key etc. static_config: Arc, } @@ -638,7 +631,6 @@ impl Endpoint { let metrics = msock.metrics.magicsock.clone(); let ep = Self { msock, - rtt_actor: Arc::new(rtt_actor::RttHandle::new(metrics)), static_config: Arc::new(static_config), }; Ok(ep) @@ -1624,7 +1616,6 @@ impl Future for IncomingFuture { Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Ready(Ok(inner)) => { let conn = Connection::new(inner, None, &this.ep); - try_send_rtt_msg(&conn, this.ep, None); Poll::Ready(Ok(conn)) } } @@ -1711,19 +1702,18 @@ impl Connecting { pub fn into_0rtt(self) -> Result<(Connection, ZeroRttAccepted), Self> { match self.inner.into_0rtt() { Ok((inner, zrtt_accepted)) => { + // This call is why `self.remote_node_id` was introduced. + // When we `Connecting::into_0rtt`, then we don't yet have `handshake_data` + // in our `Connection`, thus we won't be able to pick up + // `Connection::remote_node_id`. + // Instead, we provide `self.remote_node_id` here - we know it in advance, + // after all. let conn = Connection::new(inner, self.remote_node_id, &self.ep); let zrtt_accepted = ZeroRttAccepted { inner: zrtt_accepted, _discovery_drop_guard: self._discovery_drop_guard, }; - // This call is why `self.remote_node_id` was introduced. - // When we `Connecting::into_0rtt`, then we don't yet have `handshake_data` - // in our `Connection`, thus `try_send_rtt_msg` won't be able to pick up - // `Connection::remote_node_id`. - // Instead, we provide `self.remote_node_id` here - we know it in advance, - // after all. - try_send_rtt_msg(&conn, &self.ep, self.remote_node_id); Ok((conn, zrtt_accepted)) } Err(inner) => Err(Self { @@ -1763,8 +1753,6 @@ impl Future for Connecting { Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Ready(Ok(inner)) => { let conn = Connection::new(inner, *this.remote_node_id, &this.ep); - - try_send_rtt_msg(&conn, this.ep, *this.remote_node_id); Poll::Ready(Ok(conn)) } } @@ -2139,30 +2127,6 @@ impl Connection { } } -/// Try send a message to the rtt-actor. -/// -/// If we can't notify the actor that will impact performance a little, but we can still -/// function. -fn try_send_rtt_msg(conn: &Connection, magic_ep: &Endpoint, remote_node_id: Option) { - // If we can't notify the rtt-actor that's not great but not critical. - let Some(node_id) = remote_node_id.or_else(|| conn.remote_node_id().ok()) else { - warn!(?conn, "failed to get remote node id"); - return; - }; - let Some(conn_type_changes) = magic_ep.conn_type(node_id) else { - warn!(?conn, "failed to create conn_type stream"); - return; - }; - let rtt_msg = RttMessage::NewConnection { - connection: conn.inner.weak_handle(), - conn_type_changes: conn_type_changes.stream(), - node_id, - }; - if let Err(err) = magic_ep.rtt_actor.msg_tx.try_send(rtt_msg) { - warn!(?conn, "rtt-actor not reachable: {err:#}"); - } -} - /// Read a proxy url from the environment, in this order /// /// - `HTTP_PROXY` diff --git a/iroh/src/endpoint/rtt_actor.rs b/iroh/src/endpoint/rtt_actor.rs deleted file mode 100644 index 5bc9e6310f8..00000000000 --- a/iroh/src/endpoint/rtt_actor.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! Actor which coordinates the congestion controller for the magic socket - -use std::{pin::Pin, sync::Arc, task::Poll}; - -use iroh_base::NodeId; -use n0_future::{ - MergeUnbounded, Stream, StreamExt, - task::{self, AbortOnDropHandle}, -}; -use tokio::sync::mpsc; -use tracing::{Instrument, debug, info_span}; - -use crate::{magicsock::ConnectionType, metrics::MagicsockMetrics}; - -#[derive(Debug)] -pub(super) struct RttHandle { - // We should and some point use this to propagate panics and errors. - pub(super) _handle: AbortOnDropHandle<()>, - pub(super) msg_tx: mpsc::Sender, -} - -impl RttHandle { - pub(super) fn new(metrics: Arc) -> Self { - let mut actor = RttActor { - connection_events: Default::default(), - metrics, - }; - let (msg_tx, msg_rx) = mpsc::channel(16); - let handle = task::spawn( - async move { - actor.run(msg_rx).await; - } - .instrument(info_span!("rtt-actor")), - ); - Self { - _handle: AbortOnDropHandle::new(handle), - msg_tx, - } - } -} - -/// Messages to send to the [`RttActor`]. -#[derive(Debug)] -pub(super) enum RttMessage { - /// Informs the [`RttActor`] of a new connection is should monitor. - NewConnection { - /// The connection. - connection: quinn::WeakConnectionHandle, - /// Path changes for this connection from the magic socket. - conn_type_changes: n0_watcher::Stream>, - /// For reporting-only, the Node ID of this connection. - node_id: NodeId, - }, -} - -/// Actor to coordinate congestion controller state with magic socket state. -/// -/// The magic socket can change the underlying network path, between two nodes. If we can -/// inform the QUIC congestion controller of this event it will work much more efficiently. -#[derive(derive_more::Debug)] -struct RttActor { - /// Stream of connection type changes. - #[debug("MergeUnbounded>")] - connection_events: MergeUnbounded, - metrics: Arc, -} - -#[derive(Debug)] -struct MappedStream { - stream: n0_watcher::Stream>, - node_id: NodeId, - /// Reference to the connection. - connection: quinn::WeakConnectionHandle, - /// This an indiciator of whether this connection was direct before. - /// This helps establish metrics on number of connections that became direct. - was_direct_before: bool, -} - -struct ConnectionEvent { - became_direct: bool, -} - -impl Stream for MappedStream { - type Item = ConnectionEvent; - - /// Performs the congestion controller reset for a magic socket path change. - /// - /// Regardless of which kind of path we are changed to, the congestion controller needs - /// resetting. Even when switching to mixed we should reset the state as e.g. switching - /// from direct to mixed back to direct should be a rare exception and is a bug if this - /// happens commonly. - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - match Pin::new(&mut self.stream).poll_next(cx) { - Poll::Ready(Some(new_conn_type)) => { - let mut became_direct = false; - if self.connection.network_path_changed() { - debug!( - node_id = %self.node_id.fmt_short(), - new_type = ?new_conn_type, - "Congestion controller state reset", - ); - if !self.was_direct_before && matches!(new_conn_type, ConnectionType::Direct(_)) - { - self.was_direct_before = true; - became_direct = true - } - }; - Poll::Ready(Some(ConnectionEvent { became_direct })) - } - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } -} - -impl RttActor { - /// Runs the actor main loop. - /// - /// The main loop will finish when the sender is dropped. - async fn run(&mut self, mut msg_rx: mpsc::Receiver) { - loop { - tokio::select! { - biased; - msg = msg_rx.recv() => { - match msg { - Some(msg) => self.handle_msg(msg), - None => break, - } - } - event = self.connection_events.next(), if !self.connection_events.is_empty() => { - if event.map(|e| e.became_direct).unwrap_or(false) { - self.metrics.connection_became_direct.inc(); - } - } - } - } - debug!("rtt-actor finished"); - } - - /// Handle actor messages. - fn handle_msg(&mut self, msg: RttMessage) { - match msg { - RttMessage::NewConnection { - connection, - conn_type_changes, - node_id, - } => { - self.handle_new_connection(connection, conn_type_changes, node_id); - } - } - } - - /// Handles the new connection message. - fn handle_new_connection( - &mut self, - connection: quinn::WeakConnectionHandle, - conn_type_changes: n0_watcher::Stream>, - node_id: NodeId, - ) { - self.connection_events.push(MappedStream { - stream: conn_type_changes, - connection, - node_id, - was_direct_before: false, - }); - self.metrics.connection_handshake_success.inc(); - } -} From d4484dae0cfdd035f25a1ffb575bccf71b911c27 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 21 Jul 2025 13:07:11 +0200 Subject: [PATCH 017/164] open additional paths after the initial connection --- Cargo.lock | 4 ++-- iroh/src/endpoint.rs | 10 ++-------- iroh/src/magicsock.rs | 20 ++++++++++---------- iroh/src/magicsock/node_map.rs | 8 ++++++++ iroh/src/magicsock/node_map/node_state.rs | 11 +++++++++++ iroh/src/magicsock/node_map/udp_paths.rs | 3 +++ 6 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aeb847038a4..45faf1e4dd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2489,7 +2489,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#0dc50edf689ee5c6cf21b4ee5c0fea6af548680d" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#89df901286301de17aac88d42adc2aa7de32bc18" dependencies = [ "bytes", "cfg_aliases", @@ -2508,7 +2508,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#0dc50edf689ee5c6cf21b4ee5c0fea6af548680d" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#89df901286301de17aac88d42adc2aa7de32bc18" dependencies = [ "bytes", "fastbloom", diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 15ab21abda9..cb2c8dae87a 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1815,15 +1815,9 @@ impl Connection { // Grab the remote identity and register this connection if let Some(remote) = remote_id { - let weak_handle = conn.inner.weak_handle(); - let path_events = conn.inner.path_events(); - ep.msock - .register_connection(remote, weak_handle, path_events); + ep.msock.register_connection(remote, &conn.inner); } else if let Ok(remote) = conn.remote_node_id() { - let weak_handle = conn.inner.weak_handle(); - let path_events = conn.inner.path_events(); - ep.msock - .register_connection(remote, weak_handle, path_events); + ep.msock.register_connection(remote, &conn.inner); } else { warn!("unable to determine node id for the remote"); } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 4729873c26d..e5bc896672a 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -43,7 +43,6 @@ use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; use quinn::{AsyncUdpSocket, ServerConfig, WeakConnectionHandle}; -use quinn_proto::PathEvent; use rand::Rng; use relay_mapped_addrs::{IpMappedAddr, RelayMappedAddresses}; use smallvec::SmallVec; @@ -292,18 +291,14 @@ impl MagicSock { self.local_addrs_watch.clone().get() } - pub(crate) fn register_connection( - &self, - remote: NodeId, - conn: WeakConnectionHandle, - mut path_events: tokio::sync::broadcast::Receiver, - ) { - self.connection_map.insert(remote, conn); + pub(crate) fn register_connection(&self, remote: NodeId, conn: &quinn::Connection) { + let weak_handle = conn.weak_handle(); + self.connection_map.insert(remote, weak_handle); - // TODO: open additional paths // TODO: track task // TODO: find a good home for this - task::spawn(async move { + let mut path_events = conn.path_events(); + let _task = task::spawn(async move { loop { match path_events.recv().await { Ok(event) => { @@ -316,6 +311,11 @@ impl MagicSock { } } }); + + // open additional paths + if let Some(addr) = self.node_map.get_current_addr(remote) { + self.add_paths(addr); + } } #[cfg(not(wasm_browser))] diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 4eb8e3df716..bb0fd98734b 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -186,6 +186,14 @@ impl NodeMap { .unwrap_or_default() } + pub(super) fn get_current_addr(&self, node_key: NodeId) -> Option { + self.inner + .lock() + .expect("poisoned") + .get(NodeStateKey::NodeId(node_key)) + .map(|ep| ep.get_current_addr()) + } + pub(super) fn handle_call_me_maybe( &self, sender: PublicKey, diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 96162223663..36ad6a0de13 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -667,6 +667,17 @@ impl NodeState { (udp_addr, relay_url, ping_msgs) } + pub(crate) fn get_current_addr(&self) -> NodeAddr { + // TODO: more selective? + let mut node_addr = + NodeAddr::new(self.node_id).with_direct_addresses(self.udp_paths.addrs()); + if let Some((url, _)) = &self.relay_url { + node_addr = node_addr.with_relay_url(url.clone()); + } + + node_addr + } + /// Get the direct addresses for this endpoint. pub(super) fn direct_addresses(&self) -> impl Iterator + '_ { self.udp_paths.paths.keys().copied() diff --git a/iroh/src/magicsock/node_map/udp_paths.rs b/iroh/src/magicsock/node_map/udp_paths.rs index 475f865e59b..d89cf10ed2c 100644 --- a/iroh/src/magicsock/node_map/udp_paths.rs +++ b/iroh/src/magicsock/node_map/udp_paths.rs @@ -106,6 +106,9 @@ impl NodeUdpPaths { best, } } + pub(super) fn addrs(&self) -> Vec { + self.paths.keys().map(|ip| (*ip).into()).collect() + } /// Returns the current UDP address to send on. pub(super) fn send_addr(&self, have_ipv6: bool) -> &UdpSendAddr { From 6cb94e4bdf2132a813ebee00f1e754a573daa695 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Tue, 22 Jul 2025 12:10:14 +0200 Subject: [PATCH 018/164] ensure path open --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- iroh/src/magicsock.rs | 7 +++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45faf1e4dd8..92171f43d7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2489,7 +2489,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#89df901286301de17aac88d42adc2aa7de32bc18" +source = "git+https://github.com//n0-computer/quinn?branch=flub%2Fopen-path-ensure#646e849d540886ee58e25e3da509d6021ec94430" dependencies = [ "bytes", "cfg_aliases", @@ -2508,7 +2508,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#89df901286301de17aac88d42adc2aa7de32bc18" +source = "git+https://github.com//n0-computer/quinn?branch=flub%2Fopen-path-ensure#646e849d540886ee58e25e3da509d6021ec94430" dependencies = [ "bytes", "fastbloom", @@ -2544,7 +2544,7 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#79e3fcc710de68b40fd05be5421048bab658ddf4" +source = "git+https://github.com//n0-computer/quinn?branch=flub%2Fopen-path-ensure#646e849d540886ee58e25e3da509d6021ec94430" dependencies = [ "cfg_aliases", "libc", diff --git a/Cargo.toml b/Cargo.toml index eb098793da4..4e6324f349d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,6 @@ netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-mu # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } -# iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "flub/quinn-path-events-status" } -# iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "flub/quinn-path-events-status" } -# iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "flub/quinn-path-events-status" } +iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "flub/open-path-ensure" } +iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "flub/open-path-ensure" } +iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "flub/open-path-ensure" } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index e5bc896672a..626621ac37b 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -476,7 +476,7 @@ impl MagicSock { let addr = *addr; task::spawn(async move { match conn - .open_path(addr, quinn_proto::PathStatus::Available) + .open_path_ensure(addr, quinn_proto::PathStatus::Available) .await { Ok(path) => { @@ -497,7 +497,10 @@ impl MagicSock { let conn = conn.clone(); let addr = addr.private_socket_addr(); task::spawn(async move { - match conn.open_path(addr, quinn_proto::PathStatus::Backup).await { + match conn + .open_path_ensure(addr, quinn_proto::PathStatus::Backup) + .await + { Ok(path) => { // Keep the relay path open path.set_max_idle_timeout(None).ok(); From 998e2833a21b4dd47da6bb3b4d8e2ce2d77528c7 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Wed, 23 Jul 2025 11:44:28 +0200 Subject: [PATCH 019/164] some debugging --- iroh/src/magicsock.rs | 128 +++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 626621ac37b..c0cb0b0a256 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -292,25 +292,29 @@ impl MagicSock { } pub(crate) fn register_connection(&self, remote: NodeId, conn: &quinn::Connection) { + debug!(%remote, "register connection"); let weak_handle = conn.weak_handle(); self.connection_map.insert(remote, weak_handle); // TODO: track task // TODO: find a good home for this let mut path_events = conn.path_events(); - let _task = task::spawn(async move { - loop { - match path_events.recv().await { - Ok(event) => { - info!(remote = %remote, "path event: {:?}", event); - } - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { - warn!("lagged path events"); + let _task = task::spawn( + async move { + loop { + match path_events.recv().await { + Ok(event) => { + info!(remote = %remote, "path event: {:?}", event); + } + Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { + warn!("lagged path events"); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, } - Err(tokio::sync::broadcast::error::RecvError::Closed) => break, } } - }); + .instrument(info_span!("path events", %remote)), + ); // open additional paths if let Some(addr) = self.node_map.get_current_addr(remote) { @@ -474,43 +478,51 @@ impl MagicSock { for addr in addr.direct_addresses() { let conn = conn.clone(); let addr = *addr; - task::spawn(async move { - match conn - .open_path_ensure(addr, quinn_proto::PathStatus::Available) - .await - { - Ok(path) => { - path.set_max_idle_timeout(Some( - ENDPOINTS_FRESH_ENOUGH_DURATION, - )) - .ok(); - path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); - } - Err(err) => { - warn!("failed to open path {:?}", err); + task::spawn( + async move { + debug!(%addr, "open path IP"); + match conn + .open_path_ensure(addr, quinn_proto::PathStatus::Available) + .await + { + Ok(path) => { + path.set_max_idle_timeout(Some( + ENDPOINTS_FRESH_ENOUGH_DURATION, + )) + .ok(); + path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); + } + Err(err) => { + warn!("failed to open path {:?}", err); + } } } - }); + .instrument(info_span!("open path IP")), + ); } // Insert the relay addr if let Some(addr) = self.get_mapping_addr(addr.node_id) { let conn = conn.clone(); let addr = addr.private_socket_addr(); - task::spawn(async move { - match conn - .open_path_ensure(addr, quinn_proto::PathStatus::Backup) - .await - { - Ok(path) => { - // Keep the relay path open - path.set_max_idle_timeout(None).ok(); - path.set_keep_alive_interval(None).ok(); - } - Err(err) => { - warn!("failed to open path {:?}", err); + task::spawn( + async move { + debug!(%addr, "open path relay"); + match conn + .open_path_ensure(addr, quinn_proto::PathStatus::Backup) + .await + { + Ok(path) => { + // Keep the relay path open + path.set_max_idle_timeout(None).ok(); + path.set_keep_alive_interval(None).ok(); + } + Err(err) => { + warn!("failed to open path {:?}", err); + } } } - }); + .instrument(info_span!("open path relay")), + ); } } else { to_delete.push(i); @@ -644,19 +656,21 @@ impl MagicSock { self.ipv6_reported.load(Ordering::Relaxed), &self.metrics.magicsock, ) { - Some((node_id, udp_addr, relay_url, ping_actions)) => { + Some((node_id, _udp_addr, _relay_url, ping_actions)) => { if !ping_actions.is_empty() { self.actor_sender .try_send(ActorMessage::PingActions(ping_actions)) .ok(); } - // Mixed will send all available addrs - if let Some(url) = relay_url { - active_paths.push(transports::Addr::Relay(url, node_id)); - } - if let Some(addr) = udp_addr { - active_paths.push(transports::Addr::Ip(addr)); + if let Some(addr) = self.node_map.get_current_addr(node_id) { + // Mixed will send all available addrs + if let Some(ref url) = addr.relay_url { + active_paths.push(transports::Addr::Relay(url.clone(), node_id)); + } + for ip in addr.direct_addresses() { + active_paths.push(transports::Addr::Ip(*ip)); + } } } None => { @@ -3202,18 +3216,18 @@ mod tests { let _accept_task = AbortOnDropHandle::new(accept_task); // Add an empty entry in the NodeMap of ep_1 - msock_1 - .add_node_addr( - NodeAddr { - node_id: node_id_2, - relay_url: None, - direct_addresses: Default::default(), - }, - Source::NamedApp { - name: "test".into(), - }, - ) - .unwrap(); + msock_1.node_map.add_node_addr( + NodeAddr { + node_id: node_id_2, + relay_url: None, + direct_addresses: Default::default(), + }, + Source::NamedApp { + name: "test".into(), + }, + &msock_1.metrics.magicsock, + ); + let addr_2 = msock_1.get_mapping_addr(node_id_2).unwrap(); // Set a low max_idle_timeout so quinn gives up on this quickly and our test does From 99cee6121e5d2047d3b7d129a7d967b7125e95f5 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Wed, 23 Jul 2025 15:21:20 +0200 Subject: [PATCH 020/164] update quinn branch --- Cargo.lock | 6 +++--- Cargo.toml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92171f43d7f..88205723258 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2489,7 +2489,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com//n0-computer/quinn?branch=flub%2Fopen-path-ensure#646e849d540886ee58e25e3da509d6021ec94430" +source = "git+https://github.com//n0-computer/quinn?branch=server-migrations#bc86957aa4ccb72fad70e75a6ce9fc8198f09afc" dependencies = [ "bytes", "cfg_aliases", @@ -2508,7 +2508,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com//n0-computer/quinn?branch=flub%2Fopen-path-ensure#646e849d540886ee58e25e3da509d6021ec94430" +source = "git+https://github.com//n0-computer/quinn?branch=server-migrations#bc86957aa4ccb72fad70e75a6ce9fc8198f09afc" dependencies = [ "bytes", "fastbloom", @@ -2544,7 +2544,7 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com//n0-computer/quinn?branch=flub%2Fopen-path-ensure#646e849d540886ee58e25e3da509d6021ec94430" +source = "git+https://github.com//n0-computer/quinn?branch=server-migrations#bc86957aa4ccb72fad70e75a6ce9fc8198f09afc" dependencies = [ "cfg_aliases", "libc", diff --git a/Cargo.toml b/Cargo.toml index 4e6324f349d..8e7b8f11f49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,6 @@ netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-mu # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } -iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "flub/open-path-ensure" } -iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "flub/open-path-ensure" } -iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "flub/open-path-ensure" } +iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "server-migrations" } +iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "server-migrations" } +iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "server-migrations" } From 4b60a9a2c47e986f1787b2f6dbdc833cd2df6076 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 28 Jul 2025 16:40:18 +0200 Subject: [PATCH 021/164] fixups --- Cargo.lock | 4 ++-- iroh/src/endpoint.rs | 1 - iroh/src/magicsock/relay_mapped_addrs.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 88205723258..9d6f1e92b40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6452,5 +6452,5 @@ dependencies = [ [[patch.unused]] name = "netwatch" -version = "0.6.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#b7ab98d4ff9cc947f2f084004b4cc2a979bb4d06" +version = "0.7.0" +source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#5196858f5754f906e6d205a3f3623831c9236965" diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index cb2c8dae87a..e79efd77555 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -28,7 +28,6 @@ use n0_future::{Stream, time::Duration}; use n0_watcher::Watcher; use nested_enum_utils::common_fields; use pin_project::pin_project; - // Missing still: SendDatagram and ConnectionClose::frame_type's Type. pub use quinn::{ AcceptBi, AcceptUni, AckFrequencyConfig, ApplicationClose, Chunk, ClosedStream, diff --git a/iroh/src/magicsock/relay_mapped_addrs.rs b/iroh/src/magicsock/relay_mapped_addrs.rs index 1ac1eaabacc..b473501ea60 100644 --- a/iroh/src/magicsock/relay_mapped_addrs.rs +++ b/iroh/src/magicsock/relay_mapped_addrs.rs @@ -2,8 +2,8 @@ use std::{ collections::BTreeMap, net::{IpAddr, Ipv6Addr, SocketAddr}, sync::{ - atomic::{AtomicU64, Ordering}, Arc, + atomic::{AtomicU64, Ordering}, }, }; From 04b714cced1b5898c77b5408d2c8e14436ffa2ef Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 1 Aug 2025 11:27:16 +0200 Subject: [PATCH 022/164] update deps --- Cargo.lock | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d6f1e92b40..fc60e20fe7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2316,7 +2316,7 @@ dependencies = [ "iroh-metrics", "iroh-quinn", "iroh-quinn-proto", - "iroh-quinn-udp 0.5.12", + "iroh-quinn-udp", "iroh-relay", "n0-future", "n0-snafu", @@ -2494,7 +2494,7 @@ dependencies = [ "bytes", "cfg_aliases", "iroh-quinn-proto", - "iroh-quinn-udp 0.5.12", + "iroh-quinn-udp", "pin-project-lite", "rustc-hash", "rustls", @@ -2527,20 +2527,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "iroh-quinn-udp" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53afaa1049f7c83ea1331f5ebb9e6ebc5fdd69c468b7a22dd598b02c9bcc973" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "iroh-quinn-udp" version = "0.5.12" @@ -3044,14 +3030,13 @@ dependencies = [ [[package]] name = "netwatch" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901dbb408894af3df3fc51420ba0c6faf3a7d896077b797c39b7001e2f787bd" +source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#e824fd0fb03839c4523c18cd3ab861d6b3e66a16" dependencies = [ "atomic-waker", "bytes", "cfg_aliases", "derive_more 2.0.1", - "iroh-quinn-udp 0.5.7", + "iroh-quinn-udp", "js-sys", "libc", "n0-future", @@ -5719,7 +5704,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -6449,8 +6434,3 @@ dependencies = [ "quote", "syn 2.0.101", ] - -[[patch.unused]] -name = "netwatch" -version = "0.7.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#5196858f5754f906e6d205a3f3623831c9236965" From 59efdcf9f33f12358aafe4a2b44ed3c1d3d30695 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 28 Aug 2025 15:06:13 +0200 Subject: [PATCH 023/164] bunch of renames and doc updates, no functional changes --- iroh/src/endpoint.rs | 4 +- iroh/src/magicsock.rs | 71 +++++++++++++---------- iroh/src/magicsock/node_map.rs | 19 +++--- iroh/src/magicsock/node_map/node_state.rs | 9 +-- iroh/src/magicsock/relay_mapped_addrs.rs | 55 +++++++++--------- 5 files changed, 87 insertions(+), 71 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index e79efd77555..a6ba61c4bce 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -62,7 +62,7 @@ use crate::{ DiscoverySubscribers, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, IntoDiscoveryError, Lagged, UserData, pkarr::PkarrPublisher, }, - magicsock::{self, Handle, NodeIdMappedAddr, OwnAddressSnafu}, + magicsock::{self, AllPathsMappedAddr, Handle, OwnAddressSnafu}, metrics::EndpointMetrics, net_report::Report, tls, @@ -1363,7 +1363,7 @@ impl Endpoint { async fn get_mapping_addr_and_maybe_start_discovery( &self, node_addr: NodeAddr, - ) -> Result<(NodeIdMappedAddr, Option), GetMappingAddressError> { + ) -> Result<(AllPathsMappedAddr, Option), GetMappingAddressError> { let node_id = node_addr.node_id; // Only return a mapped addr if we have some way of dialing this node, in other diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index c0cb0b0a256..c5c57055c0c 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -44,7 +44,7 @@ use netwatch::netmon; use netwatch::{UdpSocket, ip::LocalAddresses}; use quinn::{AsyncUdpSocket, ServerConfig, WeakConnectionHandle}; use rand::Rng; -use relay_mapped_addrs::{IpMappedAddr, RelayMappedAddresses}; +use relay_mapped_addrs::{RelayAddrMap, RelayMappedAddr}; use smallvec::SmallVec; use snafu::{ResultExt, Snafu}; use tokio::sync::{Mutex as AsyncMutex, mpsc}; @@ -200,7 +200,7 @@ pub(crate) struct MagicSock { connection_map: ConnectionMap, /// Tracks the mapped IP addresses - relay_mapped_addrs: RelayMappedAddresses, + relay_mapped_addrs: RelayAddrMap, /// Local addresses local_addrs_watch: LocalAddrsWatch, /// Currently bound IP addresses of all sockets @@ -291,6 +291,11 @@ impl MagicSock { self.local_addrs_watch.clone().get() } + /// Registers the connection in the connection map and opens additional paths. + /// + /// In addition to storing the connection reference this requests the current + /// [`NodeAddr`] for remote node from the [`NodeMap`] and adds all paths to the + /// connection. It also listens and logs path events. pub(crate) fn register_connection(&self, remote: NodeId, conn: &quinn::Connection) { debug!(%remote, "register connection"); let weak_handle = conn.weak_handle(); @@ -425,7 +430,7 @@ impl MagicSock { } /// Returns the socket address which can be used by the QUIC layer to dial this node. - pub(crate) fn get_mapping_addr(&self, node_id: NodeId) -> Option { + pub(crate) fn get_mapping_addr(&self, node_id: NodeId) -> Option { self.node_map.get_quic_mapped_addr_for_node_key(node_id) } @@ -616,7 +621,11 @@ impl MagicSock { } } - /// Searches the `node_map` to determine the current transports to be used. + /// Returns the transport addresses for the [`quinn_udp::Transmit`]'s destination. + /// + /// Because Quinn does only know about IP transports we map other transports to private + /// IPv6 Unique Local Address ranges. This extracts the transport addresses out of the + /// transmit's destination. #[instrument(skip_all)] fn prepare_send( &self, @@ -646,7 +655,7 @@ impl MagicSock { dst = %dest, src = ?transmit.src_ip, len = %transmit.contents.len(), - "sending", + "sending mixed", ); // Get the node's relay address and best direct address, as well @@ -680,6 +689,12 @@ impl MagicSock { } #[cfg(not(wasm_browser))] MultipathMappedAddr::Ip(addr) => { + trace!( + dst = %addr, + src = ?transmit.src_ip, + len = %transmit.contents.len(), + "sending IP", + ); active_paths.push(transports::Addr::Ip(addr)); } MultipathMappedAddr::Relay(dest) => { @@ -687,7 +702,7 @@ impl MagicSock { dst = %dest, src = ?transmit.src_ip, len = %transmit.contents.len(), - "sending", + "sending relay", ); // Check if this is a known IpMappedAddr, and if so, send over UDP @@ -706,12 +721,12 @@ impl MagicSock { Ok(active_paths) } - /// Process datagrams received from UDP sockets. + /// Process datagrams received from all the transports. /// /// All the `bufs` and `metas` should have initialized packets in them. /// - /// This fixes up the datagrams to use the correct [`NodeIdMappedAddr`] and extracts DISCO - /// packets, processing them inside the magic socket. + /// This fixes up the datagrams to use the correct [`MultipathMappedAddr`] and extracts + /// DISCO packets, processing them inside the magic socket. fn process_datagrams( &self, bufs: &mut [io::IoSliceMut<'_>], @@ -1084,9 +1099,9 @@ pub(crate) enum MultipathMappedAddr { /// Used for the initial connection. /// - Only used for sending /// - This means send on all known paths/transports - Mixed(NodeIdMappedAddr), + Mixed(AllPathsMappedAddr), /// Relay based transport address - Relay(IpMappedAddr), // TODO: RelayMappedAddr? + Relay(RelayMappedAddr), /// IP based transport address #[cfg(not(wasm_browser))] Ip(SocketAddr), @@ -1097,11 +1112,11 @@ impl From for MultipathMappedAddr { match value.ip() { IpAddr::V4(_) => Self::Ip(value), IpAddr::V6(addr) => { - if let Ok(node_id_mapped_addr) = NodeIdMappedAddr::try_from(addr) { + if let Ok(node_id_mapped_addr) = AllPathsMappedAddr::try_from(addr) { return Self::Mixed(node_id_mapped_addr); } #[cfg(not(wasm_browser))] - if let Ok(ip_mapped_addr) = IpMappedAddr::try_from(addr) { + if let Ok(ip_mapped_addr) = RelayMappedAddr::try_from(addr) { return Self::Relay(ip_mapped_addr); } Self::Ip(value) @@ -1292,7 +1307,7 @@ impl Handle { let (ip_transports, port_mapper) = bind_ip(addr_v4, addr_v6, &metrics).context(BindSocketsSnafu)?; - let relay_mapped_addrs = RelayMappedAddresses::default(); + let relay_mapped_addrs = RelayAddrMap::default(); let (actor_sender, actor_receiver) = mpsc::channel(256); @@ -2293,21 +2308,17 @@ impl DiscoveredDirectAddrs { } } -/// The fake address used by the QUIC layer to address a node. -/// -/// You can consider this as nothing more than a lookup key for a node the [`MagicSock`] knows -/// about. +/// An address used by the QUIC layer to address a node on all paths. /// -/// [`MagicSock`] can reach a node by several real socket addresses, or maybe even via the relay -/// node. The QUIC layer however needs to address a node by a stable [`SocketAddr`] so -/// that normal socket APIs can function. Thus when a new node is introduced to a [`MagicSock`] -/// it is given a new fake address. This is the type of that address. +/// This is only used for initially connecting to a remote node. We instruct Quinn to send +/// to this address, and duplicate all packets for this address to send on all paths we know +/// for the node. /// /// It is but a newtype. And in our QUIC-facing socket APIs like [`AsyncUdpSocket`] it /// comes in as the inner [`Ipv6Addr`], in those interfaces we have to be careful to do /// the conversion to this type. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) struct NodeIdMappedAddr(Ipv6Addr); +pub(crate) struct AllPathsMappedAddr(Ipv6Addr); /// Can occur when converting a [`SocketAddr`] to an [`NodeIdMappedAddr`] #[derive(Debug, Snafu)] @@ -2317,7 +2328,7 @@ pub struct NodeIdMappedAddrError; /// Counter to always generate unique addresses for [`NodeIdMappedAddr`]. static NODE_ID_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); -impl NodeIdMappedAddr { +impl AllPathsMappedAddr { /// The Prefix/L of our Unique Local Addresses. const ADDR_PREFIXL: u8 = 0xfd; /// The Global ID used in our Unique Local Addresses. @@ -2355,7 +2366,7 @@ impl NodeIdMappedAddr { } } -impl TryFrom for NodeIdMappedAddr { +impl TryFrom for AllPathsMappedAddr { type Error = NodeIdMappedAddrError; fn try_from(value: Ipv6Addr) -> Result { @@ -2370,7 +2381,7 @@ impl TryFrom for NodeIdMappedAddr { } } -impl std::fmt::Display for NodeIdMappedAddr { +impl std::fmt::Display for AllPathsMappedAddr { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "NodeIdMappedAddr({})", self.0) } @@ -2460,7 +2471,7 @@ mod tests { use tracing::{Instrument, error, info, info_span, instrument}; use tracing_test::traced_test; - use super::{NodeIdMappedAddr, Options}; + use super::{AllPathsMappedAddr, Options}; use crate::{ Endpoint, RelayMap, RelayMode, SecretKey, dns::DnsResolver, @@ -3037,7 +3048,7 @@ mod tests { async fn magicsock_connect( ep: &quinn::Endpoint, ep_secret_key: SecretKey, - addr: NodeIdMappedAddr, + addr: AllPathsMappedAddr, node_id: NodeId, ) -> Result { // Endpoint::connect sets this, do the same to have similar behaviour. @@ -3063,7 +3074,7 @@ mod tests { async fn magicsock_connect_with_transport_config( ep: &quinn::Endpoint, ep_secret_key: SecretKey, - mapped_addr: NodeIdMappedAddr, + mapped_addr: AllPathsMappedAddr, node_id: NodeId, transport_config: Arc, ) -> Result { @@ -3098,7 +3109,7 @@ mod tests { let msock_1 = magicsock_ep(secret_key_1.clone()).await.unwrap(); // Generate an address not present in the NodeMap. - let bad_addr = NodeIdMappedAddr::generate(); + let bad_addr = AllPathsMappedAddr::generate(); // 500ms is rather fast here. Running this locally it should always be the correct // timeout. If this is too slow however the test will not become flaky as we are diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index bb0fd98734b..36d4b9cdfba 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use tracing::{debug, info, instrument, trace, warn}; use self::node_state::{NodeState, Options}; -use super::{ActorMessage, NodeIdMappedAddr, metrics::Metrics, transports}; +use super::{ActorMessage, AllPathsMappedAddr, metrics::Metrics, transports}; use crate::disco::{CallMeMaybe, Pong, SendAddr}; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; @@ -54,7 +54,7 @@ pub(super) struct NodeMap { pub(super) struct NodeMapInner { by_node_key: HashMap, by_ip_port: HashMap, - by_quic_mapped_addr: HashMap, + by_quic_mapped_addr: HashMap, by_id: HashMap, next_id: usize, #[cfg(any(test, feature = "test-utils"))] @@ -68,7 +68,7 @@ pub(super) struct NodeMapInner { #[derive(Debug, Clone)] enum NodeStateKey { NodeId(NodeId), - NodeIdMappedAddr(NodeIdMappedAddr), + NodeIdMappedAddr(AllPathsMappedAddr), IpPort(IpPort), } @@ -155,11 +155,11 @@ impl NodeMap { pub(super) fn receive_udp( &self, udp_addr: SocketAddr, - ) -> Option<(PublicKey, NodeIdMappedAddr)> { + ) -> Option<(PublicKey, AllPathsMappedAddr)> { self.inner.lock().expect("poisoned").receive_udp(udp_addr) } - pub(super) fn receive_relay(&self, relay_url: &RelayUrl, src: NodeId) -> NodeIdMappedAddr { + pub(super) fn receive_relay(&self, relay_url: &RelayUrl, src: NodeId) -> AllPathsMappedAddr { self.inner .lock() .expect("poisoned") @@ -169,7 +169,7 @@ impl NodeMap { pub(super) fn get_quic_mapped_addr_for_node_key( &self, node_key: NodeId, - ) -> Option { + ) -> Option { self.inner .lock() .expect("poisoned") @@ -186,6 +186,7 @@ impl NodeMap { .unwrap_or_default() } + /// Returns a [`NodeAddr`] with all the currently known direct addresses and the relay URL. pub(super) fn get_current_addr(&self, node_key: NodeId) -> Option { self.inner .lock() @@ -209,7 +210,7 @@ impl NodeMap { #[allow(clippy::type_complexity)] pub(super) fn get_send_addrs( &self, - addr: NodeIdMappedAddr, + addr: AllPathsMappedAddr, have_ipv6: bool, metrics: &Metrics, ) -> Option<( @@ -403,7 +404,7 @@ impl NodeMapInner { /// Marks the node we believe to be at `ipp` as recently used. #[cfg(not(wasm_browser))] - fn receive_udp(&mut self, udp_addr: SocketAddr) -> Option<(NodeId, NodeIdMappedAddr)> { + fn receive_udp(&mut self, udp_addr: SocketAddr) -> Option<(NodeId, AllPathsMappedAddr)> { let ip_port: IpPort = udp_addr.into(); let Some(node_state) = self.get_mut(NodeStateKey::IpPort(ip_port)) else { trace!(src=%udp_addr, "receive_udp: no node_state found for addr, ignore"); @@ -414,7 +415,7 @@ impl NodeMapInner { } #[instrument(skip_all, fields(src = %src.fmt_short()))] - fn receive_relay(&mut self, relay_url: &RelayUrl, src: NodeId) -> NodeIdMappedAddr { + fn receive_relay(&mut self, relay_url: &RelayUrl, src: NodeId) -> AllPathsMappedAddr { #[cfg(any(test, feature = "test-utils"))] let path_selection = self.path_selection; let node_state = self.get_or_insert_with(NodeStateKey::NodeId(src), || { diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 36ad6a0de13..3f211ed359a 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -20,7 +20,7 @@ use crate::endpoint::PathSelection; use crate::{ disco::{self, SendAddr}, magicsock::{ - ActorMessage, HEARTBEAT_INTERVAL, MagicsockMetrics, NodeIdMappedAddr, + ActorMessage, AllPathsMappedAddr, HEARTBEAT_INTERVAL, MagicsockMetrics, node_map::path_validity::PathValidity, }, }; @@ -63,7 +63,7 @@ pub(super) struct NodeState { /// [`NodeMap`]: super::NodeMap id: usize, /// The UDP address used on the QUIC-layer to address this node. - quic_mapped_addr: NodeIdMappedAddr, + quic_mapped_addr: AllPathsMappedAddr, /// The global identifier for this endpoint. node_id: NodeId, /// The url of relay node that we can relay over to communicate. @@ -113,7 +113,7 @@ pub(super) struct Options { impl NodeState { pub(super) fn new(id: usize, options: Options) -> Self { - let quic_mapped_addr = NodeIdMappedAddr::generate(); + let quic_mapped_addr = AllPathsMappedAddr::generate(); // TODO(frando): I don't think we need to track the `num_relay_conns_added` // metric here. We do so in `Self::addr_for_send`. @@ -148,7 +148,7 @@ impl NodeState { &self.node_id } - pub(super) fn quic_mapped_addr(&self) -> &NodeIdMappedAddr { + pub(super) fn quic_mapped_addr(&self) -> &AllPathsMappedAddr { &self.quic_mapped_addr } @@ -667,6 +667,7 @@ impl NodeState { (udp_addr, relay_url, ping_msgs) } + /// Returns a [`NodeAddr`] with all the currently known direct addresses and the relay URL. pub(crate) fn get_current_addr(&self) -> NodeAddr { // TODO: more selective? let mut node_addr = diff --git a/iroh/src/magicsock/relay_mapped_addrs.rs b/iroh/src/magicsock/relay_mapped_addrs.rs index b473501ea60..1b21f1377e2 100644 --- a/iroh/src/magicsock/relay_mapped_addrs.rs +++ b/iroh/src/magicsock/relay_mapped_addrs.rs @@ -15,16 +15,19 @@ use snafu::Snafu; #[snafu(display("Failed to convert"))] pub struct IpMappedAddrError; -/// A map fake Ipv6 address with an actual IP address. +/// An Ipv6 ULA address, identifying a relay path for a [`NodeId`]. /// -/// It is essentially a lookup key for an IP that iroh's magicsocket knows about. +/// Since iroh nodes are reachable via a relay server we have a network path indicated by +/// the `(NodeId, RelayUrl)`. However Quinn can only handle socket addresses, so we use +/// IPv6 addresses in a private IPv6 Unique Local Address range, which map to a unique +/// `(NodeId, RelayUrl)` pair. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub(crate) struct IpMappedAddr(Ipv6Addr); +pub(crate) struct RelayMappedAddr(Ipv6Addr); /// Counter to always generate unique addresses for [`IpMappedAddr`]. static IP_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); -impl IpMappedAddr { +impl RelayMappedAddr { /// The Prefix/L of our Unique Local Addresses. const ADDR_PREFIXL: u8 = 0xfd; /// The Global ID used in our Unique Local Addresses. @@ -51,19 +54,19 @@ impl IpMappedAddr { Self(Ipv6Addr::from(addr)) } - /// Returns a consistent [`SocketAddr`] for the [`IpMappedAddr`]. + /// Returns a consistent [`SocketAddr`] for the [`RelayMappedAddr`]. /// /// This does not have a routable IP address. /// - /// This uses a made-up, but fixed port number. The [IpMappedAddresses`] map this is - /// made for creates a unique [`IpMappedAddr`] for each IP+port and thus does not use - /// the port to map back to the original [`SocketAddr`]. + /// This uses a made-up, but fixed port number. The [`RelayAddrMap`] creates a unique + /// [`RelayMappedAddr`] for each `(NodeId, RelayUrl)` pair and thus does not use the + /// port to map back to the original [`SocketAddr`]. pub(crate) fn private_socket_addr(&self) -> SocketAddr { SocketAddr::new(IpAddr::from(self.0), Self::MAPPED_ADDR_PORT) } } -impl TryFrom for IpMappedAddr { +impl TryFrom for RelayMappedAddr { type Error = IpMappedAddrError; fn try_from(value: Ipv6Addr) -> std::result::Result { @@ -78,7 +81,7 @@ impl TryFrom for IpMappedAddr { } } -impl std::fmt::Display for IpMappedAddr { +impl std::fmt::Display for RelayMappedAddr { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "IpMappedAddr({})", self.0) } @@ -87,31 +90,31 @@ impl std::fmt::Display for IpMappedAddr { /// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] #[derive(Debug, Snafu)] #[snafu(display("Failed to convert"))] -pub struct RelayMappedAddrError; +pub struct RelayAddrMapError; -/// A Map of [`RelayMappedAddresses`] to [`SocketAddr`]. +/// A Map of [`RelayMappedAddr`] to `(RelayUrl, NodeId)`. #[derive(Debug, Clone, Default)] -pub(crate) struct RelayMappedAddresses(Arc>); +pub(crate) struct RelayAddrMap(Arc>); #[derive(Debug, Default)] pub(super) struct Inner { - by_mapped_addr: BTreeMap, - by_url: BTreeMap<(RelayUrl, NodeId), IpMappedAddr>, + by_mapped_addr: BTreeMap, + by_url: BTreeMap<(RelayUrl, NodeId), RelayMappedAddr>, } -impl RelayMappedAddresses { - /// Adds a [`RelayUrl`] to the map and returns the generated [`IpMappedAddr`]. +impl RelayAddrMap { + /// Adds a new entry to the map and returns the generated [`RelayMappedAddr`]. /// - /// If this [`RelayUrl`] already exists in the map, it returns its - /// associated [`IpMappedAddr`]. + /// If this `(RelayUrl, NodeId)` already exists in the map, it returns its associated + /// [`RelayMappedAddr`]. /// - /// Otherwise a new [`IpMappedAddr`] is generated for it and returned. - pub(super) fn get_or_register(&self, relay: RelayUrl, node: NodeId) -> IpMappedAddr { + /// Otherwise a new [`RelayMappedAddr`] is generated for it and returned. + pub(super) fn get_or_register(&self, relay: RelayUrl, node: NodeId) -> RelayMappedAddr { let mut inner = self.0.lock().expect("poisoned"); if let Some(mapped_addr) = inner.by_url.get(&(relay.clone(), node)) { return *mapped_addr; } - let ip_mapped_addr = IpMappedAddr::generate(); + let ip_mapped_addr = RelayMappedAddr::generate(); inner .by_mapped_addr .insert(ip_mapped_addr, (relay.clone(), node)); @@ -119,14 +122,14 @@ impl RelayMappedAddresses { ip_mapped_addr } - /// Returns the [`IpMappedAddr`] for the given [`RelayUrl`]. - pub(crate) fn get_mapped_addr(&self, relay: RelayUrl, node: NodeId) -> Option { + /// Returns the [`RelayMappedAddr`] for the given [`RelayUrl`] and [`NodeId`]. + pub(crate) fn get_mapped_addr(&self, relay: RelayUrl, node: NodeId) -> Option { let inner = self.0.lock().expect("poisoned"); inner.by_url.get(&(relay, node)).copied() } - /// Returns the [`RelayUrl`] for the given [`IpMappedAddr`]. - pub(crate) fn get_url(&self, mapped_addr: &IpMappedAddr) -> Option<(RelayUrl, NodeId)> { + /// Returns the [`RelayUrl`] and [`NodeId`] for the given [`IpMappedAddr`]. + pub(crate) fn get_url(&self, mapped_addr: &RelayMappedAddr) -> Option<(RelayUrl, NodeId)> { let inner = self.0.lock().expect("poisoned"); inner.by_mapped_addr.get(mapped_addr).cloned() } From f9924cd5c9e106a322122b01d67be2fa69f10d67 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 29 Aug 2025 15:08:57 +0200 Subject: [PATCH 024/164] switch to main multipath branch --- Cargo.lock | 61 ++++++++++++------------------------------------------ Cargo.toml | 6 +----- 2 files changed, 14 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc60e20fe7a..6c1feec38b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,12 +497,6 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" -[[package]] -name = "bytemuck" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" - [[package]] name = "byteorder" version = "1.5.0" @@ -1236,14 +1230,13 @@ checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] name = "fastbloom" -version = "0.9.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8" +checksum = "29ec576c163744bef8707859f6aeb322bcf56b8da61215d99f77d6e33160ff01" dependencies = [ "getrandom 0.3.2", "rand 0.9.1", "siphasher", - "wide", ] [[package]] @@ -2489,7 +2482,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com//n0-computer/quinn?branch=server-migrations#bc86957aa4ccb72fad70e75a6ce9fc8198f09afc" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a68cb468bb44b1ec83a1b6ba110b2e7ecae00d91" dependencies = [ "bytes", "cfg_aliases", @@ -2498,7 +2491,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.12", "tokio", "tracing", @@ -2508,7 +2501,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com//n0-computer/quinn?branch=server-migrations#bc86957aa4ccb72fad70e75a6ce9fc8198f09afc" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a68cb468bb44b1ec83a1b6ba110b2e7ecae00d91" dependencies = [ "bytes", "fastbloom", @@ -2530,12 +2523,12 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com//n0-computer/quinn?branch=server-migrations#bc86957aa4ccb72fad70e75a6ce9fc8198f09afc" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a68cb468bb44b1ec83a1b6ba110b2e7ecae00d91" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", "windows-sys 0.59.0", ] @@ -4110,8 +4103,8 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" -source = "git+https://github.com/n0-computer/rustls?rev=be02113e7837df60953d02c2bdd0f4634fef3a80#be02113e7837df60953d02c2bdd0f4634fef3a80" +version = "0.23.27" +source = "git+https://github.com/n0-computer/rustls?rev=c636f89ae00aee19ddd5e6df4150cec5c031fa31#c636f89ae00aee19ddd5e6df4150cec5c031fa31" dependencies = [ "log", "once_cell", @@ -4190,9 +4183,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4937d110d34408e9e5ad30ba0b0ca3b6a8a390f8db3636db60144ac4fa792750" +checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" dependencies = [ "core-foundation 0.10.0", "core-foundation-sys", @@ -4205,7 +4198,7 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-root-certs 0.26.11", + "webpki-root-certs", "windows-sys 0.59.0", ] @@ -4250,15 +4243,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "safe_arch" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" -dependencies = [ - "bytemuck", -] - [[package]] name = "salsa20" version = "0.10.2" @@ -5630,15 +5614,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" -dependencies = [ - "webpki-root-certs 1.0.0", -] - [[package]] name = "webpki-root-certs" version = "1.0.0" @@ -5666,16 +5641,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "wide" -version = "0.7.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" -dependencies = [ - "bytemuck", - "safe_arch", -] - [[package]] name = "widestring" version = "1.2.0" @@ -5704,7 +5669,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8e7b8f11f49..5d06c0f4eaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,14 +43,10 @@ unused-async = "warn" [patch.crates-io] -rustls = { git = "https://github.com/n0-computer/rustls", rev = "be02113e7837df60953d02c2bdd0f4634fef3a80" } +rustls = { git = "https://github.com/n0-computer/rustls", rev = "c636f89ae00aee19ddd5e6df4150cec5c031fa31" } netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-multipath" } [patch."https://github.com/n0-computer/quinn"] # iroh-quinn = { path = "../iroh-quinn/quinn" } # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } - -iroh-quinn = { git = "https://github.com//n0-computer/quinn", branch = "server-migrations" } -iroh-quinn-proto = { git = "https://github.com//n0-computer/quinn", branch = "server-migrations" } -iroh-quinn-udp = { git = "https://github.com//n0-computer/quinn", branch = "server-migrations" } From 3058a8e2b1a28407eb170244f7c9eaf14c9a7ce7 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 29 Aug 2025 15:09:39 +0200 Subject: [PATCH 025/164] another rename --- iroh/src/endpoint.rs | 9 +++++---- iroh/src/magicsock.rs | 2 +- iroh/src/magicsock/node_map.rs | 17 ++++++++++------- iroh/src/magicsock/node_map/node_state.rs | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index a6ba61c4bce..708510a5e34 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -728,10 +728,11 @@ impl Endpoint { } let node_id = node_addr.node_id; - // Get the mapped IPv6 address from the magic socket. Quinn will connect to this - // address. Start discovery for this node if it's enabled and we have no valid or - // verified address information for this node. Dropping the discovery cancels any - // still running task. + // When we start a connection we want to send the QUIC Initial packets on all the + // known paths for the remote node. For this we use an AllPathsMappedAddr as + // destination for Quinn. Start discovery for this node if it's enabled and we have + // no valid or verified address information for this node. Dropping the discovery + // cancels any still running task. let (mapped_addr, _discovery_drop_guard) = self .get_mapping_addr_and_maybe_start_discovery(node_addr) .await diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index c5c57055c0c..c453527df15 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -431,7 +431,7 @@ impl MagicSock { /// Returns the socket address which can be used by the QUIC layer to dial this node. pub(crate) fn get_mapping_addr(&self, node_id: NodeId) -> Option { - self.node_map.get_quic_mapped_addr_for_node_key(node_id) + self.node_map.get_all_paths_add_for_node(node_id) } pub(crate) fn get_direct_addrs(&self, node_id: NodeId) -> Vec { diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 36d4b9cdfba..69d313b9717 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -166,7 +166,7 @@ impl NodeMap { .receive_relay(relay_url, src) } - pub(super) fn get_quic_mapped_addr_for_node_key( + pub(super) fn get_all_paths_add_for_node( &self, node_key: NodeId, ) -> Option { @@ -174,7 +174,7 @@ impl NodeMap { .lock() .expect("poisoned") .get(NodeStateKey::NodeId(node_key)) - .map(|ep| *ep.quic_mapped_addr()) + .map(|ep| *ep.all_paths_mapped_addr()) } pub(super) fn get_direct_addrs(&self, node_key: NodeId) -> Vec { @@ -359,7 +359,7 @@ impl NodeMapInner { node.remove_direct_addr(&ipp, now, why); if node.direct_addresses().count() == 0 { let node_id = node.public_key(); - let mapped_addr = node.quic_mapped_addr(); + let mapped_addr = node.all_paths_mapped_addr(); self.by_node_key.remove(node_id); self.by_quic_mapped_addr.remove(mapped_addr); debug!(node_id=%node_id.fmt_short(), why, "removing node"); @@ -411,7 +411,10 @@ impl NodeMapInner { return None; }; node_state.receive_udp(ip_port, Instant::now()); - Some((*node_state.public_key(), *node_state.quic_mapped_addr())) + Some(( + *node_state.public_key(), + *node_state.all_paths_mapped_addr(), + )) } #[instrument(skip_all, fields(src = %src.fmt_short()))] @@ -430,7 +433,7 @@ impl NodeMapInner { } }); node_state.receive_relay(relay_url, src, Instant::now()); - *node_state.quic_mapped_addr() + *node_state.all_paths_mapped_addr() } fn node_states(&self) -> impl Iterator { @@ -501,7 +504,7 @@ impl NodeMapInner { // update indices self.by_quic_mapped_addr - .insert(*node_state.quic_mapped_addr(), id); + .insert(*node_state.all_paths_mapped_addr(), id); self.by_node_key.insert(*node_state.public_key(), id); self.by_id.insert(id, node_state); @@ -572,7 +575,7 @@ impl NodeMapInner { self.by_ip_port.remove(&ip_port); } - self.by_quic_mapped_addr.remove(ep.quic_mapped_addr()); + self.by_quic_mapped_addr.remove(ep.all_paths_mapped_addr()); } } } diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 3f211ed359a..ec28bc5969b 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -148,7 +148,7 @@ impl NodeState { &self.node_id } - pub(super) fn quic_mapped_addr(&self) -> &AllPathsMappedAddr { + pub(super) fn all_paths_mapped_addr(&self) -> &AllPathsMappedAddr { &self.quic_mapped_addr } From 11dd04d84cc9aaf0437c3bd5fdb55d21727ea857 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 29 Aug 2025 18:05:46 +0200 Subject: [PATCH 026/164] Set max_idle_time to a good value This is what we used to do --- iroh/src/endpoint.rs | 1 - iroh/src/magicsock.rs | 15 +++++++++++---- iroh/src/magicsock/node_map.rs | 4 ++-- iroh/src/magicsock/node_map/node_state.rs | 6 +++--- iroh/src/magicsock/node_map/path_state.rs | 5 +---- iroh/src/magicsock/node_map/udp_paths.rs | 3 +-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 708510a5e34..c3a73b74ab7 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -627,7 +627,6 @@ impl Endpoint { trace!("created magicsock"); debug!(version = env!("CARGO_PKG_VERSION"), "iroh Endpoint created"); - let metrics = msock.metrics.magicsock.clone(); let ep = Self { msock, static_config: Arc::new(static_config), diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index c453527df15..165bf65d949 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -94,8 +94,18 @@ pub use self::{ /// expire at 30 seconds, so this is a few seconds shy of that. const ENDPOINTS_FRESH_ENOUGH_DURATION: Duration = Duration::from_secs(27); +/// The duration in which we send keep-alives. +/// +/// If a path is idle for this long, a PING frame will be sent to keep the connection +/// alive. const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +/// The maximum time a path can stay idle before being closed. +/// +/// This is [`HEARTBEAT_INTERVAL`] + 1.5s. This gives us a chance to send a PING frame and +/// some retries. +const MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(6500); + /// Contains options for `MagicSock::listen`. #[derive(derive_more::Debug)] pub(crate) struct Options { @@ -491,10 +501,7 @@ impl MagicSock { .await { Ok(path) => { - path.set_max_idle_timeout(Some( - ENDPOINTS_FRESH_ENOUGH_DURATION, - )) - .ok(); + path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); } Err(err) => { diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 69d313b9717..e05d96029ff 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -11,8 +11,8 @@ use serde::{Deserialize, Serialize}; use tracing::{debug, info, instrument, trace, warn}; use self::node_state::{NodeState, Options}; -use super::{ActorMessage, AllPathsMappedAddr, metrics::Metrics, transports}; -use crate::disco::{CallMeMaybe, Pong, SendAddr}; +use super::{AllPathsMappedAddr, metrics::Metrics}; +use crate::disco::CallMeMaybe; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index ec28bc5969b..b86d2f8802b 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -20,7 +20,7 @@ use crate::endpoint::PathSelection; use crate::{ disco::{self, SendAddr}, magicsock::{ - ActorMessage, AllPathsMappedAddr, HEARTBEAT_INTERVAL, MagicsockMetrics, + AllPathsMappedAddr, HEARTBEAT_INTERVAL, MagicsockMetrics, node_map::path_validity::PathValidity, }, }; @@ -921,12 +921,12 @@ pub enum ConnectionType { #[cfg(test)] mod tests { - use std::{collections::BTreeMap, net::Ipv4Addr}; + use std::net::Ipv4Addr; use iroh_base::SecretKey; use super::*; - use crate::magicsock::node_map::{NodeMap, NodeMapInner}; + // use crate::magicsock::node_map::{NodeMap, NodeMapInner}; // #[test] // fn test_remote_infos() { diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index ce3121539b0..7fd4fc7fe78 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -11,10 +11,7 @@ use super::{ }; use crate::{ disco::SendAddr, - magicsock::{ - HEARTBEAT_INTERVAL, - node_map::path_validity::{self, PathValidity}, - }, + magicsock::node_map::path_validity::{self, PathValidity}, }; /// State about a particular path to another [`NodeState`]. diff --git a/iroh/src/magicsock/node_map/udp_paths.rs b/iroh/src/magicsock/node_map/udp_paths.rs index d89cf10ed2c..2c72a95c842 100644 --- a/iroh/src/magicsock/node_map/udp_paths.rs +++ b/iroh/src/magicsock/node_map/udp_paths.rs @@ -7,8 +7,7 @@ //! [`NodeState`]: super::node_state::NodeState use std::{collections::BTreeMap, net::SocketAddr}; -use n0_future::time::{Duration, Instant}; -use rand::seq::IteratorRandom; +use n0_future::time::Instant; use tracing::{Level, event}; use super::{IpPort, path_state::PathState}; From 6869faaf04204e60864847d037367d939a540ae4 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 1 Sep 2025 14:49:17 +0200 Subject: [PATCH 027/164] fix typo --- iroh/src/magicsock.rs | 4 ++-- iroh/src/magicsock/node_map.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 165bf65d949..fe6efdf12d8 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -439,9 +439,9 @@ impl MagicSock { self.node_map.conn_type(node_id) } - /// Returns the socket address which can be used by the QUIC layer to dial this node. + /// Returns the socket address which can be used by the QUIC layer to *dial* this node. pub(crate) fn get_mapping_addr(&self, node_id: NodeId) -> Option { - self.node_map.get_all_paths_add_for_node(node_id) + self.node_map.get_all_paths_addr_for_node(node_id) } pub(crate) fn get_direct_addrs(&self, node_id: NodeId) -> Vec { diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index e05d96029ff..96acd7ea146 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -166,7 +166,7 @@ impl NodeMap { .receive_relay(relay_url, src) } - pub(super) fn get_all_paths_add_for_node( + pub(super) fn get_all_paths_addr_for_node( &self, node_key: NodeId, ) -> Option { From 539a514e83946a370f3bc4a8f6d9069ec752277b Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 16 Sep 2025 11:36:24 +0200 Subject: [PATCH 028/164] Start hooking up a new NodeStateActor This actor is responsible for sending Initial packets via the AllPathsMappedAddr (NodeIdMappedAddr). Split up Transports into MagicTransports (which replaces the UdpSender). This makes it possible to have Transports not depend on MagicSock. Which in turns allows us to have a TransportsSender in the NodeMap. --- iroh/src/magicsock.rs | 303 ++++++---------- iroh/src/magicsock/node_map.rs | 217 ++++++++++-- iroh/src/magicsock/node_map/node_state.rs | 109 +++++- iroh/src/magicsock/relay_mapped_addrs.rs | 1 + iroh/src/magicsock/transports.rs | 413 +++++++++++++++++----- 5 files changed, 730 insertions(+), 313 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index fe6efdf12d8..8f8a75ff019 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -20,12 +20,10 @@ use std::{ fmt::Display, io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, - pin::Pin, sync::{ Arc, Mutex, RwLock, atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, }, - task::{Context, Poll}, }; use bytes::Bytes; @@ -42,17 +40,16 @@ use nested_enum_utils::common_fields; use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; -use quinn::{AsyncUdpSocket, ServerConfig, WeakConnectionHandle}; +use quinn::{ServerConfig, WeakConnectionHandle}; use rand::Rng; use relay_mapped_addrs::{RelayAddrMap, RelayMappedAddr}; -use smallvec::SmallVec; use snafu::{ResultExt, Snafu}; use tokio::sync::{Mutex as AsyncMutex, mpsc}; use tokio_util::sync::CancellationToken; use tracing::{ - Instrument, Level, debug, error, event, info, info_span, instrument, trace, trace_span, warn, + Instrument, Level, debug, event, info, info_span, instrument, trace, trace_span, warn, }; -use transports::LocalAddrsWatch; +use transports::{LocalAddrsWatch, MagicTransport}; use url::Url; #[cfg(not(wasm_browser))] @@ -60,7 +57,7 @@ use self::transports::IpTransport; use self::{ metrics::Metrics as MagicsockMetrics, node_map::{NodeMap, PingAction}, - transports::{RelayActorConfig, RelayTransport, Transports, UdpSender}, + transports::{RelayActorConfig, RelayTransport, Transports, TransportsSender}, }; #[cfg(not(wasm_browser))] use crate::dns::DnsResolver; @@ -78,9 +75,9 @@ use crate::{ }; mod metrics; -mod node_map; mod relay_mapped_addrs; +pub(crate) mod node_map; pub(crate) mod transports; pub use node_map::Source; @@ -628,106 +625,6 @@ impl MagicSock { } } - /// Returns the transport addresses for the [`quinn_udp::Transmit`]'s destination. - /// - /// Because Quinn does only know about IP transports we map other transports to private - /// IPv6 Unique Local Address ranges. This extracts the transport addresses out of the - /// transmit's destination. - #[instrument(skip_all)] - fn prepare_send( - &self, - transmit: &quinn_udp::Transmit, - ) -> io::Result> { - self.metrics - .magicsock - .send_data - .inc_by(transmit.contents.len() as _); - - if self.is_closed() { - self.metrics - .magicsock - .send_data_network_down - .inc_by(transmit.contents.len() as _); - return Err(io::Error::new( - io::ErrorKind::NotConnected, - "connection closed", - )); - } - - let mut active_paths = SmallVec::<[_; 3]>::new(); - - match MultipathMappedAddr::from(transmit.destination) { - MultipathMappedAddr::Mixed(dest) => { - trace!( - dst = %dest, - src = ?transmit.src_ip, - len = %transmit.contents.len(), - "sending mixed", - ); - - // Get the node's relay address and best direct address, as well - // as any pings that need to be sent for hole-punching purposes. - match self.node_map.get_send_addrs( - dest, - self.ipv6_reported.load(Ordering::Relaxed), - &self.metrics.magicsock, - ) { - Some((node_id, _udp_addr, _relay_url, ping_actions)) => { - if !ping_actions.is_empty() { - self.actor_sender - .try_send(ActorMessage::PingActions(ping_actions)) - .ok(); - } - - if let Some(addr) = self.node_map.get_current_addr(node_id) { - // Mixed will send all available addrs - if let Some(ref url) = addr.relay_url { - active_paths.push(transports::Addr::Relay(url.clone(), node_id)); - } - for ip in addr.direct_addresses() { - active_paths.push(transports::Addr::Ip(*ip)); - } - } - } - None => { - error!(%dest, "no NodeState for mapped address"); - } - } - } - #[cfg(not(wasm_browser))] - MultipathMappedAddr::Ip(addr) => { - trace!( - dst = %addr, - src = ?transmit.src_ip, - len = %transmit.contents.len(), - "sending IP", - ); - active_paths.push(transports::Addr::Ip(addr)); - } - MultipathMappedAddr::Relay(dest) => { - trace!( - dst = %dest, - src = ?transmit.src_ip, - len = %transmit.contents.len(), - "sending relay", - ); - - // Check if this is a known IpMappedAddr, and if so, send over UDP - // Get the socket addr - match self.relay_mapped_addrs.get_url(&dest) { - Some((relay, node_id)) => { - active_paths.push(transports::Addr::Relay(relay, node_id)); - } - None => { - error!(%dest, "unknown mapped address"); - } - } - } - } - - Ok(active_paths) - } - /// Process datagrams received from all the transports. /// /// All the `bufs` and `metas` should have initialized packets in them. @@ -978,7 +875,7 @@ impl MagicSock { /// Send the given ping actions out. async fn send_ping_actions( &self, - _sender: &UdpSender, + _sender: &TransportsSender, msgs: Vec, ) -> io::Result<()> { for msg in msgs { @@ -1033,7 +930,7 @@ impl MagicSock { /// Sends out a disco message. async fn send_disco_message( &self, - sender: &UdpSender, + sender: &TransportsSender, dst: SendAddr, dst_key: PublicKey, msg: disco::Message, @@ -1104,6 +1001,7 @@ impl MagicSock { #[derive(Clone, Debug)] pub(crate) enum MultipathMappedAddr { /// Used for the initial connection. + /// /// - Only used for sending /// - This means send on all known paths/transports Mixed(AllPathsMappedAddr), @@ -1119,12 +1017,12 @@ impl From for MultipathMappedAddr { match value.ip() { IpAddr::V4(_) => Self::Ip(value), IpAddr::V6(addr) => { - if let Ok(node_id_mapped_addr) = AllPathsMappedAddr::try_from(addr) { - return Self::Mixed(node_id_mapped_addr); + if let Ok(addr) = AllPathsMappedAddr::try_from(addr) { + return Self::Mixed(addr); } #[cfg(not(wasm_browser))] - if let Ok(ip_mapped_addr) = RelayMappedAddr::try_from(addr) { - return Self::Relay(ip_mapped_addr); + if let Ok(addr) = RelayMappedAddr::try_from(addr) { + return Self::Relay(addr); } Self::Ip(value) } @@ -1318,13 +1216,6 @@ impl Handle { let (actor_sender, actor_receiver) = mpsc::channel(256); - // load the node data - let node_map = node_map.unwrap_or_default(); - #[cfg(any(test, feature = "test-utils"))] - let node_map = NodeMap::load_from_vec(node_map, path_selection, &metrics.magicsock); - #[cfg(not(any(test, feature = "test-utils")))] - let node_map = NodeMap::load_from_vec(node_map, &metrics.magicsock); - let my_relay = Watchable::new(None); let ipv6_reported = Arc::new(AtomicBool::new(false)); let max_receive_segments = Arc::new(AtomicUsize::new(1)); @@ -1352,6 +1243,21 @@ impl Handle { #[cfg(wasm_browser)] let transports = Transports::new(relay_transports, max_receive_segments); + let node_map = { + let node_map = node_map.unwrap_or_default(); + let sender = transports.create_sender(); + #[cfg(any(test, feature = "test-utils"))] + let nm = NodeMap::load_from_vec(node_map, path_selection, &metrics.magicsock, sender); + #[cfg(not(any(test, feature = "test-utils")))] + let nm = NodeMap::load_from_vec(node_map, &metrics.magicsock, sender); + nm + }; + // let node_map = node_map.unwrap_or_default(); + // #[cfg(any(test, feature = "test-utils"))] + // let node_map = NodeMap::load_from_vec(node_map, path_selection, &metrics.magicsock); + // #[cfg(not(any(test, feature = "test-utils")))] + // let node_map = NodeMap::load_from_vec(node_map, &metrics.magicsock); + let (disco, disco_receiver) = DiscoState::new(secret_encryption_key); let msock = Arc::new(MagicSock { @@ -1385,17 +1291,14 @@ impl Handle { // the packet if grease_quic_bit is set to false. endpoint_config.grease_quic_bit(false); - let sender = transports.create_sender(msock.clone()); + let sender = transports.create_sender(); let local_addrs_watch = transports.local_addrs_watch(); let network_change_sender = transports.create_network_change_sender(); let endpoint = quinn::Endpoint::new_with_abstract_socket( endpoint_config, Some(server_config), - Box::new(MagicUdpSocket { - socket: msock.clone(), - transports, - }), + Box::new(MagicTransport::new(msock.clone(), transports)), #[cfg(not(wasm_browser))] Arc::new(quinn::TokioRuntime), #[cfg(wasm_browser)] @@ -1645,65 +1548,65 @@ enum DiscoBoxError { }, } -#[derive(Debug)] -struct MagicUdpSocket { - socket: Arc, - transports: Transports, -} - -impl AsyncUdpSocket for MagicUdpSocket { - fn create_sender(&self) -> Pin> { - Box::pin(self.transports.create_sender(self.socket.clone())) - } - - /// NOTE: Receiving on a closed socket will return [`Poll::Pending`] indefinitely. - fn poll_recv( - &mut self, - cx: &mut Context, - bufs: &mut [io::IoSliceMut<'_>], - metas: &mut [quinn_udp::RecvMeta], - ) -> Poll> { - self.transports.poll_recv(cx, bufs, metas, &self.socket) - } - - #[cfg(not(wasm_browser))] - fn local_addr(&self) -> io::Result { - let addrs: Vec<_> = self - .transports - .local_addrs() - .into_iter() - .filter_map(|addr| { - let addr: SocketAddr = addr.into_socket_addr()?; - Some(addr) - }) - .collect(); - - if let Some(addr) = addrs.iter().find(|addr| addr.is_ipv6()) { - return Ok(*addr); - } - if let Some(SocketAddr::V4(addr)) = addrs.first() { - // Pretend to be IPv6, because our `MappedAddr`s need to be IPv6. - let ip = addr.ip().to_ipv6_mapped().into(); - return Ok(SocketAddr::new(ip, addr.port())); - } - - Err(io::Error::other("no valid address available")) - } - - #[cfg(wasm_browser)] - fn local_addr(&self) -> io::Result { - // Again, we need to pretend we're IPv6, because of our `MappedAddr`s. - Ok(SocketAddr::new(std::net::Ipv6Addr::LOCALHOST.into(), 0)) - } - - fn max_receive_segments(&self) -> usize { - self.transports.max_receive_segments() - } - - fn may_fragment(&self) -> bool { - self.transports.may_fragment() - } -} +// #[derive(Debug)] +// struct MagicUdpSocket { +// socket: Arc, +// transports: Transports, +// } + +// impl AsyncUdpSocket for MagicUdpSocket { +// fn create_sender(&self) -> Pin> { +// Box::pin(self.transports.create_sender(self.socket.clone())) +// } + +// /// NOTE: Receiving on a closed socket will return [`Poll::Pending`] indefinitely. +// fn poll_recv( +// &mut self, +// cx: &mut Context, +// bufs: &mut [io::IoSliceMut<'_>], +// metas: &mut [quinn_udp::RecvMeta], +// ) -> Poll> { +// self.transports.poll_recv(cx, bufs, metas, &self.socket) +// } + +// #[cfg(not(wasm_browser))] +// fn local_addr(&self) -> io::Result { +// let addrs: Vec<_> = self +// .transports +// .local_addrs() +// .into_iter() +// .filter_map(|addr| { +// let addr: SocketAddr = addr.into_socket_addr()?; +// Some(addr) +// }) +// .collect(); + +// if let Some(addr) = addrs.iter().find(|addr| addr.is_ipv6()) { +// return Ok(*addr); +// } +// if let Some(SocketAddr::V4(addr)) = addrs.first() { +// // Pretend to be IPv6, because our `MappedAddr`s need to be IPv6. +// let ip = addr.ip().to_ipv6_mapped().into(); +// return Ok(SocketAddr::new(ip, addr.port())); +// } + +// Err(io::Error::other("no valid address available")) +// } + +// #[cfg(wasm_browser)] +// fn local_addr(&self) -> io::Result { +// // Again, we need to pretend we're IPv6, because of our `MappedAddr`s. +// Ok(SocketAddr::new(std::net::Ipv6Addr::LOCALHOST.into(), 0)) +// } + +// fn max_receive_segments(&self) -> usize { +// self.transports.max_receive_segments() +// } + +// fn may_fragment(&self) -> bool { +// self.transports.may_fragment() +// } +// } #[derive(Debug)] enum ActorMessage { @@ -1788,7 +1691,7 @@ impl Actor { mut self, shutdown_token: CancellationToken, mut watcher: impl Watcher> + Send + Sync, - sender: UdpSender, + sender: TransportsSender, ) { // Initialize addresses #[cfg(not(wasm_browser))] @@ -1991,7 +1894,7 @@ impl Actor { } #[instrument(skip_all)] - async fn handle_ping_actions(&mut self, sender: &UdpSender, msgs: Vec) { + async fn handle_ping_actions(&mut self, sender: &TransportsSender, msgs: Vec) { if let Err(err) = self.msock.send_ping_actions(sender, msgs).await { warn!("Failed to send ping actions: {err:#}"); } @@ -2000,7 +1903,7 @@ impl Actor { /// Processes an incoming actor message. /// /// Returns `true` if it was a shutdown. - async fn handle_actor_message(&mut self, msg: ActorMessage, sender: &UdpSender) { + async fn handle_actor_message(&mut self, msg: ActorMessage, sender: &TransportsSender) { match msg { ActorMessage::NetworkChange => { self.network_monitor.network_change().await.ok(); @@ -2321,19 +2224,19 @@ impl DiscoveredDirectAddrs { /// to this address, and duplicate all packets for this address to send on all paths we know /// for the node. /// -/// It is but a newtype. And in our QUIC-facing socket APIs like [`AsyncUdpSocket`] it -/// comes in as the inner [`Ipv6Addr`], in those interfaces we have to be careful to do -/// the conversion to this type. +/// It is but a newtype around an IPv6 Unique Local Addr. And in our QUIC-facing socket +/// APIs like [`AsyncUdpSocket`] it comes in as the inner [`Ipv6Addr`], in those interfaces +/// we have to be careful to do the conversion to this type. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct AllPathsMappedAddr(Ipv6Addr); -/// Can occur when converting a [`SocketAddr`] to an [`NodeIdMappedAddr`] +/// Can occur when converting a [`SocketAddr`] to an [`AllPathsMappedAddr`] #[derive(Debug, Snafu)] #[snafu(display("Failed to convert"))] -pub struct NodeIdMappedAddrError; +pub struct AllPathsMappedAddrError; -/// Counter to always generate unique addresses for [`NodeIdMappedAddr`]. -static NODE_ID_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); +/// Counter to always generate unique addresses for [`AllPathsMappedAddr`]. +static ALL_PATHS_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); impl AllPathsMappedAddr { /// The Prefix/L of our Unique Local Addresses. @@ -2344,7 +2247,7 @@ impl AllPathsMappedAddr { const ADDR_SUBNET: [u8; 2] = [0; 2]; /// The dummy port used for all [`NodeIdMappedAddr`]s. - const NODE_ID_MAPPED_PORT: u16 = 12345; + const MAPPED_PORT: u16 = 12345; /// Generates a globally unique fake UDP address. /// @@ -2355,7 +2258,7 @@ impl AllPathsMappedAddr { addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID); addr[6..8].copy_from_slice(&Self::ADDR_SUBNET); - let counter = NODE_ID_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); + let counter = ALL_PATHS_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); addr[8..16].copy_from_slice(&counter.to_be_bytes()); Self(Ipv6Addr::from(addr)) @@ -2369,12 +2272,12 @@ impl AllPathsMappedAddr { /// the node in the [`NodeMap`]. This socket address is only to be used to pass into /// Quinn. pub(crate) fn private_socket_addr(&self) -> SocketAddr { - SocketAddr::new(IpAddr::from(self.0), Self::NODE_ID_MAPPED_PORT) + SocketAddr::new(IpAddr::from(self.0), Self::MAPPED_PORT) } } impl TryFrom for AllPathsMappedAddr { - type Error = NodeIdMappedAddrError; + type Error = AllPathsMappedAddrError; fn try_from(value: Ipv6Addr) -> Result { let octets = value.octets(); @@ -2384,7 +2287,7 @@ impl TryFrom for AllPathsMappedAddr { { return Ok(Self(value)); } - Err(NodeIdMappedAddrError) + Err(AllPathsMappedAddrError) } } diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 96acd7ea146..c7919859684 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -6,12 +6,22 @@ use std::{ }; use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; -use n0_future::time::Instant; +use n0_future::{task::AbortOnDropHandle, time::Instant}; +use node_state::NodeStateHandle; use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc; use tracing::{debug, info, instrument, trace, warn}; use self::node_state::{NodeState, Options}; -use super::{AllPathsMappedAddr, metrics::Metrics}; +#[cfg(any(test, feature = "test-utils"))] +use super::transports::TransportsSender; +#[cfg(not(any(test, feature = "test-utils")))] +use super::transports::TransportsSender; +use super::{ + AllPathsMappedAddr, + metrics::Metrics, + transports::{self, OwnedTransmit}, +}; use crate::disco::CallMeMaybe; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; @@ -21,7 +31,8 @@ mod path_state; mod path_validity; mod udp_paths; -pub(super) use node_state::PingAction; +pub(super) use node_state::{NodeStateMessage, PingAction}; + pub use node_state::{ConnectionType, ControlMsg, DirectAddrInfo, RemoteInfo}; /// Number of nodes that are inactive for which we keep info about. This limit is enforced @@ -45,13 +56,15 @@ const MAX_INACTIVE_NODES: usize = 30; /// These come and go as the node moves around on the internet /// /// An index of nodeInfos by node key, NodeIdMappedAddr, and discovered ip:port endpoints. -#[derive(Debug, Default)] +#[derive(Debug)] pub(super) struct NodeMap { inner: Mutex, } -#[derive(Default, Debug)] +#[derive(Debug)] pub(super) struct NodeMapInner { + /// Handle to an actor that can send over the transports. + transports_handle: TransportsSenderHandle, by_node_key: HashMap, by_ip_port: HashMap, by_quic_mapped_addr: HashMap, @@ -59,6 +72,14 @@ pub(super) struct NodeMapInner { next_id: usize, #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, + /// The [`NodeStateActor`] for each remote node. + node_states: HashMap, + /// The [`AllPathsMappedAddr`] for each node. + node_addrs: HashMap, + /// The reverse of mapping of [`Self::node_addrs`]. + node_addrs_lookup: HashMap, + // /// The [`RelayMappedAddr`] for each node. + // relay_addrs: HashMap, } /// Identifier to look up a [`NodeState`] in the [`NodeMap`]. @@ -116,10 +137,19 @@ pub enum Source { } impl NodeMap { + #[cfg(any(test, feature = "test-utils"))] + pub(super) fn new(sender: TransportsSender) -> Self { + Self::from_inner(NodeMapInner::new(sender)) + } + #[cfg(not(any(test, feature = "test-utils")))] /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. - pub(super) fn load_from_vec(nodes: Vec, metrics: &Metrics) -> Self { - Self::from_inner(NodeMapInner::load_from_vec(nodes, metrics)) + pub(super) fn load_from_vec( + nodes: Vec, + metrics: &Metrics, + sender: TransportsSender, + ) -> Self { + Self::from_inner(NodeMapInner::load_from_vec(nodes, metrics, sender)) } #[cfg(any(test, feature = "test-utils"))] @@ -128,8 +158,14 @@ impl NodeMap { nodes: Vec, path_selection: PathSelection, metrics: &Metrics, + sender: TransportsSender, ) -> Self { - Self::from_inner(NodeMapInner::load_from_vec(nodes, path_selection, metrics)) + Self::from_inner(NodeMapInner::load_from_vec( + nodes, + path_selection, + metrics, + sender, + )) } fn from_inner(inner: NodeMapInner) -> Self { @@ -168,12 +204,12 @@ impl NodeMap { pub(super) fn get_all_paths_addr_for_node( &self, - node_key: NodeId, + node_id: NodeId, ) -> Option { self.inner .lock() .expect("poisoned") - .get(NodeStateKey::NodeId(node_key)) + .get(NodeStateKey::NodeId(node_id)) .map(|ep| *ep.all_paths_mapped_addr()) } @@ -282,29 +318,86 @@ impl NodeMap { .expect("poisoned") .on_direct_addr_discovered(discovered, Instant::now()); } + + /// Returns the sender for the [`NodeStateActor`]. + pub(super) fn get_node_state_actor( + &self, + addr: AllPathsMappedAddr, + // node_id: NodeId, + ) -> Option> { + // self + // .inner + // .lock() + // .expect("poisoned") + // .new + // .entry(node_id) + // .or_insert_with_key(|node_id| { + // let mut actor = NodeStateActor::new(*node_id, transports_sender, metrics); + // actor.start() + // }); + todo!() + } } impl NodeMapInner { + #[cfg(any(test, feature = "test-utils"))] + fn new(sender: TransportsSender) -> Self { + let transports_handle = Self::start_transports_sender(sender); + Self { + transports_handle, + by_node_key: Default::default(), + by_ip_port: Default::default(), + by_quic_mapped_addr: Default::default(), + by_id: Default::default(), + next_id: 0, + path_selection: Default::default(), + node_states: Default::default(), + node_addrs: Default::default(), + node_addrs_lookup: Default::default(), + } + } + + /// Creates a new [`NodeMap`] from a list of [`NodeAddr`]s. #[cfg(not(any(test, feature = "test-utils")))] - /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. - fn load_from_vec(nodes: Vec, metrics: &Metrics) -> Self { - let mut me = Self::default(); + fn load_from_vec(nodes: Vec, metrics: &Metrics, sender: TransportsSender) -> Self { + let transports_handle = Self::start_transports_sender(sender); + let mut me = Self { + transports_handle, + by_node_key: Default::default(), + by_ip_port: Default::default(), + by_quic_mapped_addr: Default::default(), + by_id: Default::default(), + next_id: 0, + node_states: Default::default(), + node_addrs: Default::default(), + node_addrs_lookup: Default::default(), + }; for node_addr in nodes { me.add_node_addr(node_addr, Source::Saved, metrics); } me } + /// Creates a new [`NodeMap`] from a list of [`NodeAddr`]s. #[cfg(any(test, feature = "test-utils"))] - /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. fn load_from_vec( nodes: Vec, path_selection: PathSelection, metrics: &Metrics, + sender: TransportsSender, ) -> Self { + let transports_handle = Self::start_transports_sender(sender); let mut me = Self { + transports_handle, + by_node_key: Default::default(), + by_ip_port: Default::default(), + by_quic_mapped_addr: Default::default(), + by_id: Default::default(), + next_id: 0, path_selection, - ..Default::default() + node_states: Default::default(), + node_addrs: Default::default(), + node_addrs_lookup: Default::default(), }; for node_addr in nodes { me.add_node_addr(node_addr, Source::Saved, metrics); @@ -312,6 +405,11 @@ impl NodeMapInner { me } + fn start_transports_sender(sender: TransportsSender) -> TransportsSenderHandle { + let actor = TransportsSenderActor::new(sender); + actor.start() + } + /// Add the contact information for a node. #[instrument(skip_all, fields(node = %node_addr.node_id.fmt_short()))] fn add_node_addr(&mut self, node_addr: NodeAddr, source: Source, metrics: &Metrics) { @@ -617,15 +715,87 @@ impl IpPort { } } +/// An actor that can send datagrams onto iroh transports. +/// +/// The [`NodeStateActor`]s want to be able to send datagrams. Because we can not create +/// [`TransportsSender`]s on demand we must share one for the entire [`NodeMap`], which +/// lives in this actor. +#[derive(Debug)] +struct TransportsSenderActor { + sender: TransportsSender, +} + +impl TransportsSenderActor { + fn new(sender: TransportsSender) -> Self { + Self { sender } + } + + fn start(self) -> TransportsSenderHandle { + // This actor gets an inbox size of exactly 1. This is the same as if they had the + // underlying sender directly: either you can send or not, or you await until you + // can. No need to introduce extra buffering. + let (tx, rx) = mpsc::channel(1); + + // No .instrument() on task, run method has an #[instrument] attribute. + let task = tokio::spawn(async move { + self.run(rx).await; + }); + TransportsSenderHandle { + inbox: tx, + _task: AbortOnDropHandle::new(task), + } + } + + #[instrument(name = "TransportsSenderActor", skip_all)] + async fn run(self, mut inbox: mpsc::Receiver) { + use TransportsSenderMessage::SendDatagram; + + loop { + if let Some(SendDatagram(dst, owned_transmit)) = inbox.recv().await { + let transmit = transports::Transmit { + ecn: owned_transmit.ecn, + contents: owned_transmit.contents.as_ref(), + segment_size: owned_transmit.segment_size, + }; + let len = transmit.contents.len(); + match self.sender.send(&dst, None, &transmit).await { + Ok(()) => { + trace!(?dst, %len, "sent transmit"); + } + Err(err) => { + trace!(?dst, %len, "transmit failed to send: {err:#}"); + } + }; + } else { + break; + } + } + trace!("actor terminating"); + } +} + +#[derive(Debug)] +struct TransportsSenderHandle { + inbox: mpsc::Sender, + _task: AbortOnDropHandle<()>, +} + +#[derive(Debug)] +enum TransportsSenderMessage { + SendDatagram(transports::Addr, OwnedTransmit), +} + #[cfg(test)] mod tests { use std::net::Ipv4Addr; + use std::sync::Arc; use iroh_base::SecretKey; use tracing_test::traced_test; use super::{node_state::MAX_INACTIVE_DIRECT_ADDRESSES, *}; use crate::disco::SendAddr; + use crate::magicsock::transports::Transports; impl NodeMap { #[track_caller] @@ -644,7 +814,8 @@ mod tests { #[tokio::test] #[traced_test] async fn restore_from_vec() { - let node_map = NodeMap::default(); + let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); + let node_map = NodeMap::new(transports.create_sender()); let mut rng = rand::thread_rng(); let node_a = SecretKey::generate(&mut rng).public(); @@ -681,8 +852,12 @@ mod tests { Some(addr) }) .collect(); - let loaded_node_map = - NodeMap::load_from_vec(addrs.clone(), PathSelection::default(), &Default::default()); + let loaded_node_map = NodeMap::load_from_vec( + addrs.clone(), + PathSelection::default(), + &Default::default(), + transports.create_sender(), + ); let mut loaded: Vec = loaded_node_map .list_remote_infos(Instant::now()) @@ -710,7 +885,8 @@ mod tests { #[test] #[traced_test] fn test_prune_direct_addresses() { - let node_map = NodeMap::default(); + let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); + let node_map = NodeMap::new(transports.create_sender()); let public_key = SecretKey::generate(rand::thread_rng()).public(); let id = node_map .inner @@ -783,7 +959,8 @@ mod tests { #[test] fn test_prune_inactive() { - let node_map = NodeMap::default(); + let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); + let node_map = NodeMap::new(transports.create_sender()); // add one active node and more than MAX_INACTIVE_NODES inactive nodes let active_node = SecretKey::generate(rand::thread_rng()).public(); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167); diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index b86d2f8802b..8cdb35a311f 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1,13 +1,18 @@ use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, net::{IpAddr, SocketAddr}, - sync::atomic::AtomicBool, + sync::{Arc, atomic::AtomicBool}, }; use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; -use n0_future::time::{Duration, Instant}; +use n0_future::{ + task::AbortOnDropHandle, + time::{Duration, Instant}, +}; use n0_watcher::Watchable; +use quinn::WeakConnectionHandle; use serde::{Deserialize, Serialize}; +use tokio::sync::mpsc; use tracing::{Level, debug, event, info, instrument, trace, warn}; use super::{ @@ -22,6 +27,7 @@ use crate::{ magicsock::{ AllPathsMappedAddr, HEARTBEAT_INTERVAL, MagicsockMetrics, node_map::path_validity::PathValidity, + transports::{self, OwnedTransmit, TransportsSender}, }, }; @@ -694,6 +700,103 @@ impl NodeState { } } +/// The state we need to know about a single remote node. +/// +/// This actor manages all connections to the remote node. It will trigger holepunching and +/// select the best path etc. +pub(super) struct NodeStateActor { + /// The node ID of the remote node. + node_id: NodeId, + transports_sender: TransportsSender, + // TODO: Turn this into a WeakConnectionHandle + connections: Vec, + paths: BTreeMap, + metrics: Arc, +} + +impl NodeStateActor { + pub(super) fn new( + node_id: NodeId, + transports_sender: TransportsSender, + metrics: Arc, + ) -> Self { + Self { + node_id, + transports_sender, + connections: Vec::new(), + paths: BTreeMap::new(), + metrics, + } + } + + pub(super) fn start(mut self) -> NodeStateHandle { + let (tx, rx) = mpsc::channel(16); + + // No .instrument() on the task, run method has an #[instrument] attribute. + let task = tokio::spawn(async move { + self.run(rx).await; + }); + NodeStateHandle { + sender: tx, + _task: AbortOnDropHandle::new(task), + } + } + + #[instrument( + name = "NodeStateActor", + skip_all, + fields(node_id = %self.node_id.fmt_short()) + )] + async fn run(&mut self, mut inbox: mpsc::Receiver) { + loop { + if let Some(msg) = inbox.recv().await { + match msg { + NodeStateMessage::SendDatagram(transmit) => todo!(), + NodeStateMessage::AddConnection(handle) => todo!(), + NodeStateMessage::PingReceived => todo!(), + } + } else { + break; + } + } + trace!("actor terminating"); + } +} + +/// Messages to send to the [`NodeStateActor`]. +pub(crate) enum NodeStateMessage { + /// Send a datagram to all known paths. + /// + /// Used to send QUIC Initial packets. If there is no working direct path this will + /// trigger holepunching. + /// + /// This is not acceptable to use on the normal send path, as it is an async send + /// operation with a bunch more copying. So it should only be used for sending QUIC + /// Initial packets. + SendDatagram(OwnedTransmit), + /// Add an active connection to this remote node. + /// + /// The connection will now be managed by this actor. Holepunching will happen when + /// needed, any new paths discovered via holepunching will be added. And closed paths + /// will be removed etc. + AddConnection(WeakConnectionHandle), + // TODO: Add the transaction ID. + PingReceived, +} + +/// A handle to a [`NodeStateActor`]. +/// +/// Dropping this will stop the actor. +#[derive(Debug)] +pub(super) struct NodeStateHandle { + sender: mpsc::Sender, + _task: AbortOnDropHandle<()>, +} + +struct NewPathState { + addr: transports::Addr, +} + impl From for NodeAddr { fn from(info: RemoteInfo) -> Self { let direct_addresses = info diff --git a/iroh/src/magicsock/relay_mapped_addrs.rs b/iroh/src/magicsock/relay_mapped_addrs.rs index 1b21f1377e2..608e154b53c 100644 --- a/iroh/src/magicsock/relay_mapped_addrs.rs +++ b/iroh/src/magicsock/relay_mapped_addrs.rs @@ -93,6 +93,7 @@ impl std::fmt::Display for RelayMappedAddr { pub struct RelayAddrMapError; /// A Map of [`RelayMappedAddr`] to `(RelayUrl, NodeId)`. +// TODO: this could be an RwLock, or even an dashmap #[derive(Debug, Clone, Default)] pub(crate) struct RelayAddrMap(Arc>); diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index c4703b303df..887f223fcb0 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -6,11 +6,12 @@ use std::{ task::{Context, Poll}, }; +use bytes::Bytes; use iroh_base::{NodeId, RelayUrl}; use n0_watcher::Watcher; use relay::{RelayNetworkChangeSender, RelaySender}; -use smallvec::SmallVec; -use tracing::{error, trace, warn}; +use tokio::sync::mpsc; +use tracing::{debug, error, instrument, trace, warn}; #[cfg(not(wasm_browser))] mod ip; @@ -21,8 +22,8 @@ pub(crate) use self::ip::IpTransport; #[cfg(not(wasm_browser))] use self::ip::{IpNetworkChangeSender, IpSender}; pub(crate) use self::relay::{RelayActorConfig, RelayTransport}; -use super::MagicSock; -use crate::net_report::Report; +use super::{MagicSock, node_map::NodeStateMessage}; +use crate::{magicsock::MultipathMappedAddr, net_report::Report}; /// Manages the different underlying data transports that the magicsock /// can support. @@ -228,16 +229,15 @@ impl Transports { false } - pub(crate) fn create_sender(&self, msock: Arc) -> UdpSender { + pub(crate) fn create_sender(&self) -> TransportsSender { #[cfg(not(wasm_browser))] let ip = self.ip.iter().map(|t| t.create_sender()).collect(); let relay = self.relay.iter().map(|t| t.create_sender()).collect(); let max_transmit_segments = self.max_transmit_segments(); - UdpSender { + TransportsSender { #[cfg(not(wasm_browser))] ip, - msock, relay, max_transmit_segments, } @@ -310,6 +310,24 @@ pub(crate) struct Transmit<'a> { pub(crate) segment_size: Option, } +/// An outgoing packet that can be sent across channels. +#[derive(Debug)] +pub(crate) struct OwnedTransmit { + pub(crate) ecn: Option, + pub(crate) contents: Bytes, + pub(crate) segment_size: Option, +} + +impl From<&quinn_udp::Transmit<'_>> for OwnedTransmit { + fn from(source: &quinn_udp::Transmit<'_>) -> Self { + Self { + ecn: source.ecn, + contents: Bytes::copy_from_slice(source.contents), + segment_size: source.segment_size, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum Addr { Ip(SocketAddr), @@ -353,16 +371,16 @@ impl Addr { } } +/// A sender that sends to all our transports. #[derive(Debug)] -pub(crate) struct UdpSender { - msock: Arc, // :( +pub(crate) struct TransportsSender { #[cfg(not(wasm_browser))] ip: Vec, relay: Vec, max_transmit_segments: usize, } -impl UdpSender { +impl TransportsSender { pub(crate) async fn send( &self, destination: &Addr, @@ -498,103 +516,318 @@ impl UdpSender { } } -impl quinn::UdpSender for UdpSender { - fn poll_send( - mut self: Pin<&mut Self>, - transmit: &quinn_udp::Transmit, - cx: &mut Context, - ) -> Poll> { - let active_paths = self.msock.prepare_send(transmit)?; - - if active_paths.is_empty() { - // Returning Ok here means we let QUIC timeout. - // Returning an error would immediately fail a connection. - // The philosophy of quinn-udp is that a UDP connection could - // come back at any time or missing should be transient so chooses to let - // these kind of errors time out. See test_try_send_no_send_addr to try - // this out. - error!("no paths available for node, voiding transmit"); - return Poll::Ready(Ok(())); - } +/// A [`Transports`] that works with [`MultipathMappedAddr`]s and their IPv6 representation. +/// +/// The [`MultipathMappedAddr`]s have an IPv6 representation that Quinn uses. This struct +/// knows about these and maps them back to the transport [`Addr`]s used by the wrapped +/// [`Transports`]. +#[derive(Debug)] +pub(crate) struct MagicTransport { + msock: Arc, + transports: Transports, +} - let mut results = SmallVec::<[_; 3]>::new(); +impl MagicTransport { + pub(crate) fn new(msock: Arc, transports: Transports) -> Self { + Self { msock, transports } + } +} - trace!(?active_paths, "attempting to send"); +impl quinn::AsyncUdpSocket for MagicTransport { + fn create_sender(&self) -> Pin> { + Box::pin(MagicSender { + msock: self.msock.clone(), + sender: self.transports.create_sender(), + }) + } - for destination in active_paths { - let src = transmit.src_ip; - let transmit = Transmit { - ecn: transmit.ecn, - contents: transmit.contents, - segment_size: transmit.segment_size, - }; + fn poll_recv( + &mut self, + cx: &mut Context, + bufs: &mut [IoSliceMut<'_>], + meta: &mut [quinn_udp::RecvMeta], + ) -> Poll> { + self.transports.poll_recv(cx, bufs, meta, &self.msock) + } - let res = self - .as_mut() - .inner_poll_send(cx, &destination, src, &transmit); - match res { - Poll::Ready(Ok(())) => { - trace!(dst = ?destination, "sent transmit"); - } - Poll::Ready(Err(ref err)) => { - warn!(dst = ?destination, "failed to send: {err:#}"); - } - Poll::Pending => {} - } - results.push(res); - } + #[cfg(not(wasm_browser))] + fn local_addr(&self) -> io::Result { + let addrs: Vec<_> = self + .transports + .local_addrs() + .into_iter() + .filter_map(|addr| { + let addr: SocketAddr = addr.into_socket_addr()?; + Some(addr) + }) + .collect(); - if results.iter().all(|p| matches!(p, Poll::Pending)) { - // Handle backpressure. - return Poll::Pending; + if let Some(addr) = addrs.iter().find(|addr| addr.is_ipv6()) { + return Ok(*addr); + } + if let Some(SocketAddr::V4(addr)) = addrs.first() { + // Pretend to be IPv6, because our `MappedAddr`s need to be IPv6. + let ip = addr.ip().to_ipv6_mapped().into(); + return Ok(SocketAddr::new(ip, addr.port())); } - Poll::Ready(Ok(())) + + Err(io::Error::other("no valid address available")) } - fn max_transmit_segments(&self) -> usize { - self.max_transmit_segments - } - - fn try_send(self: Pin<&mut Self>, transmit: &quinn_udp::Transmit) -> io::Result<()> { - let active_paths = self.msock.prepare_send(transmit)?; - if active_paths.is_empty() { - // Returning Ok here means we let QUIC timeout. - // Returning an error would immediately fail a connection. - // The philosophy of quinn-udp is that a UDP connection could - // come back at any time or missing should be transient so chooses to let - // these kind of errors time out. See test_try_send_no_send_addr to try - // this out. - error!("no paths available for node, voiding transmit"); - return Ok(()); - } + #[cfg(wasm_browser)] + fn local_addr(&self) -> io::Result { + // Again, we need to pretend we're IPv6, because of our `MappedAddr`s. + Ok(SocketAddr::new(std::net::Ipv6Addr::LOCALHOST.into(), 0)) + } + + fn max_receive_segments(&self) -> usize { + self.transports.max_receive_segments() + } + + fn may_fragment(&self) -> bool { + self.transports.may_fragment() + } +} - let mut results = SmallVec::<[_; 3]>::new(); +/// A sender for [`MagicTransport`]. +/// +/// This is special in that it handles [`MultipathMappedAddr::Mixed`] by delegating to the +/// [`MagicSock`] which expands it back to one or more [`transport::Addr`]s and sends it +/// using the underlying [`Transports`]. +// TODO: Can I just send the TransportsSender along in the NodeStateMessage::SendDatagram +// message?? That way you don't have to hook up the sender into the NodeMap! +#[derive(Debug)] +#[pin_project::pin_project] +pub(crate) struct MagicSender { + msock: Arc, + #[pin] + sender: TransportsSender, +} - trace!(?active_paths, "attempting to send"); +impl MagicSender { + /// Extracts the right [`transports::Addr`] from the [`quinn_udp::Transmit`]. + /// + /// Because Quinn does only know about IP transports we map other transports to private + /// IPv6 Unique Local Address ranges. This extracts the transport addresses out of the + /// transmit's destination. + fn mapped_addr(&self, transmit: &quinn_udp::Transmit) -> io::Result { + self.msock + .metrics + .magicsock + .send_data + .inc_by(transmit.contents.len() as _); + + if self.msock.is_closed() { + self.msock + .metrics + .magicsock + .send_data_network_down + .inc_by(transmit.contents.len() as _); + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "connection closed", + )); + } - for destination in active_paths { - let src = transmit.src_ip; - let transmit = Transmit { - ecn: transmit.ecn, - contents: transmit.contents, - segment_size: transmit.segment_size, - }; + let addr = MultipathMappedAddr::from(transmit.destination); + trace!( + dst = ?addr, + src = ?transmit.src_ip, + len = %transmit.contents.len(), + "sending", + ); + Ok(addr) + } +} - let res = self.inner_try_send(&destination, src, &transmit); - match res { - Ok(()) => { - trace!(dst = ?destination, "sent transmit"); +impl quinn::UdpSender for MagicSender { + #[instrument( + skip_all, + fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst), + )] + fn poll_send( + self: Pin<&mut Self>, + quinn_transmit: &quinn_udp::Transmit, + cx: &mut Context, + ) -> Poll> { + // On errors this methods prefers returning Ok(()) to Quinn. Returning an error + // should only happen if the error is permanent and fatal and it will never be + // possible to send anything again. Doing so kills the Quinn EndpointDriver. Most + // send errors are intermittent errors, returning Ok(()) in those cases will mean + // Quinn eventually considers the packets that had send errors as lost and will try + // and re-send them. + let mapped_addr = self.mapped_addr(quinn_transmit)?; + + let transport_addr = match mapped_addr { + MultipathMappedAddr::Mixed(mapped_addr) => { + // TODO: Would be nicer to log the NodeId of this, but we only get an actor + // sender for it. + tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); + + // Note we drop the src_ip set in the Quinn Transmit. This is only the + // Initial packet we are sending, so we do not yet have an src address we + // need to respond from. + if let Some(src_ip) = quinn_transmit.src_ip { + warn!(?src_ip, "oops, flub didn't think this would happen"); } - Err(ref err) => { - warn!(dst = ?destination, "failed to send: {err:#}"); + return match self.msock.node_map.get_node_state_actor(mapped_addr) { + Some(sender) => { + let transmit = OwnedTransmit::from(quinn_transmit); + match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { + Ok(()) => { + trace!("sent transmit",); + Poll::Ready(Ok(())) + } + Err(err) => { + // We do not want to block the next send which might be on a + // different transport. Instead we let Quinn handle this as + // a lost datagram. + // TODO: Revisit this: we might want to do something better. + debug!("NodeStateActor inbox full ({err:#}), dropped transmit"); + Poll::Ready(Ok(())) + } + } + } + None => { + error!("unknown AllPathsMappedAddr, dropped transmit"); + Poll::Ready(Ok(())) + } + }; + } + MultipathMappedAddr::Relay(relay_mapped_addr) => { + match self.msock.relay_mapped_addrs.get_url(&relay_mapped_addr) { + Some((relay_url, node_id)) => Addr::Relay(relay_url, node_id), + None => { + error!("unknown RelayMappedAddr, dropped transmit"); + return Poll::Ready(Ok(())); + } } } - results.push(res); + MultipathMappedAddr::Ip(socket_addr) => Addr::Ip(socket_addr), + }; + tracing::Span::current().record("dst", tracing::field::debug(&transport_addr)); + + let transmit = Transmit { + ecn: quinn_transmit.ecn, + contents: quinn_transmit.contents, + segment_size: quinn_transmit.segment_size, + }; + let this = self.project(); + + match this + .sender + .inner_poll_send(cx, &transport_addr, quinn_transmit.src_ip, &transmit) + { + Poll::Ready(Ok(())) => { + trace!("sent transmit",); + Poll::Ready(Ok(())) + } + Poll::Ready(Err(ref err)) => { + warn!("dropped transmit: {err:#}"); + Poll::Ready(Ok(())) + } + Poll::Pending => { + // We do not want to block the next send which might be on a + // different transport. Instead we let Quinn handle this as a lost + // datagram. + // TODO: Revisit this: we might want to do something better. + trace!("transport pending, dropped transmit"); + Poll::Ready(Ok(())) + } } + } - if results.iter().all(|p| p.is_err()) { - return Err(io::Error::other("all failed")); + fn max_transmit_segments(&self) -> usize { + self.sender.max_transmit_segments + } + + #[instrument( + skip_all, + fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst), + )] + fn try_send(self: Pin<&mut Self>, quinn_transmit: &quinn_udp::Transmit) -> io::Result<()> { + // As opposed to poll_send this method does return normal IO errors. Calls to this + // are one-off fire-and-forget calls with no implications for the EndpointDriver. + let mapped_addr = self.mapped_addr(quinn_transmit)?; + + let transport_addr = match mapped_addr { + MultipathMappedAddr::Mixed(mapped_addr) => { + // TODO: Would be nicer to log the NodeId of this, but we only get an actor + // sender for it. + tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); + + // Note we drop the src_ip set in the Quinn Transmit. This is only the + // Initial packet we are sending, so we do not yet have an src address we + // need to respond from. + if let Some(src_ip) = quinn_transmit.src_ip { + warn!(?src_ip, "oops, flub didn't think this would happen"); + } + return match self.msock.node_map.get_node_state_actor(mapped_addr) { + Some(sender) => { + let transmit = OwnedTransmit::from(quinn_transmit); + match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { + Ok(()) => { + trace!("sent transmit",); + Ok(()) + } + Err(mpsc::error::TrySendError::Full(_)) => { + debug!("NodeStateActor inbox full, dropped transmit"); + Err(io::Error::new( + io::ErrorKind::WouldBlock, + "NodeStateActor inbox full", + )) + } + Err(mpsc::error::TrySendError::Closed(_)) => { + debug!("NodeStateActor inbox closed, dropped transmit"); + Err(io::Error::new( + io::ErrorKind::NetworkDown, + "NodeStateActor inbox closed", + )) + } + } + } + None => { + error!("unknown AllPathsMappedAddr, dropped transmit"); + Err(io::Error::new( + io::ErrorKind::HostUnreachable, + "unknown AllPathsMappedAddr", + )) + } + }; + } + MultipathMappedAddr::Relay(relay_mapped_addr) => { + match self.msock.relay_mapped_addrs.get_url(&relay_mapped_addr) { + Some((relay_url, node_id)) => Addr::Relay(relay_url, node_id), + None => { + error!("unknown RelayMappedAddr, dropped transmit"); + return Err(io::Error::new( + io::ErrorKind::HostUnreachable, + "unknown RelayMappedAddr", + )); + } + } + } + MultipathMappedAddr::Ip(socket_addr) => Addr::Ip(socket_addr), + }; + tracing::Span::current().record("dst", tracing::field::debug(&transport_addr)); + + let transmit = Transmit { + ecn: quinn_transmit.ecn, + contents: quinn_transmit.contents, + segment_size: quinn_transmit.segment_size, + }; + match self + .sender + .inner_try_send(&transport_addr, quinn_transmit.src_ip, &transmit) + { + Ok(()) => { + trace!("sent transmit",); + Ok(()) + } + Err(err) => { + warn!("transmit failed to send: {err:#}"); + Err(err) + } } - Ok(()) } } From a1a7d8936cfc24d26fb27810fe1ff2b92cd7c6d9 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 16 Sep 2025 11:50:15 +0200 Subject: [PATCH 029/164] Rename AllPathsMappedAddr to NodeIdMappedAddr Yes, it was called this before. The doc comment explains why this name makes more sense. Also fix up a bunch of doc links --- iroh/src/endpoint.rs | 4 +- iroh/src/magicsock.rs | 49 +++++++++++++---------- iroh/src/magicsock/node_map.rs | 35 ++++++++-------- iroh/src/magicsock/node_map/node_state.rs | 8 ++-- iroh/src/magicsock/relay_mapped_addrs.rs | 16 ++++---- iroh/src/magicsock/transports.rs | 4 +- 6 files changed, 63 insertions(+), 53 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index c3a73b74ab7..ae331f68f92 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -62,7 +62,7 @@ use crate::{ DiscoverySubscribers, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, IntoDiscoveryError, Lagged, UserData, pkarr::PkarrPublisher, }, - magicsock::{self, AllPathsMappedAddr, Handle, OwnAddressSnafu}, + magicsock::{self, Handle, NodeIdMappedAddr, OwnAddressSnafu}, metrics::EndpointMetrics, net_report::Report, tls, @@ -1363,7 +1363,7 @@ impl Endpoint { async fn get_mapping_addr_and_maybe_start_discovery( &self, node_addr: NodeAddr, - ) -> Result<(AllPathsMappedAddr, Option), GetMappingAddressError> { + ) -> Result<(NodeIdMappedAddr, Option), GetMappingAddressError> { let node_id = node_addr.node_id; // Only return a mapped addr if we have some way of dialing this node, in other diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 8f8a75ff019..19f0537aee3 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -437,7 +437,7 @@ impl MagicSock { } /// Returns the socket address which can be used by the QUIC layer to *dial* this node. - pub(crate) fn get_mapping_addr(&self, node_id: NodeId) -> Option { + pub(crate) fn get_mapping_addr(&self, node_id: NodeId) -> Option { self.node_map.get_all_paths_addr_for_node(node_id) } @@ -1004,7 +1004,7 @@ pub(crate) enum MultipathMappedAddr { /// /// - Only used for sending /// - This means send on all known paths/transports - Mixed(AllPathsMappedAddr), + Mixed(NodeIdMappedAddr), /// Relay based transport address Relay(RelayMappedAddr), /// IP based transport address @@ -1017,7 +1017,7 @@ impl From for MultipathMappedAddr { match value.ip() { IpAddr::V4(_) => Self::Ip(value), IpAddr::V6(addr) => { - if let Ok(addr) = AllPathsMappedAddr::try_from(addr) { + if let Ok(addr) = NodeIdMappedAddr::try_from(addr) { return Self::Mixed(addr); } #[cfg(not(wasm_browser))] @@ -1392,9 +1392,11 @@ impl Handle { /// Closes the connection. /// - /// Only the first close does anything. Any later closes return nil. - /// Polling the socket ([`AsyncUdpSocket::poll_recv`]) will return [`Poll::Pending`] - /// indefinitely after this call. + /// Only the first close does anything. Any later closes return nil. Polling the socket + /// ([`quinn::AsyncUdpSocket::poll_recv`]) will return [`Poll::Pending`] indefinitely + /// after this call. + /// + /// [`Poll::Pending`]: std::task::Poll::Pending #[instrument(skip_all)] pub(crate) async fn close(&self) { trace!(me = ?self.public_key, "magicsock closing..."); @@ -2218,27 +2220,32 @@ impl DiscoveredDirectAddrs { } } -/// An address used by the QUIC layer to address a node on all paths. +/// An address used by the QUIC layer to address a node on any or all paths. /// /// This is only used for initially connecting to a remote node. We instruct Quinn to send -/// to this address, and duplicate all packets for this address to send on all paths we know -/// for the node. +/// to this address, and duplicate all packets for this address to send on all paths we +/// might want to send the initial on: +/// +/// - If this the first connection to the remote node we don't know which path will work and +/// send to all of them. +/// +/// - If there already is an active connection to this node we now which path to use. /// /// It is but a newtype around an IPv6 Unique Local Addr. And in our QUIC-facing socket -/// APIs like [`AsyncUdpSocket`] it comes in as the inner [`Ipv6Addr`], in those interfaces -/// we have to be careful to do the conversion to this type. +/// APIs like [`quinn::AsyncUdpSocket`] it comes in as the inner [`Ipv6Addr`], in those +/// interfaces we have to be careful to do the conversion to this type. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) struct AllPathsMappedAddr(Ipv6Addr); +pub(crate) struct NodeIdMappedAddr(Ipv6Addr); -/// Can occur when converting a [`SocketAddr`] to an [`AllPathsMappedAddr`] +/// Can occur when converting a [`SocketAddr`] to an [`NodeIdMappedAddr`] #[derive(Debug, Snafu)] #[snafu(display("Failed to convert"))] pub struct AllPathsMappedAddrError; -/// Counter to always generate unique addresses for [`AllPathsMappedAddr`]. +/// Counter to always generate unique addresses for [`NodeIdMappedAddr`]. static ALL_PATHS_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); -impl AllPathsMappedAddr { +impl NodeIdMappedAddr { /// The Prefix/L of our Unique Local Addresses. const ADDR_PREFIXL: u8 = 0xfd; /// The Global ID used in our Unique Local Addresses. @@ -2276,7 +2283,7 @@ impl AllPathsMappedAddr { } } -impl TryFrom for AllPathsMappedAddr { +impl TryFrom for NodeIdMappedAddr { type Error = AllPathsMappedAddrError; fn try_from(value: Ipv6Addr) -> Result { @@ -2291,7 +2298,7 @@ impl TryFrom for AllPathsMappedAddr { } } -impl std::fmt::Display for AllPathsMappedAddr { +impl std::fmt::Display for NodeIdMappedAddr { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "NodeIdMappedAddr({})", self.0) } @@ -2381,7 +2388,7 @@ mod tests { use tracing::{Instrument, error, info, info_span, instrument}; use tracing_test::traced_test; - use super::{AllPathsMappedAddr, Options}; + use super::{NodeIdMappedAddr, Options}; use crate::{ Endpoint, RelayMap, RelayMode, SecretKey, dns::DnsResolver, @@ -2958,7 +2965,7 @@ mod tests { async fn magicsock_connect( ep: &quinn::Endpoint, ep_secret_key: SecretKey, - addr: AllPathsMappedAddr, + addr: NodeIdMappedAddr, node_id: NodeId, ) -> Result { // Endpoint::connect sets this, do the same to have similar behaviour. @@ -2984,7 +2991,7 @@ mod tests { async fn magicsock_connect_with_transport_config( ep: &quinn::Endpoint, ep_secret_key: SecretKey, - mapped_addr: AllPathsMappedAddr, + mapped_addr: NodeIdMappedAddr, node_id: NodeId, transport_config: Arc, ) -> Result { @@ -3019,7 +3026,7 @@ mod tests { let msock_1 = magicsock_ep(secret_key_1.clone()).await.unwrap(); // Generate an address not present in the NodeMap. - let bad_addr = AllPathsMappedAddr::generate(); + let bad_addr = NodeIdMappedAddr::generate(); // 500ms is rather fast here. Running this locally it should always be the correct // timeout. If this is too slow however the test will not become flaky as we are diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index c7919859684..85317e3f5f4 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -18,7 +18,7 @@ use super::transports::TransportsSender; #[cfg(not(any(test, feature = "test-utils")))] use super::transports::TransportsSender; use super::{ - AllPathsMappedAddr, + NodeIdMappedAddr, metrics::Metrics, transports::{self, OwnedTransmit}, }; @@ -67,17 +67,19 @@ pub(super) struct NodeMapInner { transports_handle: TransportsSenderHandle, by_node_key: HashMap, by_ip_port: HashMap, - by_quic_mapped_addr: HashMap, + by_quic_mapped_addr: HashMap, by_id: HashMap, next_id: usize, #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, /// The [`NodeStateActor`] for each remote node. + /// + /// [`NodeStateActor`]: node_state::NodeStateActor node_states: HashMap, - /// The [`AllPathsMappedAddr`] for each node. - node_addrs: HashMap, + /// The [`NodeIdMappedAddr`] for each node. + node_addrs: HashMap, /// The reverse of mapping of [`Self::node_addrs`]. - node_addrs_lookup: HashMap, + node_addrs_lookup: HashMap, // /// The [`RelayMappedAddr`] for each node. // relay_addrs: HashMap, } @@ -89,7 +91,7 @@ pub(super) struct NodeMapInner { #[derive(Debug, Clone)] enum NodeStateKey { NodeId(NodeId), - NodeIdMappedAddr(AllPathsMappedAddr), + NodeIdMappedAddr(NodeIdMappedAddr), IpPort(IpPort), } @@ -191,21 +193,18 @@ impl NodeMap { pub(super) fn receive_udp( &self, udp_addr: SocketAddr, - ) -> Option<(PublicKey, AllPathsMappedAddr)> { + ) -> Option<(PublicKey, NodeIdMappedAddr)> { self.inner.lock().expect("poisoned").receive_udp(udp_addr) } - pub(super) fn receive_relay(&self, relay_url: &RelayUrl, src: NodeId) -> AllPathsMappedAddr { + pub(super) fn receive_relay(&self, relay_url: &RelayUrl, src: NodeId) -> NodeIdMappedAddr { self.inner .lock() .expect("poisoned") .receive_relay(relay_url, src) } - pub(super) fn get_all_paths_addr_for_node( - &self, - node_id: NodeId, - ) -> Option { + pub(super) fn get_all_paths_addr_for_node(&self, node_id: NodeId) -> Option { self.inner .lock() .expect("poisoned") @@ -246,7 +245,7 @@ impl NodeMap { #[allow(clippy::type_complexity)] pub(super) fn get_send_addrs( &self, - addr: AllPathsMappedAddr, + addr: NodeIdMappedAddr, have_ipv6: bool, metrics: &Metrics, ) -> Option<( @@ -320,9 +319,11 @@ impl NodeMap { } /// Returns the sender for the [`NodeStateActor`]. + /// + /// [`NodeStateActor`]: node_state::NodeStateActor pub(super) fn get_node_state_actor( &self, - addr: AllPathsMappedAddr, + addr: NodeIdMappedAddr, // node_id: NodeId, ) -> Option> { // self @@ -502,7 +503,7 @@ impl NodeMapInner { /// Marks the node we believe to be at `ipp` as recently used. #[cfg(not(wasm_browser))] - fn receive_udp(&mut self, udp_addr: SocketAddr) -> Option<(NodeId, AllPathsMappedAddr)> { + fn receive_udp(&mut self, udp_addr: SocketAddr) -> Option<(NodeId, NodeIdMappedAddr)> { let ip_port: IpPort = udp_addr.into(); let Some(node_state) = self.get_mut(NodeStateKey::IpPort(ip_port)) else { trace!(src=%udp_addr, "receive_udp: no node_state found for addr, ignore"); @@ -516,7 +517,7 @@ impl NodeMapInner { } #[instrument(skip_all, fields(src = %src.fmt_short()))] - fn receive_relay(&mut self, relay_url: &RelayUrl, src: NodeId) -> AllPathsMappedAddr { + fn receive_relay(&mut self, relay_url: &RelayUrl, src: NodeId) -> NodeIdMappedAddr { #[cfg(any(test, feature = "test-utils"))] let path_selection = self.path_selection; let node_state = self.get_or_insert_with(NodeStateKey::NodeId(src), || { @@ -720,6 +721,8 @@ impl IpPort { /// The [`NodeStateActor`]s want to be able to send datagrams. Because we can not create /// [`TransportsSender`]s on demand we must share one for the entire [`NodeMap`], which /// lives in this actor. +/// +/// [`NodeStateActor`]: node_state::NodeStateActor #[derive(Debug)] struct TransportsSenderActor { sender: TransportsSender, diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 8cdb35a311f..ead6be40894 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -25,7 +25,7 @@ use crate::endpoint::PathSelection; use crate::{ disco::{self, SendAddr}, magicsock::{ - AllPathsMappedAddr, HEARTBEAT_INTERVAL, MagicsockMetrics, + HEARTBEAT_INTERVAL, MagicsockMetrics, NodeIdMappedAddr, node_map::path_validity::PathValidity, transports::{self, OwnedTransmit, TransportsSender}, }, @@ -69,7 +69,7 @@ pub(super) struct NodeState { /// [`NodeMap`]: super::NodeMap id: usize, /// The UDP address used on the QUIC-layer to address this node. - quic_mapped_addr: AllPathsMappedAddr, + quic_mapped_addr: NodeIdMappedAddr, /// The global identifier for this endpoint. node_id: NodeId, /// The url of relay node that we can relay over to communicate. @@ -119,7 +119,7 @@ pub(super) struct Options { impl NodeState { pub(super) fn new(id: usize, options: Options) -> Self { - let quic_mapped_addr = AllPathsMappedAddr::generate(); + let quic_mapped_addr = NodeIdMappedAddr::generate(); // TODO(frando): I don't think we need to track the `num_relay_conns_added` // metric here. We do so in `Self::addr_for_send`. @@ -154,7 +154,7 @@ impl NodeState { &self.node_id } - pub(super) fn all_paths_mapped_addr(&self) -> &AllPathsMappedAddr { + pub(super) fn all_paths_mapped_addr(&self) -> &NodeIdMappedAddr { &self.quic_mapped_addr } diff --git a/iroh/src/magicsock/relay_mapped_addrs.rs b/iroh/src/magicsock/relay_mapped_addrs.rs index 608e154b53c..62623fefb54 100644 --- a/iroh/src/magicsock/relay_mapped_addrs.rs +++ b/iroh/src/magicsock/relay_mapped_addrs.rs @@ -10,10 +10,10 @@ use std::{ use iroh_base::{NodeId, RelayUrl}; use snafu::Snafu; -/// Can occur when converting a [`SocketAddr`] to an [`IpMappedAddr`] +/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] #[derive(Debug, Snafu)] #[snafu(display("Failed to convert"))] -pub struct IpMappedAddrError; +pub struct RelayMappedAddrError; /// An Ipv6 ULA address, identifying a relay path for a [`NodeId`]. /// @@ -24,8 +24,8 @@ pub struct IpMappedAddrError; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub(crate) struct RelayMappedAddr(Ipv6Addr); -/// Counter to always generate unique addresses for [`IpMappedAddr`]. -static IP_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); +/// Counter to always generate unique addresses for [`RelayMappedAddr`]. +static RELAY_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); impl RelayMappedAddr { /// The Prefix/L of our Unique Local Addresses. @@ -48,7 +48,7 @@ impl RelayMappedAddr { addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID); addr[6..8].copy_from_slice(&Self::ADDR_SUBNET); - let counter = IP_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); + let counter = RELAY_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); addr[8..16].copy_from_slice(&counter.to_be_bytes()); Self(Ipv6Addr::from(addr)) @@ -67,7 +67,7 @@ impl RelayMappedAddr { } impl TryFrom for RelayMappedAddr { - type Error = IpMappedAddrError; + type Error = RelayMappedAddrError; fn try_from(value: Ipv6Addr) -> std::result::Result { let octets = value.octets(); @@ -77,7 +77,7 @@ impl TryFrom for RelayMappedAddr { { return Ok(Self(value)); } - Err(IpMappedAddrError) + Err(RelayMappedAddrError) } } @@ -129,7 +129,7 @@ impl RelayAddrMap { inner.by_url.get(&(relay, node)).copied() } - /// Returns the [`RelayUrl`] and [`NodeId`] for the given [`IpMappedAddr`]. + /// Returns the [`RelayUrl`] and [`NodeId`] for the given [`RelayMappedAddr`]. pub(crate) fn get_url(&self, mapped_addr: &RelayMappedAddr) -> Option<(RelayUrl, NodeId)> { let inner = self.0.lock().expect("poisoned"); inner.by_mapped_addr.get(mapped_addr).cloned() diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 887f223fcb0..2d684068691 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -592,7 +592,7 @@ impl quinn::AsyncUdpSocket for MagicTransport { /// A sender for [`MagicTransport`]. /// /// This is special in that it handles [`MultipathMappedAddr::Mixed`] by delegating to the -/// [`MagicSock`] which expands it back to one or more [`transport::Addr`]s and sends it +/// [`MagicSock`] which expands it back to one or more [`Addr`]s and sends it /// using the underlying [`Transports`]. // TODO: Can I just send the TransportsSender along in the NodeStateMessage::SendDatagram // message?? That way you don't have to hook up the sender into the NodeMap! @@ -605,7 +605,7 @@ pub(crate) struct MagicSender { } impl MagicSender { - /// Extracts the right [`transports::Addr`] from the [`quinn_udp::Transmit`]. + /// Extracts the right [`Addr`] from the [`quinn_udp::Transmit`]. /// /// Because Quinn does only know about IP transports we map other transports to private /// IPv6 Unique Local Address ranges. This extracts the transport addresses out of the From 4b00ad729eac6ab877ffd1ee031cfe9a7c5d9a56 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 16 Sep 2025 12:41:04 +0200 Subject: [PATCH 030/164] Move all mapped addrs to one module Should be a little tidier, probably, hopefully. --- iroh/src/endpoint.rs | 2 +- iroh/src/magicsock.rs | 135 +---------- iroh/src/magicsock/mapped_addrs.rs | 259 ++++++++++++++++++++++ iroh/src/magicsock/node_map.rs | 2 +- iroh/src/magicsock/node_map/node_state.rs | 3 +- iroh/src/magicsock/relay_mapped_addrs.rs | 137 ------------ iroh/src/magicsock/transports.rs | 7 +- 7 files changed, 276 insertions(+), 269 deletions(-) create mode 100644 iroh/src/magicsock/mapped_addrs.rs delete mode 100644 iroh/src/magicsock/relay_mapped_addrs.rs diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index ae331f68f92..741fdbab89b 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -62,7 +62,7 @@ use crate::{ DiscoverySubscribers, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, IntoDiscoveryError, Lagged, UserData, pkarr::PkarrPublisher, }, - magicsock::{self, Handle, NodeIdMappedAddr, OwnAddressSnafu}, + magicsock::{self, Handle, OwnAddressSnafu, mapped_addrs::NodeIdMappedAddr}, metrics::EndpointMetrics, net_report::Report, tls, diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 19f0537aee3..8fb637322e0 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -22,7 +22,7 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, sync::{ Arc, Mutex, RwLock, - atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, }, }; @@ -42,7 +42,6 @@ use netwatch::netmon; use netwatch::{UdpSocket, ip::LocalAddresses}; use quinn::{ServerConfig, WeakConnectionHandle}; use rand::Rng; -use relay_mapped_addrs::{RelayAddrMap, RelayMappedAddr}; use snafu::{ResultExt, Snafu}; use tokio::sync::{Mutex as AsyncMutex, mpsc}; use tokio_util::sync::CancellationToken; @@ -75,17 +74,16 @@ use crate::{ }; mod metrics; -mod relay_mapped_addrs; +pub(crate) mod mapped_addrs; pub(crate) mod node_map; pub(crate) mod transports; -pub use node_map::Source; +use mapped_addrs::{NodeIdMappedAddr, RelayAddrMap}; -pub use self::{ - metrics::Metrics, - node_map::{ConnectionType, ControlMsg, DirectAddrInfo, RemoteInfo}, -}; +pub use metrics::Metrics; +pub use node_map::Source; +pub use node_map::{ConnectionType, ControlMsg, DirectAddrInfo, RemoteInfo}; /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically /// expire at 30 seconds, so this is a few seconds shy of that. @@ -631,6 +629,8 @@ impl MagicSock { /// /// This fixes up the datagrams to use the correct [`MultipathMappedAddr`] and extracts /// DISCO packets, processing them inside the magic socket. + /// + /// [`MultipathMappedAddr`]: mapped_addrs::MultipathMappedAddr fn process_datagrams( &self, bufs: &mut [io::IoSliceMut<'_>], @@ -995,41 +995,6 @@ impl MagicSock { } } -/// Definies the translation of addresses in quinn land vs iroh land. -/// -/// This is necessary, because quinn can only reason about `SocketAddr`s. -#[derive(Clone, Debug)] -pub(crate) enum MultipathMappedAddr { - /// Used for the initial connection. - /// - /// - Only used for sending - /// - This means send on all known paths/transports - Mixed(NodeIdMappedAddr), - /// Relay based transport address - Relay(RelayMappedAddr), - /// IP based transport address - #[cfg(not(wasm_browser))] - Ip(SocketAddr), -} - -impl From for MultipathMappedAddr { - fn from(value: SocketAddr) -> Self { - match value.ip() { - IpAddr::V4(_) => Self::Ip(value), - IpAddr::V6(addr) => { - if let Ok(addr) = NodeIdMappedAddr::try_from(addr) { - return Self::Mixed(addr); - } - #[cfg(not(wasm_browser))] - if let Ok(addr) = RelayMappedAddr::try_from(addr) { - return Self::Relay(addr); - } - Self::Ip(value) - } - } - } -} - /// Manages currently running direct addr discovery, aka net_report runs. /// /// Invariants: @@ -2220,90 +2185,6 @@ impl DiscoveredDirectAddrs { } } -/// An address used by the QUIC layer to address a node on any or all paths. -/// -/// This is only used for initially connecting to a remote node. We instruct Quinn to send -/// to this address, and duplicate all packets for this address to send on all paths we -/// might want to send the initial on: -/// -/// - If this the first connection to the remote node we don't know which path will work and -/// send to all of them. -/// -/// - If there already is an active connection to this node we now which path to use. -/// -/// It is but a newtype around an IPv6 Unique Local Addr. And in our QUIC-facing socket -/// APIs like [`quinn::AsyncUdpSocket`] it comes in as the inner [`Ipv6Addr`], in those -/// interfaces we have to be careful to do the conversion to this type. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -pub(crate) struct NodeIdMappedAddr(Ipv6Addr); - -/// Can occur when converting a [`SocketAddr`] to an [`NodeIdMappedAddr`] -#[derive(Debug, Snafu)] -#[snafu(display("Failed to convert"))] -pub struct AllPathsMappedAddrError; - -/// Counter to always generate unique addresses for [`NodeIdMappedAddr`]. -static ALL_PATHS_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); - -impl NodeIdMappedAddr { - /// The Prefix/L of our Unique Local Addresses. - const ADDR_PREFIXL: u8 = 0xfd; - /// The Global ID used in our Unique Local Addresses. - const ADDR_GLOBAL_ID: [u8; 5] = [21, 7, 10, 81, 11]; - /// The Subnet ID used in our Unique Local Addresses. - const ADDR_SUBNET: [u8; 2] = [0; 2]; - - /// The dummy port used for all [`NodeIdMappedAddr`]s. - const MAPPED_PORT: u16 = 12345; - - /// Generates a globally unique fake UDP address. - /// - /// This generates and IPv6 Unique Local Address according to RFC 4193. - pub(crate) fn generate() -> Self { - let mut addr = [0u8; 16]; - addr[0] = Self::ADDR_PREFIXL; - addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID); - addr[6..8].copy_from_slice(&Self::ADDR_SUBNET); - - let counter = ALL_PATHS_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); - addr[8..16].copy_from_slice(&counter.to_be_bytes()); - - Self(Ipv6Addr::from(addr)) - } - - /// Returns a consistent [`SocketAddr`] for the [`NodeIdMappedAddr`]. - /// - /// This socket address does not have a routable IP address. - /// - /// This uses a made-up port number, since the port does not play a role in looking up - /// the node in the [`NodeMap`]. This socket address is only to be used to pass into - /// Quinn. - pub(crate) fn private_socket_addr(&self) -> SocketAddr { - SocketAddr::new(IpAddr::from(self.0), Self::MAPPED_PORT) - } -} - -impl TryFrom for NodeIdMappedAddr { - type Error = AllPathsMappedAddrError; - - fn try_from(value: Ipv6Addr) -> Result { - let octets = value.octets(); - if octets[0] == Self::ADDR_PREFIXL - && octets[1..6] == Self::ADDR_GLOBAL_ID - && octets[6..8] == Self::ADDR_SUBNET - { - return Ok(Self(value)); - } - Err(AllPathsMappedAddrError) - } -} - -impl std::fmt::Display for NodeIdMappedAddr { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "NodeIdMappedAddr({})", self.0) - } -} - fn disco_message_sent(msg: &disco::Message, metrics: &MagicsockMetrics) { match msg { disco::Message::Ping(_) => { diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs new file mode 100644 index 00000000000..3c1eb3741a5 --- /dev/null +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -0,0 +1,259 @@ +//! The various mapped addresses we use. +//! + +//! We use non-IP transports to carry datagrams. Yet Quinn needs to address those +//! transports using IPv6 addresses. These defines mappings of several IPv6 Unique Local +//! Address ranges we use to keep track of the various "fake" address types we use. + +use std::{ + collections::BTreeMap, + net::{IpAddr, Ipv6Addr, SocketAddr}, + sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; + +use iroh_base::{NodeId, RelayUrl}; +use snafu::Snafu; + +/// The Prefix/L of all Unique Local Addresses. +const ADDR_PREFIXL: u8 = 0xfd; + +/// The Global ID used in n0's Unique Local Addresses. +const ADDR_GLOBAL_ID: [u8; 5] = [21, 7, 10, 81, 11]; + +/// The Subnet ID for [`RelayMappedAddr]. +const RELAY_MAPPED_SUBNET: [u8; 2] = [0, 1]; + +/// The Subnet ID for [`NodeIdMappedAddr`]. +const NODE_ID_SUBNET: [u8; 2] = [0; 2]; + +/// The dummy port used for all mapped addresses. +/// +/// We map each entity, usually a [`NodeId`], to an IPv6 address. But socket addresses +/// involve ports, so we use a dummy fixed port when creating socket addresses. +const MAPPED_PORT: u16 = 12345; + +/// Counter to always generate unique addresses for [`RelayMappedAddr`]. +static RELAY_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); + +/// Counter to always generate unique addresses for [`NodeIdMappedAddr`]. +static NODE_ID_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); + +/// An enum encompassing all the mapped and unmapped addresses. +/// +/// This can consistently convert a socket address as we use them in Quinn and return a real +/// socket address or a mapped address. Note that this does not mean that the mapped +/// address exists, only that it is semantically a valid mapped address. +#[derive(Clone, Debug)] +pub(crate) enum MultipathMappedAddr { + /// An address for a [`NodeId`], via one or more paths. + Mixed(NodeIdMappedAddr), + /// An address for a particular [`NodeId`] via a particular relay. + Relay(RelayMappedAddr), + /// An IP based transport address. + #[cfg(not(wasm_browser))] + Ip(SocketAddr), +} + +impl From for MultipathMappedAddr { + fn from(value: SocketAddr) -> Self { + match value.ip() { + IpAddr::V4(_) => Self::Ip(value), + IpAddr::V6(addr) => { + if let Ok(addr) = NodeIdMappedAddr::try_from(addr) { + return Self::Mixed(addr); + } + #[cfg(not(wasm_browser))] + if let Ok(addr) = RelayMappedAddr::try_from(addr) { + return Self::Relay(addr); + } + Self::Ip(value) + } + } + } +} + +/// An address used to address a node on any or all paths. +/// +/// This is only used for initially connecting to a remote node. We instruct Quinn to send +/// to this address, and duplicate all packets for this address to send on all paths we +/// might want to send the initial on: +/// +/// - If this the first connection to the remote node we don't know which path will work and +/// send to all of them. +/// +/// - If there already is an active connection to this node we now which path to use. +/// +/// It is but a newtype around an IPv6 Unique Local Addr. And in our QUIC-facing socket +/// APIs like [`quinn::AsyncUdpSocket`] it comes in as the inner [`Ipv6Addr`], in those +/// interfaces we have to be careful to do the conversion to this type. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) struct NodeIdMappedAddr(Ipv6Addr); + +impl NodeIdMappedAddr { + /// Generates a globally unique fake UDP address. + /// + /// This generates and IPv6 Unique Local Address according to RFC 4193. + pub(crate) fn generate() -> Self { + let mut addr = [0u8; 16]; + addr[0] = ADDR_PREFIXL; + addr[1..6].copy_from_slice(&ADDR_GLOBAL_ID); + addr[6..8].copy_from_slice(&NODE_ID_SUBNET); + + let counter = NODE_ID_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); + addr[8..16].copy_from_slice(&counter.to_be_bytes()); + + Self(Ipv6Addr::from(addr)) + } + + /// Returns a consistent [`SocketAddr`] for the [`NodeIdMappedAddr`]. + /// + /// This socket address does not have a routable IP address. + /// + /// This uses a made-up port number, since the port does not play a role in the + /// addressing. This socket address is only to be used to pass into Quinn. + pub(crate) fn private_socket_addr(&self) -> SocketAddr { + SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) + } +} + +impl std::fmt::Display for NodeIdMappedAddr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "NodeIdMappedAddr({})", self.0) + } +} + +impl TryFrom for NodeIdMappedAddr { + type Error = NodeIdMappedAddrError; + + fn try_from(value: Ipv6Addr) -> Result { + let octets = value.octets(); + if octets[0] == ADDR_PREFIXL + && octets[1..6] == ADDR_GLOBAL_ID + && octets[6..8] == NODE_ID_SUBNET + { + return Ok(Self(value)); + } + Err(NodeIdMappedAddrError) + } +} + +/// Can occur when converting a [`SocketAddr`] to an [`NodeIdMappedAddr`] +#[derive(Debug, Snafu)] +#[snafu(display("Failed to convert"))] +pub struct NodeIdMappedAddrError; + +/// An Ipv6 ULA address, identifying a relay path for a [`NodeId`]. +/// +/// Since iroh nodes are reachable via a relay server we have a network path indicated by +/// the `(NodeId, RelayUrl)`. However Quinn can only handle socket addresses, so we use +/// IPv6 addresses in a private IPv6 Unique Local Address range, which map to a unique +/// `(NodeId, RelayUrl)` pair. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] +pub(crate) struct RelayMappedAddr(Ipv6Addr); + +impl RelayMappedAddr { + /// Generates a globally unique fake UDP address. + /// + /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) + /// which is recognised by iroh as an IP mapped address. + pub(crate) fn generate() -> Self { + let mut addr = [0u8; 16]; + addr[0] = ADDR_PREFIXL; + addr[1..6].copy_from_slice(&ADDR_GLOBAL_ID); + addr[6..8].copy_from_slice(&RELAY_MAPPED_SUBNET); + + let counter = RELAY_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); + addr[8..16].copy_from_slice(&counter.to_be_bytes()); + + Self(Ipv6Addr::from(addr)) + } + + /// Returns a consistent [`SocketAddr`] for the [`RelayMappedAddr`]. + /// + /// This does not have a routable IP address. + /// + /// This uses a made-up, but fixed port number. The [`RelayAddrMap`] creates a unique + /// [`RelayMappedAddr`] for each `(NodeId, RelayUrl)` pair and thus does not use the + /// port to map back to the original [`SocketAddr`]. + pub(crate) fn private_socket_addr(&self) -> SocketAddr { + SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) + } +} + +impl TryFrom for RelayMappedAddr { + type Error = RelayMappedAddrError; + + fn try_from(value: Ipv6Addr) -> std::result::Result { + let octets = value.octets(); + if octets[0] == ADDR_PREFIXL + && octets[1..6] == ADDR_GLOBAL_ID + && octets[6..8] == RELAY_MAPPED_SUBNET + { + return Ok(Self(value)); + } + Err(RelayMappedAddrError) + } +} + +impl std::fmt::Display for RelayMappedAddr { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "RelayMappedAddr({})", self.0) + } +} + +/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] +#[derive(Debug, Snafu)] +#[snafu(display("Failed to convert"))] +pub struct RelayMappedAddrError; + +/// A Map of [`RelayMappedAddr`] to `(RelayUrl, NodeId)`. +// TODO: this could be an RwLock, or even an dashmap +#[derive(Debug, Clone, Default)] +pub(crate) struct RelayAddrMap(Arc>); + +#[derive(Debug, Default)] +pub(super) struct Inner { + by_mapped_addr: BTreeMap, + by_url: BTreeMap<(RelayUrl, NodeId), RelayMappedAddr>, +} + +impl RelayAddrMap { + /// Adds a new entry to the map and returns the generated [`RelayMappedAddr`]. + /// + /// If this `(RelayUrl, NodeId)` already exists in the map, it returns its associated + /// [`RelayMappedAddr`]. + /// + /// Otherwise a new [`RelayMappedAddr`] is generated for it and returned. + pub(super) fn get_or_register(&self, relay: RelayUrl, node: NodeId) -> RelayMappedAddr { + let mut inner = self.0.lock().expect("poisoned"); + if let Some(mapped_addr) = inner.by_url.get(&(relay.clone(), node)) { + return *mapped_addr; + } + let ip_mapped_addr = RelayMappedAddr::generate(); + inner + .by_mapped_addr + .insert(ip_mapped_addr, (relay.clone(), node)); + inner.by_url.insert((relay, node), ip_mapped_addr); + ip_mapped_addr + } + + /// Returns the [`RelayMappedAddr`] for the given [`RelayUrl`] and [`NodeId`]. + pub(crate) fn get_mapped_addr(&self, relay: RelayUrl, node: NodeId) -> Option { + let inner = self.0.lock().expect("poisoned"); + inner.by_url.get(&(relay, node)).copied() + } + + /// Returns the [`RelayUrl`] and [`NodeId`] for the given [`RelayMappedAddr`]. + pub(crate) fn get_url(&self, mapped_addr: &RelayMappedAddr) -> Option<(RelayUrl, NodeId)> { + let inner = self.0.lock().expect("poisoned"); + inner.by_mapped_addr.get(mapped_addr).cloned() + } +} + +/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] +#[derive(Debug, Snafu)] +#[snafu(display("Failed to convert"))] +pub struct RelayAddrMapError; diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 85317e3f5f4..aa784cc9de3 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -18,7 +18,7 @@ use super::transports::TransportsSender; #[cfg(not(any(test, feature = "test-utils")))] use super::transports::TransportsSender; use super::{ - NodeIdMappedAddr, + mapped_addrs::NodeIdMappedAddr, metrics::Metrics, transports::{self, OwnedTransmit}, }; diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index ead6be40894..ea348ff9ab2 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -25,7 +25,8 @@ use crate::endpoint::PathSelection; use crate::{ disco::{self, SendAddr}, magicsock::{ - HEARTBEAT_INTERVAL, MagicsockMetrics, NodeIdMappedAddr, + HEARTBEAT_INTERVAL, MagicsockMetrics, + mapped_addrs::NodeIdMappedAddr, node_map::path_validity::PathValidity, transports::{self, OwnedTransmit, TransportsSender}, }, diff --git a/iroh/src/magicsock/relay_mapped_addrs.rs b/iroh/src/magicsock/relay_mapped_addrs.rs deleted file mode 100644 index 62623fefb54..00000000000 --- a/iroh/src/magicsock/relay_mapped_addrs.rs +++ /dev/null @@ -1,137 +0,0 @@ -use std::{ - collections::BTreeMap, - net::{IpAddr, Ipv6Addr, SocketAddr}, - sync::{ - Arc, - atomic::{AtomicU64, Ordering}, - }, -}; - -use iroh_base::{NodeId, RelayUrl}; -use snafu::Snafu; - -/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] -#[derive(Debug, Snafu)] -#[snafu(display("Failed to convert"))] -pub struct RelayMappedAddrError; - -/// An Ipv6 ULA address, identifying a relay path for a [`NodeId`]. -/// -/// Since iroh nodes are reachable via a relay server we have a network path indicated by -/// the `(NodeId, RelayUrl)`. However Quinn can only handle socket addresses, so we use -/// IPv6 addresses in a private IPv6 Unique Local Address range, which map to a unique -/// `(NodeId, RelayUrl)` pair. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] -pub(crate) struct RelayMappedAddr(Ipv6Addr); - -/// Counter to always generate unique addresses for [`RelayMappedAddr`]. -static RELAY_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); - -impl RelayMappedAddr { - /// The Prefix/L of our Unique Local Addresses. - const ADDR_PREFIXL: u8 = 0xfd; - /// The Global ID used in our Unique Local Addresses. - const ADDR_GLOBAL_ID: [u8; 5] = [21, 7, 10, 81, 11]; - /// The Subnet ID used in our Unique Local Addresses. - const ADDR_SUBNET: [u8; 2] = [0, 1]; - - /// The dummy port used for all mapped addresses. - const MAPPED_ADDR_PORT: u16 = 12345; - - /// Generates a globally unique fake UDP address. - /// - /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) - /// which is recognised by iroh as an IP mapped address. - pub(crate) fn generate() -> Self { - let mut addr = [0u8; 16]; - addr[0] = Self::ADDR_PREFIXL; - addr[1..6].copy_from_slice(&Self::ADDR_GLOBAL_ID); - addr[6..8].copy_from_slice(&Self::ADDR_SUBNET); - - let counter = RELAY_ADDR_COUNTER.fetch_add(1, Ordering::Relaxed); - addr[8..16].copy_from_slice(&counter.to_be_bytes()); - - Self(Ipv6Addr::from(addr)) - } - - /// Returns a consistent [`SocketAddr`] for the [`RelayMappedAddr`]. - /// - /// This does not have a routable IP address. - /// - /// This uses a made-up, but fixed port number. The [`RelayAddrMap`] creates a unique - /// [`RelayMappedAddr`] for each `(NodeId, RelayUrl)` pair and thus does not use the - /// port to map back to the original [`SocketAddr`]. - pub(crate) fn private_socket_addr(&self) -> SocketAddr { - SocketAddr::new(IpAddr::from(self.0), Self::MAPPED_ADDR_PORT) - } -} - -impl TryFrom for RelayMappedAddr { - type Error = RelayMappedAddrError; - - fn try_from(value: Ipv6Addr) -> std::result::Result { - let octets = value.octets(); - if octets[0] == Self::ADDR_PREFIXL - && octets[1..6] == Self::ADDR_GLOBAL_ID - && octets[6..8] == Self::ADDR_SUBNET - { - return Ok(Self(value)); - } - Err(RelayMappedAddrError) - } -} - -impl std::fmt::Display for RelayMappedAddr { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "IpMappedAddr({})", self.0) - } -} - -/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] -#[derive(Debug, Snafu)] -#[snafu(display("Failed to convert"))] -pub struct RelayAddrMapError; - -/// A Map of [`RelayMappedAddr`] to `(RelayUrl, NodeId)`. -// TODO: this could be an RwLock, or even an dashmap -#[derive(Debug, Clone, Default)] -pub(crate) struct RelayAddrMap(Arc>); - -#[derive(Debug, Default)] -pub(super) struct Inner { - by_mapped_addr: BTreeMap, - by_url: BTreeMap<(RelayUrl, NodeId), RelayMappedAddr>, -} - -impl RelayAddrMap { - /// Adds a new entry to the map and returns the generated [`RelayMappedAddr`]. - /// - /// If this `(RelayUrl, NodeId)` already exists in the map, it returns its associated - /// [`RelayMappedAddr`]. - /// - /// Otherwise a new [`RelayMappedAddr`] is generated for it and returned. - pub(super) fn get_or_register(&self, relay: RelayUrl, node: NodeId) -> RelayMappedAddr { - let mut inner = self.0.lock().expect("poisoned"); - if let Some(mapped_addr) = inner.by_url.get(&(relay.clone(), node)) { - return *mapped_addr; - } - let ip_mapped_addr = RelayMappedAddr::generate(); - inner - .by_mapped_addr - .insert(ip_mapped_addr, (relay.clone(), node)); - inner.by_url.insert((relay, node), ip_mapped_addr); - ip_mapped_addr - } - - /// Returns the [`RelayMappedAddr`] for the given [`RelayUrl`] and [`NodeId`]. - pub(crate) fn get_mapped_addr(&self, relay: RelayUrl, node: NodeId) -> Option { - let inner = self.0.lock().expect("poisoned"); - inner.by_url.get(&(relay, node)).copied() - } - - /// Returns the [`RelayUrl`] and [`NodeId`] for the given [`RelayMappedAddr`]. - pub(crate) fn get_url(&self, mapped_addr: &RelayMappedAddr) -> Option<(RelayUrl, NodeId)> { - let inner = self.0.lock().expect("poisoned"); - inner.by_mapped_addr.get(mapped_addr).cloned() - } -} diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 2d684068691..868447495b9 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -13,6 +13,10 @@ use relay::{RelayNetworkChangeSender, RelaySender}; use tokio::sync::mpsc; use tracing::{debug, error, instrument, trace, warn}; +use crate::net_report::Report; + +use super::{MagicSock, mapped_addrs::MultipathMappedAddr, node_map::NodeStateMessage}; + #[cfg(not(wasm_browser))] mod ip; mod relay; @@ -21,9 +25,8 @@ mod relay; pub(crate) use self::ip::IpTransport; #[cfg(not(wasm_browser))] use self::ip::{IpNetworkChangeSender, IpSender}; + pub(crate) use self::relay::{RelayActorConfig, RelayTransport}; -use super::{MagicSock, node_map::NodeStateMessage}; -use crate::{magicsock::MultipathMappedAddr, net_report::Report}; /// Manages the different underlying data transports that the magicsock /// can support. From 31ab033a0f4af98cc5d5bf31bcc90202f21b9309 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 16 Sep 2025 17:10:00 +0200 Subject: [PATCH 031/164] allow me to send via the NodeStateActor --- iroh/src/magicsock.rs | 5 +- iroh/src/magicsock/mapped_addrs.rs | 33 ++++++- iroh/src/magicsock/node_map.rs | 108 ++++++++++++---------- iroh/src/magicsock/node_map/node_state.rs | 19 ++-- iroh/src/magicsock/transports.rs | 95 ++++++++++--------- 5 files changed, 151 insertions(+), 109 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 8fb637322e0..43d6bdb24e4 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1212,9 +1212,10 @@ impl Handle { let node_map = node_map.unwrap_or_default(); let sender = transports.create_sender(); #[cfg(any(test, feature = "test-utils"))] - let nm = NodeMap::load_from_vec(node_map, path_selection, &metrics.magicsock, sender); + let nm = + NodeMap::load_from_vec(node_map, path_selection, metrics.magicsock.clone(), sender); #[cfg(not(any(test, feature = "test-utils")))] - let nm = NodeMap::load_from_vec(node_map, &metrics.magicsock, sender); + let nm = NodeMap::load_from_vec(node_map, metrics.magicsock.clone(), sender); nm }; // let node_map = node_map.unwrap_or_default(); diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 3c1eb3741a5..0df09e8107a 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -6,7 +6,7 @@ //! Address ranges we use to keep track of the various "fake" address types we use. use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, net::{IpAddr, Ipv6Addr, SocketAddr}, sync::{ Arc, @@ -140,6 +140,37 @@ impl TryFrom for NodeIdMappedAddr { } } +/// A map between [`NodeId`] and [`NodeIdMappedAddr`]. +#[derive(Debug, Clone, Default)] +pub(super) struct NodeIdAddrMap { + inner: Arc>, +} + +impl NodeIdAddrMap { + pub(super) fn get(&self, node_id: NodeId) -> NodeIdMappedAddr { + let mut inner = self.inner.lock().expect("poisoned"); + match inner.node_addrs.get(&node_id) { + Some(addr) => *addr, + None => { + let addr = NodeIdMappedAddr::generate(); + inner.lookup.insert(addr, node_id); + addr + } + } + } + + pub(super) fn lookup(&self, addr: NodeIdMappedAddr) -> Option { + let inner = self.inner.lock().expect("poisoned"); + inner.lookup.get(&addr).copied() + } +} + +#[derive(Debug, Default)] +struct NodeIdAddrMapInner { + node_addrs: HashMap, + lookup: HashMap, +} + /// Can occur when converting a [`SocketAddr`] to an [`NodeIdMappedAddr`] #[derive(Debug, Snafu)] #[snafu(display("Failed to convert"))] diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index aa784cc9de3..6fed669912f 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -1,3 +1,5 @@ +#[cfg(any(test, feature = "test-utils"))] +use std::sync::Arc; use std::{ collections::{BTreeSet, HashMap, hash_map::Entry}, hash::Hash, @@ -7,7 +9,7 @@ use std::{ use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; use n0_future::{task::AbortOnDropHandle, time::Instant}; -use node_state::NodeStateHandle; +use node_state::{NodeStateActor, NodeStateHandle}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{debug, info, instrument, trace, warn}; @@ -18,8 +20,8 @@ use super::transports::TransportsSender; #[cfg(not(any(test, feature = "test-utils")))] use super::transports::TransportsSender; use super::{ - mapped_addrs::NodeIdMappedAddr, - metrics::Metrics, + MagicsockMetrics, + mapped_addrs::{NodeIdAddrMap, NodeIdMappedAddr}, transports::{self, OwnedTransmit}, }; use crate::disco::CallMeMaybe; @@ -59,10 +61,13 @@ const MAX_INACTIVE_NODES: usize = 30; #[derive(Debug)] pub(super) struct NodeMap { inner: Mutex, + /// The mapping between [`NodeId`]s and [`NodeIdMappedAddr`]s. + pub(super) node_mapped_addrs: NodeIdAddrMap, } #[derive(Debug)] pub(super) struct NodeMapInner { + metrics: Arc, /// Handle to an actor that can send over the transports. transports_handle: TransportsSenderHandle, by_node_key: HashMap, @@ -76,12 +81,6 @@ pub(super) struct NodeMapInner { /// /// [`NodeStateActor`]: node_state::NodeStateActor node_states: HashMap, - /// The [`NodeIdMappedAddr`] for each node. - node_addrs: HashMap, - /// The reverse of mapping of [`Self::node_addrs`]. - node_addrs_lookup: HashMap, - // /// The [`RelayMappedAddr`] for each node. - // relay_addrs: HashMap, } /// Identifier to look up a [`NodeState`] in the [`NodeMap`]. @@ -140,15 +139,15 @@ pub enum Source { impl NodeMap { #[cfg(any(test, feature = "test-utils"))] - pub(super) fn new(sender: TransportsSender) -> Self { - Self::from_inner(NodeMapInner::new(sender)) + pub(super) fn new(metrics: Arc, sender: TransportsSender) -> Self { + Self::from_inner(NodeMapInner::new(metrics, sender)) } #[cfg(not(any(test, feature = "test-utils")))] /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. pub(super) fn load_from_vec( nodes: Vec, - metrics: &Metrics, + metrics: Arc, sender: TransportsSender, ) -> Self { Self::from_inner(NodeMapInner::load_from_vec(nodes, metrics, sender)) @@ -159,7 +158,7 @@ impl NodeMap { pub(super) fn load_from_vec( nodes: Vec, path_selection: PathSelection, - metrics: &Metrics, + metrics: Arc, sender: TransportsSender, ) -> Self { Self::from_inner(NodeMapInner::load_from_vec( @@ -173,11 +172,17 @@ impl NodeMap { fn from_inner(inner: NodeMapInner) -> Self { Self { inner: Mutex::new(inner), + node_mapped_addrs: Default::default(), } } /// Add the contact information for a node. - pub(super) fn add_node_addr(&self, node_addr: NodeAddr, source: Source, metrics: &Metrics) { + pub(super) fn add_node_addr( + &self, + node_addr: NodeAddr, + source: Source, + metrics: &MagicsockMetrics, + ) { self.inner .lock() .expect("poisoned") @@ -234,7 +239,7 @@ impl NodeMap { &self, sender: PublicKey, cm: CallMeMaybe, - metrics: &Metrics, + metrics: &MagicsockMetrics, ) { self.inner .lock() @@ -247,7 +252,7 @@ impl NodeMap { &self, addr: NodeIdMappedAddr, have_ipv6: bool, - metrics: &Metrics, + metrics: &MagicsockMetrics, ) -> Option<( PublicKey, Option, @@ -321,30 +326,29 @@ impl NodeMap { /// Returns the sender for the [`NodeStateActor`]. /// /// [`NodeStateActor`]: node_state::NodeStateActor - pub(super) fn get_node_state_actor( - &self, - addr: NodeIdMappedAddr, - // node_id: NodeId, - ) -> Option> { - // self - // .inner - // .lock() - // .expect("poisoned") - // .new - // .entry(node_id) - // .or_insert_with_key(|node_id| { - // let mut actor = NodeStateActor::new(*node_id, transports_sender, metrics); - // actor.start() - // }); - todo!() + pub(super) fn node_state_actor(&self, node_id: NodeId) -> mpsc::Sender { + let mut inner = self.inner.lock().expect("poisoned"); + match inner.node_states.get(&node_id) { + Some(handle) => handle.sender.clone(), + None => { + let sender = inner.transports_handle.inbox.clone(); + let metrics = inner.metrics.clone(); + let actor = NodeStateActor::new(node_id, sender, metrics); + let handle = actor.start(); + let sender = handle.sender.clone(); + inner.node_states.insert(node_id, handle); + sender + } + } } } impl NodeMapInner { #[cfg(any(test, feature = "test-utils"))] - fn new(sender: TransportsSender) -> Self { + fn new(metrics: Arc, sender: TransportsSender) -> Self { let transports_handle = Self::start_transports_sender(sender); Self { + metrics, transports_handle, by_node_key: Default::default(), by_ip_port: Default::default(), @@ -353,16 +357,19 @@ impl NodeMapInner { next_id: 0, path_selection: Default::default(), node_states: Default::default(), - node_addrs: Default::default(), - node_addrs_lookup: Default::default(), } } /// Creates a new [`NodeMap`] from a list of [`NodeAddr`]s. #[cfg(not(any(test, feature = "test-utils")))] - fn load_from_vec(nodes: Vec, metrics: &Metrics, sender: TransportsSender) -> Self { + fn load_from_vec( + nodes: Vec, + metrics: Arc, + sender: TransportsSender, + ) -> Self { let transports_handle = Self::start_transports_sender(sender); let mut me = Self { + metrics: metrics.clone(), transports_handle, by_node_key: Default::default(), by_ip_port: Default::default(), @@ -370,11 +377,9 @@ impl NodeMapInner { by_id: Default::default(), next_id: 0, node_states: Default::default(), - node_addrs: Default::default(), - node_addrs_lookup: Default::default(), }; for node_addr in nodes { - me.add_node_addr(node_addr, Source::Saved, metrics); + me.add_node_addr(node_addr, Source::Saved, &metrics); } me } @@ -384,11 +389,12 @@ impl NodeMapInner { fn load_from_vec( nodes: Vec, path_selection: PathSelection, - metrics: &Metrics, + metrics: Arc, sender: TransportsSender, ) -> Self { let transports_handle = Self::start_transports_sender(sender); let mut me = Self { + metrics: metrics.clone(), transports_handle, by_node_key: Default::default(), by_ip_port: Default::default(), @@ -397,11 +403,9 @@ impl NodeMapInner { next_id: 0, path_selection, node_states: Default::default(), - node_addrs: Default::default(), - node_addrs_lookup: Default::default(), }; for node_addr in nodes { - me.add_node_addr(node_addr, Source::Saved, metrics); + me.add_node_addr(node_addr, Source::Saved, &metrics); } me } @@ -413,7 +417,8 @@ impl NodeMapInner { /// Add the contact information for a node. #[instrument(skip_all, fields(node = %node_addr.node_id.fmt_short()))] - fn add_node_addr(&mut self, node_addr: NodeAddr, source: Source, metrics: &Metrics) { + fn add_node_addr(&mut self, node_addr: NodeAddr, source: Source, metrics: &MagicsockMetrics) { + // TODO: Add to the NodeStateActor here. let source0 = source.clone(); let node_id = node_addr.node_id; let relay_url = node_addr.relay_url.clone(); @@ -568,7 +573,12 @@ impl NodeMapInner { .map(|ep| ep.conn_type()) } - fn handle_call_me_maybe(&mut self, sender: NodeId, cm: CallMeMaybe, metrics: &Metrics) { + fn handle_call_me_maybe( + &mut self, + sender: NodeId, + cm: CallMeMaybe, + metrics: &MagicsockMetrics, + ) { let ns_id = NodeStateKey::NodeId(sender); if let Some(id) = self.get_id(ns_id.clone()) { for number in &cm.my_numbers { @@ -818,7 +828,7 @@ mod tests { #[traced_test] async fn restore_from_vec() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let node_map = NodeMap::new(transports.create_sender()); + let node_map = NodeMap::new(Default::default(), transports.create_sender()); let mut rng = rand::thread_rng(); let node_a = SecretKey::generate(&mut rng).public(); @@ -858,7 +868,7 @@ mod tests { let loaded_node_map = NodeMap::load_from_vec( addrs.clone(), PathSelection::default(), - &Default::default(), + Default::default(), transports.create_sender(), ); @@ -889,7 +899,7 @@ mod tests { #[traced_test] fn test_prune_direct_addresses() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let node_map = NodeMap::new(transports.create_sender()); + let node_map = NodeMap::new(Default::default(), transports.create_sender()); let public_key = SecretKey::generate(rand::thread_rng()).public(); let id = node_map .inner @@ -963,7 +973,7 @@ mod tests { #[test] fn test_prune_inactive() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let node_map = NodeMap::new(transports.create_sender()); + let node_map = NodeMap::new(Default::default(), transports.create_sender()); // add one active node and more than MAX_INACTIVE_NODES inactive nodes let active_node = SecretKey::generate(rand::thread_rng()).public(); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167); diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index ea348ff9ab2..98cf7b2e282 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -15,11 +15,6 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{Level, debug, event, info, instrument, trace, warn}; -use super::{ - IpPort, Source, - path_state::{PathState, summarize_node_paths}, - udp_paths::{NodeUdpPaths, UdpSendAddr}, -}; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; use crate::{ @@ -28,10 +23,16 @@ use crate::{ HEARTBEAT_INTERVAL, MagicsockMetrics, mapped_addrs::NodeIdMappedAddr, node_map::path_validity::PathValidity, - transports::{self, OwnedTransmit, TransportsSender}, + transports::{self, OwnedTransmit}, }, }; +use super::{ + IpPort, Source, TransportsSenderMessage, + path_state::{PathState, summarize_node_paths}, + udp_paths::{NodeUdpPaths, UdpSendAddr}, +}; + /// Number of addresses that are not active that we keep around per node. /// /// See [`NodeState::prune_direct_addresses`]. @@ -708,7 +709,7 @@ impl NodeState { pub(super) struct NodeStateActor { /// The node ID of the remote node. node_id: NodeId, - transports_sender: TransportsSender, + transports_sender: mpsc::Sender, // TODO: Turn this into a WeakConnectionHandle connections: Vec, paths: BTreeMap, @@ -718,7 +719,7 @@ pub(super) struct NodeStateActor { impl NodeStateActor { pub(super) fn new( node_id: NodeId, - transports_sender: TransportsSender, + transports_sender: mpsc::Sender, metrics: Arc, ) -> Self { Self { @@ -790,7 +791,7 @@ pub(crate) enum NodeStateMessage { /// Dropping this will stop the actor. #[derive(Debug)] pub(super) struct NodeStateHandle { - sender: mpsc::Sender, + pub(super) sender: mpsc::Sender, _task: AbortOnDropHandle<()>, } diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 868447495b9..a66ce7555c8 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -646,7 +646,7 @@ impl MagicSender { impl quinn::UdpSender for MagicSender { #[instrument( skip_all, - fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst), + fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst, node_id), )] fn poll_send( self: Pin<&mut Self>, @@ -666,6 +666,12 @@ impl quinn::UdpSender for MagicSender { // TODO: Would be nicer to log the NodeId of this, but we only get an actor // sender for it. tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); + let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(mapped_addr) + else { + error!("unknown NodeIdMappedAddr, dropped transmit"); + return Poll::Ready(Ok(())); + }; + tracing::Span::current().record("node_id", node_id.fmt_short()); // Note we drop the src_ip set in the Quinn Transmit. This is only the // Initial packet we are sending, so we do not yet have an src address we @@ -673,26 +679,20 @@ impl quinn::UdpSender for MagicSender { if let Some(src_ip) = quinn_transmit.src_ip { warn!(?src_ip, "oops, flub didn't think this would happen"); } - return match self.msock.node_map.get_node_state_actor(mapped_addr) { - Some(sender) => { - let transmit = OwnedTransmit::from(quinn_transmit); - match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { - Ok(()) => { - trace!("sent transmit",); - Poll::Ready(Ok(())) - } - Err(err) => { - // We do not want to block the next send which might be on a - // different transport. Instead we let Quinn handle this as - // a lost datagram. - // TODO: Revisit this: we might want to do something better. - debug!("NodeStateActor inbox full ({err:#}), dropped transmit"); - Poll::Ready(Ok(())) - } - } + + let sender = self.msock.node_map.node_state_actor(node_id); + let transmit = OwnedTransmit::from(quinn_transmit); + return match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { + Ok(()) => { + trace!("sent transmit",); + Poll::Ready(Ok(())) } - None => { - error!("unknown AllPathsMappedAddr, dropped transmit"); + Err(err) => { + // We do not want to block the next send which might be on a + // different transport. Instead we let Quinn handle this as + // a lost datagram. + // TODO: Revisit this: we might want to do something better. + debug!("NodeStateActor inbox full ({err:#}), dropped transmit"); Poll::Ready(Ok(())) } }; @@ -746,7 +746,7 @@ impl quinn::UdpSender for MagicSender { #[instrument( skip_all, - fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst), + fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst, node_id), )] fn try_send(self: Pin<&mut Self>, quinn_transmit: &quinn_udp::Transmit) -> io::Result<()> { // As opposed to poll_send this method does return normal IO errors. Calls to this @@ -758,6 +758,15 @@ impl quinn::UdpSender for MagicSender { // TODO: Would be nicer to log the NodeId of this, but we only get an actor // sender for it. tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); + let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(mapped_addr) + else { + error!("unknown NodeIdMappedAddr, dropped transmit"); + return Err(io::Error::new( + io::ErrorKind::HostUnreachable, + "Unknown NodeIdMappedAddr", + )); + }; + tracing::Span::current().record("node_id", node_id.fmt_short()); // Note we drop the src_ip set in the Quinn Transmit. This is only the // Initial packet we are sending, so we do not yet have an src address we @@ -765,35 +774,25 @@ impl quinn::UdpSender for MagicSender { if let Some(src_ip) = quinn_transmit.src_ip { warn!(?src_ip, "oops, flub didn't think this would happen"); } - return match self.msock.node_map.get_node_state_actor(mapped_addr) { - Some(sender) => { - let transmit = OwnedTransmit::from(quinn_transmit); - match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { - Ok(()) => { - trace!("sent transmit",); - Ok(()) - } - Err(mpsc::error::TrySendError::Full(_)) => { - debug!("NodeStateActor inbox full, dropped transmit"); - Err(io::Error::new( - io::ErrorKind::WouldBlock, - "NodeStateActor inbox full", - )) - } - Err(mpsc::error::TrySendError::Closed(_)) => { - debug!("NodeStateActor inbox closed, dropped transmit"); - Err(io::Error::new( - io::ErrorKind::NetworkDown, - "NodeStateActor inbox closed", - )) - } - } + let sender = self.msock.node_map.node_state_actor(node_id); + let transmit = OwnedTransmit::from(quinn_transmit); + return match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { + Ok(()) => { + trace!("sent transmit",); + Ok(()) } - None => { - error!("unknown AllPathsMappedAddr, dropped transmit"); + Err(mpsc::error::TrySendError::Full(_)) => { + debug!("NodeStateActor inbox full, dropped transmit"); Err(io::Error::new( - io::ErrorKind::HostUnreachable, - "unknown AllPathsMappedAddr", + io::ErrorKind::WouldBlock, + "NodeStateActor inbox full", + )) + } + Err(mpsc::error::TrySendError::Closed(_)) => { + debug!("NodeStateActor inbox closed, dropped transmit"); + Err(io::Error::new( + io::ErrorKind::NetworkDown, + "NodeStateActor inbox closed", )) } }; From 5ae71edb7d39dbe8bd26d99fc266779ca22ba50d Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 16 Sep 2025 18:26:27 +0200 Subject: [PATCH 032/164] Unify NodeIdMappedAddr and RelayMappedAddr a bit more This moves all the mappings into the NodeMap and uses a generic struct for the mapping. --- iroh/src/endpoint.rs | 5 +- iroh/src/magicsock.rs | 93 +++---------- iroh/src/magicsock/mapped_addrs.rs | 153 ++++++++++------------ iroh/src/magicsock/node_map.rs | 9 +- iroh/src/magicsock/node_map/node_state.rs | 2 +- iroh/src/magicsock/transports.rs | 18 ++- 6 files changed, 110 insertions(+), 170 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 741fdbab89b..e517aec1584 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -62,7 +62,10 @@ use crate::{ DiscoverySubscribers, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, IntoDiscoveryError, Lagged, UserData, pkarr::PkarrPublisher, }, - magicsock::{self, Handle, OwnAddressSnafu, mapped_addrs::NodeIdMappedAddr}, + magicsock::{ + self, Handle, OwnAddressSnafu, + mapped_addrs::{MappedAddr, NodeIdMappedAddr}, + }, metrics::EndpointMetrics, net_report::Report, tls, diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 43d6bdb24e4..cf9da756afe 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -51,13 +51,6 @@ use tracing::{ use transports::{LocalAddrsWatch, MagicTransport}; use url::Url; -#[cfg(not(wasm_browser))] -use self::transports::IpTransport; -use self::{ - metrics::Metrics as MagicsockMetrics, - node_map::{NodeMap, PingAction}, - transports::{RelayActorConfig, RelayTransport, Transports, TransportsSender}, -}; #[cfg(not(wasm_browser))] use crate::dns::DnsResolver; #[cfg(any(test, feature = "test-utils"))] @@ -73,13 +66,21 @@ use crate::{ net_report::{self, IfStateDetails, Report}, }; +#[cfg(not(wasm_browser))] +use self::transports::IpTransport; +use self::{ + metrics::Metrics as MagicsockMetrics, + node_map::{NodeMap, PingAction}, + transports::{RelayActorConfig, RelayTransport, Transports, TransportsSender}, +}; + mod metrics; pub(crate) mod mapped_addrs; pub(crate) mod node_map; pub(crate) mod transports; -use mapped_addrs::{NodeIdMappedAddr, RelayAddrMap}; +use mapped_addrs::{MappedAddr, NodeIdMappedAddr}; pub use metrics::Metrics; pub use node_map::Source; @@ -204,8 +205,6 @@ pub(crate) struct MagicSock { /// Tracks existing connections connection_map: ConnectionMap, - /// Tracks the mapped IP addresses - relay_mapped_addrs: RelayAddrMap, /// Local addresses local_addrs_watch: LocalAddrsWatch, /// Currently bound IP addresses of all sockets @@ -462,9 +461,8 @@ impl MagicSock { self.node_map .add_node_addr(addr.clone(), source, &self.metrics.magicsock); - if let Some(url) = addr.relay_url() { - self.relay_mapped_addrs - .get_or_register(url.clone(), addr.node_id); + if let Some(url) = addr.relay_url().cloned() { + self.node_map.relay_mapped_addrs.get(&(url, addr.node_id)); } // Add paths to the existing connections @@ -763,8 +761,9 @@ impl MagicSock { // Relay let _quic_mapped_addr = self.node_map.receive_relay(src_url, *src_node); let mapped_addr = self + .node_map .relay_mapped_addrs - .get_or_register(src_url.clone(), *src_node); + .get(&(src_url.clone(), *src_node)); quinn_meta.addr = mapped_addr.private_socket_addr(); } } @@ -1177,8 +1176,6 @@ impl Handle { let (ip_transports, port_mapper) = bind_ip(addr_v4, addr_v6, &metrics).context(BindSocketsSnafu)?; - let relay_mapped_addrs = RelayAddrMap::default(); - let (actor_sender, actor_receiver) = mpsc::channel(256); let my_relay = Watchable::new(None); @@ -1235,7 +1232,6 @@ impl Handle { ipv6_reported, node_map, connection_map: Default::default(), - relay_mapped_addrs, discovery, discovery_user_data: RwLock::new(discovery_user_data), direct_addrs: Default::default(), @@ -1516,66 +1512,6 @@ enum DiscoBoxError { }, } -// #[derive(Debug)] -// struct MagicUdpSocket { -// socket: Arc, -// transports: Transports, -// } - -// impl AsyncUdpSocket for MagicUdpSocket { -// fn create_sender(&self) -> Pin> { -// Box::pin(self.transports.create_sender(self.socket.clone())) -// } - -// /// NOTE: Receiving on a closed socket will return [`Poll::Pending`] indefinitely. -// fn poll_recv( -// &mut self, -// cx: &mut Context, -// bufs: &mut [io::IoSliceMut<'_>], -// metas: &mut [quinn_udp::RecvMeta], -// ) -> Poll> { -// self.transports.poll_recv(cx, bufs, metas, &self.socket) -// } - -// #[cfg(not(wasm_browser))] -// fn local_addr(&self) -> io::Result { -// let addrs: Vec<_> = self -// .transports -// .local_addrs() -// .into_iter() -// .filter_map(|addr| { -// let addr: SocketAddr = addr.into_socket_addr()?; -// Some(addr) -// }) -// .collect(); - -// if let Some(addr) = addrs.iter().find(|addr| addr.is_ipv6()) { -// return Ok(*addr); -// } -// if let Some(SocketAddr::V4(addr)) = addrs.first() { -// // Pretend to be IPv6, because our `MappedAddr`s need to be IPv6. -// let ip = addr.ip().to_ipv6_mapped().into(); -// return Ok(SocketAddr::new(ip, addr.port())); -// } - -// Err(io::Error::other("no valid address available")) -// } - -// #[cfg(wasm_browser)] -// fn local_addr(&self) -> io::Result { -// // Again, we need to pretend we're IPv6, because of our `MappedAddr`s. -// Ok(SocketAddr::new(std::net::Ipv6Addr::LOCALHOST.into(), 0)) -// } - -// fn max_receive_segments(&self) -> usize { -// self.transports.max_receive_segments() -// } - -// fn may_fragment(&self) -> bool { -// self.transports.may_fragment() -// } -// } - #[derive(Debug)] enum ActorMessage { PingActions(Vec), @@ -2270,7 +2206,6 @@ mod tests { use tracing::{Instrument, error, info, info_span, instrument}; use tracing_test::traced_test; - use super::{NodeIdMappedAddr, Options}; use crate::{ Endpoint, RelayMap, RelayMode, SecretKey, dns::DnsResolver, @@ -2279,6 +2214,8 @@ mod tests { tls, }; + use super::{NodeIdMappedAddr, Options, mapped_addrs::MappedAddr}; + const ALPN: &[u8] = b"n0/test/1"; impl Default for Options { diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 0df09e8107a..d246aa84eff 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -6,7 +6,8 @@ //! Address ranges we use to keep track of the various "fake" address types we use. use std::{ - collections::{BTreeMap, HashMap}, + collections::HashMap, + hash::Hash, net::{IpAddr, Ipv6Addr, SocketAddr}, sync::{ Arc, @@ -14,7 +15,6 @@ use std::{ }, }; -use iroh_base::{NodeId, RelayUrl}; use snafu::Snafu; /// The Prefix/L of all Unique Local Addresses. @@ -41,6 +41,21 @@ static RELAY_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); /// Counter to always generate unique addresses for [`NodeIdMappedAddr`]. static NODE_ID_ADDR_COUNTER: AtomicU64 = AtomicU64::new(1); +/// Generic mapped address. +/// +/// Allows implementing [`AddrMap`]. +pub(crate) trait MappedAddr { + /// Generates a new mapped address in the IPv6 Unique Local Address space. + fn generate() -> Self; + + /// Returns a consistent [`SocketAddr`] for the mapped addr. + /// + /// This socket address does not have a routable IP address. It uses a fake but + /// consistent port number, since the port does not play a role in the addressing. This + /// socket address is only to be used to pass into Quinn. + fn private_socket_addr(&self) -> SocketAddr; +} + /// An enum encompassing all the mapped and unmapped addresses. /// /// This can consistently convert a socket address as we use them in Quinn and return a real @@ -92,11 +107,11 @@ impl From for MultipathMappedAddr { #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub(crate) struct NodeIdMappedAddr(Ipv6Addr); -impl NodeIdMappedAddr { +impl MappedAddr for NodeIdMappedAddr { /// Generates a globally unique fake UDP address. /// /// This generates and IPv6 Unique Local Address according to RFC 4193. - pub(crate) fn generate() -> Self { + fn generate() -> Self { let mut addr = [0u8; 16]; addr[0] = ADDR_PREFIXL; addr[1..6].copy_from_slice(&ADDR_GLOBAL_ID); @@ -114,7 +129,7 @@ impl NodeIdMappedAddr { /// /// This uses a made-up port number, since the port does not play a role in the /// addressing. This socket address is only to be used to pass into Quinn. - pub(crate) fn private_socket_addr(&self) -> SocketAddr { + fn private_socket_addr(&self) -> SocketAddr { SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) } } @@ -140,41 +155,10 @@ impl TryFrom for NodeIdMappedAddr { } } -/// A map between [`NodeId`] and [`NodeIdMappedAddr`]. -#[derive(Debug, Clone, Default)] -pub(super) struct NodeIdAddrMap { - inner: Arc>, -} - -impl NodeIdAddrMap { - pub(super) fn get(&self, node_id: NodeId) -> NodeIdMappedAddr { - let mut inner = self.inner.lock().expect("poisoned"); - match inner.node_addrs.get(&node_id) { - Some(addr) => *addr, - None => { - let addr = NodeIdMappedAddr::generate(); - inner.lookup.insert(addr, node_id); - addr - } - } - } - - pub(super) fn lookup(&self, addr: NodeIdMappedAddr) -> Option { - let inner = self.inner.lock().expect("poisoned"); - inner.lookup.get(&addr).copied() - } -} - -#[derive(Debug, Default)] -struct NodeIdAddrMapInner { - node_addrs: HashMap, - lookup: HashMap, -} - /// Can occur when converting a [`SocketAddr`] to an [`NodeIdMappedAddr`] #[derive(Debug, Snafu)] #[snafu(display("Failed to convert"))] -pub struct NodeIdMappedAddrError; +pub(crate) struct NodeIdMappedAddrError; /// An Ipv6 ULA address, identifying a relay path for a [`NodeId`]. /// @@ -185,12 +169,12 @@ pub struct NodeIdMappedAddrError; #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub(crate) struct RelayMappedAddr(Ipv6Addr); -impl RelayMappedAddr { +impl MappedAddr for RelayMappedAddr { /// Generates a globally unique fake UDP address. /// /// This generates a new IPv6 address in the Unique Local Address range (RFC 4193) /// which is recognised by iroh as an IP mapped address. - pub(crate) fn generate() -> Self { + fn generate() -> Self { let mut addr = [0u8; 16]; addr[0] = ADDR_PREFIXL; addr[1..6].copy_from_slice(&ADDR_GLOBAL_ID); @@ -209,7 +193,7 @@ impl RelayMappedAddr { /// This uses a made-up, but fixed port number. The [`RelayAddrMap`] creates a unique /// [`RelayMappedAddr`] for each `(NodeId, RelayUrl)` pair and thus does not use the /// port to map back to the original [`SocketAddr`]. - pub(crate) fn private_socket_addr(&self) -> SocketAddr { + fn private_socket_addr(&self) -> SocketAddr { SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) } } @@ -229,62 +213,65 @@ impl TryFrom for RelayMappedAddr { } } +/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] +#[derive(Debug, Snafu)] +#[snafu(display("Failed to convert"))] +pub(crate) struct RelayMappedAddrError; + impl std::fmt::Display for RelayMappedAddr { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "RelayMappedAddr({})", self.0) } } -/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] -#[derive(Debug, Snafu)] -#[snafu(display("Failed to convert"))] -pub struct RelayMappedAddrError; - -/// A Map of [`RelayMappedAddr`] to `(RelayUrl, NodeId)`. -// TODO: this could be an RwLock, or even an dashmap -#[derive(Debug, Clone, Default)] -pub(crate) struct RelayAddrMap(Arc>); - -#[derive(Debug, Default)] -pub(super) struct Inner { - by_mapped_addr: BTreeMap, - by_url: BTreeMap<(RelayUrl, NodeId), RelayMappedAddr>, +/// A bi-directional map between a key and a [`MappedAddr`]. +#[derive(Debug, Clone)] +pub(super) struct AddrMap { + inner: Arc>>, } -impl RelayAddrMap { - /// Adds a new entry to the map and returns the generated [`RelayMappedAddr`]. - /// - /// If this `(RelayUrl, NodeId)` already exists in the map, it returns its associated - /// [`RelayMappedAddr`]. - /// - /// Otherwise a new [`RelayMappedAddr`] is generated for it and returned. - pub(super) fn get_or_register(&self, relay: RelayUrl, node: NodeId) -> RelayMappedAddr { - let mut inner = self.0.lock().expect("poisoned"); - if let Some(mapped_addr) = inner.by_url.get(&(relay.clone(), node)) { - return *mapped_addr; +// Manual impl because derive ends up requiring T: Default. +impl Default for AddrMap { + fn default() -> Self { + Self { + inner: Default::default(), } - let ip_mapped_addr = RelayMappedAddr::generate(); - inner - .by_mapped_addr - .insert(ip_mapped_addr, (relay.clone(), node)); - inner.by_url.insert((relay, node), ip_mapped_addr); - ip_mapped_addr } +} - /// Returns the [`RelayMappedAddr`] for the given [`RelayUrl`] and [`NodeId`]. - pub(crate) fn get_mapped_addr(&self, relay: RelayUrl, node: NodeId) -> Option { - let inner = self.0.lock().expect("poisoned"); - inner.by_url.get(&(relay, node)).copied() +impl AddrMap { + /// Returns the [`MappedAddr`], generating one if needed. + pub(super) fn get(&self, key: &K) -> V { + let mut inner = self.inner.lock().expect("poisoned"); + match inner.addrs.get(&key) { + Some(addr) => *addr, + None => { + let addr = V::generate(); + inner.lookup.insert(addr, key.clone()); + addr + } + } } - /// Returns the [`RelayUrl`] and [`NodeId`] for the given [`RelayMappedAddr`]. - pub(crate) fn get_url(&self, mapped_addr: &RelayMappedAddr) -> Option<(RelayUrl, NodeId)> { - let inner = self.0.lock().expect("poisoned"); - inner.by_mapped_addr.get(mapped_addr).cloned() + /// Performs the reverse lookup. + pub(super) fn lookup(&self, addr: &V) -> Option { + let inner = self.inner.lock().expect("poisoned"); + inner.lookup.get(addr).cloned() } } -/// Can occur when converting a [`SocketAddr`] to an [`RelayMappedAddr`] -#[derive(Debug, Snafu)] -#[snafu(display("Failed to convert"))] -pub struct RelayAddrMapError; +#[derive(Debug)] +struct AddrMapInner { + addrs: HashMap, + lookup: HashMap, +} + +// Manual impl because derive ends up requiring T: Default. +impl Default for AddrMapInner { + fn default() -> Self { + Self { + addrs: Default::default(), + lookup: Default::default(), + } + } +} diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 6fed669912f..d773586d54e 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -1,4 +1,3 @@ -#[cfg(any(test, feature = "test-utils"))] use std::sync::Arc; use std::{ collections::{BTreeSet, HashMap, hash_map::Entry}, @@ -15,13 +14,14 @@ use tokio::sync::mpsc; use tracing::{debug, info, instrument, trace, warn}; use self::node_state::{NodeState, Options}; +use super::mapped_addrs::{AddrMap, RelayMappedAddr}; #[cfg(any(test, feature = "test-utils"))] use super::transports::TransportsSender; #[cfg(not(any(test, feature = "test-utils")))] use super::transports::TransportsSender; use super::{ MagicsockMetrics, - mapped_addrs::{NodeIdAddrMap, NodeIdMappedAddr}, + mapped_addrs::NodeIdMappedAddr, transports::{self, OwnedTransmit}, }; use crate::disco::CallMeMaybe; @@ -62,7 +62,9 @@ const MAX_INACTIVE_NODES: usize = 30; pub(super) struct NodeMap { inner: Mutex, /// The mapping between [`NodeId`]s and [`NodeIdMappedAddr`]s. - pub(super) node_mapped_addrs: NodeIdAddrMap, + pub(super) node_mapped_addrs: AddrMap, + /// The mapping between nodes via a relay and their [`RelayMappedAddr`]s. + pub(super) relay_mapped_addrs: AddrMap<(RelayUrl, NodeId), RelayMappedAddr>, } #[derive(Debug)] @@ -173,6 +175,7 @@ impl NodeMap { Self { inner: Mutex::new(inner), node_mapped_addrs: Default::default(), + relay_mapped_addrs: Default::default(), } } diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 98cf7b2e282..970284018b2 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -21,7 +21,7 @@ use crate::{ disco::{self, SendAddr}, magicsock::{ HEARTBEAT_INTERVAL, MagicsockMetrics, - mapped_addrs::NodeIdMappedAddr, + mapped_addrs::{MappedAddr, NodeIdMappedAddr}, node_map::path_validity::PathValidity, transports::{self, OwnedTransmit}, }, diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index a66ce7555c8..cdfe8c068fc 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -666,7 +666,7 @@ impl quinn::UdpSender for MagicSender { // TODO: Would be nicer to log the NodeId of this, but we only get an actor // sender for it. tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); - let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(mapped_addr) + let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(&mapped_addr) else { error!("unknown NodeIdMappedAddr, dropped transmit"); return Poll::Ready(Ok(())); @@ -698,7 +698,12 @@ impl quinn::UdpSender for MagicSender { }; } MultipathMappedAddr::Relay(relay_mapped_addr) => { - match self.msock.relay_mapped_addrs.get_url(&relay_mapped_addr) { + match self + .msock + .node_map + .relay_mapped_addrs + .lookup(&relay_mapped_addr) + { Some((relay_url, node_id)) => Addr::Relay(relay_url, node_id), None => { error!("unknown RelayMappedAddr, dropped transmit"); @@ -758,7 +763,7 @@ impl quinn::UdpSender for MagicSender { // TODO: Would be nicer to log the NodeId of this, but we only get an actor // sender for it. tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); - let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(mapped_addr) + let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(&mapped_addr) else { error!("unknown NodeIdMappedAddr, dropped transmit"); return Err(io::Error::new( @@ -798,7 +803,12 @@ impl quinn::UdpSender for MagicSender { }; } MultipathMappedAddr::Relay(relay_mapped_addr) => { - match self.msock.relay_mapped_addrs.get_url(&relay_mapped_addr) { + match self + .msock + .node_map + .relay_mapped_addrs + .lookup(&relay_mapped_addr) + { Some((relay_url, node_id)) => Addr::Relay(relay_url, node_id), None => { error!("unknown RelayMappedAddr, dropped transmit"); From 599f25db91eabb1fef9b8b6680e58d53050565fb Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 17 Sep 2025 16:03:18 +0200 Subject: [PATCH 033/164] start implementing AddConnection --- iroh/src/magicsock/node_map/node_state.rs | 25 ++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 970284018b2..96be833a826 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -6,6 +6,7 @@ use std::{ use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; use n0_future::{ + MergeUnbounded, task::AbortOnDropHandle, time::{Duration, Instant}, }; @@ -13,6 +14,7 @@ use n0_watcher::Watchable; use quinn::WeakConnectionHandle; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; +use tokio_stream::wrappers::BroadcastStream; use tracing::{Level, debug, event, info, instrument, trace, warn}; #[cfg(any(test, feature = "test-utils"))] @@ -712,6 +714,13 @@ pub(super) struct NodeStateActor { transports_sender: mpsc::Sender, // TODO: Turn this into a WeakConnectionHandle connections: Vec, + // TODO: Do we need to know which event comes from which connection? We could store + // connections in an FxHashMap using a u64 counter as index and map event streams to + // this index if so. Events only come with a PathId, so to know which actual path + // this refers to we need to know more. + path_events: MergeUnbounded>, + // TODO: We probably need some indexes from (Connection, PathId) pairs to + // transports::Addr. paths: BTreeMap, metrics: Arc, } @@ -726,6 +735,7 @@ impl NodeStateActor { node_id, transports_sender, connections: Vec::new(), + path_events: Default::default(), paths: BTreeMap::new(), metrics, } @@ -753,8 +763,17 @@ impl NodeStateActor { loop { if let Some(msg) = inbox.recv().await { match msg { - NodeStateMessage::SendDatagram(transmit) => todo!(), - NodeStateMessage::AddConnection(handle) => todo!(), + NodeStateMessage::SendDatagram(transmit) => { + // - do we have a currently selected path? + // - if not initiate holepunching + // - and then send along all paths + todo!(); + } + NodeStateMessage::AddConnection(handle) => { + let events = BroadcastStream::new(handle.path_events()); + self.path_events.push(events); + self.connections.push(handle); + } NodeStateMessage::PingReceived => todo!(), } } else { @@ -781,7 +800,7 @@ pub(crate) enum NodeStateMessage { /// The connection will now be managed by this actor. Holepunching will happen when /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. - AddConnection(WeakConnectionHandle), + AddConnection(quinn::Connection), // TODO: Add the transaction ID. PingReceived, } From 2417e6b0b29f5ccd7a431db25223a78ad62db11f Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 18 Sep 2025 13:33:35 +0200 Subject: [PATCH 034/164] make add_node_addr async and send it to the NodeStateActor Also cleans up some of the load_from_vec constructors between NodeMap and NodeMapInner. Some duplication deleted and the resulting constructors are simpler. --- iroh/src/discovery.rs | 4 +- iroh/src/endpoint.rs | 20 +-- iroh/src/magicsock.rs | 76 +++++++----- iroh/src/magicsock/node_map.rs | 142 ++++++---------------- iroh/src/magicsock/node_map/node_state.rs | 20 +-- 5 files changed, 112 insertions(+), 150 deletions(-) diff --git a/iroh/src/discovery.rs b/iroh/src/discovery.rs index a1907c2728b..c082cdb1488 100644 --- a/iroh/src/discovery.rs +++ b/iroh/src/discovery.rs @@ -648,7 +648,9 @@ impl DiscoveryTask { continue; } debug!(%provenance, addr = ?node_addr, "new address found"); - ep.add_node_addr_with_source(node_addr, provenance).ok(); + ep.add_node_addr_with_source(node_addr, provenance) + .await + .ok(); if let Some(tx) = on_first_tx.take() { tx.send(Ok(())).ok(); } diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index e517aec1584..21735528901 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -726,7 +726,7 @@ impl Endpoint { ensure!(node_addr.node_id != self.node_id(), SelfConnectSnafu); if !node_addr.is_empty() { - self.add_node_addr(node_addr.clone())?; + self.add_node_addr(node_addr.clone()).await?; } let node_id = node_addr.node_id; @@ -814,8 +814,9 @@ impl Endpoint { /// if the direct addresses are a subset of ours. /// /// [`StaticProvider`]: crate::discovery::static_provider::StaticProvider - pub fn add_node_addr(&self, node_addr: NodeAddr) -> Result<(), AddNodeAddrError> { + pub async fn add_node_addr(&self, node_addr: NodeAddr) -> Result<(), AddNodeAddrError> { self.add_node_addr_inner(node_addr, magicsock::Source::App) + .await } /// Informs this [`Endpoint`] about addresses of the iroh node, noting the source. @@ -837,7 +838,7 @@ impl Endpoint { /// if the direct addresses are a subset of ours. /// /// [`StaticProvider`]: crate::discovery::static_provider::StaticProvider - pub fn add_node_addr_with_source( + pub async fn add_node_addr_with_source( &self, node_addr: NodeAddr, source: &'static str, @@ -848,16 +849,17 @@ impl Endpoint { name: source.into(), }, ) + .await } - fn add_node_addr_inner( + async fn add_node_addr_inner( &self, node_addr: NodeAddr, source: magicsock::Source, ) -> Result<(), AddNodeAddrError> { // Connecting to ourselves is not supported. snafu::ensure!(node_addr.node_id != self.node_id(), OwnAddressSnafu); - self.msock.add_node_addr(node_addr, source) + self.msock.add_node_addr(node_addr, source).await } // # Getter methods for properties of this Endpoint itself. @@ -2258,7 +2260,7 @@ mod tests { let err = res.err().unwrap(); assert!(err.to_string().starts_with("Connecting to ourself")); - let res = ep.add_node_addr(my_addr); + let res = ep.add_node_addr(my_addr).await; assert!(res.is_err()); let err = res.err().unwrap(); assert!(err.to_string().starts_with("Adding our own address")); @@ -2394,7 +2396,7 @@ mod tests { // information for a peer let endpoint = new_endpoint(secret_key.clone(), None).await?; assert_eq!(endpoint.remote_info_iter().count(), 0); - endpoint.add_node_addr(node_addr.clone())?; + endpoint.add_node_addr(node_addr.clone()).await?; // Grab the current addrs let node_addrs: Vec = endpoint.remote_info_iter().map(Into::into).collect(); @@ -2589,8 +2591,8 @@ mod tests { let ep1_nodeaddr = ep1.node_addr().initialized().await; let ep2_nodeaddr = ep2.node_addr().initialized().await; - ep1.add_node_addr(ep2_nodeaddr.clone())?; - ep2.add_node_addr(ep1_nodeaddr.clone())?; + ep1.add_node_addr(ep2_nodeaddr.clone()).await?; + ep2.add_node_addr(ep1_nodeaddr.clone()).await?; let ep1_nodeid = ep1.node_id(); let ep2_nodeid = ep2.node_id(); eprintln!("node id 1 {ep1_nodeid}"); diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index cf9da756afe..6d8847142a4 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -442,9 +442,13 @@ impl MagicSock { self.node_map.get_direct_addrs(node_id) } - /// Add addresses for a node to the magic socket's addresbook. + /// Add potential addresses for a node to the [`NodeState`]. + /// + /// This is used to add possible paths that the remote node might be reachable on. They + /// will be used when there is no active connection to the node to attempt to establish + /// a connection. #[instrument(skip_all)] - pub fn add_node_addr( + pub(crate) async fn add_node_addr( &self, mut addr: NodeAddr, source: node_map::Source, @@ -452,21 +456,25 @@ impl MagicSock { let mut pruned: usize = 0; for my_addr in self.direct_addrs.sockaddrs() { if addr.direct_addresses.remove(&my_addr) { - warn!( node_id=addr.node_id.fmt_short(), %my_addr, %source, "not adding our addr for node"); + warn!( + node_id = addr.node_id.fmt_short(), + %my_addr, + %source, + "not adding our addr for node", + ); pruned += 1; } } if !addr.is_empty() { // Add addr to the internal NodeMap - self.node_map - .add_node_addr(addr.clone(), source, &self.metrics.magicsock); + self.node_map.add_node_addr(addr.clone(), source).await; if let Some(url) = addr.relay_url().cloned() { self.node_map.relay_mapped_addrs.get(&(url, addr.node_id)); } - // Add paths to the existing connections - self.add_paths(addr); + // // Add paths to the existing connections + // self.add_paths(addr); Ok(()) } else if pruned != 0 { @@ -1210,9 +1218,10 @@ impl Handle { let sender = transports.create_sender(); #[cfg(any(test, feature = "test-utils"))] let nm = - NodeMap::load_from_vec(node_map, path_selection, metrics.magicsock.clone(), sender); + NodeMap::load_from_vec(node_map, path_selection, metrics.magicsock.clone(), sender) + .await; #[cfg(not(any(test, feature = "test-utils")))] - let nm = NodeMap::load_from_vec(node_map, metrics.magicsock.clone(), sender); + let nm = NodeMap::load_from_vec(node_map, metrics.magicsock.clone(), sender).await; nm }; // let node_map = node_map.unwrap_or_default(); @@ -1758,7 +1767,7 @@ impl Actor { node_addr, Source::Discovery { name: provenance.to_string() - }) { + }).await { let node_addr = discovery_item.to_node_addr(); warn!(?node_addr, "unable to add discovered node address to the node map: {e:?}"); } @@ -2253,14 +2262,15 @@ mod tests { impl MagicSock { #[track_caller] - pub fn add_test_addr(&self, node_addr: NodeAddr) { + pub async fn add_test_addr(&self, node_addr: NodeAddr) { self.add_node_addr( node_addr, Source::NamedApp { name: "test".into(), }, ) - .unwrap() + .await + .ok(); } } @@ -2318,7 +2328,7 @@ mod tests { #[instrument(skip_all)] async fn mesh_stacks(stacks: Vec) -> Result> { /// Registers endpoint addresses of a node to all other nodes. - fn update_direct_addrs( + async fn update_direct_addrs( stacks: &[MagicStack], my_idx: usize, new_addrs: BTreeSet, @@ -2334,7 +2344,7 @@ mod tests { relay_url: None, direct_addresses: new_addrs.iter().map(|ep| ep.addr).collect(), }; - m.endpoint.magic_sock().add_test_addr(addr); + m.endpoint.magic_sock().add_test_addr(addr).await; } } @@ -2349,7 +2359,7 @@ mod tests { let mut stream = m.endpoint.direct_addresses().stream().filter_map(|i| i); while let Some(new_eps) = stream.next().await { info!(%me, "conn{} endpoints update: {:?}", my_idx + 1, new_eps); - update_direct_addrs(&stacks, my_idx, new_eps); + update_direct_addrs(&stacks, my_idx, new_eps).await; } }); } @@ -2909,6 +2919,7 @@ mod tests { name: "test".into(), }, ) + .await .unwrap(); let addr = msock_1.get_mapping_addr(node_id_2).unwrap(); let res = tokio::time::timeout( @@ -2963,17 +2974,19 @@ mod tests { let _accept_task = AbortOnDropHandle::new(accept_task); // Add an empty entry in the NodeMap of ep_1 - msock_1.node_map.add_node_addr( - NodeAddr { - node_id: node_id_2, - relay_url: None, - direct_addresses: Default::default(), - }, - Source::NamedApp { - name: "test".into(), - }, - &msock_1.metrics.magicsock, - ); + msock_1 + .node_map + .add_node_addr( + NodeAddr { + node_id: node_id_2, + relay_url: None, + direct_addresses: Default::default(), + }, + Source::NamedApp { + name: "test".into(), + }, + ) + .await; let addr_2 = msock_1.get_mapping_addr(node_id_2).unwrap(); @@ -3018,6 +3031,7 @@ mod tests { name: "test".into(), }, ) + .await .unwrap(); // We can now connect @@ -3058,6 +3072,7 @@ mod tests { .endpoint .magic_sock() .add_node_addr(empty_addr, node_map::Source::App) + .await .unwrap_err(); assert!( err.to_string() @@ -3074,7 +3089,8 @@ mod tests { stack .endpoint .magic_sock() - .add_node_addr(addr, node_map::Source::App)?; + .add_node_addr(addr, node_map::Source::App) + .await?; assert_eq!(stack.endpoint.magic_sock().node_map.node_count(), 1); // addrs only @@ -3086,7 +3102,8 @@ mod tests { stack .endpoint .magic_sock() - .add_node_addr(addr, node_map::Source::App)?; + .add_node_addr(addr, node_map::Source::App) + .await?; assert_eq!(stack.endpoint.magic_sock().node_map.node_count(), 2); // both @@ -3098,7 +3115,8 @@ mod tests { stack .endpoint .magic_sock() - .add_node_addr(addr, node_map::Source::App)?; + .add_node_addr(addr, node_map::Source::App) + .await?; assert_eq!(stack.endpoint.magic_sock().node_map.node_count(), 3); Ok(()) diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index d773586d54e..8ada4dc3838 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -147,28 +147,33 @@ impl NodeMap { #[cfg(not(any(test, feature = "test-utils")))] /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. - pub(super) fn load_from_vec( + pub(super) async fn load_from_vec( nodes: Vec, metrics: Arc, sender: TransportsSender, ) -> Self { - Self::from_inner(NodeMapInner::load_from_vec(nodes, metrics, sender)) + let me = Self::from_inner(NodeMapInner::new(metrics, sender)); + for addr in nodes { + me.add_node_addr(addr, Source::Saved).await; + } + me } #[cfg(any(test, feature = "test-utils"))] /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. - pub(super) fn load_from_vec( + pub(super) async fn load_from_vec( nodes: Vec, path_selection: PathSelection, metrics: Arc, sender: TransportsSender, ) -> Self { - Self::from_inner(NodeMapInner::load_from_vec( - nodes, - path_selection, - metrics, - sender, - )) + let mut inner = NodeMapInner::new(metrics, sender); + inner.path_selection = path_selection; + let me = Self::from_inner(inner); + for addr in nodes { + me.add_node_addr(addr, Source::Saved).await; + } + me } fn from_inner(inner: NodeMapInner) -> Self { @@ -179,17 +184,21 @@ impl NodeMap { } } - /// Add the contact information for a node. - pub(super) fn add_node_addr( - &self, - node_addr: NodeAddr, - source: Source, - metrics: &MagicsockMetrics, - ) { - self.inner - .lock() - .expect("poisoned") - .add_node_addr(node_addr, source, metrics) + /// Adds addresses where a node might be contactable. + pub(super) async fn add_node_addr(&self, node_addr: NodeAddr, source: Source) { + if let Some(ref relay_url) = node_addr.relay_url { + // Ensure we have a RelayMappedAddress for this. + self.relay_mapped_addrs + .get(&(relay_url.clone(), node_addr.node_id)); + } + let node_state = self.node_state_actor(node_addr.node_id); + + // This only fails if the sender is closed. That means the NodeStateActor has + // stopped, which only happens during shutdown. + node_state + .send(NodeStateMessage::AddNodeAddr(node_addr, source)) + .await + .ok(); } /// Number of nodes currently listed. @@ -328,18 +337,24 @@ impl NodeMap { /// Returns the sender for the [`NodeStateActor`]. /// + /// If needed a new actor is started on demand. + /// /// [`NodeStateActor`]: node_state::NodeStateActor pub(super) fn node_state_actor(&self, node_id: NodeId) -> mpsc::Sender { let mut inner = self.inner.lock().expect("poisoned"); match inner.node_states.get(&node_id) { Some(handle) => handle.sender.clone(), None => { + // Create a new NodeStateActor and insert it into the node map. let sender = inner.transports_handle.inbox.clone(); let metrics = inner.metrics.clone(); let actor = NodeStateActor::new(node_id, sender, metrics); let handle = actor.start(); let sender = handle.sender.clone(); inner.node_states.insert(node_id, handle); + + // Ensure there is a NodeMappedAddr for this NodeId. + self.node_mapped_addrs.get(&node_id); sender } } @@ -347,7 +362,6 @@ impl NodeMap { } impl NodeMapInner { - #[cfg(any(test, feature = "test-utils"))] fn new(metrics: Arc, sender: TransportsSender) -> Self { let transports_handle = Self::start_transports_sender(sender); Self { @@ -358,95 +372,17 @@ impl NodeMapInner { by_quic_mapped_addr: Default::default(), by_id: Default::default(), next_id: 0, + #[cfg(any(test, feature = "test-utils"))] path_selection: Default::default(), node_states: Default::default(), } } - /// Creates a new [`NodeMap`] from a list of [`NodeAddr`]s. - #[cfg(not(any(test, feature = "test-utils")))] - fn load_from_vec( - nodes: Vec, - metrics: Arc, - sender: TransportsSender, - ) -> Self { - let transports_handle = Self::start_transports_sender(sender); - let mut me = Self { - metrics: metrics.clone(), - transports_handle, - by_node_key: Default::default(), - by_ip_port: Default::default(), - by_quic_mapped_addr: Default::default(), - by_id: Default::default(), - next_id: 0, - node_states: Default::default(), - }; - for node_addr in nodes { - me.add_node_addr(node_addr, Source::Saved, &metrics); - } - me - } - - /// Creates a new [`NodeMap`] from a list of [`NodeAddr`]s. - #[cfg(any(test, feature = "test-utils"))] - fn load_from_vec( - nodes: Vec, - path_selection: PathSelection, - metrics: Arc, - sender: TransportsSender, - ) -> Self { - let transports_handle = Self::start_transports_sender(sender); - let mut me = Self { - metrics: metrics.clone(), - transports_handle, - by_node_key: Default::default(), - by_ip_port: Default::default(), - by_quic_mapped_addr: Default::default(), - by_id: Default::default(), - next_id: 0, - path_selection, - node_states: Default::default(), - }; - for node_addr in nodes { - me.add_node_addr(node_addr, Source::Saved, &metrics); - } - me - } - fn start_transports_sender(sender: TransportsSender) -> TransportsSenderHandle { let actor = TransportsSenderActor::new(sender); actor.start() } - /// Add the contact information for a node. - #[instrument(skip_all, fields(node = %node_addr.node_id.fmt_short()))] - fn add_node_addr(&mut self, node_addr: NodeAddr, source: Source, metrics: &MagicsockMetrics) { - // TODO: Add to the NodeStateActor here. - let source0 = source.clone(); - let node_id = node_addr.node_id; - let relay_url = node_addr.relay_url.clone(); - #[cfg(any(test, feature = "test-utils"))] - let path_selection = self.path_selection; - let node_state = self.get_or_insert_with(NodeStateKey::NodeId(node_id), || Options { - node_id, - relay_url, - active: false, - source, - #[cfg(any(test, feature = "test-utils"))] - path_selection, - }); - node_state.update_from_node_addr( - node_addr.relay_url.as_ref(), - &node_addr.direct_addresses, - source0, - metrics, - ); - let id = node_state.id(); - for addr in node_addr.direct_addresses() { - self.set_node_state_for_ip_port(*addr, id); - } - } - /// Prunes direct addresses from nodes that claim to share an address we know points to us. pub(super) fn on_direct_addr_discovered( &mut self, @@ -821,8 +757,7 @@ mod tests { Source::NamedApp { name: "test".into(), }, - &Default::default(), - ) + ); } } @@ -873,7 +808,8 @@ mod tests { PathSelection::default(), Default::default(), transports.create_sender(), - ); + ) + .await; let mut loaded: Vec = loaded_node_map .list_remote_infos(Instant::now()) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 96be833a826..aa8bebf1778 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -712,8 +712,7 @@ pub(super) struct NodeStateActor { /// The node ID of the remote node. node_id: NodeId, transports_sender: mpsc::Sender, - // TODO: Turn this into a WeakConnectionHandle - connections: Vec, + connections: Vec, // TODO: Do we need to know which event comes from which connection? We could store // connections in an FxHashMap using a u64 counter as index and map event streams to // this index if so. Events only come with a PathId, so to know which actual path @@ -770,11 +769,14 @@ impl NodeStateActor { todo!(); } NodeStateMessage::AddConnection(handle) => { - let events = BroadcastStream::new(handle.path_events()); - self.path_events.push(events); - self.connections.push(handle); + if let Some(conn) = handle.upgrade() { + let events = BroadcastStream::new(conn.path_events()); + self.path_events.push(events); + self.connections.push(handle); + } } NodeStateMessage::PingReceived => todo!(), + NodeStateMessage::AddNodeAddr(addr, source) => todo!(), } } else { break; @@ -786,7 +788,7 @@ impl NodeStateActor { /// Messages to send to the [`NodeStateActor`]. pub(crate) enum NodeStateMessage { - /// Send a datagram to all known paths. + /// Sends a datagram to all known paths. /// /// Used to send QUIC Initial packets. If there is no working direct path this will /// trigger holepunching. @@ -795,14 +797,16 @@ pub(crate) enum NodeStateMessage { /// operation with a bunch more copying. So it should only be used for sending QUIC /// Initial packets. SendDatagram(OwnedTransmit), - /// Add an active connection to this remote node. + /// Adds an active connection to this remote node. /// /// The connection will now be managed by this actor. Holepunching will happen when /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. - AddConnection(quinn::Connection), + AddConnection(WeakConnectionHandle), // TODO: Add the transaction ID. PingReceived, + /// Adds a [`NodeAddr`] with locations where the node might be reachable. + AddNodeAddr(NodeAddr, Source), } /// A handle to a [`NodeStateActor`]. From 9820e60880b3675d8fed5c8fa46912c7e9c0c317 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 18 Sep 2025 16:24:48 +0200 Subject: [PATCH 035/164] start handling AddNodeAddr message --- iroh/src/magicsock/node_map/node_state.rs | 21 +++++++++++++++------ iroh/src/magicsock/node_map/path_state.rs | 9 +++++++++ iroh/src/magicsock/transports.rs | 2 +- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index aa8bebf1778..a0d96e0bdfb 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -31,7 +31,7 @@ use crate::{ use super::{ IpPort, Source, TransportsSenderMessage, - path_state::{PathState, summarize_node_paths}, + path_state::{NewPathState, PathState, summarize_node_paths}, udp_paths::{NodeUdpPaths, UdpSendAddr}, }; @@ -776,7 +776,20 @@ impl NodeStateActor { } } NodeStateMessage::PingReceived => todo!(), - NodeStateMessage::AddNodeAddr(addr, source) => todo!(), + NodeStateMessage::AddNodeAddr(node_addr, source) => { + for sockaddr in node_addr.direct_addresses { + let addr = transports::Addr::from(sockaddr); + let path = self.paths.entry(addr).or_default(); + path.sources.insert(source.clone(), Instant::now()); + } + if let Some(relay_url) = node_addr.relay_url { + let addr = transports::Addr::from((relay_url, self.node_id)); + let path = self.paths.entry(addr).or_default(); + path.sources.insert(source, Instant::now()); + } + // TODO: Now check if we need to start holepunching or something for + // any existing connections. + } } } else { break; @@ -818,10 +831,6 @@ pub(super) struct NodeStateHandle { _task: AbortOnDropHandle<()>, } -struct NewPathState { - addr: transports::Addr, -} - impl From for NodeAddr { fn from(info: RemoteInfo) -> Self { let direct_addresses = info diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index 7fd4fc7fe78..93a07a699ae 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -190,3 +190,12 @@ pub(super) fn summarize_node_paths(paths: &BTreeMap) -> Strin write!(&mut w, "]").ok(); w } + +#[derive(Debug, Default)] +pub(super) struct NewPathState { + /// How we learned about this path, and when. + /// + /// We keep track of only the latest [`Instant`] for each [`Source`], keeping the size + /// of the map of sources down to one entry per type of source. + pub(super) sources: HashMap, +} diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index cdfe8c068fc..b2d718f8c8e 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -331,7 +331,7 @@ impl From<&quinn_udp::Transmit<'_>> for OwnedTransmit { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum Addr { Ip(SocketAddr), Relay(RelayUrl, NodeId), From ef7a6f30e4e30736a381995e238b46ab442032f3 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 18 Sep 2025 17:46:30 +0200 Subject: [PATCH 036/164] start sending datagrams from the actor --- iroh/src/magicsock.rs | 1 - iroh/src/magicsock/node_map.rs | 6 ++ iroh/src/magicsock/node_map/node_state.rs | 84 ++++++++++++++++++----- iroh/src/magicsock/transports.rs | 2 +- 4 files changed, 74 insertions(+), 19 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 6d8847142a4..d04b67eb44d 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -2261,7 +2261,6 @@ mod tests { } impl MagicSock { - #[track_caller] pub async fn add_test_addr(&self, node_addr: NodeAddr) { self.add_node_addr( node_addr, diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 8ada4dc3838..0d595ef91ab 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -737,6 +737,12 @@ enum TransportsSenderMessage { SendDatagram(transports::Addr, OwnedTransmit), } +impl From<(transports::Addr, OwnedTransmit)> for TransportsSenderMessage { + fn from(source: (transports::Addr, OwnedTransmit)) -> Self { + Self::SendDatagram(source.0, source.1) + } +} + #[cfg(test)] mod tests { use std::net::Ipv4Addr; diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index a0d96e0bdfb..4f7f14076da 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -13,9 +13,10 @@ use n0_future::{ use n0_watcher::Watchable; use quinn::WeakConnectionHandle; use serde::{Deserialize, Serialize}; +use snafu::{ResultExt, Whatever}; use tokio::sync::mpsc; use tokio_stream::wrappers::BroadcastStream; -use tracing::{Level, debug, event, info, instrument, trace, warn}; +use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; @@ -711,16 +712,32 @@ impl NodeState { pub(super) struct NodeStateActor { /// The node ID of the remote node. node_id: NodeId, + /// Allowing us to directly send datagrams. + /// + /// Used for handling [`NodeStateMessage::SendDatagram`] messages. transports_sender: mpsc::Sender, + /// All connections we have to this remote node. connections: Vec, + /// Events emitted by Quinn about path changes. // TODO: Do we need to know which event comes from which connection? We could store // connections in an FxHashMap using a u64 counter as index and map event streams to // this index if so. Events only come with a PathId, so to know which actual path // this refers to we need to know more. path_events: MergeUnbounded>, + /// All possible paths we are aware of. + /// + /// These paths might be entirely impossible to use, since they are added by discovery + /// mechanisms. The are only potentially usable. // TODO: We probably need some indexes from (Connection, PathId) pairs to // transports::Addr. paths: BTreeMap, + /// The path we currently consider the preferred path to the remote node. + /// + /// **We expect this path to work.** If we become aware this path is broken then it is + /// set back to `None`. Having a selected path does not mean we may not be able to get + /// a better path: e.g. when the selected path is a relay path we still need to trigger + /// holepunching regularly. + selected_path: Option, metrics: Arc, } @@ -736,6 +753,7 @@ impl NodeStateActor { connections: Vec::new(), path_events: Default::default(), paths: BTreeMap::new(), + selected_path: None, metrics, } } @@ -743,30 +761,42 @@ impl NodeStateActor { pub(super) fn start(mut self) -> NodeStateHandle { let (tx, rx) = mpsc::channel(16); - // No .instrument() on the task, run method has an #[instrument] attribute. - let task = tokio::spawn(async move { - self.run(rx).await; - }); + let task = tokio::spawn( + async move { + if let Err(err) = self.run(rx).await { + error!("actor failed: {err:#}"); + } + } + .instrument(info_span!("NodeStateActor")), + ); NodeStateHandle { sender: tx, _task: AbortOnDropHandle::new(task), } } - #[instrument( - name = "NodeStateActor", - skip_all, - fields(node_id = %self.node_id.fmt_short()) - )] - async fn run(&mut self, mut inbox: mpsc::Receiver) { + #[instrument(skip_all, fields(node_id = %self.node_id.fmt_short()))] + async fn run(&mut self, mut inbox: mpsc::Receiver) -> Result<(), Whatever> { loop { if let Some(msg) = inbox.recv().await { match msg { NodeStateMessage::SendDatagram(transmit) => { - // - do we have a currently selected path? - // - if not initiate holepunching - // - and then send along all paths - todo!(); + if let Some(ref addr) = self.selected_path { + self.transports_sender + .send((addr.clone(), transmit).into()) + .await + .whatever_context("TransportSenderActor stopped")?; + } else { + for addr in self.paths.keys() { + self.transports_sender + .send((addr.clone(), transmit.clone()).into()) + .await + .whatever_context("TransportSenerActor stopped")?; + if addr.is_relay() { + self.call_me_maybe(addr.clone()); + } + } + } } NodeStateMessage::AddConnection(handle) => { if let Some(conn) = handle.upgrade() { @@ -787,8 +817,6 @@ impl NodeStateActor { let path = self.paths.entry(addr).or_default(); path.sources.insert(source, Instant::now()); } - // TODO: Now check if we need to start holepunching or something for - // any existing connections. } } } else { @@ -796,6 +824,28 @@ impl NodeStateActor { } } trace!("actor terminating"); + Ok(()) + } + + /// Tries to select a new path out of the available ones. + /// + /// We only want to accept any path which we know can be used. If we do not know of any + /// working paths we must set it to `None`. + fn select_path(&mut self) { + // TODO + self.selected_path = None; + } + + /// Sends a call-me-maybe message to the remote node. + /// + /// This will first send pings to any paths the remote advertised in their own + /// call-me-maybe message. + /// + /// If a call-me-maybe message was recently sent it will instead schedule a new + /// call-me-maybe after a delay if by then it is still needed. + fn call_me_maybe(&mut self, dst: transports::Addr) { + debug_assert!(dst.is_relay(), "must send call-me-maybe via a relay server"); + todo!() } } diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index b2d718f8c8e..968347f3ca4 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -314,7 +314,7 @@ pub(crate) struct Transmit<'a> { } /// An outgoing packet that can be sent across channels. -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct OwnedTransmit { pub(crate) ecn: Option, pub(crate) contents: Bytes, From 76f376838d90a5ac3debcfc1ffda8993f799cd23 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 23 Sep 2025 12:27:49 +0200 Subject: [PATCH 037/164] Start adding state for holepunching decisions This plugs through the information we need from the magic socket via some watchers. --- iroh/src/magicsock.rs | 28 ++++-- iroh/src/magicsock/node_map.rs | 51 ++++++++-- iroh/src/magicsock/node_map/node_state.rs | 110 +++++++++++++++++----- 3 files changed, 148 insertions(+), 41 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index d04b67eb44d..e66159f1dfa 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1213,22 +1213,30 @@ impl Handle { #[cfg(wasm_browser)] let transports = Transports::new(relay_transports, max_receive_segments); + let direct_addrs = DiscoveredDirectAddrs::default(); + let node_map = { let node_map = node_map.unwrap_or_default(); let sender = transports.create_sender(); #[cfg(any(test, feature = "test-utils"))] - let nm = - NodeMap::load_from_vec(node_map, path_selection, metrics.magicsock.clone(), sender) - .await; + let nm = NodeMap::load_from_vec( + node_map, + path_selection, + metrics.magicsock.clone(), + sender, + direct_addrs.addrs.watch(), + ) + .await; #[cfg(not(any(test, feature = "test-utils")))] - let nm = NodeMap::load_from_vec(node_map, metrics.magicsock.clone(), sender).await; + let nm = NodeMap::load_from_vec( + node_map, + metrics.magicsock.clone(), + sender, + direct_addrs.addrs.watch(), + ) + .await; nm }; - // let node_map = node_map.unwrap_or_default(); - // #[cfg(any(test, feature = "test-utils"))] - // let node_map = NodeMap::load_from_vec(node_map, path_selection, &metrics.magicsock); - // #[cfg(not(any(test, feature = "test-utils")))] - // let node_map = NodeMap::load_from_vec(node_map, &metrics.magicsock); let (disco, disco_receiver) = DiscoState::new(secret_encryption_key); @@ -1243,7 +1251,7 @@ impl Handle { connection_map: Default::default(), discovery, discovery_user_data: RwLock::new(discovery_user_data), - direct_addrs: Default::default(), + direct_addrs, net_report: Watchable::new((None, UpdateReason::None)), #[cfg(not(wasm_browser))] dns_resolver: dns_resolver.clone(), diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 0d595ef91ab..d5a86d3e66f 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -14,6 +14,7 @@ use tokio::sync::mpsc; use tracing::{debug, info, instrument, trace, warn}; use self::node_state::{NodeState, Options}; +use super::DirectAddr; use super::mapped_addrs::{AddrMap, RelayMappedAddr}; #[cfg(any(test, feature = "test-utils"))] use super::transports::TransportsSender; @@ -72,6 +73,7 @@ pub(super) struct NodeMapInner { metrics: Arc, /// Handle to an actor that can send over the transports. transports_handle: TransportsSenderHandle, + local_addrs: n0_watcher::Direct>>, by_node_key: HashMap, by_ip_port: HashMap, by_quic_mapped_addr: HashMap, @@ -137,12 +139,18 @@ pub enum Source { /// The name of the application that added the node name: String, }, + /// The address was advertised by a call-me-maybe DISCO message. + CallMeMaybe, } impl NodeMap { #[cfg(any(test, feature = "test-utils"))] - pub(super) fn new(metrics: Arc, sender: TransportsSender) -> Self { - Self::from_inner(NodeMapInner::new(metrics, sender)) + pub(super) fn new( + metrics: Arc, + sender: TransportsSender, + local_addrs: n0_watcher::Direct>>, + ) -> Self { + Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs)) } #[cfg(not(any(test, feature = "test-utils")))] @@ -151,8 +159,9 @@ impl NodeMap { nodes: Vec, metrics: Arc, sender: TransportsSender, + local_addrs: n0_watcher::Direct>>, ) -> Self { - let me = Self::from_inner(NodeMapInner::new(metrics, sender)); + let me = Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs)); for addr in nodes { me.add_node_addr(addr, Source::Saved).await; } @@ -166,8 +175,9 @@ impl NodeMap { path_selection: PathSelection, metrics: Arc, sender: TransportsSender, + local_addrs: n0_watcher::Direct>>, ) -> Self { - let mut inner = NodeMapInner::new(metrics, sender); + let mut inner = NodeMapInner::new(metrics, sender, local_addrs); inner.path_selection = path_selection; let me = Self::from_inner(inner); for addr in nodes { @@ -347,8 +357,9 @@ impl NodeMap { None => { // Create a new NodeStateActor and insert it into the node map. let sender = inner.transports_handle.inbox.clone(); + let local_addrs = inner.local_addrs.clone(); let metrics = inner.metrics.clone(); - let actor = NodeStateActor::new(node_id, sender, metrics); + let actor = NodeStateActor::new(node_id, sender, local_addrs, metrics); let handle = actor.start(); let sender = handle.sender.clone(); inner.node_states.insert(node_id, handle); @@ -362,11 +373,16 @@ impl NodeMap { } impl NodeMapInner { - fn new(metrics: Arc, sender: TransportsSender) -> Self { + fn new( + metrics: Arc, + sender: TransportsSender, + local_addrs: n0_watcher::Direct>>, + ) -> Self { let transports_handle = Self::start_transports_sender(sender); Self { metrics, transports_handle, + local_addrs, by_node_key: Default::default(), by_ip_port: Default::default(), by_quic_mapped_addr: Default::default(), @@ -753,6 +769,7 @@ mod tests { use super::{node_state::MAX_INACTIVE_DIRECT_ADDRESSES, *}; use crate::disco::SendAddr; + use crate::magicsock::DiscoveredDirectAddrs; use crate::magicsock::transports::Transports; impl NodeMap { @@ -772,7 +789,12 @@ mod tests { #[traced_test] async fn restore_from_vec() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let node_map = NodeMap::new(Default::default(), transports.create_sender()); + let direct_addrs = DiscoveredDirectAddrs::default(); + let node_map = NodeMap::new( + Default::default(), + transports.create_sender(), + direct_addrs.addrs.watch(), + ); let mut rng = rand::thread_rng(); let node_a = SecretKey::generate(&mut rng).public(); @@ -814,6 +836,7 @@ mod tests { PathSelection::default(), Default::default(), transports.create_sender(), + direct_addrs.addrs.watch(), ) .await; @@ -844,7 +867,12 @@ mod tests { #[traced_test] fn test_prune_direct_addresses() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let node_map = NodeMap::new(Default::default(), transports.create_sender()); + let direct_addrs = DiscoveredDirectAddrs::default(); + let node_map = NodeMap::new( + Default::default(), + transports.create_sender(), + direct_addrs.addrs.watch(), + ); let public_key = SecretKey::generate(rand::thread_rng()).public(); let id = node_map .inner @@ -918,7 +946,12 @@ mod tests { #[test] fn test_prune_inactive() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let node_map = NodeMap::new(Default::default(), transports.create_sender()); + let direct_addrs = DiscoveredDirectAddrs::default(); + let node_map = NodeMap::new( + Default::default(), + transports.create_sender(), + direct_addrs.addrs.watch(), + ); // add one active node and more than MAX_INACTIVE_NODES inactive nodes let active_node = SecretKey::generate(rand::thread_rng()).public(); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167); diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 4f7f14076da..7007ea34b31 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -10,7 +10,7 @@ use n0_future::{ task::AbortOnDropHandle, time::{Duration, Instant}, }; -use n0_watcher::Watchable; +use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Whatever}; @@ -22,6 +22,7 @@ use tracing::{Instrument, Level, debug, error, event, info, info_span, instrumen use crate::endpoint::PathSelection; use crate::{ disco::{self, SendAddr}, + endpoint::DirectAddr, magicsock::{ HEARTBEAT_INTERVAL, MagicsockMetrics, mapped_addrs::{MappedAddr, NodeIdMappedAddr}, @@ -712,10 +713,20 @@ impl NodeState { pub(super) struct NodeStateActor { /// The node ID of the remote node. node_id: NodeId, + + // Hooks into the rest of the MagicSocket. + // + /// Metrics. + metrics: Arc, /// Allowing us to directly send datagrams. /// /// Used for handling [`NodeStateMessage::SendDatagram`] messages. transports_sender: mpsc::Sender, + /// Our local addresses. + local_addrs: n0_watcher::Direct>>, + + // Internal state - Quinn Connections we are managing. + // /// All connections we have to this remote node. connections: Vec, /// Events emitted by Quinn about path changes. @@ -724,6 +735,9 @@ pub(super) struct NodeStateActor { // this index if so. Events only come with a PathId, so to know which actual path // this refers to we need to know more. path_events: MergeUnbounded>, + + // Internal state - Holepunching and path state. + // /// All possible paths we are aware of. /// /// These paths might be entirely impossible to use, since they are added by discovery @@ -731,6 +745,8 @@ pub(super) struct NodeStateActor { // TODO: We probably need some indexes from (Connection, PathId) pairs to // transports::Addr. paths: BTreeMap, + /// Information about the last holepunching attempt. + last_holepunch: Option, /// The path we currently consider the preferred path to the remote node. /// /// **We expect this path to work.** If we become aware this path is broken then it is @@ -738,23 +754,25 @@ pub(super) struct NodeStateActor { /// a better path: e.g. when the selected path is a relay path we still need to trigger /// holepunching regularly. selected_path: Option, - metrics: Arc, } impl NodeStateActor { pub(super) fn new( node_id: NodeId, transports_sender: mpsc::Sender, + local_addrs: n0_watcher::Direct>>, metrics: Arc, ) -> Self { Self { node_id, + metrics, transports_sender, + local_addrs, connections: Vec::new(), path_events: Default::default(), paths: BTreeMap::new(), + last_holepunch: None, selected_path: None, - metrics, } } @@ -792,10 +810,8 @@ impl NodeStateActor { .send((addr.clone(), transmit.clone()).into()) .await .whatever_context("TransportSenerActor stopped")?; - if addr.is_relay() { - self.call_me_maybe(addr.clone()); - } } + self.trigger_holepunching(); } } NodeStateMessage::AddConnection(handle) => { @@ -827,24 +843,58 @@ impl NodeStateActor { Ok(()) } - /// Tries to select a new path out of the available ones. + /// Triggers holepunching to the remote node. /// - /// We only want to accept any path which we know can be used. If we do not know of any - /// working paths we must set it to `None`. - fn select_path(&mut self) { - // TODO - self.selected_path = None; - } - - /// Sends a call-me-maybe message to the remote node. + /// This will manage the entire process of holepunching with the remote node. /// - /// This will first send pings to any paths the remote advertised in their own - /// call-me-maybe message. - /// - /// If a call-me-maybe message was recently sent it will instead schedule a new - /// call-me-maybe after a delay if by then it is still needed. - fn call_me_maybe(&mut self, dst: transports::Addr) { - debug_assert!(dst.is_relay(), "must send call-me-maybe via a relay server"); + /// - If there is no relay address known, nothing happens. + /// - If there was a recent attempt, it will schedule holepunching instead. + /// - Unless there are new addresses to try. + /// - The scheduled attempt will only run if holepunching has not yet succeeded by + /// then. + /// - DISCO pings will be sent to addresses recently advertised in a call-me-maybe + /// message. + /// - A DISCO call-me-maybe message advertising our own addresses will be sent. + fn trigger_holepunching(&mut self) { + const CALL_ME_MAYBE_VALIDITY: Duration = Duration::from_secs(30); + const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); + + let remote_addrs: BTreeSet = self + .paths + .iter() + .filter_map(|(addr, state)| match addr { + transports::Addr::Ip(socket_addr) => Some((socket_addr, state)), + transports::Addr::Relay(_, _) => None, + }) + .filter_map(|(addr, state)| { + state + .sources + .get(&Source::CallMeMaybe) + .map(|when| when.elapsed() >= CALL_ME_MAYBE_VALIDITY) + .and(Some(*addr)) + }) + .collect(); + let local_addrs: BTreeSet = self + .local_addrs + .get() + .unwrap_or_default() + .iter() + .map(|daddr| daddr.addr) + .collect(); + // let local_addrs: BTreeSet = self.local_addrs.get(); + let mut do_hp = true; + if let Some(ref last_hp) = self.last_holepunch { + do_hp = last_hp.when.elapsed() >= HOLEPUNCH_ATTEMPTS_INTERVAL; + if remote_addrs != last_hp.remote_addrs { + do_hp = true; + } + if local_addrs != last_hp.local_addrs { + do_hp = true; + } + } + if !do_hp { + return; + } todo!() } } @@ -897,6 +947,22 @@ impl From for NodeAddr { } } +/// Information about a holepunch attempt. +#[derive(Debug)] +struct HolepunchAttempt { + when: Instant, + /// The set of local addresses which could take part in holepunching. + /// + /// This does not mean every address here participated in the holepunching. E.g. we + /// could have tried only a sub-set of the addresses because a previous attempt already + /// covered part of the range. + local_addrs: BTreeSet, + /// The set of remote addresses which could take part in holepunching. + /// + /// Like `local_addrs` we may not have used them. + remote_addrs: BTreeSet, +} + /// Whether to send a call-me-maybe message after sending pings to all known paths. /// /// `IfNoRecent` will only send a call-me-maybe if no previous one was sent in the last From 1366161841ab99c18d3c0db7066c08e4a3ddc5f0 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 23 Sep 2025 15:36:21 +0200 Subject: [PATCH 038/164] refactor to allow scheduling holepunching attempts --- iroh/src/magicsock.rs | 3 + iroh/src/magicsock/node_map/node_state.rs | 176 ++++++++++++++-------- 2 files changed, 114 insertions(+), 65 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index e66159f1dfa..7728e2eeef0 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -2158,6 +2158,9 @@ fn disco_message_sent(msg: &disco::Message, metrics: &MagicsockMetrics) { /// Direct addresses are UDP socket addresses on which an iroh node could potentially be /// contacted. These can come from various sources depending on the network topology of the /// iroh node, see [`DirectAddrType`] for the several kinds of sources. +/// +/// This is essentially a combination of our local addresses combined with any reflexive +/// transport addresses we disovered using QAD. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct DirectAddr { /// The address. diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 7007ea34b31..e5fff2a6917 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -29,6 +29,7 @@ use crate::{ node_map::path_validity::PathValidity, transports::{self, OwnedTransmit}, }, + util::MaybeFuture, }; use super::{ @@ -723,6 +724,8 @@ pub(super) struct NodeStateActor { /// Used for handling [`NodeStateMessage::SendDatagram`] messages. transports_sender: mpsc::Sender, /// Our local addresses. + /// + /// These are our local addresses and any reflexive transport addresses. local_addrs: n0_watcher::Direct>>, // Internal state - Quinn Connections we are managing. @@ -754,6 +757,8 @@ pub(super) struct NodeStateActor { /// a better path: e.g. when the selected path is a relay path we still need to trigger /// holepunching regularly. selected_path: Option, + /// Time at which we should schedule the next holepunch attempt. + scheduled_holepunch: Option, } impl NodeStateActor { @@ -773,6 +778,7 @@ impl NodeStateActor { paths: BTreeMap::new(), last_holepunch: None, selected_path: None, + scheduled_holepunch: None, } } @@ -796,53 +802,70 @@ impl NodeStateActor { #[instrument(skip_all, fields(node_id = %self.node_id.fmt_short()))] async fn run(&mut self, mut inbox: mpsc::Receiver) -> Result<(), Whatever> { loop { - if let Some(msg) = inbox.recv().await { - match msg { - NodeStateMessage::SendDatagram(transmit) => { - if let Some(ref addr) = self.selected_path { - self.transports_sender - .send((addr.clone(), transmit).into()) - .await - .whatever_context("TransportSenderActor stopped")?; - } else { - for addr in self.paths.keys() { - self.transports_sender - .send((addr.clone(), transmit.clone()).into()) - .await - .whatever_context("TransportSenerActor stopped")?; - } - self.trigger_holepunching(); - } - } - NodeStateMessage::AddConnection(handle) => { - if let Some(conn) = handle.upgrade() { - let events = BroadcastStream::new(conn.path_events()); - self.path_events.push(events); - self.connections.push(handle); - } - } - NodeStateMessage::PingReceived => todo!(), - NodeStateMessage::AddNodeAddr(node_addr, source) => { - for sockaddr in node_addr.direct_addresses { - let addr = transports::Addr::from(sockaddr); - let path = self.paths.entry(addr).or_default(); - path.sources.insert(source.clone(), Instant::now()); - } - if let Some(relay_url) = node_addr.relay_url { - let addr = transports::Addr::from((relay_url, self.node_id)); - let path = self.paths.entry(addr).or_default(); - path.sources.insert(source, Instant::now()); - } + let scheduled_hp = match self.scheduled_holepunch { + Some(when) => MaybeFuture::Some(tokio::time::sleep_until(when)), + None => MaybeFuture::None, + }; + let mut scheduled_hp = std::pin::pin!(scheduled_hp); + tokio::select! { + biased; + msg = inbox.recv() => { + match msg { + Some(msg) => self.handle_message(msg).await?, + None => break, } } - } else { - break; + _ = &mut scheduled_hp => { + self.trigger_holepunching(); + } } } trace!("actor terminating"); Ok(()) } + async fn handle_message(&mut self, msg: NodeStateMessage) -> Result<(), Whatever> { + match msg { + NodeStateMessage::SendDatagram(transmit) => { + if let Some(ref addr) = self.selected_path { + self.transports_sender + .send((addr.clone(), transmit).into()) + .await + .whatever_context("TransportSenderActor stopped")?; + } else { + for addr in self.paths.keys() { + self.transports_sender + .send((addr.clone(), transmit.clone()).into()) + .await + .whatever_context("TransportSenerActor stopped")?; + } + self.trigger_holepunching(); + } + } + NodeStateMessage::AddConnection(handle) => { + if let Some(conn) = handle.upgrade() { + let events = BroadcastStream::new(conn.path_events()); + self.path_events.push(events); + self.connections.push(handle); + } + } + NodeStateMessage::PingReceived => todo!(), + NodeStateMessage::AddNodeAddr(node_addr, source) => { + for sockaddr in node_addr.direct_addresses { + let addr = transports::Addr::from(sockaddr); + let path = self.paths.entry(addr).or_default(); + path.sources.insert(source.clone(), Instant::now()); + } + if let Some(relay_url) = node_addr.relay_url { + let addr = transports::Addr::from((relay_url, self.node_id)); + let path = self.paths.entry(addr).or_default(); + path.sources.insert(source, Instant::now()); + } + } + } + Ok(()) + } + /// Triggers holepunching to the remote node. /// /// This will manage the entire process of holepunching with the remote node. @@ -855,12 +878,45 @@ impl NodeStateActor { /// - DISCO pings will be sent to addresses recently advertised in a call-me-maybe /// message. /// - A DISCO call-me-maybe message advertising our own addresses will be sent. + /// + /// If a next trigger needs to be scheduled the delay until when to call this again is + /// returned. fn trigger_holepunching(&mut self) { - const CALL_ME_MAYBE_VALIDITY: Duration = Duration::from_secs(30); const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); - let remote_addrs: BTreeSet = self - .paths + let remote_addrs: BTreeSet = self.remote_hp_addrs(); + let local_addrs: BTreeSet = self + .local_addrs + .get() + .unwrap_or_default() + .iter() + .map(|daddr| daddr.addr) + .collect(); + let addrs_changed = self + .last_holepunch + .as_ref() + .map(|last_hp| { + last_hp.remote_addrs != remote_addrs || last_hp.local_addrs != local_addrs + }) + .unwrap_or(true); + if !addrs_changed { + if let Some(ref last_hp) = self.last_holepunch { + let next_hp = last_hp.when + HOLEPUNCH_ATTEMPTS_INTERVAL; + if next_hp > Instant::now() { + self.scheduled_holepunch = Some(next_hp); + return; + } + } + } + + self.do_holepunching(); + } + + /// Returns the remote addresses to holepunch against. + fn remote_hp_addrs(&self) -> BTreeSet { + const CALL_ME_MAYBE_VALIDITY: Duration = Duration::from_secs(30); + + self.paths .iter() .filter_map(|(addr, state)| match addr { transports::Addr::Ip(socket_addr) => Some((socket_addr, state)), @@ -873,29 +929,16 @@ impl NodeStateActor { .map(|when| when.elapsed() >= CALL_ME_MAYBE_VALIDITY) .and(Some(*addr)) }) - .collect(); - let local_addrs: BTreeSet = self - .local_addrs - .get() - .unwrap_or_default() - .iter() - .map(|daddr| daddr.addr) - .collect(); - // let local_addrs: BTreeSet = self.local_addrs.get(); - let mut do_hp = true; - if let Some(ref last_hp) = self.last_holepunch { - do_hp = last_hp.when.elapsed() >= HOLEPUNCH_ATTEMPTS_INTERVAL; - if remote_addrs != last_hp.remote_addrs { - do_hp = true; - } - if local_addrs != last_hp.local_addrs { - do_hp = true; - } - } - if !do_hp { - return; - } - todo!() + .collect() + } + + /// Unconditionally perform holepunching. + /// + /// - DISCO pings will be sent to addresses recently advertised in a call-me-maybe + /// message. + /// - A DISCO call-me-maybe message advertising our own addresses will be sent. + fn do_holepunching(&mut self) { + todo!(); } } @@ -956,6 +999,9 @@ struct HolepunchAttempt { /// This does not mean every address here participated in the holepunching. E.g. we /// could have tried only a sub-set of the addresses because a previous attempt already /// covered part of the range. + /// + /// We do not store this as a [`DirectAddr`] because this is checked for equality and we + /// do not want to compare the sources of these addresses. local_addrs: BTreeSet, /// The set of remote addresses which could take part in holepunching. /// From 3436423717867b72cd408173e12309602a8b3999 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 23 Sep 2025 18:53:49 +0200 Subject: [PATCH 039/164] plug in DiscoState to the NodeStateActor --- iroh/src/magicsock.rs | 13 +++--- iroh/src/magicsock/node_map.rs | 56 +++++++++++++++-------- iroh/src/magicsock/node_map/node_state.rs | 23 ++++++++-- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 7728e2eeef0..3baa8beaf72 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1214,6 +1214,7 @@ impl Handle { let transports = Transports::new(relay_transports, max_receive_segments); let direct_addrs = DiscoveredDirectAddrs::default(); + let (disco, disco_receiver) = DiscoState::new(secret_encryption_key); let node_map = { let node_map = node_map.unwrap_or_default(); @@ -1225,6 +1226,7 @@ impl Handle { metrics.magicsock.clone(), sender, direct_addrs.addrs.watch(), + disco.clone(), ) .await; #[cfg(not(any(test, feature = "test-utils")))] @@ -1233,13 +1235,12 @@ impl Handle { metrics.magicsock.clone(), sender, direct_addrs.addrs.watch(), + disco.clone(), ) .await; nm }; - let (disco, disco_receiver) = DiscoState::new(secret_encryption_key); - let msock = Arc::new(MagicSock { public_key: secret_key.public(), closing: AtomicBool::new(false), @@ -1443,12 +1444,12 @@ fn default_quic_client_config() -> rustls::ClientConfig { .with_no_client_auth() } -#[derive(Debug)] +#[derive(Debug, Clone)] struct DiscoState { /// Encryption key for this node. - secret_encryption_key: crypto_box::SecretKey, + secret_encryption_key: Arc, /// The state for an active DiscoKey. - secrets: Mutex>, + secrets: Arc>>, /// Disco (ping) queue sender: mpsc::Sender<(SendAddr, PublicKey, disco::Message)>, } @@ -1461,7 +1462,7 @@ impl DiscoState { ( Self { - secret_encryption_key, + secret_encryption_key: Arc::new(secret_encryption_key), secrets: Default::default(), sender: disco_sender, }, diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index d5a86d3e66f..7be84668d4e 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -14,12 +14,12 @@ use tokio::sync::mpsc; use tracing::{debug, info, instrument, trace, warn}; use self::node_state::{NodeState, Options}; -use super::DirectAddr; use super::mapped_addrs::{AddrMap, RelayMappedAddr}; #[cfg(any(test, feature = "test-utils"))] use super::transports::TransportsSender; #[cfg(not(any(test, feature = "test-utils")))] use super::transports::TransportsSender; +use super::{DirectAddr, DiscoState}; use super::{ MagicsockMetrics, mapped_addrs::NodeIdMappedAddr, @@ -74,6 +74,7 @@ pub(super) struct NodeMapInner { /// Handle to an actor that can send over the transports. transports_handle: TransportsSenderHandle, local_addrs: n0_watcher::Direct>>, + disco: DiscoState, by_node_key: HashMap, by_ip_port: HashMap, by_quic_mapped_addr: HashMap, @@ -149,8 +150,9 @@ impl NodeMap { metrics: Arc, sender: TransportsSender, local_addrs: n0_watcher::Direct>>, + disco: DiscoState, ) -> Self { - Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs)) + Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs, disco)) } #[cfg(not(any(test, feature = "test-utils")))] @@ -160,8 +162,9 @@ impl NodeMap { metrics: Arc, sender: TransportsSender, local_addrs: n0_watcher::Direct>>, + disco: DiscoState, ) -> Self { - let me = Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs)); + let me = Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs, disco)); for addr in nodes { me.add_node_addr(addr, Source::Saved).await; } @@ -176,8 +179,9 @@ impl NodeMap { metrics: Arc, sender: TransportsSender, local_addrs: n0_watcher::Direct>>, + disco: DiscoState, ) -> Self { - let mut inner = NodeMapInner::new(metrics, sender, local_addrs); + let mut inner = NodeMapInner::new(metrics, sender, local_addrs, disco); inner.path_selection = path_selection; let me = Self::from_inner(inner); for addr in nodes { @@ -358,8 +362,9 @@ impl NodeMap { // Create a new NodeStateActor and insert it into the node map. let sender = inner.transports_handle.inbox.clone(); let local_addrs = inner.local_addrs.clone(); + let disco = inner.disco.clone(); let metrics = inner.metrics.clone(); - let actor = NodeStateActor::new(node_id, sender, local_addrs, metrics); + let actor = NodeStateActor::new(node_id, sender, local_addrs, disco, metrics); let handle = actor.start(); let sender = handle.sender.clone(); inner.node_states.insert(node_id, handle); @@ -377,12 +382,14 @@ impl NodeMapInner { metrics: Arc, sender: TransportsSender, local_addrs: n0_watcher::Direct>>, + disco: DiscoState, ) -> Self { let transports_handle = Self::start_transports_sender(sender); Self { metrics, transports_handle, local_addrs, + disco, by_node_key: Default::default(), by_ip_port: Default::default(), by_quic_mapped_addr: Default::default(), @@ -773,14 +780,14 @@ mod tests { use crate::magicsock::transports::Transports; impl NodeMap { - #[track_caller] - fn add_test_addr(&self, node_addr: NodeAddr) { + async fn add_test_addr(&self, node_addr: NodeAddr) { self.add_node_addr( node_addr, Source::NamedApp { name: "test".into(), }, - ); + ) + .await; } } @@ -790,10 +797,12 @@ mod tests { async fn restore_from_vec() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); + let (disco, _) = DiscoState::new(crypto_box::SecretKey::generate(&mut rand::rngs::OsRng)); let node_map = NodeMap::new( Default::default(), transports.create_sender(), direct_addrs.addrs.watch(), + disco.clone(), ); let mut rng = rand::thread_rng(); @@ -815,10 +824,10 @@ mod tests { let node_addr_c = NodeAddr::new(node_c).with_direct_addresses(direct_addresses_c); let node_addr_d = NodeAddr::new(node_d); - node_map.add_test_addr(node_addr_a); - node_map.add_test_addr(node_addr_b); - node_map.add_test_addr(node_addr_c); - node_map.add_test_addr(node_addr_d); + node_map.add_test_addr(node_addr_a).await; + node_map.add_test_addr(node_addr_b).await; + node_map.add_test_addr(node_addr_c).await; + node_map.add_test_addr(node_addr_d).await; let mut addrs: Vec = node_map .list_remote_infos(Instant::now()) @@ -837,6 +846,7 @@ mod tests { Default::default(), transports.create_sender(), direct_addrs.addrs.watch(), + disco, ) .await; @@ -863,15 +873,17 @@ mod tests { (std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), port).into() } - #[test] + #[tokio::test] #[traced_test] - fn test_prune_direct_addresses() { + async fn test_prune_direct_addresses() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); + let (disco, _) = DiscoState::new(crypto_box::SecretKey::generate(&mut rand::rngs::OsRng)); let node_map = NodeMap::new( Default::default(), transports.create_sender(), direct_addrs.addrs.watch(), + disco, ); let public_key = SecretKey::generate(rand::thread_rng()).public(); let id = node_map @@ -899,7 +911,7 @@ mod tests { let addr = SocketAddr::new(LOCALHOST, 5000 + i as u16); let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); // add address - node_map.add_test_addr(node_addr); + node_map.add_test_addr(node_addr).await; // make it active node_map.inner.lock().unwrap().receive_udp(addr); } @@ -908,7 +920,7 @@ mod tests { for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES * 2 { let addr = SocketAddr::new(LOCALHOST, 6000 + i as u16); let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); - node_map.add_test_addr(node_addr); + node_map.add_test_addr(node_addr).await; } let mut node_map_inner = node_map.inner.lock().unwrap(); @@ -943,19 +955,23 @@ mod tests { ) } - #[test] - fn test_prune_inactive() { + #[tokio::test] + async fn test_prune_inactive() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); + let (disco, _) = DiscoState::new(crypto_box::SecretKey::generate(&mut rand::rngs::OsRng)); let node_map = NodeMap::new( Default::default(), transports.create_sender(), direct_addrs.addrs.watch(), + disco, ); // add one active node and more than MAX_INACTIVE_NODES inactive nodes let active_node = SecretKey::generate(rand::thread_rng()).public(); let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167); - node_map.add_test_addr(NodeAddr::new(active_node).with_direct_addresses([addr])); + node_map + .add_test_addr(NodeAddr::new(active_node).with_direct_addresses([addr])) + .await; node_map .inner .lock() @@ -965,7 +981,7 @@ mod tests { for _ in 0..MAX_INACTIVE_NODES + 1 { let node = SecretKey::generate(rand::thread_rng()).public(); - node_map.add_test_addr(NodeAddr::new(node)); + node_map.add_test_addr(NodeAddr::new(node)).await; } assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 2); diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index e5fff2a6917..e9c2912d4c6 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -24,7 +24,7 @@ use crate::{ disco::{self, SendAddr}, endpoint::DirectAddr, magicsock::{ - HEARTBEAT_INTERVAL, MagicsockMetrics, + DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, mapped_addrs::{MappedAddr, NodeIdMappedAddr}, node_map::path_validity::PathValidity, transports::{self, OwnedTransmit}, @@ -727,6 +727,8 @@ pub(super) struct NodeStateActor { /// /// These are our local addresses and any reflexive transport addresses. local_addrs: n0_watcher::Direct>>, + /// Shared state to allow to encrypt DISCO messages to peers. + disco: DiscoState, // Internal state - Quinn Connections we are managing. // @@ -766,6 +768,7 @@ impl NodeStateActor { node_id: NodeId, transports_sender: mpsc::Sender, local_addrs: n0_watcher::Direct>>, + disco: DiscoState, metrics: Arc, ) -> Self { Self { @@ -773,6 +776,7 @@ impl NodeStateActor { metrics, transports_sender, local_addrs, + disco, connections: Vec::new(), path_events: Default::default(), paths: BTreeMap::new(), @@ -807,6 +811,8 @@ impl NodeStateActor { None => MaybeFuture::None, }; let mut scheduled_hp = std::pin::pin!(scheduled_hp); + // TODO: Watch our local direct addresses. If they change we need to holepunch + // again. tokio::select! { biased; msg = inbox.recv() => { @@ -815,6 +821,9 @@ impl NodeStateActor { None => break, } } + _ = self.local_addrs.updated() => { + self.trigger_holepunching(); + } _ = &mut scheduled_hp => { self.trigger_holepunching(); } @@ -849,7 +858,6 @@ impl NodeStateActor { self.connections.push(handle); } } - NodeStateMessage::PingReceived => todo!(), NodeStateMessage::AddNodeAddr(node_addr, source) => { for sockaddr in node_addr.direct_addresses { let addr = transports::Addr::from(sockaddr); @@ -862,6 +870,8 @@ impl NodeStateActor { path.sources.insert(source, Instant::now()); } } + NodeStateMessage::CallMeMaybeReceived => todo!(), + NodeStateMessage::PingReceived => todo!(), } Ok(()) } @@ -938,6 +948,7 @@ impl NodeStateActor { /// message. /// - A DISCO call-me-maybe message advertising our own addresses will be sent. fn do_holepunching(&mut self) { + // If direct addrs are out of date we need to schedule an update? todo!(); } } @@ -959,10 +970,14 @@ pub(crate) enum NodeStateMessage { /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. AddConnection(WeakConnectionHandle), - // TODO: Add the transaction ID. - PingReceived, /// Adds a [`NodeAddr`] with locations where the node might be reachable. AddNodeAddr(NodeAddr, Source), + /// Process a received DISCO CallMeMaybe message. + // TODO: Add the message contents. + CallMeMaybeReceived, + /// Process a received DISCO Ping message. + // TODO: Add the transaction ID. + PingReceived, } /// A handle to a [`NodeStateActor`]. From 01bf15c43b1c70f3433c7c579489e532a9da8f5d Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 23 Sep 2025 19:28:52 +0200 Subject: [PATCH 040/164] a sane method to send disco messages --- iroh/src/magicsock.rs | 21 ++++++++--------- iroh/src/magicsock/node_map.rs | 6 ++--- iroh/src/magicsock/node_map/node_state.rs | 28 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 3baa8beaf72..bb56d1e7f52 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -955,7 +955,7 @@ impl MagicSock { )); } - let pkt = self.disco.encode_and_seal(self.public_key, dst_key, &msg); + let pkt = self.disco.encode_and_seal(dst_key, &msg); let transmit = transports::Transmit { contents: &pkt, @@ -1204,7 +1204,6 @@ impl Handle { }); let relay_transports = vec![relay_transport]; - let secret_encryption_key = secret_ed_box(secret_key.secret()); #[cfg(not(wasm_browser))] let ipv6 = ip_transports.iter().any(|t| t.bind_addr().is_ipv6()); @@ -1214,7 +1213,7 @@ impl Handle { let transports = Transports::new(relay_transports, max_receive_segments); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, disco_receiver) = DiscoState::new(secret_encryption_key); + let (disco, disco_receiver) = DiscoState::new(&secret_key); let node_map = { let node_map = node_map.unwrap_or_default(); @@ -1446,6 +1445,8 @@ fn default_quic_client_config() -> rustls::ClientConfig { #[derive(Debug, Clone)] struct DiscoState { + /// The NodeId/PublicKey of this node. + this_node_id: NodeId, /// Encryption key for this node. secret_encryption_key: Arc, /// The state for an active DiscoKey. @@ -1456,12 +1457,15 @@ struct DiscoState { impl DiscoState { fn new( - secret_encryption_key: crypto_box::SecretKey, + secret_key: &SecretKey, ) -> (Self, mpsc::Receiver<(SendAddr, PublicKey, disco::Message)>) { + let this_node_id = secret_key.public(); + let secret_encryption_key = secret_ed_box(secret_key.secret()); let (disco_sender, disco_receiver) = mpsc::channel(256); ( Self { + this_node_id, secret_encryption_key: Arc::new(secret_encryption_key), secrets: Default::default(), sender: disco_sender, @@ -1474,15 +1478,10 @@ impl DiscoState { self.sender.try_send((dst, node_id, msg)).is_ok() } - fn encode_and_seal( - &self, - this_node_id: NodeId, - other_node_id: NodeId, - msg: &disco::Message, - ) -> Bytes { + fn encode_and_seal(&self, other_node_id: NodeId, msg: &disco::Message) -> Bytes { let mut seal = msg.as_bytes(); self.get_secret(other_node_id, |secret| secret.seal(&mut seal)); - disco::encode_message(&this_node_id, seal).into() + disco::encode_message(&self.this_node_id, seal).into() } fn unseal_and_decode( diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 7be84668d4e..1ea7716984e 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -797,7 +797,7 @@ mod tests { async fn restore_from_vec() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, _) = DiscoState::new(crypto_box::SecretKey::generate(&mut rand::rngs::OsRng)); + let (disco, _) = DiscoState::new(&SecretKey::generate(&mut rand::rngs::OsRng)); let node_map = NodeMap::new( Default::default(), transports.create_sender(), @@ -878,7 +878,7 @@ mod tests { async fn test_prune_direct_addresses() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, _) = DiscoState::new(crypto_box::SecretKey::generate(&mut rand::rngs::OsRng)); + let (disco, _) = DiscoState::new(&SecretKey::generate(&mut rand::rngs::OsRng)); let node_map = NodeMap::new( Default::default(), transports.create_sender(), @@ -959,7 +959,7 @@ mod tests { async fn test_prune_inactive() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, _) = DiscoState::new(crypto_box::SecretKey::generate(&mut rand::rngs::OsRng)); + let (disco, _) = DiscoState::new(&SecretKey::generate(&mut rand::rngs::OsRng)); let node_map = NodeMap::new( Default::default(), transports.create_sender(), diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index e9c2912d4c6..a248aa557ea 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -951,6 +951,34 @@ impl NodeStateActor { // If direct addrs are out of date we need to schedule an update? todo!(); } + + #[instrument(skip(self, dst_key), fields(dst_key = %dst_key.fmt_short()))] + async fn send_disco_message( + &self, + dst: transports::Addr, + dst_key: PublicKey, + msg: disco::Message, + ) { + let pkt = self.disco.encode_and_seal(dst_key, &msg); + let transmit = transports::OwnedTransmit { + ecn: None, + contents: pkt, + segment_size: None, + }; + let counter = match dst { + transports::Addr::Ip(_) => &self.metrics.send_disco_udp, + transports::Addr::Relay(_, _) => &self.metrics.send_disco_relay, + }; + match self.transports_sender.send((dst, transmit).into()).await { + Ok(()) => { + trace!("sent"); + counter.inc(); + } + Err(err) => { + warn!("failed to send disco message: {err:#}"); + } + } + } } /// Messages to send to the [`NodeStateActor`]. From 001d16d9fa8d9c59492e68f5aea2d8b3c0255ea4 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 24 Sep 2025 14:21:55 +0200 Subject: [PATCH 041/164] Implement starting of holepunching Hook up pings and pongs received to go back to the right place. Look how simple that is! --- iroh/src/disco.rs | 66 +++++++++++-------- iroh/src/magicsock.rs | 9 ++- iroh/src/magicsock/node_map.rs | 24 ++++++- iroh/src/magicsock/node_map/node_state.rs | 78 ++++++++++++++++++----- iroh/src/magicsock/node_map/path_state.rs | 4 +- 5 files changed, 134 insertions(+), 47 deletions(-) diff --git a/iroh/src/disco.rs b/iroh/src/disco.rs index e2fc2d26c21..20f44922032 100644 --- a/iroh/src/disco.rs +++ b/iroh/src/disco.rs @@ -24,7 +24,7 @@ use std::{ }; use data_encoding::HEXLOWER; -use iroh_base::{PublicKey, RelayUrl}; +use iroh_base::{NodeId, PublicKey, RelayUrl}; use nested_enum_utils::common_fields; use serde::{Deserialize, Serialize}; use snafu::{Snafu, ensure}; @@ -118,12 +118,46 @@ pub struct Ping { /// Random client-generated per-ping transaction ID. pub tx_id: stun_rs::TransactionId, - /// Allegedly the ping sender's wireguard public key. - /// It shouldn't be trusted by itself, but can be combined with - /// netmap data to reduce the discokey:nodekey relation from 1:N to 1:1. + /// Allegedly the ping sender's public key. + /// + /// It shouldn't be trusted by itself. pub node_key: PublicKey, } +impl Ping { + /// Creates a ping message to ping `node_id`. + /// + /// Uses a randomly generated STUN transaction ID. + pub(crate) fn new(node_id: NodeId) -> Self { + Self { + tx_id: stun_rs::TransactionId::default(), + node_key: node_id, + } + } + + fn from_bytes(p: &[u8]) -> Result { + // Deliberately lax on longer-than-expected messages, for future compatibility. + ensure!(p.len() >= PING_LEN, TooShortSnafu); + let tx_id: [u8; TX_LEN] = p[..TX_LEN].try_into().expect("length checked"); + let raw_key = &p[TX_LEN..TX_LEN + iroh_base::PublicKey::LENGTH]; + let node_key = PublicKey::try_from(raw_key).map_err(|_| InvalidEncodingSnafu.build())?; + let tx_id = stun_rs::TransactionId::from(tx_id); + + Ok(Ping { tx_id, node_key }) + } + + fn as_bytes(&self) -> Vec { + let header = msg_header(MessageType::Ping, V0); + let mut out = vec![0u8; PING_LEN + HEADER_LEN]; + + out[..HEADER_LEN].copy_from_slice(&header); + out[HEADER_LEN..HEADER_LEN + TX_LEN].copy_from_slice(&self.tx_id); + out[HEADER_LEN + TX_LEN..].copy_from_slice(self.node_key.as_ref()); + + out + } +} + /// A response a Ping. /// /// It includes the sender's source IP + port, so it's effectively a STUN response. @@ -213,30 +247,6 @@ pub struct CallMeMaybe { pub my_numbers: Vec, } -impl Ping { - fn from_bytes(p: &[u8]) -> Result { - // Deliberately lax on longer-than-expected messages, for future compatibility. - ensure!(p.len() >= PING_LEN, TooShortSnafu); - let tx_id: [u8; TX_LEN] = p[..TX_LEN].try_into().expect("length checked"); - let raw_key = &p[TX_LEN..TX_LEN + iroh_base::PublicKey::LENGTH]; - let node_key = PublicKey::try_from(raw_key).map_err(|_| InvalidEncodingSnafu.build())?; - let tx_id = stun_rs::TransactionId::from(tx_id); - - Ok(Ping { tx_id, node_key }) - } - - fn as_bytes(&self) -> Vec { - let header = msg_header(MessageType::Ping, V0); - let mut out = vec![0u8; PING_LEN + HEADER_LEN]; - - out[..HEADER_LEN].copy_from_slice(&header); - out[HEADER_LEN..HEADER_LEN + TX_LEN].copy_from_slice(&self.tx_id); - out[HEADER_LEN + TX_LEN..].copy_from_slice(self.node_key.as_ref()); - - out - } -} - #[allow(missing_docs)] #[common_fields({ backtrace: Option, diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index bb56d1e7f52..8f53080b8e8 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -844,8 +844,13 @@ impl MagicSock { let _guard = span.enter(); trace!("receive disco message"); match dm { - disco::Message::Ping(..) | disco::Message::Pong(..) => { - unreachable!("not used anymore"); + disco::Message::Ping(ping) => { + self.metrics.magicsock.recv_disco_ping.inc(); + self.node_map.handle_ping(ping, sender, src.clone()); + } + disco::Message::Pong(pong) => { + self.metrics.magicsock.recv_disco_pong.inc(); + self.node_map.handle_pong(pong, sender, src.clone()); } disco::Message::CallMeMaybe(cm) => { self.metrics.magicsock.recv_disco_call_me_maybe.inc(); diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 1ea7716984e..1ce712bda91 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -25,7 +25,7 @@ use super::{ mapped_addrs::NodeIdMappedAddr, transports::{self, OwnedTransmit}, }; -use crate::disco::CallMeMaybe; +use crate::disco::{self, CallMeMaybe}; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; @@ -375,6 +375,28 @@ impl NodeMap { } } } + + pub(super) fn handle_ping(&self, msg: disco::Ping, sender: NodeId, src: transports::Addr) { + if msg.node_key != sender { + warn!("DISCO Ping NodeId mismatch, ignoring ping"); + return; + } + let node_state = self.node_state_actor(sender); + if let Err(err) = node_state.try_send(NodeStateMessage::PingReceived(msg, src)) { + // TODO: This is really, really bad and will drop pings under load. But + // DISCO pings are going away with QUIC-NAT-TRAVERSAL so I don't care. + warn!("DISCO Ping dropped: {err:#}"); + } + } + + pub(super) fn handle_pong(&self, msg: disco::Pong, sender: NodeId, src: transports::Addr) { + let node_state = self.node_state_actor(sender); + if let Err(err) = node_state.try_send(NodeStateMessage::PongReceived(msg, src)) { + // TODO: This is really, really bad and will drop pings under load. But + // DISCO pings are going away with QUIC-NAT-TRAVERSAL so I don't care. + warn!("DISCO Pong dropped: {err:#}"); + } + } } impl NodeMapInner { diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index a248aa557ea..bd63f26a78e 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -871,7 +871,8 @@ impl NodeStateActor { } } NodeStateMessage::CallMeMaybeReceived => todo!(), - NodeStateMessage::PingReceived => todo!(), + NodeStateMessage::PingReceived(msg, src) => todo!(), + NodeStateMessage::PongReceived(msg, src) => todo!(), } Ok(()) } @@ -906,7 +907,10 @@ impl NodeStateActor { .last_holepunch .as_ref() .map(|last_hp| { - last_hp.remote_addrs != remote_addrs || last_hp.local_addrs != local_addrs + // Addrs are allowed to disappear, but if there are new ones we need to + // holepunch again. + !remote_addrs.is_subset(&last_hp.remote_addrs) + || !local_addrs.is_subset(&last_hp.local_addrs) }) .unwrap_or(true); if !addrs_changed { @@ -947,19 +951,62 @@ impl NodeStateActor { /// - DISCO pings will be sent to addresses recently advertised in a call-me-maybe /// message. /// - A DISCO call-me-maybe message advertising our own addresses will be sent. - fn do_holepunching(&mut self) { - // If direct addrs are out of date we need to schedule an update? - todo!(); + async fn do_holepunching(&mut self) { + let Some(relay_addr) = self + .paths + .iter() + .filter_map(|(addr, state)| match addr { + transports::Addr::Ip(_) => None, + transports::Addr::Relay(_, _) => Some(addr), + }) + .next() + .cloned() + else { + warn!("holepunching requested but have no relay address"); + return; + }; + let remote_addrs = self.remote_hp_addrs(); + + // Send DISCO Ping messages to all CallMeMaybe-advertised paths. + for dst in remote_addrs { + let msg = disco::Ping::new(self.node_id); + event!( + target: "iroh::_events::ping::sent", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + ?dst, + txn = ?msg.tx_id, + ); + let addr = transports::Addr::Ip(dst); + self.paths.entry(addr.clone()).or_default().ping = Some(msg.clone()); + self.send_disco_message(addr, disco::Message::Ping(msg)) + .await; + } + + // Send the DISCO CallMeMaybe message over the relay. + let my_numbers: Vec = self + .local_addrs + .get() + .unwrap_or_default() + .iter() + .map(|daddr| daddr.addr) + .collect(); + let msg = disco::CallMeMaybe { my_numbers }; + event!( + target: "iroh::_events::call-me-maybe::sent", + Level::DEBUG, + remote_node = &self.node_id.fmt_short(), + dst = ?relay_addr, + my_numbers = ?msg.my_numbers, + ); + self.send_disco_message(relay_addr, disco::Message::CallMeMaybe(msg)) + .await; } - #[instrument(skip(self, dst_key), fields(dst_key = %dst_key.fmt_short()))] - async fn send_disco_message( - &self, - dst: transports::Addr, - dst_key: PublicKey, - msg: disco::Message, - ) { - let pkt = self.disco.encode_and_seal(dst_key, &msg); + /// Sends a DISCO message to *this* remote node. + #[instrument(skip(self), fields(dst_node = self.node_id.fmt_short()))] + async fn send_disco_message(&self, dst: transports::Addr, msg: disco::Message) { + let pkt = self.disco.encode_and_seal(self.node_id, &msg); let transmit = transports::OwnedTransmit { ecn: None, contents: pkt, @@ -1004,8 +1051,9 @@ pub(crate) enum NodeStateMessage { // TODO: Add the message contents. CallMeMaybeReceived, /// Process a received DISCO Ping message. - // TODO: Add the transaction ID. - PingReceived, + PingReceived(disco::Ping, transports::Addr), + /// Process a received DISCO Pong message. + PongReceived(disco::Pong, transports::Addr), } /// A handle to a [`NodeStateActor`]. diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index 93a07a699ae..f6e79f83600 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -10,7 +10,7 @@ use super::{ node_state::{ControlMsg, SESSION_ACTIVE_TIMEOUT}, }; use crate::{ - disco::SendAddr, + disco::{self, SendAddr}, magicsock::node_map::path_validity::{self, PathValidity}, }; @@ -198,4 +198,6 @@ pub(super) struct NewPathState { /// We keep track of only the latest [`Instant`] for each [`Source`], keeping the size /// of the map of sources down to one entry per type of source. pub(super) sources: HashMap, + /// The last ping sent on this path. + pub(super) ping: Option, } From 7a9023f5409d10c11c3c984a5cf0f40aff191849 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 24 Sep 2025 15:45:49 +0200 Subject: [PATCH 042/164] handle receiving pings --- iroh/src/magicsock/node_map.rs | 2 + iroh/src/magicsock/node_map/node_state.rs | 89 ++++++++++++++++++++--- iroh/src/magicsock/node_map/path_state.rs | 2 +- iroh/src/magicsock/transports.rs | 4 + 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 1ce712bda91..6f887dc0a3e 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -142,6 +142,8 @@ pub enum Source { }, /// The address was advertised by a call-me-maybe DISCO message. CallMeMaybe, + /// We received a ping on the path. + Ping, } impl NodeMap { diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index bd63f26a78e..89f4711224a 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -871,8 +871,44 @@ impl NodeStateActor { } } NodeStateMessage::CallMeMaybeReceived => todo!(), - NodeStateMessage::PingReceived(msg, src) => todo!(), - NodeStateMessage::PongReceived(msg, src) => todo!(), + NodeStateMessage::PingReceived(ping, src) => { + let transports::Addr::Ip(addr) = src else { + warn!("received ping via relay transport, ignored"); + return Ok(()); + }; + event!( + target: "iroh::_events::ping::recv", + Level::DEBUG, + remote_node = self.node_id.fmt_short(), + ?src, + txn = ?ping.tx_id, + ); + let pong = disco::Pong { + tx_id: ping.tx_id, + ping_observed_addr: addr.into(), + }; + event!( + target: "iroh::_events::pong::sent", + Level::DEBUG, + remote_node = self.node_id.fmt_short(), + dst = ?src, + txn = ?pong.tx_id, + ); + self.send_disco_message(src.clone(), disco::Message::Pong(pong)) + .await; + + let path = self.paths.entry(src).or_default(); + path.sources.insert(Source::Ping, Instant::now()); + + self.trigger_holepunching().await; + } + NodeStateMessage::PongReceived(msg, src) => { + for (path, state) in self.paths.iter() { + if let Some(ref ping) = state.ping_sent { + todo!(); + } + } + } } Ok(()) } @@ -881,6 +917,7 @@ impl NodeStateActor { /// /// This will manage the entire process of holepunching with the remote node. /// + /// - If there already is a direct connection, nothing happens. /// - If there is no relay address known, nothing happens. /// - If there was a recent attempt, it will schedule holepunching instead. /// - Unless there are new addresses to try. @@ -892,9 +929,20 @@ impl NodeStateActor { /// /// If a next trigger needs to be scheduled the delay until when to call this again is /// returned. - fn trigger_holepunching(&mut self) { + async fn trigger_holepunching(&mut self) { const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); + if self + .selected_path + .as_ref() + .map(|addr| addr.is_ip()) + .unwrap_or_default() + { + trace!("not holepunching, already have a direct connection"); + // TODO: If the latency is kind of bad we should retry holepunching at times. + return; + } + let remote_addrs: BTreeSet = self.remote_hp_addrs(); let local_addrs: BTreeSet = self .local_addrs @@ -903,7 +951,7 @@ impl NodeStateActor { .iter() .map(|daddr| daddr.addr) .collect(); - let addrs_changed = self + let new_addrs = self .last_holepunch .as_ref() .map(|last_hp| { @@ -913,17 +961,18 @@ impl NodeStateActor { || !local_addrs.is_subset(&last_hp.local_addrs) }) .unwrap_or(true); - if !addrs_changed { + if !new_addrs { if let Some(ref last_hp) = self.last_holepunch { let next_hp = last_hp.when + HOLEPUNCH_ATTEMPTS_INTERVAL; if next_hp > Instant::now() { + trace!(scheduled_in = ?next_hp, "not holepunching: no new addresses"); self.scheduled_holepunch = Some(next_hp); return; } } } - self.do_holepunching(); + self.do_holepunching().await; } /// Returns the remote addresses to holepunch against. @@ -937,11 +986,21 @@ impl NodeStateActor { transports::Addr::Relay(_, _) => None, }) .filter_map(|(addr, state)| { - state + if state .sources .get(&Source::CallMeMaybe) .map(|when| when.elapsed() >= CALL_ME_MAYBE_VALIDITY) - .and(Some(*addr)) + .unwrap_or_default() + || state + .sources + .get(&Source::Ping) + .map(|when| when.elapsed() >= CALL_ME_MAYBE_VALIDITY) + .unwrap_or_default() + { + Some(*addr) + } else { + None + } }) .collect() } @@ -952,6 +1011,7 @@ impl NodeStateActor { /// message. /// - A DISCO call-me-maybe message advertising our own addresses will be sent. async fn do_holepunching(&mut self) { + trace!("holepunching"); let Some(relay_addr) = self .paths .iter() @@ -968,7 +1028,7 @@ impl NodeStateActor { let remote_addrs = self.remote_hp_addrs(); // Send DISCO Ping messages to all CallMeMaybe-advertised paths. - for dst in remote_addrs { + for dst in remote_addrs.iter() { let msg = disco::Ping::new(self.node_id); event!( target: "iroh::_events::ping::sent", @@ -977,8 +1037,8 @@ impl NodeStateActor { ?dst, txn = ?msg.tx_id, ); - let addr = transports::Addr::Ip(dst); - self.paths.entry(addr.clone()).or_default().ping = Some(msg.clone()); + let addr = transports::Addr::Ip(*dst); + self.paths.entry(addr.clone()).or_default().ping_sent = Some(msg.clone()); self.send_disco_message(addr, disco::Message::Ping(msg)) .await; } @@ -991,6 +1051,7 @@ impl NodeStateActor { .iter() .map(|daddr| daddr.addr) .collect(); + let local_addrs: BTreeSet = my_numbers.iter().copied().collect(); let msg = disco::CallMeMaybe { my_numbers }; event!( target: "iroh::_events::call-me-maybe::sent", @@ -1001,6 +1062,12 @@ impl NodeStateActor { ); self.send_disco_message(relay_addr, disco::Message::CallMeMaybe(msg)) .await; + + self.last_holepunch = Some(HolepunchAttempt { + when: Instant::now(), + local_addrs, + remote_addrs, + }); } /// Sends a DISCO message to *this* remote node. diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index f6e79f83600..d75da7fc97d 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -199,5 +199,5 @@ pub(super) struct NewPathState { /// of the map of sources down to one entry per type of source. pub(super) sources: HashMap, /// The last ping sent on this path. - pub(super) ping: Option, + pub(super) ping_sent: Option, } diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 968347f3ca4..714023ef295 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -365,6 +365,10 @@ impl Addr { matches!(self, Self::Relay(..)) } + pub(crate) fn is_ip(&self) -> bool { + matches!(self, Self::Ip(_)) + } + /// Returns `None` if not an `Ip`. pub(crate) fn into_socket_addr(self) -> Option { match self { From 92dd37cb791fa7e0e243a8c62dadb2efc2bf479c Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 24 Sep 2025 18:21:21 +0200 Subject: [PATCH 043/164] handle receiving CallMeMaybe messages --- iroh/src/magicsock.rs | 5 +- iroh/src/magicsock/node_map.rs | 63 +++++++++++++++-------- iroh/src/magicsock/node_map/node_state.rs | 36 +++++++++++-- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 8f53080b8e8..9e2d2886b5a 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -877,8 +877,7 @@ impl MagicSock { direct_addresses: cm.my_numbers.iter().copied().collect(), }); - self.node_map - .handle_call_me_maybe(sender, cm, &self.metrics.magicsock); + self.node_map.handle_call_me_maybe(sender, cm); } } trace!("disco message handled"); @@ -1225,6 +1224,7 @@ impl Handle { let sender = transports.create_sender(); #[cfg(any(test, feature = "test-utils"))] let nm = NodeMap::load_from_vec( + secret_key.public(), node_map, path_selection, metrics.magicsock.clone(), @@ -1235,6 +1235,7 @@ impl Handle { .await; #[cfg(not(any(test, feature = "test-utils")))] let nm = NodeMap::load_from_vec( + secret_key.public(), node_map, metrics.magicsock.clone(), sender, diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 6f887dc0a3e..4adc9237aab 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -61,6 +61,8 @@ const MAX_INACTIVE_NODES: usize = 30; /// An index of nodeInfos by node key, NodeIdMappedAddr, and discovered ip:port endpoints. #[derive(Debug)] pub(super) struct NodeMap { + /// The node ID of the local node. + local_node_id: NodeId, inner: Mutex, /// The mapping between [`NodeId`]s and [`NodeIdMappedAddr`]s. pub(super) node_mapped_addrs: AddrMap, @@ -149,24 +151,28 @@ pub enum Source { impl NodeMap { #[cfg(any(test, feature = "test-utils"))] pub(super) fn new( + local_node_id: NodeId, metrics: Arc, sender: TransportsSender, local_addrs: n0_watcher::Direct>>, disco: DiscoState, ) -> Self { - Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs, disco)) + let inner = NodeMapInner::new(metrics, sender, local_addrs, disco); + Self::from_inner(inner, local_node_id) } #[cfg(not(any(test, feature = "test-utils")))] /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. pub(super) async fn load_from_vec( + local_node_id: NodeId, nodes: Vec, metrics: Arc, sender: TransportsSender, local_addrs: n0_watcher::Direct>>, disco: DiscoState, ) -> Self { - let me = Self::from_inner(NodeMapInner::new(metrics, sender, local_addrs, disco)); + let inner = NodeMapInner::new(metrics, sender, local_addrs, disco); + let me = Self::from_inner(inner, local_node_id); for addr in nodes { me.add_node_addr(addr, Source::Saved).await; } @@ -176,6 +182,7 @@ impl NodeMap { #[cfg(any(test, feature = "test-utils"))] /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. pub(super) async fn load_from_vec( + local_node_id: NodeId, nodes: Vec, path_selection: PathSelection, metrics: Arc, @@ -185,15 +192,16 @@ impl NodeMap { ) -> Self { let mut inner = NodeMapInner::new(metrics, sender, local_addrs, disco); inner.path_selection = path_selection; - let me = Self::from_inner(inner); + let me = Self::from_inner(inner, local_node_id); for addr in nodes { me.add_node_addr(addr, Source::Saved).await; } me } - fn from_inner(inner: NodeMapInner) -> Self { + fn from_inner(inner: NodeMapInner, local_node_id: NodeId) -> Self { Self { + local_node_id, inner: Mutex::new(inner), node_mapped_addrs: Default::default(), relay_mapped_addrs: Default::default(), @@ -263,18 +271,6 @@ impl NodeMap { .map(|ep| ep.get_current_addr()) } - pub(super) fn handle_call_me_maybe( - &self, - sender: PublicKey, - cm: CallMeMaybe, - metrics: &MagicsockMetrics, - ) { - self.inner - .lock() - .expect("poisoned") - .handle_call_me_maybe(sender, cm, metrics); - } - #[allow(clippy::type_complexity)] pub(super) fn get_send_addrs( &self, @@ -366,7 +362,14 @@ impl NodeMap { let local_addrs = inner.local_addrs.clone(); let disco = inner.disco.clone(); let metrics = inner.metrics.clone(); - let actor = NodeStateActor::new(node_id, sender, local_addrs, disco, metrics); + let actor = NodeStateActor::new( + node_id, + self.local_node_id, + sender, + local_addrs, + disco, + metrics, + ); let handle = actor.start(); let sender = handle.sender.clone(); inner.node_states.insert(node_id, handle); @@ -394,11 +397,20 @@ impl NodeMap { pub(super) fn handle_pong(&self, msg: disco::Pong, sender: NodeId, src: transports::Addr) { let node_state = self.node_state_actor(sender); if let Err(err) = node_state.try_send(NodeStateMessage::PongReceived(msg, src)) { - // TODO: This is really, really bad and will drop pings under load. But - // DISCO pings are going away with QUIC-NAT-TRAVERSAL so I don't care. + // TODO: This is really, really bad and will drop pongs under load. But + // DISCO pongs are going away with QUIC-NAT-TRAVERSAL so I don't care. warn!("DISCO Pong dropped: {err:#}"); } } + + pub(super) fn handle_call_me_maybe(&self, sender: NodeId, msg: CallMeMaybe) { + let node_state = self.node_state_actor(sender); + if let Err(err) = node_state.try_send(NodeStateMessage::CallMeMaybeReceived(msg)) { + // TODO: This is bad and will drop call-me-maybe's under load. But + // DISCO CallMeMaybe going away with QUIC-NAT-TRAVERSAL so I don't care. + warn!("DISCO CallMeMaybe dropped: {err:#}"); + } + } } impl NodeMapInner { @@ -821,8 +833,10 @@ mod tests { async fn restore_from_vec() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, _) = DiscoState::new(&SecretKey::generate(&mut rand::rngs::OsRng)); + let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); + let (disco, _) = DiscoState::new(&secret_key); let node_map = NodeMap::new( + secret_key.public(), Default::default(), transports.create_sender(), direct_addrs.addrs.watch(), @@ -865,6 +879,7 @@ mod tests { }) .collect(); let loaded_node_map = NodeMap::load_from_vec( + secret_key.public(), addrs.clone(), PathSelection::default(), Default::default(), @@ -902,8 +917,10 @@ mod tests { async fn test_prune_direct_addresses() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, _) = DiscoState::new(&SecretKey::generate(&mut rand::rngs::OsRng)); + let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); + let (disco, _) = DiscoState::new(&secret_key); let node_map = NodeMap::new( + secret_key.public(), Default::default(), transports.create_sender(), direct_addrs.addrs.watch(), @@ -983,8 +1000,10 @@ mod tests { async fn test_prune_inactive() { let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, _) = DiscoState::new(&SecretKey::generate(&mut rand::rngs::OsRng)); + let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); + let (disco, _) = DiscoState::new(&secret_key); let node_map = NodeMap::new( + secret_key.public(), Default::default(), transports.create_sender(), direct_addrs.addrs.watch(), diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 89f4711224a..cf66cae6b62 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -714,6 +714,8 @@ impl NodeState { pub(super) struct NodeStateActor { /// The node ID of the remote node. node_id: NodeId, + /// The node ID of the local node. + local_node_id: NodeId, // Hooks into the rest of the MagicSocket. // @@ -766,6 +768,7 @@ pub(super) struct NodeStateActor { impl NodeStateActor { pub(super) fn new( node_id: NodeId, + local_node_id: NodeId, transports_sender: mpsc::Sender, local_addrs: n0_watcher::Direct>>, disco: DiscoState, @@ -773,6 +776,7 @@ impl NodeStateActor { ) -> Self { Self { node_id, + local_node_id, metrics, transports_sender, local_addrs, @@ -870,7 +874,32 @@ impl NodeStateActor { path.sources.insert(source, Instant::now()); } } - NodeStateMessage::CallMeMaybeReceived => todo!(), + NodeStateMessage::CallMeMaybeReceived(msg) => { + event!( + target: "iroh::_events::call-me-maybe::recv", + Level::DEBUG, + remote_node = self.node_id.fmt_short(), + addrs = ?msg.my_numbers, + ); + let now = Instant::now(); + for addr in msg.my_numbers { + let dst = transports::Addr::Ip(addr); + let ping = disco::Ping::new(self.local_node_id); + + let path = self.paths.entry(dst.clone()).or_default(); + path.sources.insert(Source::CallMeMaybe, now); + path.ping_sent = Some(ping.clone()); + + event!( + target: "iroh::_events::ping::sent", + Level::DEBUG, + remote_node = self.node_id.fmt_short(), + ?dst, + ); + self.send_disco_message(dst, disco::Message::Ping(ping)) + .await; + } + } NodeStateMessage::PingReceived(ping, src) => { let transports::Addr::Ip(addr) = src else { warn!("received ping via relay transport, ignored"); @@ -1029,7 +1058,7 @@ impl NodeStateActor { // Send DISCO Ping messages to all CallMeMaybe-advertised paths. for dst in remote_addrs.iter() { - let msg = disco::Ping::new(self.node_id); + let msg = disco::Ping::new(self.local_node_id); event!( target: "iroh::_events::ping::sent", Level::DEBUG, @@ -1115,8 +1144,7 @@ pub(crate) enum NodeStateMessage { /// Adds a [`NodeAddr`] with locations where the node might be reachable. AddNodeAddr(NodeAddr, Source), /// Process a received DISCO CallMeMaybe message. - // TODO: Add the message contents. - CallMeMaybeReceived, + CallMeMaybeReceived(disco::CallMeMaybe), /// Process a received DISCO Ping message. PingReceived(disco::Ping, transports::Addr), /// Process a received DISCO Pong message. From ac2db4d29277979354fb699f36101c9712e38aa3 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 25 Sep 2025 14:52:19 +0200 Subject: [PATCH 044/164] open a path when we receive a pong --- iroh/src/magicsock/node_map.rs | 1 + iroh/src/magicsock/node_map/node_state.rs | 68 ++++++++++++++++++++--- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 4adc9237aab..b59de1d22b8 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -368,6 +368,7 @@ impl NodeMap { sender, local_addrs, disco, + self.relay_mapped_addrs.clone(), metrics, ); let handle = actor.start(); diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index cf66cae6b62..c9d4886837a 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -12,6 +12,7 @@ use n0_future::{ }; use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; +use quinn_proto::PathStatus; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Whatever}; use tokio::sync::mpsc; @@ -24,8 +25,8 @@ use crate::{ disco::{self, SendAddr}, endpoint::DirectAddr, magicsock::{ - DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, - mapped_addrs::{MappedAddr, NodeIdMappedAddr}, + DiscoState, HEARTBEAT_INTERVAL, MAX_IDLE_TIMEOUT, MagicsockMetrics, + mapped_addrs::{AddrMap, MappedAddr, NodeIdMappedAddr, RelayMappedAddr}, node_map::path_validity::PathValidity, transports::{self, OwnedTransmit}, }, @@ -731,6 +732,8 @@ pub(super) struct NodeStateActor { local_addrs: n0_watcher::Direct>>, /// Shared state to allow to encrypt DISCO messages to peers. disco: DiscoState, + /// The mapping between nodes via a relay and their [`RelayMappedAddr`]s. + relay_mapped_addrs: AddrMap<(RelayUrl, NodeId), RelayMappedAddr>, // Internal state - Quinn Connections we are managing. // @@ -760,6 +763,8 @@ pub(super) struct NodeStateActor { /// set back to `None`. Having a selected path does not mean we may not be able to get /// a better path: e.g. when the selected path is a relay path we still need to trigger /// holepunching regularly. + /// + /// We only select a path once the path is functional in Quinn. selected_path: Option, /// Time at which we should schedule the next holepunch attempt. scheduled_holepunch: Option, @@ -772,6 +777,7 @@ impl NodeStateActor { transports_sender: mpsc::Sender, local_addrs: n0_watcher::Direct>>, disco: DiscoState, + relay_mapped_addrs: AddrMap<(RelayUrl, NodeId), RelayMappedAddr>, metrics: Arc, ) -> Self { Self { @@ -780,6 +786,7 @@ impl NodeStateActor { metrics, transports_sender, local_addrs, + relay_mapped_addrs, disco, connections: Vec::new(), path_events: Default::default(), @@ -931,12 +938,26 @@ impl NodeStateActor { self.trigger_holepunching().await; } - NodeStateMessage::PongReceived(msg, src) => { - for (path, state) in self.paths.iter() { - if let Some(ref ping) = state.ping_sent { - todo!(); - } + NodeStateMessage::PongReceived(pong, src) => { + let Some(state) = self.paths.get(&src) else { + warn!(path = ?src, "ignoring DISCO Pong for unknown path"); + return Ok(()); + }; + let ping_tx = state.ping_sent.as_ref().map(|ping| ping.tx_id); + if ping_tx != Some(pong.tx_id) { + debug!(path = ?src, ?ping_tx, pong_tx = ?pong.tx_id, + "ignoring unknown DISCO Pong for path"); + return Ok(()); } + event!( + target: "iroh::_events::pong::recv", + Level::DEBUG, + remote_node = self.node_id.fmt_short(), + ?src, + txn = ?pong.tx_id, + ); + + self.open_quic_path(src); } } Ok(()) @@ -1044,7 +1065,7 @@ impl NodeStateActor { let Some(relay_addr) = self .paths .iter() - .filter_map(|(addr, state)| match addr { + .filter_map(|(addr, _)| match addr { transports::Addr::Ip(_) => None, transports::Addr::Relay(_, _) => Some(addr), }) @@ -1122,6 +1143,37 @@ impl NodeStateActor { } } } + + /// Asks Quinn to open a new path on connections, but only if we are the client. + async fn open_quic_path(&self, addr: transports::Addr) { + let path_status = match addr { + transports::Addr::Ip(_) => PathStatus::Available, + transports::Addr::Relay(_, _) => PathStatus::Backup, + }; + let quic_addr = match &addr { + transports::Addr::Ip(socket_addr) => *socket_addr, + transports::Addr::Relay(relay_url, node_id) => self + .relay_mapped_addrs + .get(&(relay_url.clone(), *node_id)) + .private_socket_addr(), + }; + for conn in self + .connections + .iter() + .filter_map(|weak| weak.upgrade()) + .filter(|conn| conn.side().is_client()) + { + match conn.open_path(quic_addr, path_status).await { + Ok(path) => { + path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); + path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); + } + Err(err) => { + warn!(?addr, "Failed to open path: {err:#}"); + } + } + } + } } /// Messages to send to the [`NodeStateActor`]. From 6d69555068cd542917caae824b3b54e6bce940dc Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 29 Sep 2025 13:58:25 +0200 Subject: [PATCH 045/164] Move open path to not block the actor This instead uses the events to open the path while the actor does other stuff. Opening the path involves the whole address/path validation dance so will take a while. This puts in the infrastructure to handle path events and figure out which events come from which connection. --- Cargo.lock | 1 + iroh/Cargo.toml | 1 + iroh/src/magicsock/mapped_addrs.rs | 6 +- iroh/src/magicsock/node_map/node_state.rs | 147 +++++++++++++++++----- iroh/src/magicsock/transports.rs | 2 +- 5 files changed, 123 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c1feec38b9..db7ed45c184 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2327,6 +2327,7 @@ dependencies = [ "rand_chacha 0.3.1", "reqwest", "ring", + "rustc-hash", "rustls", "rustls-pki-types", "rustls-webpki", diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index 713e0ebb67f..a64c240b1d0 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -57,6 +57,7 @@ reqwest = { version = "0.12", default-features = false, features = [ "stream", ] } ring = "0.17" +rustc-hash = "2" rustls = { version = "0.23", default-features = false, features = ["ring"] } serde = { version = "1.0.219", features = ["derive", "rc"] } smallvec = "1.11.1" diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index d246aa84eff..ceb92f9348a 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -6,7 +6,6 @@ //! Address ranges we use to keep track of the various "fake" address types we use. use std::{ - collections::HashMap, hash::Hash, net::{IpAddr, Ipv6Addr, SocketAddr}, sync::{ @@ -15,6 +14,7 @@ use std::{ }, }; +use rustc_hash::FxHashMap; use snafu::Snafu; /// The Prefix/L of all Unique Local Addresses. @@ -262,8 +262,8 @@ impl AddrMap { #[derive(Debug)] struct AddrMapInner { - addrs: HashMap, - lookup: HashMap, + addrs: FxHashMap, + lookup: FxHashMap, } // Manual impl because derive ends up requiring T: Default. diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index c9d4886837a..36e92ede39a 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1,22 +1,24 @@ use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, + collections::{BTreeSet, HashMap}, net::{IpAddr, SocketAddr}, + pin::Pin, sync::{Arc, atomic::AtomicBool}, }; use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; use n0_future::{ - MergeUnbounded, + MergeUnbounded, Stream, StreamExt, task::AbortOnDropHandle, time::{Duration, Instant}, }; use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; -use quinn_proto::PathStatus; +use quinn_proto::{PathEvent, PathId, PathStatus}; +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; -use snafu::{ResultExt, Whatever}; +use snafu::{OptionExt, ResultExt, Whatever}; use tokio::sync::mpsc; -use tokio_stream::wrappers::BroadcastStream; +use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; #[cfg(any(test, feature = "test-utils"))] @@ -738,13 +740,20 @@ pub(super) struct NodeStateActor { // Internal state - Quinn Connections we are managing. // /// All connections we have to this remote node. - connections: Vec, + /// + /// The key is the [`quinn::Connection::stable_id`]. + connections: FxHashMap, /// Events emitted by Quinn about path changes. - // TODO: Do we need to know which event comes from which connection? We could store - // connections in an FxHashMap using a u64 counter as index and map event streams to - // this index if so. Events only come with a PathId, so to know which actual path - // this refers to we need to know more. - path_events: MergeUnbounded>, + // path_events: MergeUnbounded>, + path_events: MergeUnbounded< + Pin< + Box< + dyn Stream)> + + Send + + Sync, + >, + >, + >, // Internal state - Holepunching and path state. // @@ -752,9 +761,17 @@ pub(super) struct NodeStateActor { /// /// These paths might be entirely impossible to use, since they are added by discovery /// mechanisms. The are only potentially usable. - // TODO: We probably need some indexes from (Connection, PathId) pairs to - // transports::Addr. - paths: BTreeMap, + paths: FxHashMap, + /// Maps connections and path IDs to the transport addr. + /// + /// The [`transports::Addr`] can be looked up in [`Self::paths`]. + /// + /// The `usize` is the [`Connection::stable_id`] of a connection. It is important that + /// this map is cleared of the stable ID of a new connection received from + /// [`NodeStateMessage::AddConnection`], because this ID is only unique within + /// *currently active* connections. So there could be conflicts if we did not yet know + /// a previous connection no longer exists. + path_id_map: FxHashMap<(usize, PathId), transports::Addr>, /// Information about the last holepunching attempt. last_holepunch: Option, /// The path we currently consider the preferred path to the remote node. @@ -788,9 +805,10 @@ impl NodeStateActor { local_addrs, relay_mapped_addrs, disco, - connections: Vec::new(), + connections: FxHashMap::default(), path_events: Default::default(), - paths: BTreeMap::new(), + paths: FxHashMap::default(), + path_id_map: FxHashMap::default(), last_holepunch: None, selected_path: None, scheduled_holepunch: None, @@ -822,8 +840,6 @@ impl NodeStateActor { None => MaybeFuture::None, }; let mut scheduled_hp = std::pin::pin!(scheduled_hp); - // TODO: Watch our local direct addresses. If they change we need to holepunch - // again. tokio::select! { biased; msg = inbox.recv() => { @@ -832,11 +848,15 @@ impl NodeStateActor { None => break, } } + Some((id, evt)) = self.path_events.next() => { + self.handle_path_event(id, evt).await?; + } _ = self.local_addrs.updated() => { - self.trigger_holepunching(); + self.trigger_holepunching().await; } _ = &mut scheduled_hp => { - self.trigger_holepunching(); + self.scheduled_holepunch = None; + self.trigger_holepunching().await; } } } @@ -864,9 +884,19 @@ impl NodeStateActor { } NodeStateMessage::AddConnection(handle) => { if let Some(conn) = handle.upgrade() { + // Remove any conflicting stable_ids from the local state. + let stable_id = conn.stable_id(); + self.connections.remove(&stable_id); + self.path_id_map.retain(|(id, _), _| *id != stable_id); + + // This is a good time to clean up connections. + self.cleanup_connections(); + + let stable_id = conn.stable_id(); let events = BroadcastStream::new(conn.path_events()); - self.path_events.push(events); - self.connections.push(handle); + let stream = events.map(move |evt| (stable_id, evt)); + self.path_events.push(Box::pin(stream)); + self.connections.insert(stable_id, handle); } } NodeStateMessage::AddNodeAddr(node_addr, source) => { @@ -1145,7 +1175,8 @@ impl NodeStateActor { } /// Asks Quinn to open a new path on connections, but only if we are the client. - async fn open_quic_path(&self, addr: transports::Addr) { + #[instrument(level = "warn", skip(self))] + fn open_quic_path(&mut self, addr: transports::Addr) { let path_status = match addr { transports::Addr::Ip(_) => PathStatus::Available, transports::Addr::Relay(_, _) => PathStatus::Backup, @@ -1159,21 +1190,77 @@ impl NodeStateActor { }; for conn in self .connections - .iter() + .values() .filter_map(|weak| weak.upgrade()) .filter(|conn| conn.side().is_client()) { - match conn.open_path(quic_addr, path_status).await { - Ok(path) => { - path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); - path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); + match conn.open_path_ensure(quic_addr, path_status).path_id() { + Some(path_id) => { + self.path_id_map + .insert((conn.stable_id(), path_id), addr.clone()); } - Err(err) => { - warn!(?addr, "Failed to open path: {err:#}"); + None => { + warn!("Opening path failed"); } } } } + + #[instrument(skip(self), fields(path_id))] + async fn handle_path_event( + &mut self, + stable_id: usize, + event: Result, + ) -> Result<(), Whatever> { + let Ok(event) = event else { + warn!("missed a PathEvent, NodeStateActor lagging"); + // TODO: Is it possible to recover using the sync APIs to figure out what the + // state of the connection and it's paths are? + return Ok(()); + }; + let Some(handle) = self.connections.get(&stable_id) else { + trace!("event for removed connection"); + return Ok(()); + }; + let Some(conn) = handle.upgrade() else { + trace!("event for closed connection"); + return Ok(()); + }; + match event { + PathEvent::Opened { id: path_id } => { + tracing::Span::current().record("path_id", tracing::field::debug(path_id)); + let Some(path) = conn.path(path_id) else { + trace!("event for unknown path"); + return Ok(()); + }; + path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); + path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); + } + PathEvent::Closed { id, error_code } => todo!(), + PathEvent::Abandoned { id, path_stats } => todo!(), + PathEvent::LocallyClosed { id, error } => todo!(), + PathEvent::RemoteStatus { id, status } => todo!(), + PathEvent::ObservedAddr { id, addr } => todo!(), + } + Ok(()) + } + + /// Clean up connections which no longer exist. + // TODO: Call this on a schedule. + fn cleanup_connections(&mut self) { + self.connections + .retain(|_, handle| handle.upgrade().is_some()); + + let mut stable_ids = BTreeSet::new(); + for handle in self.connections.values() { + handle + .upgrade() + .map(|conn| stable_ids.insert(conn.stable_id())); + } + + self.path_id_map + .retain(|(stable_id, _), _| stable_ids.contains(stable_id)); + } } /// Messages to send to the [`NodeStateActor`]. diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 714023ef295..706efc8e155 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -331,7 +331,7 @@ impl From<&quinn_udp::Transmit<'_>> for OwnedTransmit { } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum Addr { Ip(SocketAddr), Relay(RelayUrl, NodeId), From 12d12c0f414d1529d6ed0df2032231de22917c8f Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 30 Sep 2025 16:09:43 +0200 Subject: [PATCH 046/164] select the right path --- iroh/src/magicsock/node_map/node_state.rs | 122 ++++++++++++++++++++-- iroh/src/magicsock/node_map/path_state.rs | 6 ++ iroh/src/magicsock/transports.rs | 14 +++ 3 files changed, 135 insertions(+), 7 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 36e92ede39a..9ef9583956f 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -849,7 +849,7 @@ impl NodeStateActor { } } Some((id, evt)) = self.path_events.next() => { - self.handle_path_event(id, evt).await?; + self.handle_path_event(id, evt).await; } _ = self.local_addrs.updated() => { self.trigger_holepunching().await; @@ -1211,30 +1211,32 @@ impl NodeStateActor { &mut self, stable_id: usize, event: Result, - ) -> Result<(), Whatever> { + ) { let Ok(event) = event else { warn!("missed a PathEvent, NodeStateActor lagging"); // TODO: Is it possible to recover using the sync APIs to figure out what the // state of the connection and it's paths are? - return Ok(()); + return; }; let Some(handle) = self.connections.get(&stable_id) else { trace!("event for removed connection"); - return Ok(()); + return; }; let Some(conn) = handle.upgrade() else { trace!("event for closed connection"); - return Ok(()); + return; }; match event { PathEvent::Opened { id: path_id } => { tracing::Span::current().record("path_id", tracing::field::debug(path_id)); let Some(path) = conn.path(path_id) else { trace!("event for unknown path"); - return Ok(()); + return; }; path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); + + self.select_path(); } PathEvent::Closed { id, error_code } => todo!(), PathEvent::Abandoned { id, path_stats } => todo!(), @@ -1242,7 +1244,6 @@ impl NodeStateActor { PathEvent::RemoteStatus { id, status } => todo!(), PathEvent::ObservedAddr { id, addr } => todo!(), } - Ok(()) } /// Clean up connections which no longer exist. @@ -1261,6 +1262,113 @@ impl NodeStateActor { self.path_id_map .retain(|(stable_id, _), _| stable_ids.contains(stable_id)); } + + /// Selects the path with the lowest RTT, prefers direct paths. + /// + /// If there are direct paths, this selects the direct path with the lowest RTT. If + /// there are only relay paths, the relay path with the lowest RTT is chosen. + /// + /// Any unused direct paths are closed. + fn select_path(&mut self) { + // Find the lowest RTT across all connections for each open path. The long way, so + // we get to trace-log ALL RTTs. + let mut all_path_rtts: FxHashMap> = FxHashMap::default(); + for (conn_id, conn) in self + .connections + .iter() + .filter_map(|(id, handle)| handle.upgrade().map(|conn| (*id, conn))) + { + let stats = conn.stats(); + for (path_id, stats) in stats.paths { + if let Some(addr) = self.path_id_map.get(&(conn_id, path_id)) { + all_path_rtts + .entry(addr.clone()) + .or_default() + .push(stats.rtt); + } else { + trace!(?path_id, "unknown PathId in ConnectionStats"); + } + } + } + trace!(?all_path_rtts, "dumping all path RTTs"); + let path_rtts: FxHashMap = all_path_rtts + .into_iter() + .filter_map(|(addr, rtts)| rtts.into_iter().min().map(|rtt| (addr, rtt))) + .collect(); + + // Find the fastest direct path. + const IPV6_RTT_ADVANTAGE: Duration = Duration::from_millis(3); + let direct_path = path_rtts + .iter() + .filter(|(addr, _rtt)| addr.is_ip()) + .map(|(addr, rtt)| { + if addr.is_ipv4() { + (*rtt + IPV6_RTT_ADVANTAGE, addr) + } else { + (*rtt, addr) + } + }) + .min() + .map(|(_rtt, addr)| addr.clone()); + if let Some(addr) = direct_path { + let prev = self.selected_path.replace(addr.clone()); + if prev.as_ref() != Some(&addr) { + debug!(?addr, ?prev, "selected new direct path"); + } + self.close_redundant_paths(addr); + return; + } + + // Still here? Find the fastest relay path. + let relay_path = path_rtts + .iter() + .filter(|(addr, _rtt)| addr.is_relay()) + .map(|(addr, rtt)| (rtt, addr)) + .min() + .map(|(_rtt, addr)| addr.clone()); + if let Some(addr) = relay_path { + let prev = self.selected_path.replace(addr.clone()); + if prev.as_ref() != Some(&addr) { + debug!(?addr, ?prev, "selected new relay path"); + } + self.close_redundant_paths(addr); + return; + } + } + + /// Closes any direct paths not selected. + fn close_redundant_paths(&mut self, selected_path: transports::Addr) { + // TODO: Quinn should just do this. Also, I made this value up. + const APPLICATION_ABANDON_PATH: u8 = 30; + + debug_assert_eq!(self.selected_path.as_ref(), Some(&selected_path)); + + self.path_id_map.retain(|(conn_id, path_id), addr| { + if !addr.is_ip() || *addr == selected_path { + return true; + } + if let Some(conn) = self + .connections + .get(conn_id) + .map(|handle| handle.upgrade()) + .flatten() + { + trace!(?addr, ?conn_id, ?path_id, "closing direct path"); + if let Some(path) = conn.path(*path_id) { + match path.close(APPLICATION_ABANDON_PATH.into()) { + Err(quinn_proto::ClosePathError::LastOpenPath) => { + error!("could not close last open path"); + } + Err(quinn_proto::ClosePathError::ClosedPath) => (), + Ok(_fut) => { + // TODO: Should investigate if we care about this future. + } + } + } + } + false + }); + } } /// Messages to send to the [`NodeStateActor`]. diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index d75da7fc97d..d268436f968 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -191,6 +191,12 @@ pub(super) fn summarize_node_paths(paths: &BTreeMap) -> Strin w } +/// The state of a single path to the remote endpoint. +/// +/// Each path is identified by the destination [`transports::Addr`] and they are stored in +/// the [`NodeStateActor::paths`] map. +/// +/// [`NodeStateActor::paths`]: super::node_state::NodeStateActor #[derive(Debug, Default)] pub(super) struct NewPathState { /// How we learned about this path, and when. diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 706efc8e155..25f91c3974c 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -369,6 +369,20 @@ impl Addr { matches!(self, Self::Ip(_)) } + pub(crate) fn is_ipv4(&self) -> bool { + match self { + Addr::Ip(socket_addr) => socket_addr.is_ipv4(), + Addr::Relay(_, _) => false, + } + } + + pub(crate) fn is_ipv6(&self) -> bool { + match self { + Addr::Ip(socket_addr) => socket_addr.is_ipv6(), + Addr::Relay(_, _) => false, + } + } + /// Returns `None` if not an `Ip`. pub(crate) fn into_socket_addr(self) -> Option { match self { From 9e7be2db1500d7cfc3bdf8a0b1580225e949f1cc Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 30 Sep 2025 17:10:26 +0200 Subject: [PATCH 047/164] close paths on all connections need to check this out for real. wonder if this gets into loops --- iroh/src/magicsock/node_map/node_state.rs | 59 ++++++++++++++++++----- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 9ef9583956f..eaa3b6bcf84 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -59,6 +59,10 @@ pub(super) const SESSION_ACTIVE_TIMEOUT: Duration = Duration::from_secs(45); /// How often we try to upgrade to a better patheven if we have some non-relay route that works. const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); +/// The value which we close paths. +// TODO: Quinn should just do this. Also, I made this value up. +const APPLICATION_ABANDON_PATH: u8 = 30; + #[derive(Debug)] pub(in crate::magicsock) enum PingAction { SendCallMeMaybe { @@ -771,6 +775,8 @@ pub(super) struct NodeStateActor { /// [`NodeStateMessage::AddConnection`], because this ID is only unique within /// *currently active* connections. So there could be conflicts if we did not yet know /// a previous connection no longer exists. + // TODO: We do exhaustive searches through this map to find items based on + // transports::Addr. Perhaps a bi-directional map could be considered. path_id_map: FxHashMap<(usize, PathId), transports::Addr>, /// Information about the last holepunching attempt. last_holepunch: Option, @@ -1206,10 +1212,10 @@ impl NodeStateActor { } } - #[instrument(skip(self), fields(path_id))] + #[instrument(skip(self))] async fn handle_path_event( &mut self, - stable_id: usize, + conn_id: usize, event: Result, ) { let Ok(event) = event else { @@ -1218,7 +1224,7 @@ impl NodeStateActor { // state of the connection and it's paths are? return; }; - let Some(handle) = self.connections.get(&stable_id) else { + let Some(handle) = self.connections.get(&conn_id) else { trace!("event for removed connection"); return; }; @@ -1226,11 +1232,11 @@ impl NodeStateActor { trace!("event for closed connection"); return; }; + trace!("path event"); match event { PathEvent::Opened { id: path_id } => { - tracing::Span::current().record("path_id", tracing::field::debug(path_id)); let Some(path) = conn.path(path_id) else { - trace!("event for unknown path"); + trace!("path open event for unknown path"); return; }; path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); @@ -1238,11 +1244,41 @@ impl NodeStateActor { self.select_path(); } - PathEvent::Closed { id, error_code } => todo!(), - PathEvent::Abandoned { id, path_stats } => todo!(), - PathEvent::LocallyClosed { id, error } => todo!(), - PathEvent::RemoteStatus { id, status } => todo!(), - PathEvent::ObservedAddr { id, addr } => todo!(), + PathEvent::Abandoned { id, path_stats } => { + trace!(?path_stats, "path abandoned"); + // This is the last event for this path. + self.path_id_map.remove(&(conn_id, id)); + } + PathEvent::Closed { id, .. } | PathEvent::LocallyClosed { id, .. } => { + // If one connection closes this path, close it on all connections. + let Some(addr) = self.path_id_map.get(&(conn_id, id)) else { + debug!("path not in path_id_map"); + return; + }; + for (conn_id, path_id) in self + .path_id_map + .iter() + .filter(|(key, path_addr)| *path_addr == addr) + .map(|(key, _)| key) + { + if let Some(conn) = self + .connections + .get(&conn_id) + .map(|handle| handle.upgrade()) + .flatten() + { + if let Some(path) = conn.path(*path_id) { + trace!(?addr, ?conn_id, ?path_id, "closing path"); + if let Err(err) = path.close(APPLICATION_ABANDON_PATH.into()) { + trace!(?addr, ?conn_id, ?path_id, "path close failed"); + } + } + } + } + } + PathEvent::RemoteStatus { .. } | PathEvent::ObservedAddr { .. } => { + // Nothing to do for these events. + } } } @@ -1338,9 +1374,6 @@ impl NodeStateActor { /// Closes any direct paths not selected. fn close_redundant_paths(&mut self, selected_path: transports::Addr) { - // TODO: Quinn should just do this. Also, I made this value up. - const APPLICATION_ABANDON_PATH: u8 = 30; - debug_assert_eq!(self.selected_path.as_ref(), Some(&selected_path)); self.path_id_map.retain(|(conn_id, path_id), addr| { From c1bfdf55488fdf2e90711c20edec27eea6495bb4 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 30 Sep 2025 18:22:04 +0200 Subject: [PATCH 048/164] send connections to the NodeStateActor --- iroh/src/magicsock.rs | 165 +++------------------- iroh/src/magicsock/node_map.rs | 11 +- iroh/src/magicsock/node_map/node_state.rs | 12 +- 3 files changed, 37 insertions(+), 151 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 9e2d2886b5a..4801e8ae664 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -40,14 +40,13 @@ use nested_enum_utils::common_fields; use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; -use quinn::{ServerConfig, WeakConnectionHandle}; +use node_map::NodeStateMessage; +use quinn::ServerConfig; use rand::Rng; use snafu::{ResultExt, Snafu}; use tokio::sync::{Mutex as AsyncMutex, mpsc}; use tokio_util::sync::CancellationToken; -use tracing::{ - Instrument, Level, debug, event, info, info_span, instrument, trace, trace_span, warn, -}; +use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; use transports::{LocalAddrsWatch, MagicTransport}; use url::Url; @@ -202,8 +201,6 @@ pub(crate) struct MagicSock { ipv6_reported: Arc, /// Tracks the networkmap node entity for each node discovery key. node_map: NodeMap, - /// Tracks existing connections - connection_map: ConnectionMap, /// Local addresses local_addrs_watch: LocalAddrsWatch, @@ -229,22 +226,6 @@ pub(crate) struct MagicSock { pub(crate) metrics: EndpointMetrics, } -#[derive(Default, Debug)] -struct ConnectionMap { - map: std::sync::Mutex>>, -} - -impl ConnectionMap { - fn insert(&self, remote: NodeId, handle: WeakConnectionHandle) { - self.map - .lock() - .expect("poisoned") - .entry(remote) - .or_default() - .push(handle); - } -} - #[allow(missing_docs)] #[common_fields({ backtrace: Option, @@ -303,31 +284,22 @@ impl MagicSock { pub(crate) fn register_connection(&self, remote: NodeId, conn: &quinn::Connection) { debug!(%remote, "register connection"); let weak_handle = conn.weak_handle(); - self.connection_map.insert(remote, weak_handle); - - // TODO: track task - // TODO: find a good home for this - let mut path_events = conn.path_events(); - let _task = task::spawn( - async move { - loop { - match path_events.recv().await { - Ok(event) => { - info!(remote = %remote, "path event: {:?}", event); - } - Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => { - warn!("lagged path events"); - } - Err(tokio::sync::broadcast::error::RecvError::Closed) => break, - } + let node_state = self.node_map.node_state_actor(remote); + let mut msg = NodeStateMessage::AddConnection(weak_handle); + loop { + match node_state.try_send(msg) { + Ok(()) => break, + Err(mpsc::error::TrySendError::Closed(msg)) => { + error!(?msg, "NodeStateActor closed"); + break; + } + Err(mpsc::error::TrySendError::Full(ret_msg)) => { + warn!("NodeStateActor inbox full when adding new connection"); + msg = ret_msg; + // TODO: Yikes! + tokio::task::yield_now(); } } - .instrument(info_span!("path events", %remote)), - ); - - // open additional paths - if let Some(addr) = self.node_map.get_current_addr(remote) { - self.add_paths(addr); } } @@ -438,10 +410,6 @@ impl MagicSock { self.node_map.get_all_paths_addr_for_node(node_id) } - pub(crate) fn get_direct_addrs(&self, node_id: NodeId) -> Vec { - self.node_map.get_direct_addrs(node_id) - } - /// Add potential addresses for a node to the [`NodeState`]. /// /// This is used to add possible paths that the remote node might be reachable on. They @@ -484,74 +452,6 @@ impl MagicSock { } } - /// Adds all available addresses in the given `addr` as paths - fn add_paths(&self, addr: NodeAddr) { - let mut map = self.connection_map.map.lock().expect("poisoned"); - let mut to_delete = Vec::new(); - if let Some(conns) = map.get_mut(&addr.node_id) { - for (i, conn) in conns.into_iter().enumerate() { - if let Some(conn) = conn.upgrade() { - for addr in addr.direct_addresses() { - let conn = conn.clone(); - let addr = *addr; - task::spawn( - async move { - debug!(%addr, "open path IP"); - match conn - .open_path_ensure(addr, quinn_proto::PathStatus::Available) - .await - { - Ok(path) => { - path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); - path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); - } - Err(err) => { - warn!("failed to open path {:?}", err); - } - } - } - .instrument(info_span!("open path IP")), - ); - } - // Insert the relay addr - if let Some(addr) = self.get_mapping_addr(addr.node_id) { - let conn = conn.clone(); - let addr = addr.private_socket_addr(); - task::spawn( - async move { - debug!(%addr, "open path relay"); - match conn - .open_path_ensure(addr, quinn_proto::PathStatus::Backup) - .await - { - Ok(path) => { - // Keep the relay path open - path.set_max_idle_timeout(None).ok(); - path.set_keep_alive_interval(None).ok(); - } - Err(err) => { - warn!("failed to open path {:?}", err); - } - } - } - .instrument(info_span!("open path relay")), - ); - } - } else { - to_delete.push(i); - } - } - // cleanup dead connections - let mut i = 0; - conns.retain(|_| { - let remove = to_delete.contains(&i); - i += 1; - - !remove - }); - } - } - /// Stores a new set of direct addresses. /// /// If the direct addresses have changed from the previous set, they are published to @@ -840,9 +740,7 @@ impl MagicSock { self.metrics.magicsock.recv_disco_udp.inc(); } - let span = trace_span!("handle_disco", ?dm); - let _guard = span.enter(); - trace!("receive disco message"); + trace!(?dm, "receive disco message"); match dm { disco::Message::Ping(ping) => { self.metrics.magicsock.recv_disco_ping.inc(); @@ -854,33 +752,9 @@ impl MagicSock { } disco::Message::CallMeMaybe(cm) => { self.metrics.magicsock.recv_disco_call_me_maybe.inc(); - match src { - transports::Addr::Relay(url, _) => { - event!( - target: "iroh::_events::call-me-maybe::recv", - Level::DEBUG, - remote_node = sender.fmt_short(), - via = ?url, - their_addrs = ?cm.my_numbers, - ); - } - _ => { - warn!("call-me-maybe packets should only come via relay"); - return; - } - } - - // Add new addresses as paths - self.add_paths(NodeAddr { - node_id: sender, - relay_url: None, - direct_addresses: cm.my_numbers.iter().copied().collect(), - }); - - self.node_map.handle_call_me_maybe(sender, cm); + self.node_map.handle_call_me_maybe(cm, sender, src.clone()); } } - trace!("disco message handled"); } /// Send the given ping actions out. @@ -1254,7 +1128,6 @@ impl Handle { actor_sender: actor_sender.clone(), ipv6_reported, node_map, - connection_map: Default::default(), discovery, discovery_user_data: RwLock::new(discovery_user_data), direct_addrs, diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index b59de1d22b8..2dcb1037b5d 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -404,7 +404,16 @@ impl NodeMap { } } - pub(super) fn handle_call_me_maybe(&self, sender: NodeId, msg: CallMeMaybe) { + pub(super) fn handle_call_me_maybe( + &self, + msg: disco::CallMeMaybe, + sender: NodeId, + src: transports::Addr, + ) { + if !src.is_relay() { + warn!("DISCO CallMeMaybe packets should only come via relay"); + return; + } let node_state = self.node_state_actor(sender); if let Err(err) = node_state.try_send(NodeStateMessage::CallMeMaybeReceived(msg)) { // TODO: This is bad and will drop call-me-maybe's under load. But diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index eaa3b6bcf84..638568f80f0 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -16,7 +16,7 @@ use quinn::WeakConnectionHandle; use quinn_proto::{PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; -use snafu::{OptionExt, ResultExt, Whatever}; +use snafu::{ResultExt, Whatever}; use tokio::sync::mpsc; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; @@ -53,10 +53,13 @@ const LAST_ALIVE_PRUNE_DURATION: Duration = Duration::from_secs(120); const GOOD_ENOUGH_LATENCY: Duration = Duration::from_millis(5); /// How long since the last activity we try to keep an established endpoint peering alive. +/// /// It's also the idle time at which we stop doing QAD queries to keep NAT mappings alive. pub(super) const SESSION_ACTIVE_TIMEOUT: Duration = Duration::from_secs(45); -/// How often we try to upgrade to a better patheven if we have some non-relay route that works. +/// How often we try to upgrade to a better path. +/// +/// Even if we have some non-relay route that works. const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); /// The value which we close paths. @@ -1258,7 +1261,7 @@ impl NodeStateActor { for (conn_id, path_id) in self .path_id_map .iter() - .filter(|(key, path_addr)| *path_addr == addr) + .filter(|(_, path_addr)| *path_addr == addr) .map(|(key, _)| key) { if let Some(conn) = self @@ -1270,7 +1273,7 @@ impl NodeStateActor { if let Some(path) = conn.path(*path_id) { trace!(?addr, ?conn_id, ?path_id, "closing path"); if let Err(err) = path.close(APPLICATION_ABANDON_PATH.into()) { - trace!(?addr, ?conn_id, ?path_id, "path close failed"); + trace!(?addr, ?conn_id, ?path_id, "path close failed: {err:#}"); } } } @@ -1405,6 +1408,7 @@ impl NodeStateActor { } /// Messages to send to the [`NodeStateActor`]. +#[derive(Debug)] pub(crate) enum NodeStateMessage { /// Sends a datagram to all known paths. /// From 7b4f056cc51f07a40f52459dc27c046a6b8c4380 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 30 Sep 2025 18:27:20 +0200 Subject: [PATCH 049/164] remove PingActions from magicsock Actor Message --- iroh/src/magicsock.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 4801e8ae664..ce0cc6dd54f 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1410,7 +1410,6 @@ enum DiscoBoxError { #[derive(Debug)] enum ActorMessage { - PingActions(Vec), NetworkChange, ScheduleDirectAddrUpdate(UpdateReason, Option<(NodeId, RelayUrl)>), #[cfg(test)] @@ -1627,8 +1626,6 @@ impl Actor { // TODO: this might trigger too many packets at once, pace this self.msock.node_map.prune_inactive(); - let msgs = self.msock.node_map.nodes_stayin_alive(); - self.handle_ping_actions(&sender, msgs).await; } } state = self.netmon_watcher.updated() => { @@ -1693,13 +1690,6 @@ impl Actor { .schedule_run(why, state.into()); } - #[instrument(skip_all)] - async fn handle_ping_actions(&mut self, sender: &TransportsSender, msgs: Vec) { - if let Err(err) = self.msock.send_ping_actions(sender, msgs).await { - warn!("Failed to send ping actions: {err:#}"); - } - } - /// Processes an incoming actor message. /// /// Returns `true` if it was a shutdown. @@ -1720,9 +1710,6 @@ impl Actor { ActorMessage::ForceNetworkChange(is_major) => { self.handle_network_change(is_major).await; } - ActorMessage::PingActions(ping_actions) => { - self.handle_ping_actions(sender, ping_actions).await; - } } } From d3b81e781bea6ade0615fbbaa647d59ca921522a Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 30 Sep 2025 18:46:59 +0200 Subject: [PATCH 050/164] Kill a bunch of dead code --- iroh/src/disco.rs | 15 --- iroh/src/magicsock.rs | 83 +--------------- iroh/src/magicsock/node_map.rs | 109 ++-------------------- iroh/src/magicsock/node_map/node_state.rs | 14 --- iroh/src/magicsock/node_map/udp_paths.rs | 11 --- iroh/src/magicsock/transports.rs | 7 -- 6 files changed, 10 insertions(+), 229 deletions(-) diff --git a/iroh/src/disco.rs b/iroh/src/disco.rs index 20f44922032..725648df25e 100644 --- a/iroh/src/disco.rs +++ b/iroh/src/disco.rs @@ -179,21 +179,6 @@ pub enum SendAddr { Relay(RelayUrl), } -impl SendAddr { - /// Returns if this is a `relay` addr. - pub fn is_relay(&self) -> bool { - matches!(self, Self::Relay(_)) - } - - /// Returns the `Some(Url)` if it is a relay addr. - pub fn relay_url(&self) -> Option { - match self { - Self::Relay(url) => Some(url.clone()), - Self::Udp(_) => None, - } - } -} - impl From for SendAddr { fn from(addr: transports::Addr) -> Self { match addr { diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index ce0cc6dd54f..b60b31f4b68 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -69,7 +69,7 @@ use crate::{ use self::transports::IpTransport; use self::{ metrics::Metrics as MagicsockMetrics, - node_map::{NodeMap, PingAction}, + node_map::NodeMap, transports::{RelayActorConfig, RelayTransport, Transports, TransportsSender}, }; @@ -757,61 +757,6 @@ impl MagicSock { } } - /// Send the given ping actions out. - async fn send_ping_actions( - &self, - _sender: &TransportsSender, - msgs: Vec, - ) -> io::Result<()> { - for msg in msgs { - // Abort sending as soon as we know we are shutting down. - if self.is_closing() || self.is_closed() { - return Ok(()); - } - match msg { - PingAction::SendCallMeMaybe { - relay_url, - dst_node, - } => { - // Sends the call-me-maybe DISCO message, queuing if addresses are too stale. - // - // To send the call-me-maybe message, we need to know our current direct addresses. If - // this information is too stale, the call-me-maybe is queued while a net_report run is - // scheduled. Once this run finishes, the call-me-maybe will be sent. - match self.direct_addrs.fresh_enough() { - Ok(()) => { - let msg = disco::Message::CallMeMaybe( - self.direct_addrs.to_call_me_maybe_message(), - ); - if !self.disco.try_send( - SendAddr::Relay(relay_url.clone()), - dst_node, - msg.clone(), - ) { - warn!(dstkey = %dst_node.fmt_short(), %relay_url, "relay channel full, dropping call-me-maybe"); - } else { - debug!(dstkey = %dst_node.fmt_short(), %relay_url, "call-me-maybe sent"); - } - } - Err(last_refresh_ago) => { - debug!( - ?last_refresh_ago, - "want call-me-maybe but direct addrs stale; queuing after restun", - ); - self.actor_sender - .try_send(ActorMessage::ScheduleDirectAddrUpdate( - UpdateReason::RefreshForPeering, - Some((dst_node, relay_url)), - )) - .ok(); - } - } - } - } - } - Ok(()) - } - /// Sends out a disco message. async fn send_disco_message( &self, @@ -1549,7 +1494,7 @@ impl Actor { trace!(?msg, "tick: msg"); self.msock.metrics.magicsock.actor_tick_msg.inc(); - self.handle_actor_message(msg, &sender).await; + self.handle_actor_message(msg).await; } tick = self.periodic_re_stun_timer.tick() => { trace!("tick: re_stun {:?}", tick); @@ -1693,7 +1638,7 @@ impl Actor { /// Processes an incoming actor message. /// /// Returns `true` if it was a shutdown. - async fn handle_actor_message(&mut self, msg: ActorMessage, sender: &TransportsSender) { + async fn handle_actor_message(&mut self, msg: ActorMessage) { match msg { ActorMessage::NetworkChange => { self.network_monitor.network_change().await.ok(); @@ -1971,28 +1916,6 @@ impl DiscoveredDirectAddrs { .collect() } - /// Whether the direct addr information is considered "fresh". - /// - /// If not fresh you should probably update the direct addresses before using this info. - /// - /// Returns `Ok(())` if fresh enough and `Err(elapsed)` if not fresh enough. - /// `elapsed` is the time elapsed since the direct addresses were last updated. - /// - /// If there is no direct address information `Err(Duration::ZERO)` is returned. - fn fresh_enough(&self) -> Result<(), Duration> { - match *self.updated_at.read().expect("poisoned") { - None => Err(Duration::ZERO), - Some(time) => { - let elapsed = time.elapsed(); - if elapsed <= ENDPOINTS_FRESH_ENOUGH_DURATION { - Ok(()) - } else { - Err(elapsed) - } - } - } - } - fn to_call_me_maybe_message(&self) -> disco::CallMeMaybe { let my_numbers = self .addrs diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 2dcb1037b5d..3f70c5cc238 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -13,7 +13,10 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{debug, info, instrument, trace, warn}; -use self::node_state::{NodeState, Options}; +use crate::disco::{self}; +#[cfg(any(test, feature = "test-utils"))] +use crate::endpoint::PathSelection; + use super::mapped_addrs::{AddrMap, RelayMappedAddr}; #[cfg(any(test, feature = "test-utils"))] use super::transports::TransportsSender; @@ -25,16 +28,15 @@ use super::{ mapped_addrs::NodeIdMappedAddr, transports::{self, OwnedTransmit}, }; -use crate::disco::{self, CallMeMaybe}; -#[cfg(any(test, feature = "test-utils"))] -use crate::endpoint::PathSelection; + +use self::node_state::{NodeState, Options}; mod node_state; mod path_state; mod path_validity; mod udp_paths; -pub(super) use node_state::{NodeStateMessage, PingAction}; +pub(super) use node_state::NodeStateMessage; pub use node_state::{ConnectionType, ControlMsg, DirectAddrInfo, RemoteInfo}; @@ -253,44 +255,6 @@ impl NodeMap { .map(|ep| *ep.all_paths_mapped_addr()) } - pub(super) fn get_direct_addrs(&self, node_key: NodeId) -> Vec { - self.inner - .lock() - .expect("poisoned") - .get(NodeStateKey::NodeId(node_key)) - .map(|ep| ep.direct_addresses().map(Into::into).collect()) - .unwrap_or_default() - } - - /// Returns a [`NodeAddr`] with all the currently known direct addresses and the relay URL. - pub(super) fn get_current_addr(&self, node_key: NodeId) -> Option { - self.inner - .lock() - .expect("poisoned") - .get(NodeStateKey::NodeId(node_key)) - .map(|ep| ep.get_current_addr()) - } - - #[allow(clippy::type_complexity)] - pub(super) fn get_send_addrs( - &self, - addr: NodeIdMappedAddr, - have_ipv6: bool, - metrics: &MagicsockMetrics, - ) -> Option<( - PublicKey, - Option, - Option, - Vec, - )> { - let mut inner = self.inner.lock().expect("poisoned"); - let ep = inner.get_mut(NodeStateKey::NodeIdMappedAddr(addr))?; - let public_key = *ep.public_key(); - trace!(dest = %addr, node_id = %public_key.fmt_short(), "dst mapped to NodeId"); - let (udp_addr, relay_url, ping_actions) = ep.get_send_addrs(have_ipv6, metrics); - Some((public_key, udp_addr, relay_url, ping_actions)) - } - pub(super) fn reset_node_states(&self) { let now = Instant::now(); let mut inner = self.inner.lock().expect("poisoned"); @@ -299,14 +263,6 @@ impl NodeMap { } } - pub(super) fn nodes_stayin_alive(&self) -> Vec { - let mut inner = self.inner.lock().expect("poisoned"); - inner - .node_states_mut() - .flat_map(|(_idx, node_state)| node_state.stayin_alive()) - .collect() - } - /// Returns the [`RemoteInfo`]s for each node in the node map. pub(super) fn list_remote_infos(&self, now: Instant) -> Vec { // NOTE: calls to this method will often call `into_iter` (or similar methods). Note that @@ -581,32 +537,6 @@ impl NodeMapInner { .map(|ep| ep.conn_type()) } - fn handle_call_me_maybe( - &mut self, - sender: NodeId, - cm: CallMeMaybe, - metrics: &MagicsockMetrics, - ) { - let ns_id = NodeStateKey::NodeId(sender); - if let Some(id) = self.get_id(ns_id.clone()) { - for number in &cm.my_numbers { - // ensure the new addrs are known - self.set_node_state_for_ip_port(*number, id); - } - } - match self.get_mut(ns_id) { - None => { - debug!("received call-me-maybe: ignore, node is unknown"); - metrics.recv_disco_call_me_maybe_bad_disco.inc(); - } - Some(ns) => { - debug!(endpoints = ?cm.my_numbers, "received call-me-maybe"); - - ns.handle_call_me_maybe(cm); - } - } - } - /// Inserts a new node into the [`NodeMap`]. fn insert_node(&mut self, options: Options) -> &mut NodeState { info!( @@ -628,31 +558,6 @@ impl NodeMapInner { self.by_id.get_mut(&id).expect("just inserted") } - /// Makes future node lookups by ipp return the same endpoint as a lookup by nk. - /// - /// This should only be called with a fully verified mapping of ipp to - /// nk, because calling this function defines the endpoint we hand to - /// WireGuard for packets received from ipp. - fn set_node_key_for_ip_port(&mut self, ipp: impl Into, nk: &PublicKey) { - let ipp = ipp.into(); - if let Some(id) = self.by_ip_port.get(&ipp) { - if !self.by_node_key.contains_key(nk) { - self.by_node_key.insert(*nk, *id); - } - self.by_ip_port.remove(&ipp); - } - if let Some(id) = self.by_node_key.get(nk) { - trace!("insert ip -> id: {:?} -> {}", ipp, id); - self.by_ip_port.insert(ipp, *id); - } - } - - fn set_node_state_for_ip_port(&mut self, ipp: impl Into, id: usize) { - let ipp = ipp.into(); - trace!(?ipp, ?id, "set endpoint for ip:port"); - self.by_ip_port.insert(ipp, id); - } - /// Prunes nodes without recent activity so that at most [`MAX_INACTIVE_NODES`] are kept. fn prune_inactive(&mut self) { let now = Instant::now(); diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 638568f80f0..e71a0ebcf6f 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1489,20 +1489,6 @@ enum SendCallMeMaybe { IfNoRecent, } -/// The reason why a discovery ping message was sent. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DiscoPingPurpose { - /// The purpose of a ping was to see if a path was valid. - Discovery, - /// Ping to ensure the current route is still valid. - StayinAlive, - /// When a ping was received and no direct connection exists yet. - /// - /// When a ping was received we suspect a direct connection is possible. If we do not - /// yet have one that triggers a ping, indicated with this reason. - PingBack, -} - /// The type of control message we have received. #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, derive_more::Display)] pub enum ControlMsg { diff --git a/iroh/src/magicsock/node_map/udp_paths.rs b/iroh/src/magicsock/node_map/udp_paths.rs index 2c72a95c842..9ebe6590344 100644 --- a/iroh/src/magicsock/node_map/udp_paths.rs +++ b/iroh/src/magicsock/node_map/udp_paths.rs @@ -51,17 +51,6 @@ pub(super) enum UdpSendAddr { None, } -impl UdpSendAddr { - pub fn get_addr(&self) -> Option { - match self { - UdpSendAddr::Valid(addr) - | UdpSendAddr::Outdated(addr) - | UdpSendAddr::Unconfirmed(addr) => Some(*addr), - UdpSendAddr::None => None, - } - } -} - /// The UDP paths for a single node. /// /// Paths are identified by the [`IpPort`] of their UDP address. diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 25f91c3974c..7feaff87611 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -376,13 +376,6 @@ impl Addr { } } - pub(crate) fn is_ipv6(&self) -> bool { - match self { - Addr::Ip(socket_addr) => socket_addr.is_ipv6(), - Addr::Relay(_, _) => false, - } - } - /// Returns `None` if not an `Ip`. pub(crate) fn into_socket_addr(self) -> Option { match self { From d9ed9be4c79dabd3c962eab8f7bea8ebf17e5d05 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 30 Sep 2025 20:21:02 +0200 Subject: [PATCH 051/164] use a better way to send to this channel can't use task::yield_now in a sync function --- iroh/src/magicsock.rs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index b60b31f4b68..26ec8f78087 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -286,21 +286,24 @@ impl MagicSock { let weak_handle = conn.weak_handle(); let node_state = self.node_map.node_state_actor(remote); let mut msg = NodeStateMessage::AddConnection(weak_handle); - loop { - match node_state.try_send(msg) { - Ok(()) => break, - Err(mpsc::error::TrySendError::Closed(msg)) => { - error!(?msg, "NodeStateActor closed"); - break; - } - Err(mpsc::error::TrySendError::Full(ret_msg)) => { - warn!("NodeStateActor inbox full when adding new connection"); - msg = ret_msg; - // TODO: Yikes! - tokio::task::yield_now(); + + tokio::task::block_in_place(move || { + loop { + match node_state.try_send(msg) { + Ok(()) => break, + Err(mpsc::error::TrySendError::Closed(msg)) => { + error!(?msg, "NodeStateActor closed"); + break; + } + Err(mpsc::error::TrySendError::Full(ret_msg)) => { + warn!("NodeStateActor inbox full when adding new connection"); + msg = ret_msg; + // TODO: Yikes! + std::thread::yield_now(); + } } } - } + }); } #[cfg(not(wasm_browser))] From 2c521b08120b388db94c65f8701e576c2eb12b58 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 1 Oct 2025 15:46:03 +0200 Subject: [PATCH 052/164] Hook up connecting check for a valid send addr There's a silly bug in how discovery is hooked up that if you don't do this check you fail hard. Rather than clean up that discovery logic I would prefer to just get on with the NodeStateActor refactor. --- iroh-base/src/node_addr.rs | 3 +- iroh/src/endpoint.rs | 72 ++++++++++++++++++++--- iroh/src/magicsock.rs | 25 +++++--- iroh/src/magicsock/node_map.rs | 21 +++---- iroh/src/magicsock/node_map/node_state.rs | 35 +++++++++-- iroh/src/magicsock/transports/ip.rs | 8 ++- 6 files changed, 124 insertions(+), 40 deletions(-) diff --git a/iroh-base/src/node_addr.rs b/iroh-base/src/node_addr.rs index 08ca55dee84..ef047d56196 100644 --- a/iroh-base/src/node_addr.rs +++ b/iroh-base/src/node_addr.rs @@ -36,9 +36,10 @@ use crate::{NodeId, PublicKey, RelayUrl}; /// [discovery]: https://docs.rs/iroh/*/iroh/index.html#node-discovery /// [home relay]: https://docs.rs/iroh/*/iroh/relay/index.html /// [Relay server]: https://docs.rs/iroh/*/iroh/index.html#relay-servers -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +#[derive(derive_more::Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub struct NodeAddr { /// The node's identifier. + #[debug("{}", node_id.fmt_short())] pub node_id: NodeId, /// The node's home relay url. pub relay_url: Option, diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 21735528901..ab8aa060633 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -729,6 +729,7 @@ impl Endpoint { self.add_node_addr(node_addr.clone()).await?; } let node_id = node_addr.node_id; + trace!(dst_node_id = node_id.fmt_short(), "connecting"); // When we start a connection we want to send the QUIC Initial packets on all the // known paths for the remote node. For this we use an AllPathsMappedAddr as @@ -747,7 +748,6 @@ impl Endpoint { // Start connecting via quinn. This will time out after 10 seconds if no reachable // address is available. - debug!(?mapped_addr, "Attempting connection..."); let client_config = { let mut alpn_protocols = vec![alpn.to_vec()]; alpn_protocols.extend(options.additional_alpns); @@ -1373,8 +1373,8 @@ impl Endpoint { // Only return a mapped addr if we have some way of dialing this node, in other // words, we have either a relay URL or at least one direct address. - let addr = if self.msock.has_send_address(node_id) { - self.msock.get_mapping_addr(node_id) + let addr = if self.msock.has_send_address(node_id).await { + Some(self.msock.get_node_mapped_addr(node_id)) } else { None }; @@ -1405,11 +1405,8 @@ impl Endpoint { .first_arrived() .await .context(get_mapping_address_error::DiscoverSnafu)?; - if let Some(addr) = self.msock.get_mapping_addr(node_id) { - Ok((addr, Some(discovery))) - } else { - Err(get_mapping_address_error::NoAddressSnafu.build()) - } + let addr = self.msock.get_node_mapped_addr(node_id); + Ok((addr, Some(discovery))) } } } @@ -2234,7 +2231,7 @@ mod tests { use n0_watcher::Watcher; use quinn::ConnectionError; use rand::SeedableRng; - use tracing::{Instrument, error_span, info, info_span}; + use tracing::{Instrument, error_span, info, info_span, instrument}; use tracing_test::traced_test; use super::Endpoint; @@ -2575,6 +2572,63 @@ mod tests { Ok(()) } + #[tokio::test] + #[traced_test] + async fn endpoint_two_direct_only() -> Result { + // Connect two endpoints on the same network, without a relay server. + // let discovery = StaticProvider::new(); + let ep1 = Endpoint::builder() + .alpns(vec![TEST_ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + // .discovery(discovery.clone()) + .bind() + .await?; + let ep2 = Endpoint::builder() + .alpns(vec![TEST_ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + // .discovery(discovery.clone()) + .bind() + .await?; + ep1.direct_addresses().initialized().await; + ep2.direct_addresses().initialized().await; + let ep1_nodeaddr = ep1.node_addr().initialized().await; + // let ep2_nodeaddr = ep2.node_addr().initialized().await; + // discovery.set_node_info(ep1_nodeaddr.clone()); + // discovery.set_node_info(ep2_nodeaddr.clone()); + + #[instrument(name = "connect", skip_all)] + async fn connect(ep: Endpoint, dst: NodeAddr) -> Result { + info!(me = ep.node_id().fmt_short(), "connect starting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + let mut send = conn.open_uni().await.e()?; + send.write_all(b"hello").await.e()?; + send.finish().e()?; + Ok(conn.closed().await) + } + + #[instrument(name = "accept", skip_all)] + async fn accept(ep: Endpoint, src: NodeId) -> Result { + info!(me = ep.node_id().fmt_short(), "accept starting"); + let conn = ep.accept().await.e()?.await.e()?; + let node_id = conn.remote_node_id()?; + assert_eq!(node_id, src); + let mut recv = conn.accept_uni().await.e()?; + let msg = recv.read_to_end(100).await.e()?; + assert_eq!(msg, b"hello"); + // Dropping the connection closes it just fine. + Ok(()) + } + + let ep1_accept = tokio::spawn(accept(ep1.clone(), ep2.node_id())); + let ep2_connect = tokio::spawn(connect(ep2.clone(), ep1_nodeaddr)); + + ep1_accept.await.e()??; + let conn_closed = ep2_connect.await.e()??; + assert_eq!(conn_closed, quinn::ConnectionError::CidsExhausted); + + Ok(()) + } + #[tokio::test] #[traced_test] async fn endpoint_bidi_send_recv() -> Result { diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 26ec8f78087..25db075af43 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -44,7 +44,7 @@ use node_map::NodeStateMessage; use quinn::ServerConfig; use rand::Rng; use snafu::{ResultExt, Snafu}; -use tokio::sync::{Mutex as AsyncMutex, mpsc}; +use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot}; use tokio_util::sync::CancellationToken; use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; use transports::{LocalAddrsWatch, MagicTransport}; @@ -318,10 +318,17 @@ impl MagicSock { } /// Returns `true` if we have at least one candidate address where we can send packets to. - pub(crate) fn has_send_address(&self, node_key: PublicKey) -> bool { - self.remote_info(node_key) - .map(|info| info.has_send_address()) - .unwrap_or(false) + pub(crate) async fn has_send_address(&self, node_id: NodeId) -> bool { + let node_state = self.node_map.node_state_actor(node_id); + let (tx, rx) = oneshot::channel(); + if node_state + .send(NodeStateMessage::CanSend(tx)) + .await + .is_err() + { + return false; + } + rx.await.unwrap_or(false) } /// Return the [`RemoteInfo`]s of all nodes in the node map. @@ -409,8 +416,8 @@ impl MagicSock { } /// Returns the socket address which can be used by the QUIC layer to *dial* this node. - pub(crate) fn get_mapping_addr(&self, node_id: NodeId) -> Option { - self.node_map.get_all_paths_addr_for_node(node_id) + pub(crate) fn get_node_mapped_addr(&self, node_id: NodeId) -> NodeIdMappedAddr { + self.node_map.node_mapped_addr(node_id) } /// Add potential addresses for a node to the [`NodeState`]. @@ -2723,7 +2730,7 @@ mod tests { ) .await .unwrap(); - let addr = msock_1.get_mapping_addr(node_id_2).unwrap(); + let addr = msock_1.get_node_mapped_addr(node_id_2); let res = tokio::time::timeout( Duration::from_secs(10), magicsock_connect(msock_1.endpoint(), secret_key_1.clone(), addr, node_id_2), @@ -2790,7 +2797,7 @@ mod tests { ) .await; - let addr_2 = msock_1.get_mapping_addr(node_id_2).unwrap(); + let addr_2 = msock_1.get_node_mapped_addr(node_id_2); // Set a low max_idle_timeout so quinn gives up on this quickly and our test does // not take forever. You need to check the log output to verify this is really diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 3f70c5cc238..6b5c48a603c 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -11,7 +11,7 @@ use n0_future::{task::AbortOnDropHandle, time::Instant}; use node_state::{NodeStateActor, NodeStateHandle}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::{debug, info, instrument, trace, warn}; +use tracing::{Instrument, debug, info, info_span, instrument, trace, warn}; use crate::disco::{self}; #[cfg(any(test, feature = "test-utils"))] @@ -247,12 +247,8 @@ impl NodeMap { .receive_relay(relay_url, src) } - pub(super) fn get_all_paths_addr_for_node(&self, node_id: NodeId) -> Option { - self.inner - .lock() - .expect("poisoned") - .get(NodeStateKey::NodeId(node_id)) - .map(|ep| *ep.all_paths_mapped_addr()) + pub(super) fn node_mapped_addr(&self, node_id: NodeId) -> NodeIdMappedAddr { + self.node_mapped_addrs.get(&node_id) } pub(super) fn reset_node_states(&self) { @@ -662,17 +658,18 @@ impl TransportsSenderActor { // can. No need to introduce extra buffering. let (tx, rx) = mpsc::channel(1); - // No .instrument() on task, run method has an #[instrument] attribute. - let task = tokio::spawn(async move { - self.run(rx).await; - }); + let task = tokio::spawn( + async move { + self.run(rx).await; + } + .instrument(info_span!("TransportsSenderActor")), + ); TransportsSenderHandle { inbox: tx, _task: AbortOnDropHandle::new(task), } } - #[instrument(name = "TransportsSenderActor", skip_all)] async fn run(self, mut inbox: mpsc::Receiver) { use TransportsSenderMessage::SendDatagram; diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index e71a0ebcf6f..4fda6b5c391 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -17,7 +17,7 @@ use quinn_proto::{PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Whatever}; -use tokio::sync::mpsc; +use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; @@ -826,6 +826,7 @@ impl NodeStateActor { pub(super) fn start(mut self) -> NodeStateHandle { let (tx, rx) = mpsc::channel(16); + let node_id = self.node_id; let task = tokio::spawn( async move { @@ -833,7 +834,7 @@ impl NodeStateActor { error!("actor failed: {err:#}"); } } - .instrument(info_span!("NodeStateActor")), + .instrument(info_span!("NodeStateActor", node_id = node_id.fmt_short())), ); NodeStateHandle { sender: tx, @@ -841,8 +842,8 @@ impl NodeStateActor { } } - #[instrument(skip_all, fields(node_id = %self.node_id.fmt_short()))] async fn run(&mut self, mut inbox: mpsc::Receiver) -> Result<(), Whatever> { + trace!("actor started"); loop { let scheduled_hp = match self.scheduled_holepunch { Some(when) => MaybeFuture::Some(tokio::time::sleep_until(when)), @@ -861,9 +862,11 @@ impl NodeStateActor { self.handle_path_event(id, evt).await; } _ = self.local_addrs.updated() => { + trace!("local addrs updated, triggering holepunching"); self.trigger_holepunching().await; } _ = &mut scheduled_hp => { + trace!("triggering scheduled holepunching"); self.scheduled_holepunch = None; self.trigger_holepunching().await; } @@ -873,7 +876,9 @@ impl NodeStateActor { Ok(()) } + #[instrument(skip(self))] async fn handle_message(&mut self, msg: NodeStateMessage) -> Result<(), Whatever> { + trace!("handling message"); match msg { NodeStateMessage::SendDatagram(transmit) => { if let Some(ref addr) = self.selected_path { @@ -888,7 +893,8 @@ impl NodeStateActor { .await .whatever_context("TransportSenerActor stopped")?; } - self.trigger_holepunching(); + trace!("connecting without selected path: triggering holepunching"); + self.trigger_holepunching().await; } } NodeStateMessage::AddConnection(handle) => { @@ -975,6 +981,7 @@ impl NodeStateActor { let path = self.paths.entry(src).or_default(); path.sources.insert(Source::Ping, Instant::now()); + trace!("ping received, triggering holepunching"); self.trigger_holepunching().await; } NodeStateMessage::PongReceived(pong, src) => { @@ -998,6 +1005,10 @@ impl NodeStateActor { self.open_quic_path(src); } + NodeStateMessage::CanSend(tx) => { + let can_send = !self.paths.is_empty(); + tx.send(can_send).ok(); + } } Ok(()) } @@ -1021,13 +1032,18 @@ impl NodeStateActor { async fn trigger_holepunching(&mut self) { const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); + if self.connections.is_empty() { + trace!("not holepunching: no connections"); + return; + } + if self .selected_path .as_ref() .map(|addr| addr.is_ip()) .unwrap_or_default() { - trace!("not holepunching, already have a direct connection"); + trace!("not holepunching: already have a direct connection"); // TODO: If the latency is kind of bad we should retry holepunching at times. return; } @@ -1408,7 +1424,7 @@ impl NodeStateActor { } /// Messages to send to the [`NodeStateActor`]. -#[derive(Debug)] +#[derive(derive_more::Debug)] pub(crate) enum NodeStateMessage { /// Sends a datagram to all known paths. /// @@ -1418,12 +1434,14 @@ pub(crate) enum NodeStateMessage { /// This is not acceptable to use on the normal send path, as it is an async send /// operation with a bunch more copying. So it should only be used for sending QUIC /// Initial packets. + #[debug("SendDatagram(OwnedTransmit)")] SendDatagram(OwnedTransmit), /// Adds an active connection to this remote node. /// /// The connection will now be managed by this actor. Holepunching will happen when /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. + #[debug("AddConnection(WeakConnectionHandle)")] AddConnection(WeakConnectionHandle), /// Adds a [`NodeAddr`] with locations where the node might be reachable. AddNodeAddr(NodeAddr, Source), @@ -1433,6 +1451,11 @@ pub(crate) enum NodeStateMessage { PingReceived(disco::Ping, transports::Addr), /// Process a received DISCO Pong message. PongReceived(disco::Pong, transports::Addr), + /// Asks if there is any possible path that could be used. + /// + /// This does not mean there is any guarantee that the remote endpoint is reachable. + #[debug("CanSend(onseshot::Sender)")] + CanSend(oneshot::Sender), } /// A handle to a [`NodeStateActor`]. diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index 68033695305..391f76ba3e4 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -144,7 +144,6 @@ impl IpSender { src: Option, transmit: &Transmit<'_>, ) -> io::Result<()> { - trace!("sending to {}", destination); let total_bytes = transmit.contents.len() as u64; let res = self .sender @@ -156,10 +155,10 @@ impl IpSender { src_ip: src, }) .await; - trace!("send res: {:?}", res); match res { Ok(res) => { + trace!(dst = ?destination, "sent on IP"); match destination { SocketAddr::V4(_) => { self.metrics.send_ipv4.inc_by(total_bytes); @@ -170,7 +169,10 @@ impl IpSender { } Ok(res) } - Err(err) => Err(err), + Err(err) => { + trace!(dst = ?destination, "IP send error: {err:#}"); + Err(err) + } } } From 21894e785e94cba7ae5d671fab0d05434a93c23e Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 1 Oct 2025 16:26:05 +0200 Subject: [PATCH 053/164] Do not send incoming packets through the NodeMap --- iroh/src/magicsock.rs | 35 +++++++---------------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 25db075af43..28b1ac642b3 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -647,37 +647,16 @@ impl MagicSock { } #[cfg(not(wasm_browser))] transports::Addr::Ip(addr) => { - // UDP - - // Update the NodeMap and remap RecvMeta to the NodeIdMappedAddr. - match self.node_map.receive_udp(*addr) { - None => { - trace!( - src = %addr, - count = %quic_datagram_count, - len = quinn_meta.len, - "UDP recv quic packets", - ); - - quic_packets_total += quic_datagram_count; - quinn_meta.addr = *addr; - } - Some((node_id, quic_mapped_addr)) => { - trace!( - src = %addr, - node = %node_id.fmt_short(), - count = %quic_datagram_count, - len = quinn_meta.len, - "UDP recv quic packets", - ); - quic_packets_total += quic_datagram_count; - quinn_meta.addr = quic_mapped_addr.private_socket_addr(); - } - } + trace!( + src = ?addr, + count = %quic_datagram_count, + len = quinn_meta.len, + "UDP recv QUIC packets", + ); + quic_packets_total += quic_datagram_count; } transports::Addr::Relay(src_url, src_node) => { // Relay - let _quic_mapped_addr = self.node_map.receive_relay(src_url, *src_node); let mapped_addr = self .node_map .relay_mapped_addrs From fc9700900a323837d52ac8b449ff91d33c65db42 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 1 Oct 2025 16:32:51 +0200 Subject: [PATCH 054/164] delete a whole bunch of unused code --- iroh/src/magicsock.rs | 21 - iroh/src/magicsock/node_map.rs | 336 ++++----- iroh/src/magicsock/node_map/node_state.rs | 814 +--------------------- 3 files changed, 128 insertions(+), 1043 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 28b1ac642b3..a1230e5c347 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1433,9 +1433,6 @@ impl Actor { // Setup network monitoring let mut current_netmon_state = self.netmon_watcher.get(); - #[cfg(not(wasm_browser))] - let mut direct_addr_heartbeat_timer = time::interval(HEARTBEAT_INTERVAL); - #[cfg(not(wasm_browser))] let mut portmap_watcher = self .direct_addr_update_state @@ -1462,11 +1459,6 @@ impl Actor { #[cfg(wasm_browser)] let portmap_watcher_changed = n0_future::future::pending(); - #[cfg(not(wasm_browser))] - let direct_addr_heartbeat_timer_tick = direct_addr_heartbeat_timer.tick(); - #[cfg(wasm_browser)] - let direct_addr_heartbeat_timer_tick = n0_future::future::pending(); - tokio::select! { _ = shutdown_token.cancelled() => { debug!("shutting down"); @@ -1549,19 +1541,6 @@ impl Actor { #[cfg(wasm_browser)] let _unused_in_browsers = change; }, - _ = direct_addr_heartbeat_timer_tick => { - #[cfg(not(wasm_browser))] - { - trace!( - "tick: direct addr heartbeat {} direct addrs", - self.msock.node_map.node_count(), - ); - self.msock.metrics.magicsock.actor_tick_direct_addr_heartbeat.inc(); - // TODO: this might trigger too many packets at once, pace this - - self.msock.node_map.prune_inactive(); - } - } state = self.netmon_watcher.updated() => { let Ok(state) = state else { trace!("tick: link change receiver closed"); diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 6b5c48a603c..b1eafb5ac50 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -6,12 +6,12 @@ use std::{ sync::Mutex, }; -use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; +use iroh_base::{NodeAddr, NodeId, RelayUrl}; use n0_future::{task::AbortOnDropHandle, time::Instant}; use node_state::{NodeStateActor, NodeStateHandle}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::{Instrument, debug, info, info_span, instrument, trace, warn}; +use tracing::{Instrument, debug, info_span, trace, warn}; use crate::disco::{self}; #[cfg(any(test, feature = "test-utils"))] @@ -29,7 +29,7 @@ use super::{ transports::{self, OwnedTransmit}, }; -use self::node_state::{NodeState, Options}; +use self::node_state::NodeState; mod node_state; mod path_state; @@ -83,7 +83,6 @@ pub(super) struct NodeMapInner { by_ip_port: HashMap, by_quic_mapped_addr: HashMap, by_id: HashMap, - next_id: usize, #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, /// The [`NodeStateActor`] for each remote node. @@ -232,21 +231,6 @@ impl NodeMap { self.inner.lock().expect("poisoned").node_count() } - #[cfg(not(wasm_browser))] - pub(super) fn receive_udp( - &self, - udp_addr: SocketAddr, - ) -> Option<(PublicKey, NodeIdMappedAddr)> { - self.inner.lock().expect("poisoned").receive_udp(udp_addr) - } - - pub(super) fn receive_relay(&self, relay_url: &RelayUrl, src: NodeId) -> NodeIdMappedAddr { - self.inner - .lock() - .expect("poisoned") - .receive_relay(relay_url, src) - } - pub(super) fn node_mapped_addr(&self, node_id: NodeId) -> NodeIdMappedAddr { self.node_mapped_addrs.get(&node_id) } @@ -287,11 +271,6 @@ impl NodeMap { self.inner.lock().expect("poisoned").remote_info(node_id) } - /// Prunes nodes without recent activity so that at most [`MAX_INACTIVE_NODES`] are kept. - pub(super) fn prune_inactive(&self) { - self.inner.lock().expect("poisoned").prune_inactive(); - } - pub(crate) fn on_direct_addr_discovered(&self, discovered: BTreeSet) { self.inner .lock() @@ -392,7 +371,6 @@ impl NodeMapInner { by_ip_port: Default::default(), by_quic_mapped_addr: Default::default(), by_id: Default::default(), - next_id: 0, #[cfg(any(test, feature = "test-utils"))] path_selection: Default::default(), node_states: Default::default(), @@ -441,65 +419,15 @@ impl NodeMapInner { } } - fn get_mut(&mut self, id: NodeStateKey) -> Option<&mut NodeState> { - self.get_id(id).and_then(|id| self.by_id.get_mut(&id)) - } - fn get(&self, id: NodeStateKey) -> Option<&NodeState> { self.get_id(id).and_then(|id| self.by_id.get(&id)) } - fn get_or_insert_with( - &mut self, - id: NodeStateKey, - f: impl FnOnce() -> Options, - ) -> &mut NodeState { - let id = self.get_id(id); - match id { - None => self.insert_node(f()), - Some(id) => self.by_id.get_mut(&id).expect("is not empty"), - } - } - /// Number of nodes currently listed. fn node_count(&self) -> usize { self.by_id.len() } - /// Marks the node we believe to be at `ipp` as recently used. - #[cfg(not(wasm_browser))] - fn receive_udp(&mut self, udp_addr: SocketAddr) -> Option<(NodeId, NodeIdMappedAddr)> { - let ip_port: IpPort = udp_addr.into(); - let Some(node_state) = self.get_mut(NodeStateKey::IpPort(ip_port)) else { - trace!(src=%udp_addr, "receive_udp: no node_state found for addr, ignore"); - return None; - }; - node_state.receive_udp(ip_port, Instant::now()); - Some(( - *node_state.public_key(), - *node_state.all_paths_mapped_addr(), - )) - } - - #[instrument(skip_all, fields(src = %src.fmt_short()))] - fn receive_relay(&mut self, relay_url: &RelayUrl, src: NodeId) -> NodeIdMappedAddr { - #[cfg(any(test, feature = "test-utils"))] - let path_selection = self.path_selection; - let node_state = self.get_or_insert_with(NodeStateKey::NodeId(src), || { - trace!("packets from unknown node, insert into node map"); - Options { - node_id: src, - relay_url: Some(relay_url.clone()), - active: true, - source: Source::Relay, - #[cfg(any(test, feature = "test-utils"))] - path_selection, - } - }); - node_state.receive_relay(relay_url, src, Instant::now()); - *node_state.all_paths_mapped_addr() - } - fn node_states(&self) -> impl Iterator { self.by_id.iter() } @@ -533,27 +461,6 @@ impl NodeMapInner { .map(|ep| ep.conn_type()) } - /// Inserts a new node into the [`NodeMap`]. - fn insert_node(&mut self, options: Options) -> &mut NodeState { - info!( - node = %options.node_id.fmt_short(), - relay_url = ?options.relay_url, - source = %options.source, - "inserting new node in NodeMap", - ); - let id = self.next_id; - self.next_id = self.next_id.wrapping_add(1); - let node_state = NodeState::new(id, options); - - // update indices - self.by_quic_mapped_addr - .insert(*node_state.all_paths_mapped_addr(), id); - self.by_node_key.insert(*node_state.public_key(), id); - - self.by_id.insert(id, node_state); - self.by_id.get_mut(&id).expect("just inserted") - } - /// Prunes nodes without recent activity so that at most [`MAX_INACTIVE_NODES`] are kept. fn prune_inactive(&mut self) { let now = Instant::now(); @@ -723,7 +630,6 @@ mod tests { use tracing_test::traced_test; use super::{node_state::MAX_INACTIVE_DIRECT_ADDRESSES, *}; - use crate::disco::SendAddr; use crate::magicsock::DiscoveredDirectAddrs; use crate::magicsock::transports::Transports; @@ -827,126 +733,128 @@ mod tests { #[tokio::test] #[traced_test] async fn test_prune_direct_addresses() { - let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let direct_addrs = DiscoveredDirectAddrs::default(); - let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); - let (disco, _) = DiscoState::new(&secret_key); - let node_map = NodeMap::new( - secret_key.public(), - Default::default(), - transports.create_sender(), - direct_addrs.addrs.watch(), - disco, - ); - let public_key = SecretKey::generate(rand::thread_rng()).public(); - let id = node_map - .inner - .lock() - .unwrap() - .insert_node(Options { - node_id: public_key, - relay_url: None, - active: false, - source: Source::NamedApp { - name: "test".into(), - }, - path_selection: PathSelection::default(), - }) - .id(); - - const LOCALHOST: IpAddr = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST); - - // add [`MAX_INACTIVE_DIRECT_ADDRESSES`] active direct addresses and double - // [`MAX_INACTIVE_DIRECT_ADDRESSES`] that are inactive - - info!("Adding active addresses"); - for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES { - let addr = SocketAddr::new(LOCALHOST, 5000 + i as u16); - let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); - // add address - node_map.add_test_addr(node_addr).await; - // make it active - node_map.inner.lock().unwrap().receive_udp(addr); - } - - info!("Adding offline/inactive addresses"); - for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES * 2 { - let addr = SocketAddr::new(LOCALHOST, 6000 + i as u16); - let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); - node_map.add_test_addr(node_addr).await; - } - - let mut node_map_inner = node_map.inner.lock().unwrap(); - let endpoint = node_map_inner.by_id.get_mut(&id).unwrap(); - - info!("Adding alive addresses"); - for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES { - let addr = SendAddr::Udp(SocketAddr::new(LOCALHOST, 7000 + i as u16)); - let txid = stun_rs::TransactionId::from([i as u8; 12]); - // Note that this already invokes .prune_direct_addresses() because these are - // new UDP paths. - // endpoint.handle_ping(addr, txid); - } - - info!("Pruning addresses"); - endpoint.prune_direct_addresses(Instant::now()); - - // Half the offline addresses should have been pruned. All the active and alive - // addresses should have been kept. - assert_eq!( - endpoint.direct_addresses().count(), - MAX_INACTIVE_DIRECT_ADDRESSES * 3 - ); - - // We should have both offline and alive addresses which are not active. - assert_eq!( - endpoint - .direct_address_states() - .filter(|(_addr, state)| !state.is_active()) - .count(), - MAX_INACTIVE_DIRECT_ADDRESSES * 2 - ) + panic!("support this again"); + // let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); + // let direct_addrs = DiscoveredDirectAddrs::default(); + // let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); + // let (disco, _) = DiscoState::new(&secret_key); + // let node_map = NodeMap::new( + // secret_key.public(), + // Default::default(), + // transports.create_sender(), + // direct_addrs.addrs.watch(), + // disco, + // ); + // let public_key = SecretKey::generate(rand::thread_rng()).public(); + // let id = node_map + // .inner + // .lock() + // .unwrap() + // .insert_node(Options { + // node_id: public_key, + // relay_url: None, + // active: false, + // source: Source::NamedApp { + // name: "test".into(), + // }, + // path_selection: PathSelection::default(), + // }) + // .id(); + + // const LOCALHOST: IpAddr = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST); + + // // add [`MAX_INACTIVE_DIRECT_ADDRESSES`] active direct addresses and double + // // [`MAX_INACTIVE_DIRECT_ADDRESSES`] that are inactive + + // info!("Adding active addresses"); + // for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES { + // let addr = SocketAddr::new(LOCALHOST, 5000 + i as u16); + // let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); + // // add address + // node_map.add_test_addr(node_addr).await; + // // make it active + // node_map.inner.lock().unwrap().receive_udp(addr); + // } + + // info!("Adding offline/inactive addresses"); + // for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES * 2 { + // let addr = SocketAddr::new(LOCALHOST, 6000 + i as u16); + // let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); + // node_map.add_test_addr(node_addr).await; + // } + + // let mut node_map_inner = node_map.inner.lock().unwrap(); + // let endpoint = node_map_inner.by_id.get_mut(&id).unwrap(); + + // info!("Adding alive addresses"); + // for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES { + // let addr = SendAddr::Udp(SocketAddr::new(LOCALHOST, 7000 + i as u16)); + // let txid = stun_rs::TransactionId::from([i as u8; 12]); + // // Note that this already invokes .prune_direct_addresses() because these are + // // new UDP paths. + // // endpoint.handle_ping(addr, txid); + // } + + // info!("Pruning addresses"); + // endpoint.prune_direct_addresses(Instant::now()); + + // // Half the offline addresses should have been pruned. All the active and alive + // // addresses should have been kept. + // assert_eq!( + // endpoint.direct_addresses().count(), + // MAX_INACTIVE_DIRECT_ADDRESSES * 3 + // ); + + // // We should have both offline and alive addresses which are not active. + // assert_eq!( + // endpoint + // .direct_address_states() + // .filter(|(_addr, state)| !state.is_active()) + // .count(), + // MAX_INACTIVE_DIRECT_ADDRESSES * 2 + // ) } #[tokio::test] async fn test_prune_inactive() { - let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let direct_addrs = DiscoveredDirectAddrs::default(); - let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); - let (disco, _) = DiscoState::new(&secret_key); - let node_map = NodeMap::new( - secret_key.public(), - Default::default(), - transports.create_sender(), - direct_addrs.addrs.watch(), - disco, - ); - // add one active node and more than MAX_INACTIVE_NODES inactive nodes - let active_node = SecretKey::generate(rand::thread_rng()).public(); - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167); - node_map - .add_test_addr(NodeAddr::new(active_node).with_direct_addresses([addr])) - .await; - node_map - .inner - .lock() - .unwrap() - .receive_udp(addr) - .expect("registered"); - - for _ in 0..MAX_INACTIVE_NODES + 1 { - let node = SecretKey::generate(rand::thread_rng()).public(); - node_map.add_test_addr(NodeAddr::new(node)).await; - } - - assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 2); - node_map.prune_inactive(); - assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 1); - node_map - .inner - .lock() - .unwrap() - .get(NodeStateKey::NodeId(active_node)) - .expect("should not be pruned"); + panic!("support this again"); + // let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); + // let direct_addrs = DiscoveredDirectAddrs::default(); + // let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); + // let (disco, _) = DiscoState::new(&secret_key); + // let node_map = NodeMap::new( + // secret_key.public(), + // Default::default(), + // transports.create_sender(), + // direct_addrs.addrs.watch(), + // disco, + // ); + // // add one active node and more than MAX_INACTIVE_NODES inactive nodes + // let active_node = SecretKey::generate(rand::thread_rng()).public(); + // let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167); + // node_map + // .add_test_addr(NodeAddr::new(active_node).with_direct_addresses([addr])) + // .await; + // node_map + // .inner + // .lock() + // .unwrap() + // .receive_udp(addr) + // .expect("registered"); + + // for _ in 0..MAX_INACTIVE_NODES + 1 { + // let node = SecretKey::generate(rand::thread_rng()).public(); + // node_map.add_test_addr(NodeAddr::new(node)).await; + // } + + // assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 2); + // node_map.prune_inactive(); + // assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 1); + // node_map + // .inner + // .lock() + // .unwrap() + // .get(NodeStateKey::NodeId(active_node)) + // .expect("should not be pruned"); } } diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 4fda6b5c391..f89e0771fd4 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1,8 +1,8 @@ use std::{ collections::{BTreeSet, HashMap}, - net::{IpAddr, SocketAddr}, + net::SocketAddr, pin::Pin, - sync::{Arc, atomic::AtomicBool}, + sync::Arc, }; use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; @@ -19,17 +19,16 @@ use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Whatever}; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; -use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; +use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; #[cfg(any(test, feature = "test-utils"))] use crate::endpoint::PathSelection; use crate::{ - disco::{self, SendAddr}, + disco::{self}, endpoint::DirectAddr, magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MAX_IDLE_TIMEOUT, MagicsockMetrics, mapped_addrs::{AddrMap, MappedAddr, NodeIdMappedAddr, RelayMappedAddr}, - node_map::path_validity::PathValidity, transports::{self, OwnedTransmit}, }, util::MaybeFuture, @@ -37,8 +36,8 @@ use crate::{ use super::{ IpPort, Source, TransportsSenderMessage, - path_state::{NewPathState, PathState, summarize_node_paths}, - udp_paths::{NodeUdpPaths, UdpSendAddr}, + path_state::{NewPathState, PathState}, + udp_paths::NodeUdpPaths, }; /// Number of addresses that are not active that we keep around per node. @@ -81,10 +80,6 @@ pub(in crate::magicsock) enum PingAction { /// connection we'll hopefully discover more direct paths. #[derive(Debug)] pub(super) struct NodeState { - /// The ID used as index in the [`NodeMap`]. - /// - /// [`NodeMap`]: super::NodeMap - id: usize, /// The UDP address used on the QUIC-layer to address this node. quic_mapped_addr: NodeIdMappedAddr, /// The global identifier for this endpoint. @@ -102,71 +97,14 @@ pub(super) struct NodeState { /// /// Note that sending datagrams to a node does not mean the node receives them. last_used: Option, - /// Last time we sent a call-me-maybe. - /// - /// When we do not have a direct connection and we try to send some data, we will try to - /// do a full ping + call-me-maybe. Usually each side only needs to send one - /// call-me-maybe to the other for holes to be punched in both directions however. So - /// we only try and send one per [`HEARTBEAT_INTERVAL`]. Each [`HEARTBEAT_INTERVAL`] - /// the [`NodeState::stayin_alive`] function is called, which will trigger new - /// call-me-maybe messages as backup. - last_call_me_maybe: Option, /// The type of connection we have to the node, either direct, relay, mixed, or none. conn_type: Watchable, - /// Whether the conn_type was ever observed to be `Direct` at some point. - /// - /// Used for metric reporting. - has_been_direct: AtomicBool, /// Configuration for what path selection to use #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, } -/// Options for creating a new [`NodeState`]. -#[derive(Debug)] -pub(super) struct Options { - pub(super) node_id: NodeId, - pub(super) relay_url: Option, - /// Is this endpoint currently active (sending data)? - pub(super) active: bool, - pub(super) source: super::Source, - #[cfg(any(test, feature = "test-utils"))] - pub(super) path_selection: PathSelection, -} - impl NodeState { - pub(super) fn new(id: usize, options: Options) -> Self { - let quic_mapped_addr = NodeIdMappedAddr::generate(); - - // TODO(frando): I don't think we need to track the `num_relay_conns_added` - // metric here. We do so in `Self::addr_for_send`. - // if options.relay_url.is_some() { - // // we potentially have a relay connection to the node - // inc!(MagicsockMetrics, num_relay_conns_added); - // } - - let now = Instant::now(); - - NodeState { - id, - quic_mapped_addr, - node_id: options.node_id, - relay_url: options.relay_url.map(|url| { - ( - url.clone(), - PathState::new(options.node_id, SendAddr::Relay(url), options.source, now), - ) - }), - udp_paths: NodeUdpPaths::new(), - last_used: options.active.then(Instant::now), - last_call_me_maybe: None, - conn_type: Watchable::new(ConnectionType::None), - has_been_direct: AtomicBool::new(false), - #[cfg(any(test, feature = "test-utils"))] - path_selection: options.path_selection, - } - } - pub(super) fn public_key(&self) -> &PublicKey { &self.node_id } @@ -175,10 +113,6 @@ impl NodeState { &self.quic_mapped_addr } - pub(super) fn id(&self) -> usize { - self.id - } - pub(super) fn conn_type(&self) -> n0_watcher::Direct { self.conn_type.watch() } @@ -221,104 +155,6 @@ impl NodeState { } } - /// Returns the relay url of this endpoint - pub(super) fn relay_url(&self) -> Option { - self.relay_url.as_ref().map(|(url, _state)| url.clone()) - } - - /// Returns the address(es) that should be used for sending the next packet. - /// - /// This may return to send on one, both or no paths. - fn addr_for_send( - &self, - have_ipv6: bool, - metrics: &MagicsockMetrics, - ) -> (Option, Option) { - #[cfg(any(test, feature = "test-utils"))] - if self.path_selection == PathSelection::RelayOnly { - debug!( - "in `RelayOnly` mode, giving the relay address as the only viable address for this endpoint" - ); - return (None, self.relay_url()); - } - let (best_addr, relay_url) = match self.udp_paths.send_addr(have_ipv6) { - UdpSendAddr::Valid(addr) => { - // If we have a valid address we use it. - trace!(%addr, "UdpSendAddr is valid, use it"); - (Some(*addr), None) - } - UdpSendAddr::Outdated(addr) => { - // If the address is outdated we use it, but send via relay at the same time. - // We also send disco pings so that it will become valid again if it still - // works (i.e. we don't need to holepunch again). - trace!(%addr, "UdpSendAddr is outdated, use it together with relay"); - (Some(*addr), self.relay_url()) - } - UdpSendAddr::Unconfirmed(addr) => { - trace!(%addr, "UdpSendAddr is unconfirmed, use it together with relay"); - (Some(*addr), self.relay_url()) - } - UdpSendAddr::None => { - trace!("No UdpSendAddr, use relay"); - (None, self.relay_url()) - } - }; - let typ = match (best_addr, relay_url.clone()) { - (Some(best_addr), Some(relay_url)) => ConnectionType::Mixed(best_addr, relay_url), - (Some(best_addr), None) => ConnectionType::Direct(best_addr), - (None, Some(relay_url)) => ConnectionType::Relay(relay_url), - (None, None) => ConnectionType::None, - }; - if matches!(&typ, ConnectionType::Direct(_)) { - let before = self - .has_been_direct - .swap(true, std::sync::atomic::Ordering::Relaxed); - if !before { - metrics.nodes_contacted_directly.inc(); - } - } - if let Ok(prev_typ) = self.conn_type.set(typ.clone()) { - // The connection type has changed. - event!( - target: "iroh::_events::conn_type::changed", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - conn_type = ?typ, - ); - info!(%typ, "new connection type"); - - // Update some metrics - match (prev_typ, typ) { - (ConnectionType::Relay(_), ConnectionType::Direct(_)) - | (ConnectionType::Mixed(_, _), ConnectionType::Direct(_)) => { - metrics.num_direct_conns_added.inc(); - metrics.num_relay_conns_removed.inc(); - } - (ConnectionType::Direct(_), ConnectionType::Relay(_)) - | (ConnectionType::Direct(_), ConnectionType::Mixed(_, _)) => { - metrics.num_direct_conns_removed.inc(); - metrics.num_relay_conns_added.inc(); - } - (ConnectionType::None, ConnectionType::Direct(_)) => { - metrics.num_direct_conns_added.inc(); - } - (ConnectionType::Direct(_), ConnectionType::None) => { - metrics.num_direct_conns_removed.inc(); - } - (ConnectionType::None, ConnectionType::Relay(_)) - | (ConnectionType::None, ConnectionType::Mixed(_, _)) => { - metrics.num_relay_conns_added.inc(); - } - (ConnectionType::Relay(_), ConnectionType::None) - | (ConnectionType::Mixed(_, _), ConnectionType::None) => { - metrics.num_relay_conns_removed.inc(); - } - _ => (), - } - } - (best_addr, relay_url) - } - /// Removes a direct address for this node. /// /// If this is also the best address, it will be cleared as well. @@ -335,190 +171,6 @@ impl NodeState { self.udp_paths.update_to_best_addr(now); } - /// Whether we need to send another call-me-maybe to the endpoint. - /// - /// Basically we need to send a call-me-maybe if we need to find a better path. Maybe - /// we only have a relay path, or our path is expired. - /// - /// When a call-me-maybe message is sent we also need to send pings to all known paths - /// of the endpoint. The [`NodeState::send_call_me_maybe`] function takes care of this. - #[cfg(not(wasm_browser))] - #[instrument("want_call_me_maybe", skip_all)] - fn want_call_me_maybe(&self, now: &Instant) -> bool { - trace!("full ping: wanted?"); - match &self.udp_paths.best { - UdpSendAddr::None | UdpSendAddr::Unconfirmed(_) => { - debug!("best addr not set: need full ping"); - true - } - UdpSendAddr::Outdated(_) => { - debug!("best addr expired: need full ping"); - true - } - UdpSendAddr::Valid(addr) => { - let latency = self - .udp_paths - .paths - .get(&(*addr).into()) - .expect("send path not tracked?") - .latency() - .expect("send_addr marked valid incorrectly"); - if latency > GOOD_ENOUGH_LATENCY { - debug!( - "full ping interval expired and latency is only {}ms: need full ping", - latency.as_millis() - ); - true - } else { - trace!(?now, "best_addr valid: not needed"); - false - } - } - } - } - - #[cfg(wasm_browser)] - fn want_call_me_maybe(&self, _now: &Instant) -> bool { - trace!("full ping: skipped in browser"); - false - } - - /// Send a DISCO call-me-maybe message to the peer. - /// - /// This takes care of sending the needed pings beforehand. This ensures that we open - /// our firewall's port so that when the receiver sends us DISCO pings in response to - /// our call-me-maybe they will reach us and the other side establishes a direct - /// connection upon our subsequent pong response. - /// - /// For [`SendCallMeMaybe::IfNoRecent`], **no** paths will be pinged if there already - /// was a recent call-me-maybe sent. - /// - /// The caller is responsible for sending the messages. - #[must_use = "actions must be handled"] - fn send_call_me_maybe(&mut self, now: Instant, always: SendCallMeMaybe) -> Vec { - match always { - SendCallMeMaybe::Always => (), - SendCallMeMaybe::IfNoRecent => { - let had_recent_call_me_maybe = self - .last_call_me_maybe - .map(|when| when.elapsed() < HEARTBEAT_INTERVAL) - .unwrap_or(false); - if had_recent_call_me_maybe { - trace!("skipping call-me-maybe, still recent"); - return Vec::new(); - } - } - } - let mut msgs = Vec::new(); - - if let Some(url) = self.relay_url() { - debug!(%url, "queue call-me-maybe"); - msgs.push(PingAction::SendCallMeMaybe { - relay_url: url, - dst_node: self.node_id, - }); - self.last_call_me_maybe = Some(now); - } else { - debug!("can not send call-me-maybe, no relay URL"); - } - - msgs - } - - pub(super) fn update_from_node_addr( - &mut self, - new_relay_url: Option<&RelayUrl>, - new_addrs: &BTreeSet, - source: super::Source, - metrics: &MagicsockMetrics, - ) { - if matches!( - self.udp_paths.best, - UdpSendAddr::None | UdpSendAddr::Unconfirmed(_) - ) { - // we do not have a direct connection, so changing the relay information may - // have an effect on our connection status - if self.relay_url.is_none() && new_relay_url.is_some() { - // we did not have a relay connection before, but now we do - metrics.num_relay_conns_added.inc(); - } else if self.relay_url.is_some() && new_relay_url.is_none() { - // we had a relay connection before but do not have one now - metrics.num_relay_conns_removed.inc(); - } - } - - let now = Instant::now(); - - if new_relay_url.is_some() && new_relay_url != self.relay_url().as_ref() { - debug!( - "Changing relay node from {:?} to {:?}", - self.relay_url, new_relay_url - ); - self.relay_url = new_relay_url.map(|url| { - ( - url.clone(), - PathState::new(self.node_id, url.clone().into(), source.clone(), now), - ) - }); - } - - for &addr in new_addrs.iter() { - self.udp_paths - .paths - .entry(addr.into()) - .and_modify(|path_state| { - path_state.add_source(source.clone(), now); - }) - .or_insert_with(|| { - PathState::new(self.node_id, SendAddr::from(addr), source.clone(), now) - }); - } - let paths = summarize_node_paths(&self.udp_paths.paths); - debug!(new = ?new_addrs , %paths, "added new direct paths for endpoint"); - } - - /// Prune inactive paths. - /// - /// This trims the list of inactive paths for an endpoint. At most - /// [`MAX_INACTIVE_DIRECT_ADDRESSES`] are kept. - pub(super) fn prune_direct_addresses(&mut self, now: Instant) { - // prune candidates are addresses that are not active - let mut prune_candidates: Vec<_> = self - .udp_paths - .paths - .iter() - .filter(|(_ip_port, state)| !state.is_active()) - .map(|(ip_port, state)| (*ip_port, state.last_alive())) - .filter(|(_ipp, last_alive)| match last_alive { - Some(last_seen) => last_seen.elapsed() > LAST_ALIVE_PRUNE_DURATION, - None => true, - }) - .collect(); - let prune_count = prune_candidates - .len() - .saturating_sub(MAX_INACTIVE_DIRECT_ADDRESSES); - if prune_count == 0 { - // nothing to do, within limits - debug!( - paths = %summarize_node_paths(&self.udp_paths.paths), - "prune addresses: {prune_count} pruned", - ); - return; - } - - // sort leaving the worst addresses first (never contacted) and better ones (most recently - // used ones) last - prune_candidates.sort_unstable_by_key(|(_ip_port, last_alive)| *last_alive); - prune_candidates.truncate(prune_count); - for (ip_port, _last_alive) in prune_candidates.into_iter() { - self.remove_direct_addr(&ip_port, now, "inactive"); - } - debug!( - paths = %summarize_node_paths(&self.udp_paths.paths), - "prune addresses: {prune_count} pruned", - ); - } - /// Called when connectivity changes enough that we should question our earlier /// assumptions about which paths work. #[instrument("disco", skip_all, fields(node = %self.node_id.fmt_short()))] @@ -529,106 +181,6 @@ impl NodeState { self.udp_paths.update_to_best_addr(now); } - /// Handles a DISCO CallMeMaybe discovery message. - /// - /// The contract for use of this message is that the node has already pinged to us via - /// UDP, so their stateful firewall should be open. Now we can Ping back and make it - /// through. - /// - /// However if the remote side has no direct path information to us, they would not have - /// had any [`IpPort`]s to send pings to and our pings might end up blocked. But at - /// least open the firewalls on our side, giving the other side another change of making - /// it through when it pings in response. - pub(super) fn handle_call_me_maybe(&mut self, m: disco::CallMeMaybe) { - let now = Instant::now(); - let mut call_me_maybe_ipps = BTreeSet::new(); - - for peer_sockaddr in &m.my_numbers { - if let IpAddr::V6(ip) = peer_sockaddr.ip() { - if netwatch::ip::is_unicast_link_local(ip) { - // We send these out, but ignore them for now. - // TODO: teach the ping code to ping on all interfaces for these. - continue; - } - } - let ipp = IpPort::from(*peer_sockaddr); - call_me_maybe_ipps.insert(ipp); - self.udp_paths - .paths - .entry(ipp) - .or_insert_with(|| { - PathState::new( - self.node_id, - SendAddr::from(*peer_sockaddr), - Source::Relay, - now, - ) - }) - .call_me_maybe_time - .replace(now); - } - - // Zero out all the last_ping times to force send_pings to send new ones, even if - // it's been less than 5 seconds ago. Also clear pongs for direct addresses not - // included in the updated set. - for (ipp, st) in self.udp_paths.paths.iter_mut() { - if !call_me_maybe_ipps.contains(ipp) { - // TODO: This seems like a weird way to signal that the endpoint no longer - // thinks it has this IpPort as an available path. - if !st.validity.is_empty() { - debug!(path=?ipp ,"clearing recent pong"); - st.validity = PathValidity::empty(); - } - } - } - // Clear trust on our best_addr if it is not included in the updated set. - let changed = self.udp_paths.update_to_best_addr(now); - if changed { - // Clear the last call-me-maybe send time so we will send one again. - self.last_call_me_maybe = None; - } - debug!( - paths = %summarize_node_paths(&self.udp_paths.paths), - "updated endpoint paths from call-me-maybe", - ); - } - - /// Marks this node as having received a UDP payload message. - #[cfg(not(wasm_browser))] - pub(super) fn receive_udp(&mut self, addr: IpPort, now: Instant) { - let Some(state) = self.udp_paths.paths.get_mut(&addr) else { - debug_assert!(false, "node map inconsistency by_ip_port <-> direct addr"); - return; - }; - state.receive_payload(now); - self.last_used = Some(now); - self.udp_paths.update_to_best_addr(now); - } - - pub(super) fn receive_relay(&mut self, url: &RelayUrl, src: NodeId, now: Instant) { - match self.relay_url.as_mut() { - Some((current_home, state)) if current_home == url => { - // We received on the expected url. update state. - state.receive_payload(now); - } - Some((_current_home, _state)) => { - // we have a different url. we only update on ping, not on receive_relay. - } - None => { - self.relay_url = Some(( - url.clone(), - PathState::with_last_payload( - src, - SendAddr::from(url.clone()), - Source::Relay, - now, - ), - )); - } - } - self.last_used = Some(now); - } - /// Checks if this `Endpoint` is currently actively being used. pub(super) fn is_active(&self, now: &Instant) -> bool { match self.last_used { @@ -637,59 +189,6 @@ impl NodeState { } } - /// Send a heartbeat to the node to keep the connection alive, or trigger a full ping - /// if necessary. - #[instrument("stayin_alive", skip_all, fields(node = %self.node_id.fmt_short()))] - pub(super) fn stayin_alive(&mut self) -> Vec { - trace!("stayin_alive"); - let now = Instant::now(); - if !self.is_active(&now) { - trace!("skipping stayin alive: session is inactive"); - return Vec::new(); - } - - // If we do not have an optimal addr, send pings to all known places. - if self.want_call_me_maybe(&now) { - debug!("sending a call-me-maybe"); - return self.send_call_me_maybe(now, SendCallMeMaybe::Always); - } - - Vec::new() - } - - /// Returns the addresses on which a payload should be sent right now. - /// - /// This is in the hot path of `.poll_send()`. - // TODO(matheus23): Make this take &self. That's not quite possible yet due to `send_call_me_maybe` - // eventually calling `prune_direct_addresses` (which needs &mut self) - #[instrument("get_send_addrs", skip_all, fields(node = %self.node_id.fmt_short()))] - pub(crate) fn get_send_addrs( - &mut self, - have_ipv6: bool, - metrics: &MagicsockMetrics, - ) -> (Option, Option, Vec) { - let now = Instant::now(); - let prev = self.last_used.replace(now); - if prev.is_none() { - // this is the first time we are trying to connect to this node - metrics.nodes_contacted.inc(); - } - let (udp_addr, relay_url) = self.addr_for_send(have_ipv6, metrics); - - let ping_msgs = if self.want_call_me_maybe(&now) { - self.send_call_me_maybe(now, SendCallMeMaybe::IfNoRecent) - } else { - Vec::new() - }; - trace!( - ?udp_addr, - ?relay_url, - pings = %ping_msgs.len(), - "found send address", - ); - (udp_addr, relay_url, ping_msgs) - } - /// Returns a [`NodeAddr`] with all the currently known direct addresses and the relay URL. pub(crate) fn get_current_addr(&self) -> NodeAddr { // TODO: more selective? @@ -1696,304 +1195,3 @@ pub enum ConnectionType { #[display("none")] None, } - -#[cfg(test)] -mod tests { - use std::net::Ipv4Addr; - - use iroh_base::SecretKey; - - use super::*; - // use crate::magicsock::node_map::{NodeMap, NodeMapInner}; - - // #[test] - // fn test_remote_infos() { - // let now = Instant::now(); - // let elapsed = Duration::from_secs(3); - // let later = now + elapsed; - // let send_addr: RelayUrl = "https://my-relay.com".parse().unwrap(); - // let pong_src = SendAddr::Udp("0.0.0.0:1".parse().unwrap()); - // let latency = Duration::from_millis(50); - - // let relay_and_state = |node_id: NodeId, url: RelayUrl| { - // let relay_state = PathState::with_pong_reply( - // node_id, - // PongReply { - // latency, - // pong_at: now, - // from: SendAddr::Relay(send_addr.clone()), - // pong_src: pong_src.clone(), - // }, - // ); - // Some((url, relay_state)) - // }; - - // // endpoint with a `best_addr` that has a latency but no relay - // let (a_endpoint, a_socket_addr) = { - // let key = SecretKey::generate(rand::thread_rng()); - // let node_id = key.public(); - // let ip_port = IpPort { - // ip: Ipv4Addr::UNSPECIFIED.into(), - // port: 10, - // }; - // let endpoint_state = BTreeMap::from([( - // ip_port, - // PathState::with_pong_reply( - // node_id, - // PongReply { - // latency, - // pong_at: now, - // from: SendAddr::Udp(ip_port.into()), - // pong_src: pong_src.clone(), - // }, - // ), - // )]); - // ( - // NodeState { - // id: 0, - // quic_mapped_addr: NodeIdMappedAddr::generate(), - // node_id: key.public(), - // last_full_ping: None, - // relay_url: None, - // udp_paths: NodeUdpPaths::from_parts( - // endpoint_state, - // BestAddr::from_parts( - // ip_port.into(), - // latency, - // now, - // now + Duration::from_secs(100), - // ), - // ), - // sent_pings: HashMap::new(), - // last_used: Some(now), - // last_call_me_maybe: None, - // conn_type: Watchable::new(ConnectionType::Direct(ip_port.into())), - // has_been_direct: true, - // #[cfg(any(test, feature = "test-utils"))] - // path_selection: PathSelection::default(), - // }, - // ip_port.into(), - // ) - // }; - // // endpoint w/ no best addr but a relay w/ latency - // let b_endpoint = { - // // let socket_addr = "0.0.0.0:9".parse().unwrap(); - // let key = SecretKey::generate(rand::thread_rng()); - // NodeState { - // id: 1, - // quic_mapped_addr: NodeIdMappedAddr::generate(), - // node_id: key.public(), - // last_full_ping: None, - // relay_url: relay_and_state(key.public(), send_addr.clone()), - // udp_paths: NodeUdpPaths::new(), - // sent_pings: HashMap::new(), - // last_used: Some(now), - // last_call_me_maybe: None, - // conn_type: Watchable::new(ConnectionType::Relay(send_addr.clone())), - // has_been_direct: false, - // #[cfg(any(test, feature = "test-utils"))] - // path_selection: PathSelection::default(), - // } - // }; - - // // endpoint w/ no best addr but a relay w/ no latency - // let c_endpoint = { - // // let socket_addr = "0.0.0.0:8".parse().unwrap(); - // let key = SecretKey::generate(rand::thread_rng()); - // NodeState { - // id: 2, - // quic_mapped_addr: NodeIdMappedAddr::generate(), - // node_id: key.public(), - // last_full_ping: None, - // relay_url: Some(( - // send_addr.clone(), - // PathState::new( - // key.public(), - // SendAddr::from(send_addr.clone()), - // Source::App, - // now, - // ), - // )), - // udp_paths: NodeUdpPaths::new(), - // sent_pings: HashMap::new(), - // last_used: Some(now), - // last_call_me_maybe: None, - // conn_type: Watchable::new(ConnectionType::Relay(send_addr.clone())), - // has_been_direct: false, - // #[cfg(any(test, feature = "test-utils"))] - // path_selection: PathSelection::default(), - // } - // }; - - // // endpoint w/ expired best addr and relay w/ latency - // let (d_endpoint, d_socket_addr) = { - // let socket_addr: SocketAddr = "0.0.0.0:7".parse().unwrap(); - // let expired = now.checked_sub(Duration::from_secs(100)).unwrap(); - // let key = SecretKey::generate(rand::thread_rng()); - // let node_id = key.public(); - // let endpoint_state = BTreeMap::from([( - // IpPort::from(socket_addr), - // PathState::with_pong_reply( - // node_id, - // PongReply { - // latency, - // pong_at: now, - // from: SendAddr::Udp(socket_addr), - // pong_src: pong_src.clone(), - // }, - // ), - // )]); - // ( - // NodeState { - // id: 3, - // quic_mapped_addr: NodeIdMappedAddr::generate(), - // node_id: key.public(), - // last_full_ping: None, - // relay_url: relay_and_state(key.public(), send_addr.clone()), - // udp_paths: NodeUdpPaths::from_parts( - // endpoint_state, - // BestAddr::from_parts(socket_addr, Duration::from_millis(80), now, expired), - // ), - // sent_pings: HashMap::new(), - // last_used: Some(now), - // last_call_me_maybe: None, - // conn_type: Watchable::new(ConnectionType::Mixed( - // socket_addr, - // send_addr.clone(), - // )), - // has_been_direct: false, - // #[cfg(any(test, feature = "test-utils"))] - // path_selection: PathSelection::default(), - // }, - // socket_addr, - // ) - // }; - - // let mut expect = Vec::from([ - // RemoteInfo { - // node_id: a_endpoint.node_id, - // relay_url: None, - // addrs: Vec::from([DirectAddrInfo { - // addr: a_socket_addr, - // latency: Some(latency), - // last_control: Some((elapsed, ControlMsg::Pong)), - // last_payload: None, - // last_alive: Some(elapsed), - // sources: HashMap::new(), - // }]), - // conn_type: ConnectionType::Direct(a_socket_addr), - // latency: Some(latency), - // last_used: Some(elapsed), - // }, - // RemoteInfo { - // node_id: b_endpoint.node_id, - // relay_url: Some(RelayUrlInfo { - // relay_url: b_endpoint.relay_url.as_ref().unwrap().0.clone(), - // last_alive: None, - // latency: Some(latency), - // }), - // addrs: Vec::new(), - // conn_type: ConnectionType::Relay(send_addr.clone()), - // latency: Some(latency), - // last_used: Some(elapsed), - // }, - // RemoteInfo { - // node_id: c_endpoint.node_id, - // relay_url: Some(RelayUrlInfo { - // relay_url: c_endpoint.relay_url.as_ref().unwrap().0.clone(), - // last_alive: None, - // latency: None, - // }), - // addrs: Vec::new(), - // conn_type: ConnectionType::Relay(send_addr.clone()), - // latency: None, - // last_used: Some(elapsed), - // }, - // RemoteInfo { - // node_id: d_endpoint.node_id, - // relay_url: Some(RelayUrlInfo { - // relay_url: d_endpoint.relay_url.as_ref().unwrap().0.clone(), - // last_alive: None, - // latency: Some(latency), - // }), - // addrs: Vec::from([DirectAddrInfo { - // addr: d_socket_addr, - // latency: Some(latency), - // last_control: Some((elapsed, ControlMsg::Pong)), - // last_payload: None, - // last_alive: Some(elapsed), - // sources: HashMap::new(), - // }]), - // conn_type: ConnectionType::Mixed(d_socket_addr, send_addr.clone()), - // latency: Some(Duration::from_millis(50)), - // last_used: Some(elapsed), - // }, - // ]); - - // let node_map = NodeMap::from_inner(NodeMapInner { - // by_node_key: HashMap::from([ - // (a_endpoint.node_id, a_endpoint.id), - // (b_endpoint.node_id, b_endpoint.id), - // (c_endpoint.node_id, c_endpoint.id), - // (d_endpoint.node_id, d_endpoint.id), - // ]), - // by_ip_port: HashMap::from([ - // (a_socket_addr.into(), a_endpoint.id), - // (d_socket_addr.into(), d_endpoint.id), - // ]), - // by_quic_mapped_addr: HashMap::from([ - // (a_endpoint.quic_mapped_addr, a_endpoint.id), - // (b_endpoint.quic_mapped_addr, b_endpoint.id), - // (c_endpoint.quic_mapped_addr, c_endpoint.id), - // (d_endpoint.quic_mapped_addr, d_endpoint.id), - // ]), - // by_id: HashMap::from([ - // (a_endpoint.id, a_endpoint), - // (b_endpoint.id, b_endpoint), - // (c_endpoint.id, c_endpoint), - // (d_endpoint.id, d_endpoint), - // ]), - // next_id: 5, - // path_selection: PathSelection::default(), - // }); - // let mut got = node_map.list_remote_infos(later); - // got.sort_by_key(|p| p.node_id); - // expect.sort_by_key(|p| p.node_id); - // remove_non_deterministic_fields(&mut got); - // assert_eq!(expect, got); - // } - - fn remove_non_deterministic_fields(infos: &mut [RemoteInfo]) { - for info in infos.iter_mut() { - if info.relay_url.is_some() { - info.relay_url.as_mut().unwrap().last_alive = None; - } - } - } - - #[test] - fn test_prune_direct_addresses() { - // When we handle a call-me-maybe with more than MAX_INACTIVE_DIRECT_ADDRESSES we do - // not want to prune them right away but send pings to all of them. - - let key = SecretKey::generate(rand::thread_rng()); - let opts = Options { - node_id: key.public(), - relay_url: None, - active: true, - source: crate::magicsock::Source::NamedApp { - name: "test".into(), - }, - path_selection: PathSelection::default(), - }; - let mut ep = NodeState::new(0, opts); - - let my_numbers_count: u16 = (MAX_INACTIVE_DIRECT_ADDRESSES + 5).try_into().unwrap(); - let my_numbers = (0u16..my_numbers_count) - .map(|i| SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 1000 + i)) - .collect(); - let call_me_maybe = disco::CallMeMaybe { my_numbers }; - - ep.handle_call_me_maybe(call_me_maybe); - } -} From 0b4214bde7e6d829c9351685f26099de0d9bae38 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 1 Oct 2025 17:11:41 +0200 Subject: [PATCH 055/164] delete some more unused code --- iroh/src/magicsock.rs | 10 - iroh/src/magicsock/node_map.rs | 225 +++------------------- iroh/src/magicsock/node_map/node_state.rs | 2 + 3 files changed, 24 insertions(+), 213 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index a1230e5c347..cfaf0d3c6e7 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -469,8 +469,6 @@ impl MagicSock { pub(super) fn store_direct_addresses(&self, addrs: BTreeSet) { let updated = self.direct_addrs.update(addrs); if updated { - self.node_map - .on_direct_addr_discovered(self.direct_addrs.sockaddrs()); self.publish_my_addr(); } } @@ -1591,7 +1589,6 @@ impl Actor { #[cfg(not(wasm_browser))] self.msock.dns_resolver.reset().await; self.re_stun(UpdateReason::LinkChangeMajor); - self.reset_endpoint_states(); } else { self.re_stun(UpdateReason::LinkChangeMinor); } @@ -1793,13 +1790,6 @@ impl Actor { #[cfg(not(wasm_browser))] self.update_direct_addresses(report.as_ref()); } - - /// Resets the preferred address for all nodes. - /// This is called when connectivity changes enough that we no longer trust the old routes. - #[instrument(skip_all)] - fn reset_endpoint_states(&mut self) { - self.msock.node_map.reset_node_states() - } } fn new_re_stun_timer(initial_delay: bool) -> time::Interval { diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index b1eafb5ac50..677e7d26a02 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -1,6 +1,6 @@ use std::sync::Arc; use std::{ - collections::{BTreeSet, HashMap, hash_map::Entry}, + collections::{BTreeSet, HashMap}, hash::Hash, net::{IpAddr, SocketAddr}, sync::Mutex, @@ -8,10 +8,10 @@ use std::{ use iroh_base::{NodeAddr, NodeId, RelayUrl}; use n0_future::{task::AbortOnDropHandle, time::Instant}; -use node_state::{NodeStateActor, NodeStateHandle}; +use node_state::{NodeState, NodeStateActor, NodeStateHandle}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::{Instrument, debug, info_span, trace, warn}; +use tracing::{Instrument, info_span, trace, warn}; use crate::disco::{self}; #[cfg(any(test, feature = "test-utils"))] @@ -29,8 +29,6 @@ use super::{ transports::{self, OwnedTransmit}, }; -use self::node_state::NodeState; - mod node_state; mod path_state; mod path_validity; @@ -235,14 +233,6 @@ impl NodeMap { self.node_mapped_addrs.get(&node_id) } - pub(super) fn reset_node_states(&self) { - let now = Instant::now(); - let mut inner = self.inner.lock().expect("poisoned"); - for (_, ep) in inner.node_states_mut() { - ep.note_connectivity_change(now); - } - } - /// Returns the [`RemoteInfo`]s for each node in the node map. pub(super) fn list_remote_infos(&self, now: Instant) -> Vec { // NOTE: calls to this method will often call `into_iter` (or similar methods). Note that @@ -271,13 +261,6 @@ impl NodeMap { self.inner.lock().expect("poisoned").remote_info(node_id) } - pub(crate) fn on_direct_addr_discovered(&self, discovered: BTreeSet) { - self.inner - .lock() - .expect("poisoned") - .on_direct_addr_discovered(discovered, Instant::now()); - } - /// Returns the sender for the [`NodeStateActor`]. /// /// If needed a new actor is started on demand. @@ -382,35 +365,6 @@ impl NodeMapInner { actor.start() } - /// Prunes direct addresses from nodes that claim to share an address we know points to us. - pub(super) fn on_direct_addr_discovered( - &mut self, - discovered: BTreeSet, - now: Instant, - ) { - for addr in discovered { - self.remove_by_ipp(addr.into(), now, "matches our local addr") - } - } - - /// Removes a direct address from a node. - fn remove_by_ipp(&mut self, ipp: IpPort, now: Instant, why: &'static str) { - if let Some(id) = self.by_ip_port.remove(&ipp) { - if let Entry::Occupied(mut entry) = self.by_id.entry(id) { - let node = entry.get_mut(); - node.remove_direct_addr(&ipp, now, why); - if node.direct_addresses().count() == 0 { - let node_id = node.public_key(); - let mapped_addr = node.all_paths_mapped_addr(); - self.by_node_key.remove(node_id); - self.by_quic_mapped_addr.remove(mapped_addr); - debug!(node_id=%node_id.fmt_short(), why, "removing node"); - entry.remove(); - } - } - } - } - fn get_id(&self, id: NodeStateKey) -> Option { match id { NodeStateKey::NodeId(node_key) => self.by_node_key.get(&node_key).copied(), @@ -431,11 +385,6 @@ impl NodeMapInner { fn node_states(&self) -> impl Iterator { self.by_id.iter() } - - fn node_states_mut(&mut self) -> impl Iterator { - self.by_id.iter_mut() - } - /// Get the [`RemoteInfo`]s for all nodes. fn remote_infos_iter(&self, now: Instant) -> impl Iterator + '_ { self.node_states().map(move |(_, ep)| ep.info(now)) @@ -456,52 +405,8 @@ impl NodeMapInner { /// /// Will return `None` if there is not an entry in the [`NodeMap`] for /// the `public_key` - fn conn_type(&self, node_id: NodeId) -> Option> { - self.get(NodeStateKey::NodeId(node_id)) - .map(|ep| ep.conn_type()) - } - - /// Prunes nodes without recent activity so that at most [`MAX_INACTIVE_NODES`] are kept. - fn prune_inactive(&mut self) { - let now = Instant::now(); - let mut prune_candidates: Vec<_> = self - .by_id - .values() - .filter(|node| !node.is_active(&now)) - .map(|node| (*node.public_key(), node.last_used())) - .collect(); - - let prune_count = prune_candidates.len().saturating_sub(MAX_INACTIVE_NODES); - if prune_count == 0 { - // within limits - return; - } - - prune_candidates.sort_unstable_by_key(|(_pk, last_used)| *last_used); - prune_candidates.truncate(prune_count); - for (public_key, last_used) in prune_candidates.into_iter() { - let node = public_key.fmt_short(); - match last_used.map(|instant| instant.elapsed()) { - Some(last_used) => trace!(%node, ?last_used, "pruning inactive"), - None => trace!(%node, last_used=%"never", "pruning inactive"), - } - - let Some(id) = self.by_node_key.remove(&public_key) else { - debug_assert!(false, "missing by_node_key entry for pk in by_id"); - continue; - }; - - let Some(ep) = self.by_id.remove(&id) else { - debug_assert!(false, "missing by_id entry for id in by_node_key"); - continue; - }; - - for ip_port in ep.direct_addresses() { - self.by_ip_port.remove(&ip_port); - } - - self.by_quic_mapped_addr.remove(ep.all_paths_mapped_addr()); - } + fn conn_type(&self, _node_id: NodeId) -> Option> { + todo!(); } } @@ -623,112 +528,26 @@ impl From<(transports::Addr, OwnedTransmit)> for TransportsSenderMessage { #[cfg(test)] mod tests { - use std::net::Ipv4Addr; - use std::sync::Arc; - use iroh_base::SecretKey; use tracing_test::traced_test; - use super::{node_state::MAX_INACTIVE_DIRECT_ADDRESSES, *}; - use crate::magicsock::DiscoveredDirectAddrs; - use crate::magicsock::transports::Transports; - - impl NodeMap { - async fn add_test_addr(&self, node_addr: NodeAddr) { - self.add_node_addr( - node_addr, - Source::NamedApp { - name: "test".into(), - }, - ) - .await; - } - } - - /// Test persisting and loading of known nodes. - #[tokio::test] - #[traced_test] - async fn restore_from_vec() { - let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - let direct_addrs = DiscoveredDirectAddrs::default(); - let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); - let (disco, _) = DiscoState::new(&secret_key); - let node_map = NodeMap::new( - secret_key.public(), - Default::default(), - transports.create_sender(), - direct_addrs.addrs.watch(), - disco.clone(), - ); - - let mut rng = rand::thread_rng(); - let node_a = SecretKey::generate(&mut rng).public(); - let node_b = SecretKey::generate(&mut rng).public(); - let node_c = SecretKey::generate(&mut rng).public(); - let node_d = SecretKey::generate(&mut rng).public(); - - let relay_x: RelayUrl = "https://my-relay-1.com".parse().unwrap(); - let relay_y: RelayUrl = "https://my-relay-2.com".parse().unwrap(); - - let direct_addresses_a = [addr(4000), addr(4001)]; - let direct_addresses_c = [addr(5000)]; - - let node_addr_a = NodeAddr::new(node_a) - .with_relay_url(relay_x) - .with_direct_addresses(direct_addresses_a); - let node_addr_b = NodeAddr::new(node_b).with_relay_url(relay_y); - let node_addr_c = NodeAddr::new(node_c).with_direct_addresses(direct_addresses_c); - let node_addr_d = NodeAddr::new(node_d); - - node_map.add_test_addr(node_addr_a).await; - node_map.add_test_addr(node_addr_b).await; - node_map.add_test_addr(node_addr_c).await; - node_map.add_test_addr(node_addr_d).await; - - let mut addrs: Vec = node_map - .list_remote_infos(Instant::now()) - .into_iter() - .filter_map(|info| { - let addr: NodeAddr = info.into(); - if addr.is_empty() { - return None; - } - Some(addr) - }) - .collect(); - let loaded_node_map = NodeMap::load_from_vec( - secret_key.public(), - addrs.clone(), - PathSelection::default(), - Default::default(), - transports.create_sender(), - direct_addrs.addrs.watch(), - disco, - ) - .await; - - let mut loaded: Vec = loaded_node_map - .list_remote_infos(Instant::now()) - .into_iter() - .filter_map(|info| { - let addr: NodeAddr = info.into(); - if addr.is_empty() { - return None; - } - Some(addr) - }) - .collect(); - - loaded.sort_unstable(); - addrs.sort_unstable(); - - // compare the node maps via their known nodes - assert_eq!(addrs, loaded); - } - - fn addr(port: u16) -> SocketAddr { - (std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), port).into() - } + // use super::*; + + // impl NodeMap { + // async fn add_test_addr(&self, node_addr: NodeAddr) { + // self.add_node_addr( + // node_addr, + // Source::NamedApp { + // name: "test".into(), + // }, + // ) + // .await; + // } + // } + + // fn addr(port: u16) -> SocketAddr { + // (std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), port).into() + // } #[tokio::test] #[traced_test] diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index f89e0771fd4..3759a47d3c0 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -542,6 +542,8 @@ impl NodeStateActor { .map(|addr| addr.is_ip()) .unwrap_or_default() { + // TODO: We should ping this path to make sure it still works. Because we now + // know things could be broken. trace!("not holepunching: already have a direct connection"); // TODO: If the latency is kind of bad we should retry holepunching at times. return; From a0d5ba11896e5c133f210763f432d48804c31ad0 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 2 Oct 2025 10:10:51 +0200 Subject: [PATCH 056/164] fine tune logging --- iroh/src/endpoint.rs | 40 ++++++----- iroh/src/magicsock.rs | 13 +--- iroh/src/magicsock/mapped_addrs.rs | 7 +- iroh/src/magicsock/node_map.rs | 4 +- iroh/src/magicsock/node_map/node_state.rs | 13 +++- iroh/src/magicsock/transports.rs | 81 ++++++++++++----------- iroh/src/magicsock/transports/ip.rs | 22 ++---- 7 files changed, 96 insertions(+), 84 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index ab8aa060633..7faed37af8b 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2577,18 +2577,26 @@ mod tests { async fn endpoint_two_direct_only() -> Result { // Connect two endpoints on the same network, without a relay server. // let discovery = StaticProvider::new(); - let ep1 = Endpoint::builder() - .alpns(vec![TEST_ALPN.to_vec()]) - .relay_mode(RelayMode::Disabled) - // .discovery(discovery.clone()) - .bind() - .await?; - let ep2 = Endpoint::builder() - .alpns(vec![TEST_ALPN.to_vec()]) - .relay_mode(RelayMode::Disabled) - // .discovery(discovery.clone()) - .bind() - .await?; + let ep1 = { + let span = info_span!("server"); + let _guard = span.enter(); + Endpoint::builder() + .alpns(vec![TEST_ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + // .discovery(discovery.clone()) + .bind() + .await? + }; + let ep2 = { + let span = info_span!("client"); + let _guard = span.enter(); + Endpoint::builder() + .alpns(vec![TEST_ALPN.to_vec()]) + .relay_mode(RelayMode::Disabled) + // .discovery(discovery.clone()) + .bind() + .await? + }; ep1.direct_addresses().initialized().await; ep2.direct_addresses().initialized().await; let ep1_nodeaddr = ep1.node_addr().initialized().await; @@ -2596,9 +2604,9 @@ mod tests { // discovery.set_node_info(ep1_nodeaddr.clone()); // discovery.set_node_info(ep2_nodeaddr.clone()); - #[instrument(name = "connect", skip_all)] + #[instrument(name = "client", skip_all)] async fn connect(ep: Endpoint, dst: NodeAddr) -> Result { - info!(me = ep.node_id().fmt_short(), "connect starting"); + info!(me = ep.node_id().fmt_short(), "client starting"); let conn = ep.connect(dst, TEST_ALPN).await?; let mut send = conn.open_uni().await.e()?; send.write_all(b"hello").await.e()?; @@ -2606,9 +2614,9 @@ mod tests { Ok(conn.closed().await) } - #[instrument(name = "accept", skip_all)] + #[instrument(name = "server", skip_all)] async fn accept(ep: Endpoint, src: NodeId) -> Result { - info!(me = ep.node_id().fmt_short(), "accept starting"); + info!(me = ep.node_id().fmt_short(), "server starting"); let conn = ep.accept().await.e()?.await.e()?; let node_id = conn.remote_node_id()?; assert_eq!(node_id, src); diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index cfaf0d3c6e7..52910eb9269 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -606,11 +606,11 @@ impl MagicSock { // relies on quinn::EndpointConfig::grease_quic_bit being set to `false`, // which we do in Endpoint::bind. if let Some((sender, sealed_box)) = disco::source_and_box(datagram) { - trace!(src = ?source_addr, len = %quinn_meta.stride, "UDP recv: disco packet"); + trace!(src = ?source_addr, len = %quinn_meta.stride, "UDP recv: DISCO packet"); self.handle_disco_message(sender, sealed_box, source_addr); datagram[0] = 0u8; } else { - trace!(src = ?source_addr, len = %quinn_meta.stride, "UDP recv: quic packet"); + trace!(src = ?source_addr, len = %quinn_meta.stride, "UDP recv: QUIC packet"); match source_addr { transports::Addr::Ip(SocketAddr::V4(..)) => { self.metrics @@ -644,17 +644,10 @@ impl MagicSock { panic!("cannot use IP based addressing in the browser"); } #[cfg(not(wasm_browser))] - transports::Addr::Ip(addr) => { - trace!( - src = ?addr, - count = %quic_datagram_count, - len = quinn_meta.len, - "UDP recv QUIC packets", - ); + transports::Addr::Ip(_addr) => { quic_packets_total += quic_datagram_count; } transports::Addr::Relay(src_url, src_node) => { - // Relay let mapped_addr = self .node_map .relay_mapped_addrs diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index ceb92f9348a..2fdda4181c7 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -6,6 +6,7 @@ //! Address ranges we use to keep track of the various "fake" address types we use. use std::{ + fmt, hash::Hash, net::{IpAddr, Ipv6Addr, SocketAddr}, sync::{ @@ -16,6 +17,7 @@ use std::{ use rustc_hash::FxHashMap; use snafu::Snafu; +use tracing::trace; /// The Prefix/L of all Unique Local Addresses. const ADDR_PREFIXL: u8 = 0xfd; @@ -239,7 +241,9 @@ impl Default for AddrMap { } } -impl AddrMap { +impl + AddrMap +{ /// Returns the [`MappedAddr`], generating one if needed. pub(super) fn get(&self, key: &K) -> V { let mut inner = self.inner.lock().expect("poisoned"); @@ -248,6 +252,7 @@ impl AddrMap { None => { let addr = V::generate(); inner.lookup.insert(addr, key.clone()); + trace!(?addr, ?key, "generated new addr"); addr } } diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 677e7d26a02..78cd68be8ca 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -494,9 +494,7 @@ impl TransportsSenderActor { }; let len = transmit.contents.len(); match self.sender.send(&dst, None, &transmit).await { - Ok(()) => { - trace!(?dst, %len, "sent transmit"); - } + Ok(()) => {} Err(err) => { trace!(?dst, %len, "transmit failed to send: {err:#}"); } diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 3759a47d3c0..37efc051cb7 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -325,15 +325,26 @@ impl NodeStateActor { pub(super) fn start(mut self) -> NodeStateHandle { let (tx, rx) = mpsc::channel(16); + let me = self.local_node_id; let node_id = self.node_id; + // Ideally we'd use the endpoint span as parent. We'd have to plug that span into + // here somehow. Instead we have no parent and explicitly set the me attribute. If + // we don't explicitly set a span we get the spans from whatever call happens to + // first create the actor, which is often very confusing as it then keeps those + // spans for all logging of the actor. let task = tokio::spawn( async move { if let Err(err) = self.run(rx).await { error!("actor failed: {err:#}"); } } - .instrument(info_span!("NodeStateActor", node_id = node_id.fmt_short())), + .instrument(info_span!( + parent: None, + "NodeStateActor", + me = me.fmt_short(), + node_id = node_id.fmt_short(), + )), ); NodeStateHandle { sender: tx, diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 7feaff87611..0ffcae034c4 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -395,16 +395,15 @@ pub(crate) struct TransportsSender { } impl TransportsSender { + #[instrument(skip(self, transmit), fields(len = transmit.contents.len()))] pub(crate) async fn send( &self, - destination: &Addr, + dst: &Addr, src: Option, transmit: &Transmit<'_>, ) -> io::Result<()> { - trace!(?destination, "sending"); - let mut any_match = false; - match destination { + match dst { #[cfg(wasm_browser)] Addr::Ip(..) => return Err(io::Error::other("IP is unsupported in browser")), #[cfg(not(wasm_browser))] @@ -414,6 +413,7 @@ impl TransportsSender { any_match = true; match sender.send(*addr, src, transmit).await { Ok(()) => { + trace!("sent"); return Ok(()); } Err(err) => { @@ -429,6 +429,7 @@ impl TransportsSender { any_match = true; match sender.send(url.clone(), *node_id, transmit).await { Ok(()) => { + trace!("sent"); return Ok(()); } Err(err) => { @@ -446,16 +447,15 @@ impl TransportsSender { } } + #[instrument(name = "poll_send", skip(self, cx, transmit), fields(len = transmit.contents.len()))] pub(crate) fn inner_poll_send( mut self: Pin<&mut Self>, cx: &mut std::task::Context, - destination: &Addr, + dst: &Addr, src: Option, transmit: &Transmit<'_>, ) -> Poll> { - trace!(?destination, "sending"); - - match destination { + match dst { #[cfg(wasm_browser)] Addr::Ip(..) => { return Poll::Ready(Err(io::Error::other("IP is unsupported in browser"))); @@ -466,7 +466,13 @@ impl TransportsSender { if sender.is_valid_send_addr(addr) { match Pin::new(sender).poll_send(cx, *addr, src, transmit) { Poll::Pending => {} - Poll::Ready(res) => return Poll::Ready(res), + Poll::Ready(res) => { + match &res { + Ok(()) => trace!("sent"), + Err(err) => trace!("send failed: {err:#}"), + } + return Poll::Ready(res); + } } } } @@ -476,7 +482,13 @@ impl TransportsSender { if sender.is_valid_send_addr(url, node_id) { match sender.poll_send(cx, url.clone(), *node_id, transmit) { Poll::Pending => {} - Poll::Ready(res) => return Poll::Ready(res), + Poll::Ready(res) => { + match &res { + Ok(()) => trace!("sent"), + Err(err) => trace!("send failed: {err:#}"), + } + return Poll::Ready(res); + } } } } @@ -486,15 +498,14 @@ impl TransportsSender { } /// Best effort sending + #[instrument(name = "try_send", skip(self, transmit), fields(len = transmit.contents.len()))] fn inner_try_send( &self, - destination: &Addr, + dst: &Addr, src: Option, transmit: &Transmit<'_>, ) -> io::Result<()> { - trace!(?destination, "sending, best effort"); - - match destination { + match dst { #[cfg(wasm_browser)] Addr::Ip(..) => return Err(io::Error::other("IP is unsupported in browser")), #[cfg(not(wasm_browser))] @@ -502,8 +513,12 @@ impl TransportsSender { for transport in &self.ip { if transport.is_valid_send_addr(addr) { match transport.try_send(*addr, src, transmit) { - Ok(()) => return Ok(()), - Err(_err) => { + Ok(()) => { + trace!("sent"); + return Ok(()); + } + Err(err) => { + trace!("send failed: {err:#}"); continue; } } @@ -514,8 +529,12 @@ impl TransportsSender { for transport in &self.relay { if transport.is_valid_send_addr(url, node_id) { match transport.try_send(url.clone(), *node_id, transmit) { - Ok(()) => return Ok(()), - Err(_err) => { + Ok(()) => { + trace!("sent"); + return Ok(()); + } + Err(err) => { + trace!("send failed: {err:#}"); continue; } } @@ -643,14 +662,7 @@ impl MagicSender { )); } - let addr = MultipathMappedAddr::from(transmit.destination); - trace!( - dst = ?addr, - src = ?transmit.src_ip, - len = %transmit.contents.len(), - "sending", - ); - Ok(addr) + Ok(MultipathMappedAddr::from(transmit.destination)) } } @@ -674,15 +686,11 @@ impl quinn::UdpSender for MagicSender { let transport_addr = match mapped_addr { MultipathMappedAddr::Mixed(mapped_addr) => { - // TODO: Would be nicer to log the NodeId of this, but we only get an actor - // sender for it. - tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(&mapped_addr) else { - error!("unknown NodeIdMappedAddr, dropped transmit"); + error!(dst = ?mapped_addr, "unknown NodeIdMappedAddr, dropped transmit"); return Poll::Ready(Ok(())); }; - tracing::Span::current().record("node_id", node_id.fmt_short()); // Note we drop the src_ip set in the Quinn Transmit. This is only the // Initial packet we are sending, so we do not yet have an src address we @@ -695,7 +703,7 @@ impl quinn::UdpSender for MagicSender { let transmit = OwnedTransmit::from(quinn_transmit); return match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { Ok(()) => { - trace!("sent transmit",); + trace!(dst = ?mapped_addr, node_id = node_id.fmt_short(), "sent transmit"); Poll::Ready(Ok(())) } Err(err) => { @@ -703,7 +711,8 @@ impl quinn::UdpSender for MagicSender { // different transport. Instead we let Quinn handle this as // a lost datagram. // TODO: Revisit this: we might want to do something better. - debug!("NodeStateActor inbox full ({err:#}), dropped transmit"); + debug!(dst = ?mapped_addr, node_id = node_id.fmt_short(), + "NodeStateActor inbox {err:#}, dropped transmit"); Poll::Ready(Ok(())) } }; @@ -724,7 +733,6 @@ impl quinn::UdpSender for MagicSender { } MultipathMappedAddr::Ip(socket_addr) => Addr::Ip(socket_addr), }; - tracing::Span::current().record("dst", tracing::field::debug(&transport_addr)); let transmit = Transmit { ecn: quinn_transmit.ecn, @@ -737,10 +745,7 @@ impl quinn::UdpSender for MagicSender { .sender .inner_poll_send(cx, &transport_addr, quinn_transmit.src_ip, &transmit) { - Poll::Ready(Ok(())) => { - trace!("sent transmit",); - Poll::Ready(Ok(())) - } + Poll::Ready(Ok(())) => Poll::Ready(Ok(())), Poll::Ready(Err(ref err)) => { warn!("dropped transmit: {err:#}"); Poll::Ready(Ok(())) diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index 391f76ba3e4..94a3e010f58 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -140,7 +140,7 @@ impl IpSender { pub(super) async fn send( &self, - destination: SocketAddr, + dst: SocketAddr, src: Option, transmit: &Transmit<'_>, ) -> io::Result<()> { @@ -148,7 +148,7 @@ impl IpSender { let res = self .sender .send(&quinn_udp::Transmit { - destination, + destination: dst, ecn: transmit.ecn, contents: transmit.contents, segment_size: transmit.segment_size, @@ -158,8 +158,7 @@ impl IpSender { match res { Ok(res) => { - trace!(dst = ?destination, "sent on IP"); - match destination { + match dst { SocketAddr::V4(_) => { self.metrics.send_ipv4.inc_by(total_bytes); } @@ -169,25 +168,21 @@ impl IpSender { } Ok(res) } - Err(err) => { - trace!(dst = ?destination, "IP send error: {err:#}"); - Err(err) - } + Err(err) => Err(err), } } pub(super) fn poll_send( mut self: Pin<&mut Self>, cx: &mut std::task::Context, - destination: SocketAddr, + dst: SocketAddr, src: Option, transmit: &Transmit<'_>, ) -> Poll> { - trace!("sending to {}", destination); let total_bytes = transmit.contents.len() as u64; let res = Pin::new(&mut self.sender).poll_send( &quinn_udp::Transmit { - destination, + destination: dst, ecn: transmit.ecn, contents: transmit.contents, segment_size: transmit.segment_size, @@ -195,11 +190,10 @@ impl IpSender { }, cx, ); - trace!("send res: {:?}", res); match res { Poll::Ready(Ok(res)) => { - match destination { + match dst { SocketAddr::V4(_) => { self.metrics.send_ipv4.inc_by(total_bytes); } @@ -220,7 +214,6 @@ impl IpSender { src: Option, transmit: &Transmit<'_>, ) -> io::Result<()> { - trace!("sending to {}", destination); let total_bytes = transmit.contents.len() as u64; let res = self.sender.try_send(&quinn_udp::Transmit { destination, @@ -229,7 +222,6 @@ impl IpSender { segment_size: transmit.segment_size, src_ip: src, }); - trace!("send res: {:?}", res); match res { Ok(res) => { From 65c5d30933cef1ee517149363ab3800d87e4b6cf Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 2 Oct 2025 11:16:42 +0200 Subject: [PATCH 057/164] try_send is removed. do not log poll_send span twice --- iroh/src/magicsock/transports.rs | 106 ++----------------------------- 1 file changed, 4 insertions(+), 102 deletions(-) diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 0ffcae034c4..4f1b2e70485 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -10,7 +10,6 @@ use bytes::Bytes; use iroh_base::{NodeId, RelayUrl}; use n0_watcher::Watcher; use relay::{RelayNetworkChangeSender, RelaySender}; -use tokio::sync::mpsc; use tracing::{debug, error, instrument, trace, warn}; use crate::net_report::Report; @@ -667,10 +666,6 @@ impl MagicSender { } impl quinn::UdpSender for MagicSender { - #[instrument( - skip_all, - fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst, node_id), - )] fn poll_send( self: Pin<&mut Self>, quinn_transmit: &quinn_udp::Transmit, @@ -696,14 +691,15 @@ impl quinn::UdpSender for MagicSender { // Initial packet we are sending, so we do not yet have an src address we // need to respond from. if let Some(src_ip) = quinn_transmit.src_ip { - warn!(?src_ip, "oops, flub didn't think this would happen"); + warn!(dst = ?mapped_addr, ?src_ip, dst_node = node_id.fmt_short(), + "oops, flub didn't think this would happen"); } let sender = self.msock.node_map.node_state_actor(node_id); let transmit = OwnedTransmit::from(quinn_transmit); return match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { Ok(()) => { - trace!(dst = ?mapped_addr, node_id = node_id.fmt_short(), "sent transmit"); + trace!(dst = ?mapped_addr, dst_node = node_id.fmt_short(), "sent transmit"); Poll::Ready(Ok(())) } Err(err) => { @@ -711,7 +707,7 @@ impl quinn::UdpSender for MagicSender { // different transport. Instead we let Quinn handle this as // a lost datagram. // TODO: Revisit this: we might want to do something better. - debug!(dst = ?mapped_addr, node_id = node_id.fmt_short(), + debug!(dst = ?mapped_addr, dst_node = node_id.fmt_short(), "NodeStateActor inbox {err:#}, dropped transmit"); Poll::Ready(Ok(())) } @@ -764,98 +760,4 @@ impl quinn::UdpSender for MagicSender { fn max_transmit_segments(&self) -> usize { self.sender.max_transmit_segments } - - #[instrument( - skip_all, - fields(src = ?quinn_transmit.src_ip, len = quinn_transmit.contents.len(), dst, node_id), - )] - fn try_send(self: Pin<&mut Self>, quinn_transmit: &quinn_udp::Transmit) -> io::Result<()> { - // As opposed to poll_send this method does return normal IO errors. Calls to this - // are one-off fire-and-forget calls with no implications for the EndpointDriver. - let mapped_addr = self.mapped_addr(quinn_transmit)?; - - let transport_addr = match mapped_addr { - MultipathMappedAddr::Mixed(mapped_addr) => { - // TODO: Would be nicer to log the NodeId of this, but we only get an actor - // sender for it. - tracing::Span::current().record("dst", tracing::field::debug(&mapped_addr)); - let Some(node_id) = self.msock.node_map.node_mapped_addrs.lookup(&mapped_addr) - else { - error!("unknown NodeIdMappedAddr, dropped transmit"); - return Err(io::Error::new( - io::ErrorKind::HostUnreachable, - "Unknown NodeIdMappedAddr", - )); - }; - tracing::Span::current().record("node_id", node_id.fmt_short()); - - // Note we drop the src_ip set in the Quinn Transmit. This is only the - // Initial packet we are sending, so we do not yet have an src address we - // need to respond from. - if let Some(src_ip) = quinn_transmit.src_ip { - warn!(?src_ip, "oops, flub didn't think this would happen"); - } - let sender = self.msock.node_map.node_state_actor(node_id); - let transmit = OwnedTransmit::from(quinn_transmit); - return match sender.try_send(NodeStateMessage::SendDatagram(transmit)) { - Ok(()) => { - trace!("sent transmit",); - Ok(()) - } - Err(mpsc::error::TrySendError::Full(_)) => { - debug!("NodeStateActor inbox full, dropped transmit"); - Err(io::Error::new( - io::ErrorKind::WouldBlock, - "NodeStateActor inbox full", - )) - } - Err(mpsc::error::TrySendError::Closed(_)) => { - debug!("NodeStateActor inbox closed, dropped transmit"); - Err(io::Error::new( - io::ErrorKind::NetworkDown, - "NodeStateActor inbox closed", - )) - } - }; - } - MultipathMappedAddr::Relay(relay_mapped_addr) => { - match self - .msock - .node_map - .relay_mapped_addrs - .lookup(&relay_mapped_addr) - { - Some((relay_url, node_id)) => Addr::Relay(relay_url, node_id), - None => { - error!("unknown RelayMappedAddr, dropped transmit"); - return Err(io::Error::new( - io::ErrorKind::HostUnreachable, - "unknown RelayMappedAddr", - )); - } - } - } - MultipathMappedAddr::Ip(socket_addr) => Addr::Ip(socket_addr), - }; - tracing::Span::current().record("dst", tracing::field::debug(&transport_addr)); - - let transmit = Transmit { - ecn: quinn_transmit.ecn, - contents: quinn_transmit.contents, - segment_size: quinn_transmit.segment_size, - }; - match self - .sender - .inner_try_send(&transport_addr, quinn_transmit.src_ip, &transmit) - { - Ok(()) => { - trace!("sent transmit",); - Ok(()) - } - Err(err) => { - warn!("transmit failed to send: {err:#}"); - Err(err) - } - } - } } From e7d9268968558ba8a68b2c3252cc0e12626fc993 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 2 Oct 2025 11:17:26 +0200 Subject: [PATCH 058/164] Do not holepunch on sending the first message While that's currently possible, once we do QUIC-NAT-TRAVERSAL we can only do this once there's a connection anyway. So let's just delay this by another RTT. --- iroh/src/magicsock/node_map/node_state.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 37efc051cb7..9c067cc13eb 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -404,7 +404,9 @@ impl NodeStateActor { .whatever_context("TransportSenerActor stopped")?; } trace!("connecting without selected path: triggering holepunching"); - self.trigger_holepunching().await; + // This message is received *before* a connection is added. So we do + // not yet have a connection to holepunch. Instead we trigger + // holepunching when AddConnection is received. } } NodeStateMessage::AddConnection(handle) => { @@ -422,6 +424,7 @@ impl NodeStateActor { let stream = events.map(move |evt| (stable_id, evt)); self.path_events.push(Box::pin(stream)); self.connections.insert(stable_id, handle); + self.trigger_holepunching().await; } } NodeStateMessage::AddNodeAddr(node_addr, source) => { From 924f41bd0172d4edce4596c1c8d5004b16f668c2 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 2 Oct 2025 16:01:21 +0200 Subject: [PATCH 059/164] postpone solving this, make the test work --- iroh/src/endpoint.rs | 5 ++++- iroh/src/magicsock.rs | 46 ++++++++++++++++++++++--------------------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 7faed37af8b..3bb4f022e5b 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2632,7 +2632,10 @@ mod tests { ep1_accept.await.e()??; let conn_closed = ep2_connect.await.e()??; - assert_eq!(conn_closed, quinn::ConnectionError::CidsExhausted); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(quinn::ApplicationClose { .. }) + )); Ok(()) } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 52910eb9269..eed94727990 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -46,7 +46,7 @@ use rand::Rng; use snafu::{ResultExt, Snafu}; use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot}; use tokio_util::sync::CancellationToken; -use tracing::{Instrument, Level, debug, error, event, info, info_span, instrument, trace, warn}; +use tracing::{Instrument, Level, debug, event, info, info_span, instrument, trace, warn}; use transports::{LocalAddrsWatch, MagicTransport}; use url::Url; @@ -276,33 +276,35 @@ impl MagicSock { self.local_addrs_watch.clone().get() } - /// Registers the connection in the connection map and opens additional paths. + /// Registers the connection in the [`NodeStateActor`]. /// - /// In addition to storing the connection reference this requests the current - /// [`NodeAddr`] for remote node from the [`NodeMap`] and adds all paths to the - /// connection. It also listens and logs path events. + /// The actor is responsible for holepunching and opening additional paths to this + /// connection. + /// + /// [`NodeStateActor`]: crate::magicsock::node_map::node_state::NodeStateActor pub(crate) fn register_connection(&self, remote: NodeId, conn: &quinn::Connection) { + // TODO: Spawning tasks like this is obviously bad. But it is solvable: + // - This is only called from inside Connection::new. + // - Connection::new is called from: + // - impl Future for IncomingFuture + // - impl Future for Connecting + // - Connecting::into_0rtt() + // + // The first two can keep returning Pending until this message is also sent. It'll + // require storing the pinned future but it'll work. + // + // The last one is trickier. But we can make that function async. Or more likely + // we'll end up changing Connecting::into_0rtt() to return a ZrttConnection. Then + // have a ZrttConnection::into_connection() function which can be async and actually + // send this. Before the handshake has completed we don't have anything useful to + // do with this connection inside of the NodeStateActor anyway. debug!(%remote, "register connection"); let weak_handle = conn.weak_handle(); let node_state = self.node_map.node_state_actor(remote); - let mut msg = NodeStateMessage::AddConnection(weak_handle); + let msg = NodeStateMessage::AddConnection(weak_handle); - tokio::task::block_in_place(move || { - loop { - match node_state.try_send(msg) { - Ok(()) => break, - Err(mpsc::error::TrySendError::Closed(msg)) => { - error!(?msg, "NodeStateActor closed"); - break; - } - Err(mpsc::error::TrySendError::Full(ret_msg)) => { - warn!("NodeStateActor inbox full when adding new connection"); - msg = ret_msg; - // TODO: Yikes! - std::thread::yield_now(); - } - } - } + tokio::task::spawn(async move { + node_state.send(msg).await.ok(); }); } From 8b1fd0f75c69b86614ab157157da8408f92d1df7 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 3 Oct 2025 11:51:34 +0200 Subject: [PATCH 060/164] Add the path from a new connection, call select path --- iroh/src/magicsock.rs | 1 - iroh/src/magicsock/mapped_addrs.rs | 8 +++- iroh/src/magicsock/node_map.rs | 17 ++++--- iroh/src/magicsock/node_map/node_state.rs | 57 +++++++++++++++++++++-- 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index eed94727990..b27943164c6 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -298,7 +298,6 @@ impl MagicSock { // have a ZrttConnection::into_connection() function which can be async and actually // send this. Before the handshake has completed we don't have anything useful to // do with this connection inside of the NodeStateActor anyway. - debug!(%remote, "register connection"); let weak_handle = conn.weak_handle(); let node_state = self.node_map.node_state_actor(remote); let msg = NodeStateMessage::AddConnection(weak_handle); diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 2fdda4181c7..e1350656ee5 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -82,16 +82,22 @@ impl From for MultipathMappedAddr { if let Ok(addr) = NodeIdMappedAddr::try_from(addr) { return Self::Mixed(addr); } - #[cfg(not(wasm_browser))] if let Ok(addr) = RelayMappedAddr::try_from(addr) { return Self::Relay(addr); } + #[cfg(not(wasm_browser))] Self::Ip(value) } } } } +impl MultipathMappedAddr { + pub(crate) fn is_ip(&self) -> bool { + matches!(self, Self::Ip(_)) + } +} + /// An address used to address a node on any or all paths. /// /// This is only used for initially connecting to a remote node. We instruct Quinn to send diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 78cd68be8ca..297fd79d19c 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -109,15 +109,6 @@ enum NodeStateKey { /// A [`Source`] helps track how and where an address was learned. Multiple /// sources can be associated with a single address, if we have discovered this /// address through multiple means. -/// -/// Each time a [`NodeAddr`] is added to the node map, usually through -/// [`crate::endpoint::Endpoint::add_node_addr_with_source`], a [`Source`] must be supplied to indicate -/// how the address was obtained. -/// -/// A [`Source`] can describe a variety of places that an address or node was -/// discovered, such as a configured discovery service, the network itself -/// (if another node has reached out to us), or as a user supplied [`NodeAddr`]. - #[derive(Serialize, Deserialize, strum::Display, Debug, Clone, Eq, PartialEq, Hash)] #[strum(serialize_all = "kebab-case")] pub enum Source { @@ -145,6 +136,14 @@ pub enum Source { CallMeMaybe, /// We received a ping on the path. Ping, + /// We established a connection on this address. + /// + /// Currently this means the path was in uses as [`PathId::ZERO`] when the a connection + /// was added to the [`NodeStateActor`]. + /// + /// [`PathId::ZERO`]: quinn_proto::PathId::ZERO + /// [`NodeStateActor`]: self::node_state::NodeStateActor + Connection, } impl NodeMap { diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 9c067cc13eb..7c4c529f46e 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -28,7 +28,9 @@ use crate::{ endpoint::DirectAddr, magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MAX_IDLE_TIMEOUT, MagicsockMetrics, - mapped_addrs::{AddrMap, MappedAddr, NodeIdMappedAddr, RelayMappedAddr}, + mapped_addrs::{ + AddrMap, MappedAddr, MultipathMappedAddr, NodeIdMappedAddr, RelayMappedAddr, + }, transports::{self, OwnedTransmit}, }, util::MaybeFuture, @@ -343,7 +345,7 @@ impl NodeStateActor { parent: None, "NodeStateActor", me = me.fmt_short(), - node_id = node_id.fmt_short(), + remote_node = node_id.fmt_short(), )), ); NodeStateHandle { @@ -423,8 +425,20 @@ impl NodeStateActor { let events = BroadcastStream::new(conn.path_events()); let stream = events.map(move |evt| (stable_id, evt)); self.path_events.push(Box::pin(stream)); - self.connections.insert(stable_id, handle); - self.trigger_holepunching().await; + self.connections.insert(stable_id, handle.clone()); + if let Some(conn) = handle.upgrade() { + if let Some(addr) = self.path_transports_addr(&conn, PathId::ZERO) { + self.paths + .entry(addr) + .or_default() + .sources + .insert(Source::Connection, Instant::now()); + self.select_path(); + } + // TODO: Make sure we are adding the relay path if we're on a direct + // path. + self.trigger_holepunching().await; + } } } NodeStateMessage::AddNodeAddr(node_addr, source) => { @@ -840,8 +854,11 @@ impl NodeStateActor { /// /// Any unused direct paths are closed. fn select_path(&mut self) { + // TODO: Make sure we **add** the best path to any connections that don't have it + // yet. + // Find the lowest RTT across all connections for each open path. The long way, so - // we get to trace-log ALL RTTs. + // we get to trace-log *all* RTTs. let mut all_path_rtts: FxHashMap> = FxHashMap::default(); for (conn_id, conn) in self .connections @@ -936,6 +953,36 @@ impl NodeStateActor { false }); } + + /// Returns the remote [`transports::Addr`] for a path. + fn path_transports_addr( + &self, + conn: &quinn::Connection, + path_id: PathId, + ) -> Option { + conn.path(path_id) + .map(|path| { + path.remote_address().map_or(None, |remote| { + match MultipathMappedAddr::from(remote) { + MultipathMappedAddr::Mixed(_) => { + error!("Mixed addr in use for path"); + None + } + MultipathMappedAddr::Relay(mapped) => { + match self.relay_mapped_addrs.lookup(&mapped) { + Some(parts) => Some(transports::Addr::from(parts)), + None => { + error!("Unknown RelayMappedAddr in path"); + None + } + } + } + MultipathMappedAddr::Ip(addr) => Some(addr.into()), + } + }) + }) + .flatten() + } } /// Messages to send to the [`NodeStateActor`]. From 0522c0e461abe413c7a42f9ec14fb7d4cc010ff6 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 3 Oct 2025 15:17:35 +0200 Subject: [PATCH 061/164] Do not close the last direct path on a connection --- iroh/src/endpoint.rs | 2 +- iroh/src/magicsock/node_map/node_state.rs | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 3bb4f022e5b..96ffbac0923 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2631,7 +2631,7 @@ mod tests { let ep2_connect = tokio::spawn(connect(ep2.clone(), ep1_nodeaddr)); ep1_accept.await.e()??; - let conn_closed = ep2_connect.await.e()??; + let conn_closed = dbg!(ep2_connect.await.e()??); assert!(matches!( conn_closed, ConnectionError::ApplicationClosed(quinn::ApplicationClose { .. }) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 7c4c529f46e..432e47be8a2 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -924,11 +924,32 @@ impl NodeStateActor { } /// Closes any direct paths not selected. + /// + /// Makes sure not to close the last direct path. Relay paths are never closed + /// currently, because we only have one relay path at this time. fn close_redundant_paths(&mut self, selected_path: transports::Addr) { debug_assert_eq!(self.selected_path.as_ref(), Some(&selected_path)); + // We create this to make sure we do not close the last direct path. + let mut paths_per_conn: FxHashMap> = FxHashMap::default(); + for ((conn_id, path_id), addr) in self.path_id_map.iter() { + if !addr.is_ip() { + continue; + } + paths_per_conn.entry(*conn_id).or_default().push(*path_id); + } + self.path_id_map.retain(|(conn_id, path_id), addr| { if !addr.is_ip() || *addr == selected_path { + // This not a direct path or is the selected path. + return true; + } + if paths_per_conn + .get(conn_id) + .map(|paths| paths.len() == 1) + .unwrap_or_default() + { + // This is the only direct path on this connection. return true; } if let Some(conn) = self From 50cc92ffc0211b42b3d99b783fb16ab2d8f6ce89 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 3 Oct 2025 16:28:07 +0200 Subject: [PATCH 062/164] Open some more paths when needed - When a path is selected make sure it is open on all connections. - Do not open a path on a connection when it is already opened. --- iroh/src/magicsock/node_map/node_state.rs | 47 +++++++++++++++-------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 432e47be8a2..5e537d73035 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -530,7 +530,7 @@ impl NodeStateActor { txn = ?pong.tx_id, ); - self.open_quic_path(src); + self.open_path(&src); } NodeStateMessage::CanSend(tx) => { let can_send = !self.paths.is_empty(); @@ -728,30 +728,43 @@ impl NodeStateActor { } } - /// Asks Quinn to open a new path on connections, but only if we are the client. + /// Open the path on all connections. + /// + /// This goes through all the connections for which we are the client, and makes sure + /// the path exists, or opens it. #[instrument(level = "warn", skip(self))] - fn open_quic_path(&mut self, addr: transports::Addr) { - let path_status = match addr { + fn open_path(&mut self, open_addr: &transports::Addr) { + let path_status = match open_addr { transports::Addr::Ip(_) => PathStatus::Available, transports::Addr::Relay(_, _) => PathStatus::Backup, }; - let quic_addr = match &addr { + let quic_addr = match &open_addr { transports::Addr::Ip(socket_addr) => *socket_addr, transports::Addr::Relay(relay_url, node_id) => self .relay_mapped_addrs .get(&(relay_url.clone(), *node_id)) .private_socket_addr(), }; + + // The connections that already have this path. + let mut conns_with_path = BTreeSet::new(); + for ((conn_id, _), addr) in self.path_id_map.iter() { + if addr == open_addr { + conns_with_path.insert(*conn_id); + } + } + for conn in self .connections - .values() - .filter_map(|weak| weak.upgrade()) + .iter() + .filter_map(|(conn_id, handle)| (!conns_with_path.contains(conn_id)).then_some(handle)) + .filter_map(|handle| handle.upgrade()) .filter(|conn| conn.side().is_client()) { match conn.open_path_ensure(quic_addr, path_status).path_id() { Some(path_id) => { self.path_id_map - .insert((conn.stable_id(), path_id), addr.clone()); + .insert((conn.stable_id(), path_id), open_addr.clone()); } None => { warn!("Opening path failed"); @@ -852,11 +865,9 @@ impl NodeStateActor { /// If there are direct paths, this selects the direct path with the lowest RTT. If /// there are only relay paths, the relay path with the lowest RTT is chosen. /// - /// Any unused direct paths are closed. + /// The selected path is added to any connections which do not yet have it. Any unused + /// direct paths are close from all connections. fn select_path(&mut self) { - // TODO: Make sure we **add** the best path to any connections that don't have it - // yet. - // Find the lowest RTT across all connections for each open path. The long way, so // we get to trace-log *all* RTTs. let mut all_path_rtts: FxHashMap> = FxHashMap::default(); @@ -902,7 +913,8 @@ impl NodeStateActor { if prev.as_ref() != Some(&addr) { debug!(?addr, ?prev, "selected new direct path"); } - self.close_redundant_paths(addr); + self.open_path(&addr); + self.close_redundant_paths(&addr); return; } @@ -918,7 +930,8 @@ impl NodeStateActor { if prev.as_ref() != Some(&addr) { debug!(?addr, ?prev, "selected new relay path"); } - self.close_redundant_paths(addr); + self.open_path(&addr); + self.close_redundant_paths(&addr); return; } } @@ -927,8 +940,8 @@ impl NodeStateActor { /// /// Makes sure not to close the last direct path. Relay paths are never closed /// currently, because we only have one relay path at this time. - fn close_redundant_paths(&mut self, selected_path: transports::Addr) { - debug_assert_eq!(self.selected_path.as_ref(), Some(&selected_path)); + fn close_redundant_paths(&mut self, selected_path: &transports::Addr) { + debug_assert_eq!(self.selected_path.as_ref(), Some(selected_path)); // We create this to make sure we do not close the last direct path. let mut paths_per_conn: FxHashMap> = FxHashMap::default(); @@ -940,7 +953,7 @@ impl NodeStateActor { } self.path_id_map.retain(|(conn_id, path_id), addr| { - if !addr.is_ip() || *addr == selected_path { + if !addr.is_ip() || addr == selected_path { // This not a direct path or is the selected path. return true; } From 4a1375f2a010be298879c1b4c32722f0c0350d84 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 3 Oct 2025 16:55:59 +0200 Subject: [PATCH 063/164] Add a first relay test --- iroh/src/endpoint.rs | 77 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 96ffbac0923..4d4d93cee14 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2575,15 +2575,14 @@ mod tests { #[tokio::test] #[traced_test] async fn endpoint_two_direct_only() -> Result { - // Connect two endpoints on the same network, without a relay server. - // let discovery = StaticProvider::new(); + // Connect two endpoints on the same network, without a relay server, without + // discovery. let ep1 = { let span = info_span!("server"); let _guard = span.enter(); Endpoint::builder() .alpns(vec![TEST_ALPN.to_vec()]) .relay_mode(RelayMode::Disabled) - // .discovery(discovery.clone()) .bind() .await? }; @@ -2593,16 +2592,12 @@ mod tests { Endpoint::builder() .alpns(vec![TEST_ALPN.to_vec()]) .relay_mode(RelayMode::Disabled) - // .discovery(discovery.clone()) .bind() .await? }; ep1.direct_addresses().initialized().await; ep2.direct_addresses().initialized().await; let ep1_nodeaddr = ep1.node_addr().initialized().await; - // let ep2_nodeaddr = ep2.node_addr().initialized().await; - // discovery.set_node_info(ep1_nodeaddr.clone()); - // discovery.set_node_info(ep2_nodeaddr.clone()); #[instrument(name = "client", skip_all)] async fn connect(ep: Endpoint, dst: NodeAddr) -> Result { @@ -2640,6 +2635,74 @@ mod tests { Ok(()) } + #[tokio::test] + #[traced_test] + async fn endpoint_two_relay_only() -> Result { + // Connect two endpoints on the same network, via a relay server, without + // discovery. + let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; + let server = { + let span = info_span!("server"); + let _guard = span.enter(); + Endpoint::builder() + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map.clone())) + .bind() + .await? + }; + let client = { + let span = info_span!("client"); + let _guard = span.enter(); + Endpoint::builder() + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await? + }; + let server_node_addr = NodeAddr { + node_id: server.node_id(), + relay_url: Some(server.home_relay().initialized().await), + direct_addresses: Default::default(), + }; + + #[instrument(name = "client", skip_all)] + async fn connect(ep: Endpoint, dst: NodeAddr) -> Result { + info!(me = ep.node_id().fmt_short(), "client starting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + let mut send = conn.open_uni().await.e()?; + send.write_all(b"hello").await.e()?; + send.finish().e()?; + Ok(conn.closed().await) + } + + #[instrument(name = "server", skip_all)] + async fn accept(ep: Endpoint, src: NodeId) -> Result { + info!(me = ep.node_id().fmt_short(), "server starting"); + let conn = ep.accept().await.e()?.await.e()?; + let node_id = conn.remote_node_id()?; + assert_eq!(node_id, src); + let mut recv = conn.accept_uni().await.e()?; + let msg = recv.read_to_end(100).await.e()?; + assert_eq!(msg, b"hello"); + // Dropping the connection closes it just fine. + Ok(()) + } + + let server_task = tokio::spawn(accept(server.clone(), client.node_id())); + let client_task = tokio::spawn(connect(client.clone(), server_node_addr)); + + server_task.await.e()??; + let conn_closed = dbg!(client_task.await.e()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(quinn::ApplicationClose { .. }) + )); + + Ok(()) + } + #[tokio::test] #[traced_test] async fn endpoint_bidi_send_recv() -> Result { From fdeb6f481d91e1553617681f70b3811646181313 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 6 Oct 2025 16:35:30 +0200 Subject: [PATCH 064/164] tidy up NodeMap creation --- iroh/src/magicsock.rs | 4 +--- iroh/src/magicsock/node_map.rs | 13 ++----------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 29dec822a69..62c9162b5c4 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1039,9 +1039,8 @@ impl Handle { let node_map = { let sender = transports.create_sender(); - NodeMap::load_from_vec( + NodeMap::new( secret_key.public(), - Vec::new(), // TODO #[cfg(any(test, feature = "test-utils"))] path_selection, metrics.magicsock.clone(), @@ -1049,7 +1048,6 @@ impl Handle { direct_addrs.addrs.watch(), disco.clone(), ) - .await }; let msock = Arc::new(MagicSock { diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 89f1e0ab2d2..c18236733d6 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -147,10 +147,9 @@ pub enum Source { } impl NodeMap { - /// Create a new [`NodeMap`] from a list of [`NodeAddr`]s. - pub(super) async fn load_from_vec( + /// Creates a new [`NodeMap`]. + pub(super) fn new( local_node_id: NodeId, - nodes: Vec, #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, metrics: Arc, sender: TransportsSender, @@ -164,14 +163,6 @@ impl NodeMap { inner.path_selection = path_selection; } - let me = Self::from_inner(inner, local_node_id); - for addr in nodes { - me.add_node_addr(addr, Source::Saved).await; - } - me - } - - fn from_inner(inner: NodeMapInner, local_node_id: NodeId) -> Self { Self { local_node_id, inner: Mutex::new(inner), From d4fc291f6c5acb9df53d08a7441c8db996bf23dc Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 6 Oct 2025 16:58:48 +0200 Subject: [PATCH 065/164] Simplify delayed discovery start This no longer tries to figure out if discovery is needed based on how recently we may have communicated with a remote endpoint. Maybe that's too simple and we'll have to add back some extra check for discovery. But most active connections would have had a response by that time and not trigger the discovery. --- iroh/src/discovery.rs | 17 +++-------------- iroh/src/endpoint.rs | 25 ++++++++++--------------- 2 files changed, 13 insertions(+), 29 deletions(-) diff --git a/iroh/src/discovery.rs b/iroh/src/discovery.rs index 58bc3f9df37..50d6a228744 100644 --- a/iroh/src/discovery.rs +++ b/iroh/src/discovery.rs @@ -524,30 +524,19 @@ impl DiscoveryTask { /// If `delay` is set, the [`DiscoveryTask`] will first wait for `delay` and then check again /// if we recently received messages from remote endpoint. If true, the task will abort. /// Otherwise, or if no `delay` is set, the discovery will be started. - pub(super) fn maybe_start_after_delay( + pub(super) fn start_after_delay( ep: &Endpoint, node_id: NodeId, - delay: Option, + delay: Duration, ) -> Result, DiscoveryError> { // If discovery is not needed, don't even spawn a task. - if !ep.needs_discovery(node_id, MAX_AGE) { - return Ok(None); - } ensure!(!ep.discovery().is_empty(), NoServiceConfiguredSnafu); let (on_first_tx, on_first_rx) = oneshot::channel(); let ep = ep.clone(); let me = ep.node_id(); let task = task::spawn( async move { - // If delay is set, wait and recheck if discovery is needed. If not, early-exit. - if let Some(delay) = delay { - time::sleep(delay).await; - if !ep.needs_discovery(node_id, MAX_AGE) { - debug!("no discovery needed, abort"); - on_first_tx.send(Ok(())).ok(); - return; - } - } + time::sleep(delay).await; Self::run(ep, node_id, on_first_tx).await } .instrument( diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 18e25f33f36..efa8a8f9236 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -73,10 +73,10 @@ pub use super::magicsock::{ /// The delay to fall back to discovery when direct addresses fail. /// -/// When a connection is attempted with a [`NodeAddr`] containing direct addresses the -/// [`Endpoint`] assumes one of those addresses probably works. If after this delay there -/// is still no connection the configured [`crate::discovery::Discovery`] will be used however. -const DISCOVERY_WAIT_PERIOD: Duration = Duration::from_millis(500); +/// When a connection is attempted and we have some addressing info for the remote, we +/// assume that one of these probably works. If after this delay there is still no +/// connection, discovery will be started. +const DISCOVERY_WAIT_PERIOD: Duration = Duration::from_millis(150); /// Defines the mode of path selection for all traffic flowing through /// the endpoint. @@ -1331,17 +1331,12 @@ impl Endpoint { }; match addr { Some(maddr) => { - // We have some way of dialing this node, but that doesn't actually mean - // we can actually connect to any of these addresses. - // Therefore, we will invoke the discovery service if we haven't received from the - // endpoint on any of the existing paths recently. - // If the user provided addresses in this connect call, we will add a delay - // followed by a recheck before starting the discovery, to give the magicsocket a - // chance to test the newly provided addresses. - let delay = (!node_addr.is_empty()).then_some(DISCOVERY_WAIT_PERIOD); - let discovery = DiscoveryTask::maybe_start_after_delay(self, node_id, delay) - .ok() - .flatten(); + // We have some way of dialing this node, but that doesn't mean we can + // connect to any of these addresses. Start discovery after a small delay. + let discovery = + DiscoveryTask::start_after_delay(self, node_id, DISCOVERY_WAIT_PERIOD) + .ok() + .flatten(); Ok((maddr, discovery)) } From 8ba3fd98ee8ff321ae921cbb9c932f3035bdc466 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 6 Oct 2025 17:15:37 +0200 Subject: [PATCH 066/164] Remove try_send impls, no longer used The need for these was removed after the latest upstream quinn changes. --- iroh/src/magicsock/transports.rs | 51 -------------------------- iroh/src/magicsock/transports/ip.rs | 31 ---------------- iroh/src/magicsock/transports/relay.rs | 43 ---------------------- 3 files changed, 125 deletions(-) diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 3a3b5a23915..6ec2750e641 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -489,57 +489,6 @@ impl TransportsSender { } Poll::Pending } - - /// Best effort sending - #[instrument(name = "try_send", skip(self, transmit), fields(len = transmit.contents.len()))] - fn inner_try_send( - &self, - dst: &Addr, - src: Option, - transmit: &Transmit<'_>, - ) -> io::Result<()> { - match dst { - #[cfg(wasm_browser)] - Addr::Ip(..) => return Err(io::Error::other("IP is unsupported in browser")), - #[cfg(not(wasm_browser))] - Addr::Ip(addr) => { - for transport in &self.ip { - if transport.is_valid_send_addr(addr) { - match transport.try_send(*addr, src, transmit) { - Ok(()) => { - trace!("sent"); - return Ok(()); - } - Err(err) => { - trace!("send failed: {err:#}"); - continue; - } - } - } - } - } - Addr::Relay(url, node_id) => { - for transport in &self.relay { - if transport.is_valid_send_addr(url, node_id) { - match transport.try_send(url.clone(), *node_id, transmit) { - Ok(()) => { - trace!("sent"); - return Ok(()); - } - Err(err) => { - trace!("send failed: {err:#}"); - continue; - } - } - } - } - } - } - Err(io::Error::new( - io::ErrorKind::WouldBlock, - "no transport ready", - )) - } } /// A [`Transports`] that works with [`MultipathMappedAddr`]s and their IPv6 representation. diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index 94a3e010f58..cebda5339d2 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -207,35 +207,4 @@ impl IpSender { Poll::Pending => Poll::Pending, } } - - pub(super) fn try_send( - &self, - destination: SocketAddr, - src: Option, - transmit: &Transmit<'_>, - ) -> io::Result<()> { - let total_bytes = transmit.contents.len() as u64; - let res = self.sender.try_send(&quinn_udp::Transmit { - destination, - ecn: transmit.ecn, - contents: transmit.contents, - segment_size: transmit.segment_size, - src_ip: src, - }); - - match res { - Ok(res) => { - match destination { - SocketAddr::V4(_) => { - self.metrics.send_ipv4.inc_by(total_bytes); - } - SocketAddr::V6(_) => { - self.metrics.send_ipv6.inc_by(total_bytes); - } - } - Ok(res) - } - Err(err) => Err(err), - } - } } diff --git a/iroh/src/magicsock/transports/relay.rs b/iroh/src/magicsock/transports/relay.rs index 2613b9233fc..a1902221d50 100644 --- a/iroh/src/magicsock/transports/relay.rs +++ b/iroh/src/magicsock/transports/relay.rs @@ -324,49 +324,6 @@ impl RelaySender { } } } - - pub(super) fn try_send( - &self, - dest_url: RelayUrl, - dest_node: NodeId, - transmit: &Transmit<'_>, - ) -> io::Result<()> { - let contents = datagrams_from_transmit(transmit); - - let item = RelaySendItem { - remote_node: dest_node, - url: dest_url.clone(), - datagrams: contents, - }; - - let dest_node = item.remote_node; - let dest_url = item.url.clone(); - - let Some(sender) = self.sender.get_ref() else { - return Err(io::Error::other("channel closed")); - }; - - match sender.try_send(item) { - Ok(_) => { - trace!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message queued"); - Ok(()) - } - Err(mpsc::error::TrySendError::Closed(_)) => { - error!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message dropped, channel to actor is closed"); - Err(io::Error::new( - io::ErrorKind::ConnectionReset, - "channel to actor is closed", - )) - } - Err(mpsc::error::TrySendError::Full(_)) => { - warn!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message dropped, channel to actor is full"); - Err(io::Error::new(io::ErrorKind::WouldBlock, "channel full")) - } - } - } } /// Translate a UDP transmit to the `Datagrams` type for sending over the relay. From 23ef6fee7181758cb46123dd46138d7ca4366cd8 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 6 Oct 2025 17:18:01 +0200 Subject: [PATCH 067/164] remove dead code --- iroh/src/endpoint.rs | 23 ----------------------- iroh/src/magicsock.rs | 8 ++------ iroh/src/magicsock/mapped_addrs.rs | 6 ------ iroh/src/magicsock/node_map.rs | 5 ----- iroh/src/magicsock/node_map/node_state.rs | 8 -------- 5 files changed, 2 insertions(+), 48 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index efa8a8f9236..9d298c14290 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1280,29 +1280,6 @@ impl Endpoint { // # Remaining private methods - /// Checks if the given `NodeId` needs discovery. - pub(crate) fn needs_discovery(&self, node_id: NodeId, max_age: Duration) -> bool { - match self.msock.remote_info(node_id) { - // No info means no path to node -> start discovery. - None => true, - Some(info) => { - match ( - info.last_received(), - info.relay_url.as_ref().and_then(|r| r.last_alive), - ) { - // No path to node -> start discovery. - (None, None) => true, - // If we haven't received on direct addresses or the relay for MAX_AGE, - // start discovery. - (Some(elapsed), Some(elapsed_relay)) => { - elapsed > max_age && elapsed_relay > max_age - } - (Some(elapsed), _) | (_, Some(elapsed)) => elapsed > max_age, - } - } - } - } - /// Return the quic mapped address for this `node_id` and possibly start discovery /// services if discovery is enabled on this magic endpoint. /// diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 62c9162b5c4..f742aebcfda 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -39,6 +39,8 @@ use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; use node_map::NodeStateMessage; +#[cfg(test)] +use node_map::RemoteInfo; use quinn::ServerConfig; use rand::Rng; use snafu::{ResultExt, Snafu}; @@ -62,7 +64,6 @@ use crate::{ NodeData, UserData, }, key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, - magicsock::node_map::RemoteInfo, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, }; @@ -336,11 +337,6 @@ impl MagicSock { self.node_map.list_remote_infos(Instant::now()) } - /// Return the [`RemoteInfo`] for a single node in the node map. - pub(crate) fn remote_info(&self, node_id: NodeId) -> Option { - self.node_map.remote_info(node_id) - } - /// Returns a [`Watcher`] for this socket's direct addresses. /// /// The [`MagicSock`] continuously monitors the direct addresses, the network addresses diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index e1350656ee5..497f5072e99 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -92,12 +92,6 @@ impl From for MultipathMappedAddr { } } -impl MultipathMappedAddr { - pub(crate) fn is_ip(&self) -> bool { - matches!(self, Self::Ip(_)) - } -} - /// An address used to address a node on any or all paths. /// /// This is only used for initially connecting to a remote node. We instruct Quinn to send diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index c18236733d6..d6d4d2b4099 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -221,11 +221,6 @@ impl NodeMap { self.inner.lock().expect("poisoned").conn_type(node_id) } - /// Get the [`RemoteInfo`]s for the node identified by [`NodeId`]. - pub(super) fn remote_info(&self, node_id: NodeId) -> Option { - self.inner.lock().expect("poisoned").remote_info(node_id) - } - /// Returns the sender for the [`NodeStateActor`]. /// /// If needed a new actor is started on demand. diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index a9ac5883972..d94dd0fca1d 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1290,14 +1290,6 @@ impl RemoteInfo { .filter_map(|addr| addr.last_control.map(|x| x.0).min(addr.last_payload)) .min() } - - /// Whether there is a possible known network path to the remote node. - /// - /// Note that this does not provide any guarantees of whether any network path is - /// usable. - pub(crate) fn has_send_address(&self) -> bool { - self.relay_url.is_some() || !self.addrs.is_empty() - } } /// The type of connection we have to the endpoint. From 3f58e95513560bf5969749bf9ed69200bda99b25 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 11:48:17 +0200 Subject: [PATCH 068/164] Remove MagicStack and mesh_stacks MagicStack is just an Endpoint. mesh_stacks is a StaticProvider with a task watching the node addresses. This simplifies things a lot. And means I can delete some more unused code that this stuff was using. --- iroh/src/magicsock.rs | 264 +++++++++++++----------------------------- 1 file changed, 83 insertions(+), 181 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 61e0311da40..c8574f7b7e3 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1936,7 +1936,7 @@ mod tests { use data_encoding::HEXLOWER; use iroh_base::{NodeAddr, NodeId, PublicKey}; - use n0_future::{StreamExt, time}; + use n0_future::{MergeBounded, StreamExt, task::JoinHandle, time}; use n0_snafu::{Result, ResultExt}; use n0_watcher::Watcher; use quinn::ServerConfig; @@ -1948,6 +1948,7 @@ mod tests { use crate::{ Endpoint, RelayMap, RelayMode, SecretKey, + discovery::static_provider::StaticProvider, dns::DnsResolver, endpoint::PathSelection, magicsock::{Handle, MagicSock, node_map}, @@ -2002,126 +2003,10 @@ mod tests { } } - /// Magicsock plus wrappers for sending packets - #[derive(Clone)] - struct MagicStack { - secret_key: SecretKey, - endpoint: Endpoint, - } - - impl MagicStack { - async fn new(rng: &mut R, relay_mode: RelayMode) -> Self { - let secret_key = SecretKey::generate(rng); - - let mut transport_config = quinn::TransportConfig::default(); - transport_config.max_idle_timeout(Some(Duration::from_secs(10).try_into().unwrap())); - - let endpoint = Endpoint::builder() - .secret_key(secret_key.clone()) - .transport_config(transport_config) - .relay_mode(relay_mode) - .alpns(vec![ALPN.to_vec()]) - .bind() - .await - .unwrap(); - - Self { - secret_key, - endpoint, - } - } - - fn tracked_endpoints(&self) -> Vec { - self.endpoint - .magic_sock() - .list_remote_infos() - .into_iter() - .map(|ep| ep.node_id) - .collect() - } - - fn public(&self) -> PublicKey { - self.secret_key.public() - } - } - - /// Monitors endpoint changes and plumbs things together. - /// - /// This is a way of connecting endpoints without a relay server. Whenever the local - /// endpoints of a magic endpoint change this address is added to the other magic - /// sockets. This function will await until the endpoints are connected the first time - /// before returning. - /// - /// When the returned drop guard is dropped, the tasks doing this updating are stopped. - #[instrument(skip_all)] - async fn mesh_stacks(stacks: Vec) -> Result> { - /// Registers endpoint addresses of a node to all other nodes. - async fn update_direct_addrs( - stacks: &[MagicStack], - my_idx: usize, - new_addrs: BTreeSet, - ) { - let me = &stacks[my_idx]; - for (i, m) in stacks.iter().enumerate() { - if i == my_idx { - continue; - } - - let addr = NodeAddr { - node_id: me.public(), - relay_url: None, - direct_addresses: new_addrs.clone(), - }; - m.endpoint.magic_sock().add_test_addr(addr).await; - } - } - - // For each node, start a task which monitors its local endpoints and registers them - // with the other nodes as local endpoints become known. - let mut tasks = JoinSet::new(); - for (my_idx, m) in stacks.iter().enumerate() { - let m = m.clone(); - let stacks = stacks.clone(); - tasks.spawn(async move { - let me = m.endpoint.node_id().fmt_short(); - let mut stream = m.endpoint.watch_node_addr().stream().filter_map(|i| i); - while let Some(addr) = stream.next().await { - info!(%me, "conn{} endpoints update: {:?}", my_idx + 1, addr.direct_addresses); - update_direct_addrs(&stacks, my_idx, addr.direct_addresses); - } - }); - } - - // Wait for all nodes to be registered with each other. - time::timeout(Duration::from_secs(10), async move { - let all_node_ids: Vec<_> = stacks.iter().map(|ms| ms.endpoint.node_id()).collect(); - loop { - let mut ready = Vec::with_capacity(stacks.len()); - for ms in stacks.iter() { - let endpoints = ms.tracked_endpoints(); - let my_node_id = ms.endpoint.node_id(); - let all_nodes_meshed = all_node_ids - .iter() - .filter(|node_id| **node_id != my_node_id) - .all(|node_id| endpoints.contains(node_id)); - ready.push(all_nodes_meshed); - } - if ready.iter().all(|meshed| *meshed) { - break; - } - time::sleep(Duration::from_millis(200)).await; - } - }) - .await - .context("timeout")?; - info!("all nodes meshed"); - Ok(tasks) - } - - #[instrument(skip_all, fields(me = %ep.endpoint.node_id().fmt_short()))] - async fn echo_receiver(ep: MagicStack, loss: ExpectedLoss) -> Result { + #[instrument(skip_all, fields(me = %ep.node_id().fmt_short()))] + async fn echo_receiver(ep: Endpoint, loss: ExpectedLoss) -> Result { info!("accepting conn"); - let conn = ep.endpoint.accept().await.expect("no conn"); + let conn = ep.accept().await.expect("no conn"); info!("connecting"); let conn = conn.await.context("connecting")?; @@ -2158,21 +2043,16 @@ mod tests { info!("close"); conn.close(0u32.into(), b"done"); info!("wait idle"); - ep.endpoint.endpoint().wait_idle().await; + ep.endpoint().wait_idle().await; Ok(()) } - #[instrument(skip_all, fields(me = %ep.endpoint.node_id().fmt_short()))] - async fn echo_sender( - ep: MagicStack, - dest_id: PublicKey, - msg: &[u8], - loss: ExpectedLoss, - ) -> Result { + #[instrument(skip_all, fields(me = %ep.node_id().fmt_short()))] + async fn echo_sender(ep: Endpoint, dest_id: NodeId, msg: &[u8], loss: ExpectedLoss) -> Result { info!("connecting to {}", dest_id.fmt_short()); let dest = NodeAddr::new(dest_id); - let conn = ep.endpoint.connect(dest, ALPN).await?; + let conn = ep.connect(dest, ALPN).await?; info!("opening bi"); let (mut send_bi, mut recv_bi) = conn.open_bi().await.context("open bi")?; @@ -2211,7 +2091,7 @@ mod tests { info!("close"); conn.close(0u32.into(), b"done"); info!("wait idle"); - ep.endpoint.endpoint().wait_idle().await; + ep.endpoint().wait_idle().await; Ok(()) } @@ -2223,13 +2103,13 @@ mod tests { /// Runs a roundtrip between the [`echo_sender`] and [`echo_receiver`]. async fn run_roundtrip( - sender: MagicStack, - receiver: MagicStack, + sender: Endpoint, + receiver: Endpoint, payload: &[u8], loss: ExpectedLoss, ) { - let send_node_id = sender.endpoint.node_id(); - let recv_node_id = receiver.endpoint.node_id(); + let send_node_id = sender.node_id(); + let recv_node_id = receiver.node_id(); info!("\nroundtrip: {send_node_id:#} -> {recv_node_id:#}"); let receiver_task = tokio::spawn(echo_receiver(receiver, loss)); @@ -2261,14 +2141,50 @@ mod tests { } } + /// Returns a pair of endpoints with a shared [`StaticDiscovery`]. + /// + /// The endpoints do not use a relay server but can connect to each other via local + /// addresses. Dialing by [`NodeId`] is possible, and the addresses get updated even if + /// the endpoints rebind. + async fn endpoint_pair() -> (AbortOnDropHandle<()>, Endpoint, Endpoint) { + let discovery = StaticProvider::new(); + let ep1 = Endpoint::builder() + .relay_mode(RelayMode::Disabled) + .alpns(vec![ALPN.to_vec()]) + .discovery(discovery.clone()) + .bind() + .await + .unwrap(); + let ep2 = Endpoint::builder() + .relay_mode(RelayMode::Disabled) + .alpns(vec![ALPN.to_vec()]) + .discovery(discovery.clone()) + .bind() + .await + .unwrap(); + discovery.add_node_info(ep1.local_ready().await); + discovery.add_node_info(ep2.local_ready().await); + + let ep1_addr_stream = ep1.watch_node_addr().stream(); + let ep2_addr_stream = ep2.watch_node_addr().stream(); + let mut addr_stream = MergeBounded::from_iter([ep1_addr_stream, ep2_addr_stream]); + let task = tokio::spawn(async move { + loop { + while let Some(item) = addr_stream.next().await { + if let Some(addr) = item { + discovery.add_node_info(addr); + } + } + } + }); + + (AbortOnDropHandle::new(task), ep1, ep2) + } + #[tokio::test(flavor = "multi_thread")] #[traced_test] async fn test_two_devices_roundtrip_quinn_magic() -> Result { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let m1 = MagicStack::new(&mut rng, RelayMode::Disabled).await; - let m2 = MagicStack::new(&mut rng, RelayMode::Disabled).await; - - let _guard = mesh_stacks(vec![m1.clone(), m2.clone()]).await?; + let (_guard, m1, m2) = endpoint_pair().await; for i in 0..5 { info!("\n-- round {i}"); @@ -2289,6 +2205,7 @@ mod tests { info!("\n-- larger data"); let mut data = vec![0u8; 10 * 1024]; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); rng.fill_bytes(&mut data); run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::AlmostNone).await; run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::AlmostNone).await; @@ -2300,18 +2217,14 @@ mod tests { #[tokio::test] #[traced_test] async fn test_regression_network_change_rebind_wakes_connection_driver() -> n0_snafu::Result { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let m1 = MagicStack::new(&mut rng, RelayMode::Disabled).await; - let m2 = MagicStack::new(&mut rng, RelayMode::Disabled).await; + let (_guard, m1, m2) = endpoint_pair().await; println!("Net change"); - m1.endpoint.magic_sock().force_network_change(true).await; + m1.magic_sock().force_network_change(true).await; tokio::time::sleep(Duration::from_secs(1)).await; // wait for socket rebinding - let _guard = mesh_stacks(vec![m1.clone(), m2.clone()]).await?; - let _handle = AbortOnDropHandle::new(tokio::spawn({ - let endpoint = m2.endpoint.clone(); + let endpoint = m2.clone(); async move { while let Some(incoming) = endpoint.accept().await { println!("Incoming first conn!"); @@ -2325,8 +2238,7 @@ mod tests { println!("first conn!"); let conn = m1 - .endpoint - .connect(m2.endpoint.watch_node_addr().initialized().await, ALPN) + .connect(m2.watch_node_addr().initialized().await, ALPN) .await?; println!("Closing first conn"); conn.close(0u32.into(), b"bye lolz"); @@ -2351,10 +2263,7 @@ mod tests { /// with (simulated) network changes. async fn test_two_devices_roundtrip_network_change_impl() -> Result { let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let m1 = MagicStack::new(&mut rng, RelayMode::Disabled).await; - let m2 = MagicStack::new(&mut rng, RelayMode::Disabled).await; - - let _guard = mesh_stacks(vec![m1.clone(), m2.clone()]).await?; + let (_guard, m1, m2) = endpoint_pair().await; let offset = |rng: &mut rand_chacha::ChaCha8Rng| { let delay = rng.random_range(10..=500); @@ -2369,7 +2278,7 @@ mod tests { let task = tokio::spawn(async move { loop { println!("[m1] network change"); - m1.endpoint.magic_sock().force_network_change(true).await; + m1.magic_sock().force_network_change(true).await; time::sleep(offset(&mut rng)).await; } }); @@ -2397,7 +2306,7 @@ mod tests { let task = tokio::spawn(async move { loop { println!("[m2] network change"); - m2.endpoint.magic_sock().force_network_change(true).await; + m2.magic_sock().force_network_change(true).await; time::sleep(offset(&mut rng)).await; } }); @@ -2425,9 +2334,9 @@ mod tests { let mut rng = rng.clone(); let task = tokio::spawn(async move { println!("-- [m1] network change"); - m1.endpoint.magic_sock().force_network_change(true).await; + m1.magic_sock().force_network_change(true).await; println!("-- [m2] network change"); - m2.endpoint.magic_sock().force_network_change(true).await; + m2.magic_sock().force_network_change(true).await; time::sleep(offset(&mut rng)).await; }); AbortOnDropHandle::new(task) @@ -2452,20 +2361,16 @@ mod tests { #[tokio::test(flavor = "multi_thread")] #[traced_test] async fn test_two_devices_setup_teardown() -> Result { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); for i in 0..10 { println!("-- round {i}"); println!("setting up magic stack"); - let m1 = MagicStack::new(&mut rng, RelayMode::Disabled).await; - let m2 = MagicStack::new(&mut rng, RelayMode::Disabled).await; - - let _guard = mesh_stacks(vec![m1.clone(), m2.clone()]).await?; + let (_guard, m1, m2) = endpoint_pair().await; println!("closing endpoints"); - let msock1 = m1.endpoint.magic_sock(); - let msock2 = m2.endpoint.magic_sock(); - m1.endpoint.close().await; - m2.endpoint.close().await; + let msock1 = m1.magic_sock(); + let msock2 = m2.magic_sock(); + m1.close().await; + m2.close().await; assert!(msock1.msock.is_closed()); assert!(msock2.msock.is_closed()); @@ -2794,9 +2699,13 @@ mod tests { #[tokio::test] async fn test_add_node_addr() -> Result { let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let stack = MagicStack::new(&mut rng, RelayMode::Default).await; + let ep = Endpoint::builder() + .relay_mode(RelayMode::Default) + .bind() + .await + .unwrap(); - assert_eq!(stack.endpoint.magic_sock().node_map.node_count(), 0); + assert_eq!(ep.magic_sock().node_map.node_count(), 0); // Empty let empty_addr = NodeAddr { @@ -2804,8 +2713,7 @@ mod tests { relay_url: None, direct_addresses: Default::default(), }; - let err = stack - .endpoint + let err = ep .magic_sock() .add_node_addr(empty_addr, node_map::Source::App) .await @@ -2822,12 +2730,10 @@ mod tests { relay_url: Some("http://my-relay.com".parse().unwrap()), direct_addresses: Default::default(), }; - stack - .endpoint - .magic_sock() + ep.magic_sock() .add_node_addr(addr, node_map::Source::App) .await?; - assert_eq!(stack.endpoint.magic_sock().node_map.node_count(), 1); + assert_eq!(ep.magic_sock().node_map.node_count(), 1); // addrs only let addr = NodeAddr { @@ -2835,12 +2741,10 @@ mod tests { relay_url: None, direct_addresses: ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(), }; - stack - .endpoint - .magic_sock() + ep.magic_sock() .add_node_addr(addr, node_map::Source::App) .await?; - assert_eq!(stack.endpoint.magic_sock().node_map.node_count(), 2); + assert_eq!(ep.magic_sock().node_map.node_count(), 2); // both let addr = NodeAddr { @@ -2848,12 +2752,10 @@ mod tests { relay_url: Some("http://my-relay.com".parse().unwrap()), direct_addresses: ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(), }; - stack - .endpoint - .magic_sock() + ep.magic_sock() .add_node_addr(addr, node_map::Source::App) .await?; - assert_eq!(stack.endpoint.magic_sock().node_map.node_count(), 3); + assert_eq!(ep.magic_sock().node_map.node_count(), 3); Ok(()) } From 7ac4dec51b09d4fba3411389ec8f380d2534bbc1 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 11:51:15 +0200 Subject: [PATCH 069/164] dead code --- iroh/src/magicsock.rs | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index c8574f7b7e3..0ca6a99fa69 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1932,16 +1932,15 @@ impl Display for DirectAddrType { #[cfg(test)] mod tests { - use std::{collections::BTreeSet, net::SocketAddr, sync::Arc, time::Duration}; + use std::{sync::Arc, time::Duration}; use data_encoding::HEXLOWER; - use iroh_base::{NodeAddr, NodeId, PublicKey}; - use n0_future::{MergeBounded, StreamExt, task::JoinHandle, time}; + use iroh_base::{NodeAddr, NodeId}; + use n0_future::{MergeBounded, StreamExt, time}; use n0_snafu::{Result, ResultExt}; use n0_watcher::Watcher; use quinn::ServerConfig; use rand::{CryptoRng, Rng, RngCore, SeedableRng}; - use tokio::task::JoinSet; use tokio_util::task::AbortOnDropHandle; use tracing::{Instrument, error, info, info_span, instrument}; use tracing_test::traced_test; @@ -1990,19 +1989,6 @@ mod tests { server_config } - impl MagicSock { - pub async fn add_test_addr(&self, node_addr: NodeAddr) { - self.add_node_addr( - node_addr, - Source::NamedApp { - name: "test".into(), - }, - ) - .await - .ok(); - } - } - #[instrument(skip_all, fields(me = %ep.node_id().fmt_short()))] async fn echo_receiver(ep: Endpoint, loss: ExpectedLoss) -> Result { info!("accepting conn"); From f38e5c2a9ccf47410b01b1adf34f8656bf14ef3a Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 11:54:08 +0200 Subject: [PATCH 070/164] remove test and delete then unused function I'm not sure this test does anything useful anymore. Maybe there's a new test that needs to exist now but I don't yet know. --- iroh/src/magicsock.rs | 66 +--------------------------------- iroh/src/magicsock/node_map.rs | 5 --- 2 files changed, 1 insertion(+), 70 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 0ca6a99fa69..5f9a97bc0c1 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1950,7 +1950,7 @@ mod tests { discovery::static_provider::StaticProvider, dns::DnsResolver, endpoint::PathSelection, - magicsock::{Handle, MagicSock, node_map}, + magicsock::{Handle, MagicSock}, tls::{self, DEFAULT_MAX_TLS_TICKETS}, }; @@ -2681,68 +2681,4 @@ mod tests { // TODO: could remove the addresses again, send, add it back and see it recover. // But we don't have that much private access to the NodeMap. This will do for now. } - - #[tokio::test] - async fn test_add_node_addr() -> Result { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let ep = Endpoint::builder() - .relay_mode(RelayMode::Default) - .bind() - .await - .unwrap(); - - assert_eq!(ep.magic_sock().node_map.node_count(), 0); - - // Empty - let empty_addr = NodeAddr { - node_id: SecretKey::generate(&mut rng).public(), - relay_url: None, - direct_addresses: Default::default(), - }; - let err = ep - .magic_sock() - .add_node_addr(empty_addr, node_map::Source::App) - .await - .unwrap_err(); - assert!( - err.to_string() - .to_lowercase() - .contains("empty addressing info") - ); - - // relay url only - let addr = NodeAddr { - node_id: SecretKey::generate(&mut rng).public(), - relay_url: Some("http://my-relay.com".parse().unwrap()), - direct_addresses: Default::default(), - }; - ep.magic_sock() - .add_node_addr(addr, node_map::Source::App) - .await?; - assert_eq!(ep.magic_sock().node_map.node_count(), 1); - - // addrs only - let addr = NodeAddr { - node_id: SecretKey::generate(&mut rng).public(), - relay_url: None, - direct_addresses: ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(), - }; - ep.magic_sock() - .add_node_addr(addr, node_map::Source::App) - .await?; - assert_eq!(ep.magic_sock().node_map.node_count(), 2); - - // both - let addr = NodeAddr { - node_id: SecretKey::generate(&mut rng).public(), - relay_url: Some("http://my-relay.com".parse().unwrap()), - direct_addresses: ["127.0.0.1:1234".parse().unwrap()].into_iter().collect(), - }; - ep.magic_sock() - .add_node_addr(addr, node_map::Source::App) - .await?; - assert_eq!(ep.magic_sock().node_map.node_count(), 3); - - Ok(()) - } } diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index d6d4d2b4099..c9867e0bd19 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -188,11 +188,6 @@ impl NodeMap { .ok(); } - /// Number of nodes currently listed. - pub(super) fn node_count(&self) -> usize { - self.inner.lock().expect("poisoned").node_count() - } - pub(super) fn node_mapped_addr(&self, node_id: NodeId) -> NodeIdMappedAddr { self.node_mapped_addrs.get(&node_id) } From 1eeb4bb6669589de428105be2bb973eaef9c9151 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 12:07:24 +0200 Subject: [PATCH 071/164] start removing old nodestate --- iroh/src/magicsock.rs | 8 - iroh/src/magicsock/node_map.rs | 61 +----- iroh/src/magicsock/node_map/node_state.rs | 199 +----------------- iroh/src/magicsock/node_map/udp_paths.rs | 241 ---------------------- 4 files changed, 7 insertions(+), 502 deletions(-) delete mode 100644 iroh/src/magicsock/node_map/udp_paths.rs diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 5f9a97bc0c1..eccb866baac 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -39,8 +39,6 @@ use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; use node_map::NodeStateMessage; -#[cfg(test)] -use node_map::RemoteInfo; use quinn::ServerConfig; use rand::Rng; use snafu::{ResultExt, Snafu}; @@ -331,12 +329,6 @@ impl MagicSock { rx.await.unwrap_or(false) } - /// Return the [`RemoteInfo`]s of all nodes in the node map. - #[cfg(test)] - pub(crate) fn list_remote_infos(&self) -> Vec { - self.node_map.list_remote_infos(Instant::now()) - } - /// Returns a [`Watcher`] for this socket's direct addresses. /// /// The [`MagicSock`] continuously monitors the direct addresses, the network addresses diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index c9867e0bd19..931369b90f6 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -7,8 +7,8 @@ use std::{ }; use iroh_base::{NodeAddr, NodeId, RelayUrl}; -use n0_future::{task::AbortOnDropHandle, time::Instant}; -use node_state::{NodeState, NodeStateActor, NodeStateHandle}; +use n0_future::task::AbortOnDropHandle; +use node_state::{NodeStateActor, NodeStateHandle}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{Instrument, info_span, trace, warn}; @@ -32,9 +32,8 @@ use super::{ mod node_state; mod path_state; mod path_validity; -mod udp_paths; -pub(super) use node_state::{NodeStateMessage, RemoteInfo}; +pub(super) use node_state::NodeStateMessage; pub use node_state::{ConnectionType, ControlMsg, DirectAddrInfo}; @@ -77,10 +76,6 @@ pub(super) struct NodeMapInner { transports_handle: TransportsSenderHandle, local_addrs: n0_watcher::Direct>>, disco: DiscoState, - by_node_key: HashMap, - by_ip_port: HashMap, - by_quic_mapped_addr: HashMap, - by_id: HashMap, #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, /// The [`NodeStateActor`] for each remote node. @@ -192,20 +187,6 @@ impl NodeMap { self.node_mapped_addrs.get(&node_id) } - /// Returns the [`RemoteInfo`]s for each node in the node map. - #[cfg(test)] - pub(super) fn list_remote_infos(&self, now: Instant) -> Vec { - // NOTE: calls to this method will often call `into_iter` (or similar methods). Note that - // we can't avoid `collect` here since it would hold a lock for an indefinite time. Even if - // we were to find this acceptable, dealing with the lifetimes of the mutex's guard and the - // internal iterator will be a hassle, if possible at all. - self.inner - .lock() - .expect("poisoned") - .remote_infos_iter(now) - .collect() - } - /// Returns a [`n0_watcher::Direct`] for given node's [`ConnectionType`]. /// /// # Errors @@ -305,10 +286,6 @@ impl NodeMapInner { transports_handle, local_addrs, disco, - by_node_key: Default::default(), - by_ip_port: Default::default(), - by_quic_mapped_addr: Default::default(), - by_id: Default::default(), #[cfg(any(test, feature = "test-utils"))] path_selection: Default::default(), node_states: Default::default(), @@ -320,38 +297,6 @@ impl NodeMapInner { actor.start() } - fn get_id(&self, id: NodeStateKey) -> Option { - match id { - NodeStateKey::NodeId(node_key) => self.by_node_key.get(&node_key).copied(), - NodeStateKey::NodeIdMappedAddr(addr) => self.by_quic_mapped_addr.get(&addr).copied(), - NodeStateKey::IpPort(ipp) => self.by_ip_port.get(&ipp).copied(), - } - } - - fn get(&self, id: NodeStateKey) -> Option<&NodeState> { - self.get_id(id).and_then(|id| self.by_id.get(&id)) - } - - /// Number of nodes currently listed. - fn node_count(&self) -> usize { - self.by_id.len() - } - - fn node_states(&self) -> impl Iterator { - self.by_id.iter() - } - /// Get the [`RemoteInfo`]s for all nodes. - #[cfg(test)] - fn remote_infos_iter(&self, now: Instant) -> impl Iterator + '_ { - self.node_states().map(move |(_, ep)| ep.info(now)) - } - - /// Get the [`RemoteInfo`]s for each node. - fn remote_info(&self, node_id: NodeId) -> Option { - self.get(NodeStateKey::NodeId(node_id)) - .map(|ep| ep.info(Instant::now())) - } - /// Returns a stream of [`ConnectionType`]. /// /// Sends the current [`ConnectionType`] whenever any changes to the diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index d94dd0fca1d..208f27c5385 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -5,13 +5,13 @@ use std::{ sync::Arc, }; -use iroh_base::{NodeAddr, NodeId, PublicKey, RelayUrl}; +use iroh_base::{NodeAddr, NodeId, RelayUrl}; use n0_future::{ MergeUnbounded, Stream, StreamExt, task::AbortOnDropHandle, time::{Duration, Instant}, }; -use n0_watcher::{Watchable, Watcher}; +use n0_watcher::Watcher; use quinn::WeakConnectionHandle; use quinn_proto::{PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; @@ -28,18 +28,15 @@ use crate::{ endpoint::DirectAddr, magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MAX_IDLE_TIMEOUT, MagicsockMetrics, - mapped_addrs::{ - AddrMap, MappedAddr, MultipathMappedAddr, NodeIdMappedAddr, RelayMappedAddr, - }, + mapped_addrs::{AddrMap, MappedAddr, MultipathMappedAddr, RelayMappedAddr}, transports::{self, OwnedTransmit}, }, util::MaybeFuture, }; use super::{ - IpPort, Source, TransportsSenderMessage, + Source, TransportsSenderMessage, path_state::{NewPathState, PathState}, - udp_paths::NodeUdpPaths, }; /// Number of addresses that are not active that we keep around per node. @@ -67,184 +64,6 @@ const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); // TODO: Quinn should just do this. Also, I made this value up. const APPLICATION_ABANDON_PATH: u8 = 30; -#[derive(Debug)] -pub(in crate::magicsock) enum PingAction { - SendCallMeMaybe { - relay_url: RelayUrl, - dst_node: NodeId, - }, -} - -/// An iroh node, which we can have connections with. -/// -/// The whole point of the magicsock is that we can have multiple **paths** to a particular -/// node. One of these paths is via the endpoint's home relay node but as we establish a -/// connection we'll hopefully discover more direct paths. -#[derive(Debug)] -pub(super) struct NodeState { - /// The UDP address used on the QUIC-layer to address this node. - quic_mapped_addr: NodeIdMappedAddr, - /// The global identifier for this endpoint. - node_id: NodeId, - /// The url of relay node that we can relay over to communicate. - /// - /// The fallback/bootstrap path, if non-zero (non-zero for well-behaved clients). - relay_url: Option<(RelayUrl, PathState)>, - udp_paths: NodeUdpPaths, - /// Last time this node was used. - /// - /// A node is marked as in use when sending datagrams to them, or when having received - /// datagrams from it. Regardless of whether the datagrams are payload or DISCO, and whether - /// they go via UDP or the relay. - /// - /// Note that sending datagrams to a node does not mean the node receives them. - last_used: Option, - /// The type of connection we have to the node, either direct, relay, mixed, or none. - conn_type: Watchable, - /// Configuration for what path selection to use - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection, -} - -impl NodeState { - pub(super) fn public_key(&self) -> &PublicKey { - &self.node_id - } - - pub(super) fn all_paths_mapped_addr(&self) -> &NodeIdMappedAddr { - &self.quic_mapped_addr - } - - pub(super) fn conn_type(&self) -> n0_watcher::Direct { - self.conn_type.watch() - } - - pub(super) fn latency(&self) -> Option { - match self.conn_type.get() { - ConnectionType::Direct(addr) => self - .udp_paths - .paths() - .get(&addr.into()) - .and_then(|state| state.latency()), - ConnectionType::Relay(ref url) => self - .relay_url - .as_ref() - .filter(|(relay_url, _)| relay_url == url) - .and_then(|(_, state)| state.latency()), - ConnectionType::Mixed(addr, ref url) => { - let addr_latency = self - .udp_paths - .paths() - .get(&addr.into()) - .and_then(|state| state.latency()); - let relay_latency = self - .relay_url - .as_ref() - .filter(|(relay_url, _)| relay_url == url) - .and_then(|(_, state)| state.latency()); - addr_latency.min(relay_latency) - } - ConnectionType::None => None, - } - } - - /// Returns info about this node. - pub(super) fn info(&self, now: Instant) -> RemoteInfo { - let conn_type = self.conn_type.get(); - let latency = self.latency(); - - let addrs = self - .udp_paths - .paths() - .iter() - .map(|(addr, path_state)| DirectAddrInfo { - addr: SocketAddr::from(*addr), - latency: path_state.validity.latency(), - last_control: path_state.last_control_msg(now), - last_payload: path_state - .last_payload_msg - .as_ref() - .map(|instant| now.duration_since(*instant)), - last_alive: path_state - .last_alive() - .map(|instant| now.duration_since(instant)), - sources: path_state - .sources - .iter() - .map(|(source, instant)| (source.clone(), now.duration_since(*instant))) - .collect(), - }) - .collect(); - - RemoteInfo { - node_id: self.node_id, - relay_url: self.relay_url.clone().map(|r| r.into()), - addrs, - conn_type, - latency, - last_used: self.last_used.map(|instant| now.duration_since(instant)), - } - } - - /// Removes a direct address for this node. - /// - /// If this is also the best address, it will be cleared as well. - pub(super) fn remove_direct_addr(&mut self, ip_port: &IpPort, now: Instant, why: &'static str) { - let Some(state) = self.udp_paths.access_mut(now).paths().remove(ip_port) else { - return; - }; - - match state.last_alive().map(|instant| instant.elapsed()) { - Some(last_alive) => debug!(%ip_port, ?last_alive, why, "pruning address"), - None => debug!(%ip_port, last_seen=%"never", why, "pruning address"), - } - } - - /// Called when connectivity changes enough that we should question our earlier - /// assumptions about which paths work. - #[instrument("disco", skip_all, fields(node = %self.node_id.fmt_short()))] - pub(super) fn note_connectivity_change(&mut self, now: Instant) { - let mut guard = self.udp_paths.access_mut(now); - for es in guard.paths().values_mut() { - es.clear(); - } - } - - /// Checks if this `Endpoint` is currently actively being used. - pub(super) fn is_active(&self, now: &Instant) -> bool { - match self.last_used { - Some(last_active) => now.duration_since(last_active) <= SESSION_ACTIVE_TIMEOUT, - None => false, - } - } - - /// Returns a [`NodeAddr`] with all the currently known direct addresses and the relay URL. - pub(crate) fn get_current_addr(&self) -> NodeAddr { - // TODO: more selective? - let mut node_addr = - NodeAddr::new(self.node_id).with_direct_addresses(self.udp_paths.addrs()); - if let Some((url, _)) = &self.relay_url { - node_addr = node_addr.with_relay_url(url.clone()); - } - - node_addr - } - - /// Get the direct addresses for this endpoint. - pub(super) fn direct_addresses(&self) -> impl Iterator + '_ { - self.udp_paths.paths().keys().copied() - } - - #[cfg(test)] - pub(super) fn direct_address_states(&self) -> impl Iterator + '_ { - self.udp_paths.paths().iter() - } - - pub(super) fn last_used(&self) -> Option { - self.last_used - } -} - /// The state we need to know about a single remote node. /// /// This actor manages all connections to the remote node. It will trigger holepunching and @@ -1150,16 +969,6 @@ struct HolepunchAttempt { remote_addrs: BTreeSet, } -/// Whether to send a call-me-maybe message after sending pings to all known paths. -/// -/// `IfNoRecent` will only send a call-me-maybe if no previous one was sent in the last -/// [`HEARTBEAT_INTERVAL`]. -#[derive(Debug)] -enum SendCallMeMaybe { - Always, - IfNoRecent, -} - /// The type of control message we have received. #[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, derive_more::Display)] pub enum ControlMsg { diff --git a/iroh/src/magicsock/node_map/udp_paths.rs b/iroh/src/magicsock/node_map/udp_paths.rs deleted file mode 100644 index 74100be5119..00000000000 --- a/iroh/src/magicsock/node_map/udp_paths.rs +++ /dev/null @@ -1,241 +0,0 @@ -//! Path state for UDP addresses of a single peer node. -//! -//! This started as simply moving the [`NodeState`]'s `direct_addresses` and `best_addr` -//! into one place together. The aim is for external places to not directly interact with -//! the inside and instead only notifies this struct of state changes to each path. -//! -//! [`NodeState`]: super::node_state::NodeState -use std::{collections::BTreeMap, net::SocketAddr}; - -use n0_future::time::Instant; -use tracing::{Level, event}; - -use super::{IpPort, path_state::PathState}; - -/// The address on which to send datagrams over UDP. -/// -/// The [`MagicSock`] sends packets to zero or one UDP address, depending on the known paths -/// to the remote node. This conveys the UDP address to send on from the [`NodeUdpPaths`] -/// to the [`NodeState`]. -/// -/// [`NodeUdpPaths`] contains all the UDP path states, while [`NodeState`] has to decide the -/// bigger picture including the relay server. -/// -/// See [`NodeUdpPaths::send_addr`]. -/// -/// [`MagicSock`]: crate::magicsock::MagicSock -/// [`NodeState`]: super::node_state::NodeState -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] -pub(super) enum UdpSendAddr { - /// The UDP address can be relied on to deliver data to the remote node. - /// - /// This means this path is usable with a reasonable latency and can be fully trusted to - /// transport payload data to the remote node. - Valid(SocketAddr), - /// The UDP address is highly likely to work, but has not been used for a while. - /// - /// The path should be usable but has not carried DISCO or payload data for a little too - /// long. It is best to also use a backup, i.e. relay, path if possible. - Outdated(SocketAddr), - /// The UDP address is not known to work, but it might. - /// - /// We know this UDP address belongs to the remote node, but we do not know if the path - /// already works or may need holepunching before it will start to work. It might even - /// never work. It is still useful to send to this together with backup path, - /// i.e. relay, in case the path works: if the path does not need holepunching it might - /// be much faster. And if there is no relay path at all it might be the only way to - /// establish a connection. - Unconfirmed(SocketAddr), - /// No known UDP path exists to the remote node. - #[default] - None, -} - -/// The UDP paths for a single node. -/// -/// Paths are identified by the [`IpPort`] of their UDP address. -/// -/// Initially this collects two structs directly from the [`NodeState`] into one place, -/// leaving the APIs and astractions the same. The goal is that this slowly migrates -/// directly interacting with this data into only receiving [`PathState`] updates. This -/// will consolidate the logic of direct path selection and make this simpler to reason -/// about. However doing that all at once is too large a refactor. -/// -/// [`NodeState`]: super::node_state::NodeState -#[derive(Debug, Default)] -pub(super) struct NodeUdpPaths { - /// The state for each of this node's direct paths. - paths: BTreeMap, - /// The current address we use to send on. - /// - /// This is *almost* the same as going through `paths` and finding - /// the best one, except that this is - /// 1. Not updated in `send_addr`, but instead when there's changes to `paths`, so that `send_addr` can take `&self`. - /// 2. Slightly sticky: It only changes when - /// - the current send addr is not a validated path anymore or - /// - we received a pong with lower latency. - best: UdpSendAddr, - /// The current best address to send on from all IPv4 addresses we have available. - /// - /// Follows the same logic as `best` above, but doesn't include any IPv6 addresses. - best_ipv4: UdpSendAddr, -} - -pub(super) struct MutAccess<'a> { - now: Instant, - inner: &'a mut NodeUdpPaths, -} - -impl<'a> MutAccess<'a> { - pub fn paths(&mut self) -> &mut BTreeMap { - &mut self.inner.paths - } - - pub fn has_best_addr_changed(self) -> bool { - let changed = self.inner.update_to_best_addr(self.now); - std::mem::forget(self); // don't run drop - changed - } -} - -impl Drop for MutAccess<'_> { - fn drop(&mut self) { - self.inner.update_to_best_addr(self.now); - } -} - -impl NodeUdpPaths { - pub(super) fn new() -> Self { - Default::default() - } - - #[cfg(test)] - pub(super) fn from_parts(paths: BTreeMap, best: UdpSendAddr) -> Self { - Self { - paths, - best_ipv4: best, // we only use ipv4 addrs in tests - best, - } - } - pub(super) fn addrs(&self) -> Vec { - self.paths.keys().map(|ip| (*ip).into()).collect() - } - - /// Returns the current UDP address to send on. - pub(super) fn send_addr(&self, have_ipv6: bool) -> &UdpSendAddr { - if !have_ipv6 { - // If it's a valid address, it doesn't matter if our interface scan determined that we - // "probably" don't have IPv6, because we clearly were able to send and receive a ping/pong over IPv6. - if matches!(&self.best, UdpSendAddr::Valid(_)) { - return &self.best; - } - return &self.best_ipv4; - } - &self.best - } - - /// Returns a guard for accessing the inner paths mutably. - /// - /// This guard ensures that [`Self::send_addr`] will be updated on drop. - pub(super) fn access_mut(&mut self, now: Instant) -> MutAccess<'_> { - MutAccess { now, inner: self } - } - - /// Returns immutable access to the inner paths. - pub(super) fn paths(&self) -> &BTreeMap { - &self.paths - } - - /// Changes the current best address(es) to ones chosen as described in [`Self::best_addr`] docs. - /// - /// Returns whether one of the best addresses had to change. - /// - /// This should be called any time that `paths` is modified. - fn update_to_best_addr(&mut self, now: Instant) -> bool { - let best_ipv4 = self.best_addr(false, now); - let best = self.best_addr(true, now); - let mut changed = false; - if best_ipv4 != self.best_ipv4 { - event!( - target: "iroh::_events::udp::best_ipv4", - Level::DEBUG, - ?best_ipv4, - ); - changed = true; - } - if best != self.best { - event!( - target: "iroh::_events::udp::best", - Level::DEBUG, - ?best, - ); - changed = true; - } - self.best_ipv4 = best_ipv4; - self.best = best; - changed - } - - /// Returns the current best address of all available paths, ignoring - /// the currently chosen best address. - /// - /// We try to find the lowest latency [`UdpSendAddr::Valid`], if one exists, otherwise - /// we try to find the lowest latency [`UdpSendAddr::Outdated`], if one exists, otherwise - /// we return essentially an arbitrary [`UdpSendAddr::Unconfirmed`]. - /// - /// If we don't have any addresses, returns [`UdpSendAddr::None`]. - /// - /// If `have_ipv6` is false, we only search among ipv4 candidates. - fn best_addr(&self, have_ipv6: bool, now: Instant) -> UdpSendAddr { - let Some((ipp, path)) = self - .paths - .iter() - .filter(|(ipp, _)| have_ipv6 || ipp.ip.is_ipv4()) - .max_by_key(|(ipp, path)| { - // We find the best by sorting on a key of type (Option>, Option>, bool) - // where the first is set to Some(ReverseOrd(latency)) iff path.is_valid(now) and - // the second is set to Some(ReverseOrd(latency)) if path.is_outdated(now) and - // the third is set to whether the ipp is ipv6. - // This makes max_by_key sort for the lowest valid latency first, then sort for - // the lowest outdated latency second, and if latencies are equal, it'll sort IPv6 paths first. - let is_ipv6 = ipp.ip.is_ipv6(); - if let Some(latency) = path.validity.latency_if_valid(now) { - (Some(ReverseOrd(latency)), None, is_ipv6) - } else if let Some(latency) = path.validity.latency_if_outdated(now) { - (None, Some(ReverseOrd(latency)), is_ipv6) - } else { - (None, None, is_ipv6) - } - }) - else { - return UdpSendAddr::None; - }; - - if path.validity.is_valid(now) { - UdpSendAddr::Valid((*ipp).into()) - } else if path.validity.is_outdated(now) { - UdpSendAddr::Outdated((*ipp).into()) - } else { - UdpSendAddr::Unconfirmed((*ipp).into()) - } - } -} - -/// Implements the reverse [`Ord`] implementation for the wrapped type. -/// -/// Literally calls [`std::cmp::Ordering::reverse`] on the inner value's -/// ordering. -#[derive(PartialEq, Eq)] -struct ReverseOrd(N); - -impl Ord for ReverseOrd { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.0.cmp(&other.0).reverse() - } -} - -impl PartialOrd for ReverseOrd { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} From 145cb18ae9ef3a014ab3d36a1225be722afaa7d4 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 12:11:30 +0200 Subject: [PATCH 072/164] delete more: path_validity mod is gone --- iroh/src/magicsock/node_map.rs | 1 - iroh/src/magicsock/node_map/node_state.rs | 15 +- iroh/src/magicsock/node_map/path_state.rs | 182 +------------------ iroh/src/magicsock/node_map/path_validity.rs | 154 ---------------- 4 files changed, 2 insertions(+), 350 deletions(-) delete mode 100644 iroh/src/magicsock/node_map/path_validity.rs diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 931369b90f6..96d74fc4a2e 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -31,7 +31,6 @@ use super::{ mod node_state; mod path_state; -mod path_validity; pub(super) use node_state::NodeStateMessage; diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 208f27c5385..c3e21e9ec67 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -34,10 +34,7 @@ use crate::{ util::MaybeFuture, }; -use super::{ - Source, TransportsSenderMessage, - path_state::{NewPathState, PathState}, -}; +use super::{Source, TransportsSenderMessage, path_state::NewPathState}; /// Number of addresses that are not active that we keep around per node. /// @@ -1043,16 +1040,6 @@ pub struct RelayUrlInfo { pub latency: Option, } -impl From<(RelayUrl, PathState)> for RelayUrlInfo { - fn from(value: (RelayUrl, PathState)) -> Self { - RelayUrlInfo { - relay_url: value.0, - last_alive: value.1.last_alive().map(|i| i.elapsed()), - latency: None, - } - } -} - impl From for RelayUrl { fn from(value: RelayUrlInfo) -> Self { value.relay_url diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index d268436f968..258339fee83 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -9,187 +9,7 @@ use super::{ IpPort, Source, node_state::{ControlMsg, SESSION_ACTIVE_TIMEOUT}, }; -use crate::{ - disco::{self, SendAddr}, - magicsock::node_map::path_validity::{self, PathValidity}, -}; - -/// State about a particular path to another [`NodeState`]. -/// -/// This state is used for both the relay path and any direct UDP paths. -/// -/// [`NodeState`]: super::node_state::NodeState -#[derive(Debug, Clone)] -pub(super) struct PathState { - /// The node for which this path exists. - node_id: NodeId, - /// The path this applies for. - path: SendAddr, - - /// The time this endpoint was last advertised via a call-me-maybe DISCO message. - pub(super) call_me_maybe_time: Option, - - /// Tracks whether this path is valid. - /// - /// Also stores the latest [`PongReply`], if there is one. - /// - /// See [`PathValidity`] docs. - pub(super) validity: PathValidity, - - /// When the last payload data was **received** via this path. - /// - /// This excludes DISCO messages. - pub(super) last_payload_msg: Option, - /// Sources is a map of [`Source`]s to [`Instant`]s, keeping track of all the ways we have - /// learned about this path - /// - /// We keep track of only the latest [`Instant`] for each [`Source`], keeping the size of - /// the map of sources down to one entry per type of source. - pub(super) sources: HashMap, -} - -impl PathState { - pub(super) fn new(node_id: NodeId, path: SendAddr, source: Source, now: Instant) -> Self { - let mut sources = HashMap::new(); - sources.insert(source, now); - Self { - node_id, - path, - call_me_maybe_time: None, - validity: PathValidity::empty(), - last_payload_msg: None, - sources, - } - } - - pub(super) fn with_last_payload( - node_id: NodeId, - path: SendAddr, - source: Source, - now: Instant, - ) -> Self { - let mut sources = HashMap::new(); - sources.insert(source, now); - PathState { - node_id, - path, - call_me_maybe_time: None, - validity: PathValidity::empty(), - last_payload_msg: Some(now), - sources, - } - } - - /// Check whether this path is considered active. - /// - /// Active means the path has received payload messages within the last - /// [`SESSION_ACTIVE_TIMEOUT`]. - /// - /// Note that a path might be alive but not active if it's contactable but not in - /// use. - pub(super) fn is_active(&self) -> bool { - self.last_payload_msg - .as_ref() - .map(|instant| instant.elapsed() <= SESSION_ACTIVE_TIMEOUT) - .unwrap_or(false) - } - - pub(super) fn receive_payload(&mut self, now: Instant) { - self.last_payload_msg = Some(now); - self.validity - .receive_payload(now, path_validity::Source::QuicPayload); - } - /// Reports the last instant this path was considered alive. - /// - /// Alive means the path is considered in use by the remote endpoint. Either because we - /// received a payload message, a DISCO message (ping, pong) or it was advertised in a - /// call-me-maybe message. - /// - /// This is the most recent instant between: - /// - when last pong was received. - /// - when this path was last advertised in a received CallMeMaybe message. - /// - When the last payload transmission occurred. - /// - when the last ping from them was received. - pub(super) fn last_alive(&self) -> Option { - self.validity - .latest_pong() - .into_iter() - .chain(self.last_payload_msg) - .chain(self.call_me_maybe_time) - .max() - } - - /// The last control or DISCO message **about** this path. - /// - /// This is the most recent instant among: - /// - when this path was last advertised in a received CallMeMaybe message. - /// - when the last ping from them was received. - /// - /// Returns the time elapsed since the last control message, and the type of control message. - pub(super) fn last_control_msg(&self, now: Instant) -> Option<(Duration, ControlMsg)> { - // get every control message and assign it its kind - let last_call_me_maybe = self - .call_me_maybe_time - .as_ref() - .map(|call_me| (*call_me, ControlMsg::CallMeMaybe)); - - last_call_me_maybe - .into_iter() - .max_by_key(|(instant, _kind)| *instant) - .map(|(instant, kind)| (now.duration_since(instant), kind)) - } - - /// Returns the latency from the most recent pong, if available. - pub(super) fn latency(&self) -> Option { - self.validity.latency() - } - - pub(super) fn add_source(&mut self, source: Source, now: Instant) { - self.sources.insert(source, now); - } - - pub(super) fn clear(&mut self) { - self.call_me_maybe_time = None; - self.validity = PathValidity::empty(); - } - - fn summary(&self, mut w: impl std::fmt::Write) -> std::fmt::Result { - write!(w, "{{ ")?; - if self.is_active() { - write!(w, "active ")?; - } - if let Some(pong_at) = self.validity.latest_pong() { - write!(w, "pong-received({:?} ago) ", pong_at.elapsed())?; - } - - if let Some(last_source) = self.sources.iter().max_by_key(|&(_, instant)| instant) { - write!( - w, - "last-source: {}({:?} ago)", - last_source.0, - last_source.1.elapsed() - )?; - } - write!(w, "}}") - } -} - -// TODO: Make an `EndpointPaths` struct and do things nicely. -pub(super) fn summarize_node_paths(paths: &BTreeMap) -> String { - use std::fmt::Write; - - let mut w = String::new(); - write!(&mut w, "[").ok(); - for (i, (ipp, state)) in paths.iter().enumerate() { - if i > 0 { - write!(&mut w, ", ").ok(); - } - write!(&mut w, "{ipp}").ok(); - state.summary(&mut w).ok(); - } - write!(&mut w, "]").ok(); - w -} +use crate::disco::{self, SendAddr}; /// The state of a single path to the remote endpoint. /// diff --git a/iroh/src/magicsock/node_map/path_validity.rs b/iroh/src/magicsock/node_map/path_validity.rs deleted file mode 100644 index cd8a5f442b3..00000000000 --- a/iroh/src/magicsock/node_map/path_validity.rs +++ /dev/null @@ -1,154 +0,0 @@ -use n0_future::time::{Duration, Instant}; - -/// How long we trust a UDP address as the exclusive path (i.e. without also sending via the relay). -/// -/// Trust for a UDP address begins when we receive a DISCO UDP pong on that address. -/// It is then further extended by this duration every time we receive QUIC payload data while it's -/// currently trusted. -/// -/// If trust goes away, it can be brought back with another valid DISCO UDP pong. -const TRUST_UDP_ADDR_DURATION: Duration = Duration::from_millis(6500); - -/// Tracks a path's validity. -/// -/// A path is valid: -/// - For [`Source::trust_duration`] after a successful [`PongReply`]. -/// - For [`Source::trust_duration`] longer starting at the most recent -/// received application payload *while the path was valid*. -/// -/// [`PongReply`]: super::node_state::PongReply -#[derive(Debug, Clone, Default)] -pub(super) struct PathValidity(Option); - -#[derive(Debug, Clone)] -struct Inner { - latest_pong: Instant, - latency: Duration, - trust_until: Instant, -} - -#[derive(Debug)] -pub(super) enum Source { - ReceivedPong, - QuicPayload, -} - -impl Source { - fn trust_duration(&self) -> Duration { - match self { - Source::ReceivedPong => TRUST_UDP_ADDR_DURATION, - Source::QuicPayload => TRUST_UDP_ADDR_DURATION, - } - } -} - -impl PathValidity { - pub(super) fn new(pong_at: Instant, latency: Duration) -> Self { - Self(Some(Inner { - trust_until: pong_at + Source::ReceivedPong.trust_duration(), - latest_pong: pong_at, - latency, - })) - } - - pub(super) fn empty() -> Self { - Self(None) - } - - pub(super) fn is_empty(&self) -> bool { - self.0.is_none() - } - - pub(super) fn is_valid(&self, now: Instant) -> bool { - let Some(state) = self.0.as_ref() else { - return false; - }; - - state.is_valid(now) - } - - pub(super) fn latency_if_valid(&self, now: Instant) -> Option { - let state = self.0.as_ref()?; - state.is_valid(now).then_some(state.latency) - } - - pub(super) fn is_outdated(&self, now: Instant) -> bool { - let Some(state) = self.0.as_ref() else { - return false; - }; - - // We *used* to be valid, but are now outdated. - // This happens when we had a DISCO pong but didn't receive - // any payload data or further pongs for at least TRUST_UDP_ADDR_DURATION - state.is_outdated(now) - } - - pub(super) fn latency_if_outdated(&self, now: Instant) -> Option { - let state = self.0.as_ref()?; - state.is_outdated(now).then_some(state.latency) - } - - /// Reconfirms path validity, if a payload was received while the - /// path was valid. - pub(super) fn receive_payload(&mut self, now: Instant, source: Source) { - let Some(state) = self.0.as_mut() else { - return; - }; - - if state.is_valid(now) { - state.trust_until = now + source.trust_duration(); - } - } - - pub(super) fn latency(&self) -> Option { - Some(self.0.as_ref()?.latency) - } - - pub(super) fn latest_pong(&self) -> Option { - Some(self.0.as_ref()?.latest_pong) - } -} - -impl Inner { - fn is_valid(&self, now: Instant) -> bool { - self.latest_pong <= now && now < self.trust_until - } - - fn is_outdated(&self, now: Instant) -> bool { - self.latest_pong <= now && self.trust_until <= now - } -} - -#[cfg(test)] -mod tests { - use n0_future::time::{Duration, Instant}; - - use super::{PathValidity, Source, TRUST_UDP_ADDR_DURATION}; - - #[tokio::test(start_paused = true)] - async fn test_basic_path_validity_lifetime() { - let mut validity = PathValidity(None); - assert!(!validity.is_valid(Instant::now())); - assert!(!validity.is_outdated(Instant::now())); - - validity = PathValidity::new(Instant::now(), Duration::from_millis(20)); - assert!(validity.is_valid(Instant::now())); - assert!(!validity.is_outdated(Instant::now())); - - tokio::time::advance(TRUST_UDP_ADDR_DURATION / 2).await; - assert!(validity.is_valid(Instant::now())); - assert!(!validity.is_outdated(Instant::now())); - - validity.receive_payload(Instant::now(), Source::QuicPayload); - assert!(validity.is_valid(Instant::now())); - assert!(!validity.is_outdated(Instant::now())); - - tokio::time::advance(TRUST_UDP_ADDR_DURATION / 2).await; - assert!(validity.is_valid(Instant::now())); - assert!(!validity.is_outdated(Instant::now())); - - tokio::time::advance(TRUST_UDP_ADDR_DURATION / 2).await; - assert!(!validity.is_valid(Instant::now())); - assert!(validity.is_outdated(Instant::now())); - } -} From ccb32874661d2f8c359464e03851b1e822e95d80 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 12:18:06 +0200 Subject: [PATCH 073/164] remove more! --- iroh/src/endpoint.rs | 4 +- iroh/src/magicsock.rs | 5 +- iroh/src/magicsock/node_map.rs | 2 +- iroh/src/magicsock/node_map/node_state.rs | 128 +--------------------- iroh/src/magicsock/node_map/path_state.rs | 13 +-- 5 files changed, 9 insertions(+), 143 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index b21a3cf3050..2a9d549fcc3 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -66,9 +66,7 @@ pub use quinn_proto::{ }, }; -pub use super::magicsock::{ - AddNodeAddrError, ConnectionType, ControlMsg, DirectAddr, DirectAddrInfo, DirectAddrType, -}; +pub use super::magicsock::{AddNodeAddrError, ConnectionType, DirectAddr, DirectAddrType}; /// The delay to fall back to discovery when direct addresses fail. /// diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index eccb866baac..d59fb409880 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -82,10 +82,7 @@ pub(crate) mod transports; use mapped_addrs::{MappedAddr, NodeIdMappedAddr}; -pub use self::{ - metrics::Metrics, - node_map::{ConnectionType, ControlMsg, DirectAddrInfo}, -}; +pub use self::{metrics::Metrics, node_map::ConnectionType}; /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically /// expire at 30 seconds, so this is a few seconds shy of that. diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 96d74fc4a2e..6788847ec18 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -34,7 +34,7 @@ mod path_state; pub(super) use node_state::NodeStateMessage; -pub use node_state::{ConnectionType, ControlMsg, DirectAddrInfo}; +pub use node_state::ConnectionType; /// Number of nodes that are inactive for which we keep info about. This limit is enforced /// periodically via [`NodeMap::prune_inactive`]. diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index c3e21e9ec67..08ac044a779 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1,9 +1,4 @@ -use std::{ - collections::{BTreeSet, HashMap}, - net::SocketAddr, - pin::Pin, - sync::Arc, -}; +use std::{collections::BTreeSet, net::SocketAddr, pin::Pin, sync::Arc}; use iroh_base::{NodeAddr, NodeId, RelayUrl}; use n0_future::{ @@ -931,22 +926,6 @@ pub(super) struct NodeStateHandle { _task: AbortOnDropHandle<()>, } -impl From for NodeAddr { - fn from(info: RemoteInfo) -> Self { - let direct_addresses = info - .addrs - .into_iter() - .map(|info| info.addr) - .collect::>(); - - NodeAddr { - node_id: info.node_id, - relay_url: info.relay_url.map(Into::into), - direct_addresses, - } - } -} - /// Information about a holepunch attempt. #[derive(Debug)] struct HolepunchAttempt { @@ -966,69 +945,6 @@ struct HolepunchAttempt { remote_addrs: BTreeSet, } -/// The type of control message we have received. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, derive_more::Display)] -pub enum ControlMsg { - /// We received a Ping from the node. - #[display("ping←")] - Ping, - /// We received a Pong from the node. - #[display("pong←")] - Pong, - /// We received a CallMeMaybe. - #[display("call me")] - CallMeMaybe, -} - -/// Information about a *direct address*. -/// -/// The *direct addresses* of an iroh node are those that could be used by other nodes to -/// establish direct connectivity, depending on the network situation. Due to NAT configurations, -/// for example, not all direct addresses of a node are usable by all peers. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct DirectAddrInfo { - /// The UDP address reported by the remote node. - pub addr: SocketAddr, - /// The latency to the remote node over this network path. - /// - /// If there has never been any connectivity via this address no latency will be known. - pub latency: Option, - /// Last control message received by this node about this address. - /// - /// This contains the elapsed duration since the control message was received and the - /// kind of control message received at that time. Only the most recent control message - /// is returned. - /// - /// Note that [`ControlMsg::CallMeMaybe`] is received via a relay path, while - /// [`ControlMsg::Ping`] and [`ControlMsg::Pong`] are received on the path to - /// [`DirectAddrInfo::addr`] itself and thus convey very different information. - pub last_control: Option<(Duration, ControlMsg)>, - /// Elapsed time since the last payload message was received on this network path. - /// - /// This indicates how long ago a QUIC datagram was received from the remote node sent - /// from this [`DirectAddrInfo::addr`]. It indicates the network path was in use to - /// transport payload data. - pub last_payload: Option, - /// Elapsed time since this network path was known to exist. - /// - /// A network path is considered to exist only because the remote node advertised it. - /// It may not mean the path is usable. However, if there was any communication with - /// the remote node over this network path it also means the path exists. - /// - /// The elapsed time since *any* confirmation of the path's existence was received is - /// returned. If the remote node moved networks and no longer has this path, this could - /// be a long duration. - pub last_alive: Option, - /// A [`HashMap`] of [`Source`]s to [`Duration`]s. - /// - /// The [`Duration`] indicates the elapsed time since this source last - /// recorded this address. - /// - /// The [`Duration`] will always indicate the most recent time the source - /// recorded this address. - pub sources: HashMap, -} - /// Information about the network path to a remote node via a relay server. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct RelayUrlInfo { @@ -1046,48 +962,6 @@ impl From for RelayUrl { } } -/// Details about a remote iroh node which is known to this node. -/// -/// Having details of a node does not mean it can be connected to, nor that it has ever been -/// connected to in the past. There are various reasons a node might be known: it could have -/// been manually added via [`Endpoint::add_node_addr`], it could have been added by some -/// discovery mechanism, the node could have contacted this node, etc. -/// -/// [`Endpoint::add_node_addr`]: crate::endpoint::Endpoint::add_node_addr -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub(crate) struct RemoteInfo { - /// The globally unique identifier for this node. - pub node_id: NodeId, - /// Relay server information, if available. - pub relay_url: Option, - /// The addresses at which this node might be reachable. - /// - /// Some of these addresses might only be valid for networks we are not part of, but the remote - /// node might be a part of. - pub addrs: Vec, - /// The type of connection we have to the node, either direct or over relay. - pub conn_type: ConnectionType, - /// The latency of the current network path to the remote node. - pub latency: Option, - /// Time elapsed time since last we have sent to or received from the node. - /// - /// This is the duration since *any* data (payload or control messages) was sent or receive - /// from the remote node. Note that sending to the remote node does not imply - /// the remote node received anything. - pub last_used: Option, -} - -impl RemoteInfo { - /// Get the duration since the last activity we received from this endpoint - /// on any of its direct addresses. - pub(crate) fn last_received(&self) -> Option { - self.addrs - .iter() - .filter_map(|addr| addr.last_control.map(|x| x.0).min(addr.last_payload)) - .min() - } -} - /// The type of connection we have to the endpoint. #[derive(derive_more::Display, Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum ConnectionType { diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index 258339fee83..781bfdbb5d5 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -1,15 +1,12 @@ //! The state kept for each network path to a remote node. -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; -use iroh_base::NodeId; -use n0_future::time::{Duration, Instant}; +use n0_future::time::Instant; -use super::{ - IpPort, Source, - node_state::{ControlMsg, SESSION_ACTIVE_TIMEOUT}, -}; -use crate::disco::{self, SendAddr}; +use crate::disco; + +use super::Source; /// The state of a single path to the remote endpoint. /// From 80157d4a33476db315831188e871ba4b12f6ba45 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 12:19:57 +0200 Subject: [PATCH 074/164] and more gone --- iroh/src/magicsock/node_map.rs | 11 ----------- iroh/src/magicsock/node_map/node_state.rs | 17 ----------------- 2 files changed, 28 deletions(-) diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 6788847ec18..b1dbf44053e 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -83,17 +83,6 @@ pub(super) struct NodeMapInner { node_states: HashMap, } -/// Identifier to look up a [`NodeState`] in the [`NodeMap`]. -/// -/// You can look up entries in [`NodeMap`] with various keys, depending on the context you -/// have for the node. These are all the keys the [`NodeMap`] can use. -#[derive(Debug, Clone)] -enum NodeStateKey { - NodeId(NodeId), - NodeIdMappedAddr(NodeIdMappedAddr), - IpPort(IpPort), -} - /// The origin or *source* through which an address associated with a remote node /// was discovered. /// diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 08ac044a779..8b630c802b0 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -945,23 +945,6 @@ struct HolepunchAttempt { remote_addrs: BTreeSet, } -/// Information about the network path to a remote node via a relay server. -#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub struct RelayUrlInfo { - /// The relay URL. - pub relay_url: RelayUrl, - /// Elapsed time since this relay path last received payload or control data. - pub last_alive: Option, - /// Latency to the remote node over this relayed network path. - pub latency: Option, -} - -impl From for RelayUrl { - fn from(value: RelayUrlInfo) -> Self { - value.relay_url - } -} - /// The type of connection we have to the endpoint. #[derive(derive_more::Display, Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum ConnectionType { From b0957cb2ab02d69fba263a5a22fc6488a25f826c Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 12:24:20 +0200 Subject: [PATCH 075/164] rename temporary name, now the name is free again --- iroh/src/magicsock/node_map/node_state.rs | 4 ++-- iroh/src/magicsock/node_map/path_state.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 8b630c802b0..62b66db846f 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -29,7 +29,7 @@ use crate::{ util::MaybeFuture, }; -use super::{Source, TransportsSenderMessage, path_state::NewPathState}; +use super::{Source, TransportsSenderMessage, path_state::PathState}; /// Number of addresses that are not active that we keep around per node. /// @@ -107,7 +107,7 @@ pub(super) struct NodeStateActor { /// /// These paths might be entirely impossible to use, since they are added by discovery /// mechanisms. The are only potentially usable. - paths: FxHashMap, + paths: FxHashMap, /// Maps connections and path IDs to the transport addr. /// /// The [`transports::Addr`] can be looked up in [`Self::paths`]. diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index 781bfdbb5d5..bbd5bf8a21e 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -15,7 +15,7 @@ use super::Source; /// /// [`NodeStateActor::paths`]: super::node_state::NodeStateActor #[derive(Debug, Default)] -pub(super) struct NewPathState { +pub(super) struct PathState { /// How we learned about this path, and when. /// /// We keep track of only the latest [`Instant`] for each [`Source`], keeping the size From a3e58f8a1e61d1907df3d542ccccaa8ad689454d Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 12:37:59 +0200 Subject: [PATCH 076/164] slightly better logging --- iroh/src/net_report.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/iroh/src/net_report.rs b/iroh/src/net_report.rs index 2e409aa18e7..6b8d412523a 100644 --- a/iroh/src/net_report.rs +++ b/iroh/src/net_report.rs @@ -504,21 +504,21 @@ impl Client { } } Err(err) => { - debug!("probe v4 failed: {err:?}"); + debug!("probe v4 QAD failed: {err:?}"); } } } Some(Err(err)) => { if err.is_panic() { - panic!("probe v4 panicked: {err:?}"); + panic!("probe v4 QAD panicked: {err:?}"); } - warn!("probe v4 failed: {err:?}"); + warn!("probe v4 QAD failed: {err:?}"); } Some(Ok(None)) => { - debug!("probe v4 canceled"); + debug!("probe v4 QAD canceled"); } Some(Ok(Some(Err(time::Elapsed { .. })))) => { - debug!("probe v4 timed out"); + debug!("probe v4 QAD timed out"); } None => {} } @@ -539,21 +539,21 @@ impl Client { } } Err(err) => { - debug!("probe v6 failed: {err:?}"); + debug!("probe v6 QAD failed: {err:?}"); } } } Some(Err(err)) => { if err.is_panic() { - panic!("probe v6 panicked: {err:?}"); + panic!("probe v6 QAD panicked: {err:?}"); } - warn!("probe v6 failed: {err:?}"); + warn!("probe v6 QAD failed: {err:?}"); } Some(Ok(None)) => { - debug!("probe v6 canceled"); + debug!("probe v6 QAD canceled"); } Some(Ok(Some(Err(time::Elapsed { .. })))) => { - debug!("probe v6 timed out"); + debug!("probe v6 QAD timed out"); } None => {} } From ce564f1553dbc851001d6c997eae48b2048be281 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 12:41:39 +0200 Subject: [PATCH 077/164] avoid unneeded mut when not testing --- iroh/src/magicsock/node_map.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index b1dbf44053e..ef8b309a17d 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -139,12 +139,15 @@ impl NodeMap { local_addrs: n0_watcher::Direct>>, disco: DiscoState, ) -> Self { - let mut inner = NodeMapInner::new(metrics, sender, local_addrs, disco); + #[cfg(not(any(test, feature = "test-utils")))] + let inner = NodeMapInner::new(metrics, sender, local_addrs, disco); #[cfg(any(test, feature = "test-utils"))] - { + let inner = { + let mut inner = NodeMapInner::new(metrics, sender, local_addrs, disco); inner.path_selection = path_selection; - } + inner + }; Self { local_node_id, From 4eb0f07e34cccbb53e1842093d29c7b013ff4f5d Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 7 Oct 2025 15:02:48 +0200 Subject: [PATCH 078/164] Convert to canonical IP address in IpSender Our AsyncUdpSocket is always an IPv6 socket, because we need to sent to our IPv6 ULAs for the mapped addresses. This means Quinn will convert any IPv4 destination into an IPv4-mapped IPv6 address (::ffff:a.b.c.d). But our sockets are bound to a specific family, so even if the OS supports and IPv6 socket that can handle IPv4 it will not accept those addresses. So when we are using the destinations we need to convert back to the canonical addresses in the IPSender. --- iroh/src/magicsock/transports/ip.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index cebda5339d2..b9427c2198c 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -129,15 +129,30 @@ pub(super) struct IpSender { } impl IpSender { - pub(super) fn is_valid_send_addr(&self, addr: &SocketAddr) -> bool { + pub(super) fn is_valid_send_addr(&self, dst: &SocketAddr) -> bool { + // Our net-tools crate binds sockets to their specific family. This means an IPv6 + // socket can not sent to IPv4, on any platform. So we need to convert and + // IPv4-mapped IPv6 address back to it's canonical IPv4 address. + let dst_ip = dst.ip().to_canonical(); + #[allow(clippy::match_like_matches_macro)] - match (self.bind_addr, addr) { - (SocketAddr::V4(_), SocketAddr::V4(..)) => true, - (SocketAddr::V6(_), SocketAddr::V6(..)) => true, + match (self.bind_addr.ip(), dst_ip) { + (IpAddr::V4(_), IpAddr::V4(_)) => true, + (IpAddr::V6(_), IpAddr::V6(_)) => true, _ => false, } } + /// Creates a canonical socket address. + /// + /// We may be asked to send IPv4-mapped IPv6 addresses. But our sockets are configured + /// to only send their actual family. So we need to map those back to the canonical + /// addresses. + #[inline] + fn canonical_addr(addr: SocketAddr) -> SocketAddr { + SocketAddr::new(addr.ip().to_canonical(), addr.port()) + } + pub(super) async fn send( &self, dst: SocketAddr, @@ -148,7 +163,7 @@ impl IpSender { let res = self .sender .send(&quinn_udp::Transmit { - destination: dst, + destination: Self::canonical_addr(dst), ecn: transmit.ecn, contents: transmit.contents, segment_size: transmit.segment_size, @@ -182,7 +197,7 @@ impl IpSender { let total_bytes = transmit.contents.len() as u64; let res = Pin::new(&mut self.sender).poll_send( &quinn_udp::Transmit { - destination: dst, + destination: Self::canonical_addr(dst), ecn: transmit.ecn, contents: transmit.contents, segment_size: transmit.segment_size, From 28bf51c98d2cecacce7bf60f6ef4d7276bfd9a6e Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Oct 2025 10:35:11 +0200 Subject: [PATCH 079/164] remove duplicate adding --- iroh/src/magicsock.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index d59fb409880..ddd59666a7f 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -441,10 +441,6 @@ impl MagicSock { // Add addr to the internal NodeMap self.node_map.add_node_addr(addr.clone(), source).await; - if let Some(url) = addr.relay_url().cloned() { - self.node_map.relay_mapped_addrs.get(&(url, addr.node_id)); - } - // // Add paths to the existing connections // self.add_paths(addr); From befde2974fe9757306cfeb51969d97a1ba7fbac9 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Oct 2025 10:35:30 +0200 Subject: [PATCH 080/164] clearer bounds writing --- iroh/src/magicsock/mapped_addrs.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 497f5072e99..33b1a1164d6 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -241,8 +241,10 @@ impl Default for AddrMap { } } -impl - AddrMap +impl AddrMap +where + K: Eq + Hash + Clone + fmt::Debug, + V: MappedAddr + Eq + Hash + Copy + fmt::Debug, { /// Returns the [`MappedAddr`], generating one if needed. pub(super) fn get(&self, key: &K) -> V { From 79cd50c5ac7d94705090f4732edce9e433a717c6 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Oct 2025 11:46:07 +0200 Subject: [PATCH 081/164] fix AddrMap impl to update both maps --- iroh/src/magicsock/mapped_addrs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 33b1a1164d6..1446194444c 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -253,6 +253,7 @@ where Some(addr) => *addr, None => { let addr = V::generate(); + inner.addrs.insert(key.clone(), addr); inner.lookup.insert(addr, key.clone()); trace!(?addr, ?key, "generated new addr"); addr From c2a192ed4c79e662d641aff8ab77fa82cbe76e49 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Oct 2025 13:08:36 +0200 Subject: [PATCH 082/164] Always use IPv6 addresses Because our AsyncUdpSocket is an IPv6-capable socket Quinn will send to IPv4-mapped IPv6 addresses for IPv4. On the sending side we map them to IPv4 addresses. But if we don't map them back on the receiving side Quinn will see the response from a different remote. --- iroh/src/magicsock/transports/ip.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index b9427c2198c..dfcd62ab791 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -1,6 +1,6 @@ use std::{ io, - net::{IpAddr, SocketAddr}, + net::{IpAddr, SocketAddr, SocketAddrV6}, pin::Pin, sync::Arc, task::{Context, Poll}, @@ -51,7 +51,15 @@ impl IpTransport { match self.socket.poll_recv_quinn(cx, bufs, metas) { Poll::Pending => Poll::Pending, Poll::Ready(Ok(n)) => { - for (addr, el) in source_addrs.iter_mut().zip(metas.iter()).take(n) { + for (addr, el) in source_addrs.iter_mut().zip(metas.iter_mut()).take(n) { + if el.addr.is_ipv4() { + // We always used IPv6 addresses in our AsyncUdpSocket. + let v6_ip = match el.addr.ip() { + IpAddr::V4(ipv4_addr) => ipv4_addr.to_ipv6_mapped(), + IpAddr::V6(ipv6_addr) => ipv6_addr, + }; + el.addr = SocketAddr::new(v6_ip.into(), el.addr.port()); + } *addr = el.addr.into(); } Poll::Ready(Ok(n)) From 04e1e3d6dcbf8f9c742432bb0433188e3c23d8c8 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Oct 2025 16:25:06 +0200 Subject: [PATCH 083/164] tweak logging, this is too noisy --- iroh-relay/src/client/tls.rs | 3 +- iroh/src/magicsock/node_map/node_state.rs | 1 - iroh/src/magicsock/transports/relay/actor.rs | 2 +- iroh/src/net_report.rs | 47 +++++++++++--------- iroh/src/net_report/report.rs | 6 +-- iroh/src/net_report/reportgen.rs | 41 +++++++++++++---- 6 files changed, 62 insertions(+), 38 deletions(-) diff --git a/iroh-relay/src/client/tls.rs b/iroh-relay/src/client/tls.rs index add947bfda9..a88157ad1a2 100644 --- a/iroh-relay/src/client/tls.rs +++ b/iroh-relay/src/client/tls.rs @@ -138,7 +138,6 @@ impl MaybeTlsStreamBuilder { async fn dial_url_direct(&self) -> Result { use tokio::net::TcpStream; - debug!(%self.url, "dial url"); let dst_ip = self .dns_resolver .resolve_host(&self.url, self.prefer_ipv6, DNS_TIMEOUT) @@ -147,7 +146,7 @@ impl MaybeTlsStreamBuilder { let port = url_port(&self.url).context(InvalidTargetPortSnafu)?; let addr = SocketAddr::new(dst_ip, port); - debug!("connecting to {}", addr); + trace!(%self.url, %addr, "connecting to"); let tcp_stream = time::timeout( DIAL_NODE_TIMEOUT, async move { TcpStream::connect(addr).await }, diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index fb1eb64fc92..65aa2a657a0 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -243,7 +243,6 @@ impl NodeStateActor { .await .whatever_context("TransportSenerActor stopped")?; } - trace!("connecting without selected path: triggering holepunching"); // This message is received *before* a connection is added. So we do // not yet have a connection to holepunch. Instead we trigger // holepunching when AddConnection is received. diff --git a/iroh/src/magicsock/transports/relay/actor.rs b/iroh/src/magicsock/transports/relay/actor.rs index b38fd3b99d8..b3386235ea7 100644 --- a/iroh/src/magicsock/transports/relay/actor.rs +++ b/iroh/src/magicsock/transports/relay/actor.rs @@ -406,7 +406,7 @@ impl ActiveRelayActor { /// Returns `None` if the actor needs to shut down. Returns `Some(Ok(client))` when the /// connection is established, and `Some(Err(err))` if dialing the relay failed. async fn run_dialing(&mut self) -> Option> { - debug!("Actor loop: connecting to relay."); + trace!("Actor loop: connecting to relay."); // We regularly flush the relay_datagrams_send queue so it is not full of stale // packets while reconnecting. Those datagrams are dropped and the QUIC congestion diff --git a/iroh/src/net_report.rs b/iroh/src/net_report.rs index 6b8d412523a..ca79c6f47f1 100644 --- a/iroh/src/net_report.rs +++ b/iroh/src/net_report.rs @@ -39,6 +39,8 @@ use n0_future::{ use n0_watcher::{Watchable, Watcher}; use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; +#[cfg(not(wasm_browser))] +use tracing::instrument; use tracing::{debug, trace, warn}; #[cfg(not(wasm_browser))] @@ -392,8 +394,6 @@ impl Client { ) -> Vec { use tracing::{Instrument, warn_span}; - debug!("spawning QAD probes"); - let Some(ref quic_client) = self.socket_state.quic_client else { return Vec::new(); }; @@ -422,6 +422,8 @@ impl Client { return self.qad_conns.current(); } + trace!("spawning QAD probes"); + // TODO: randomize choice? const MAX_RELAYS: usize = 5; @@ -432,7 +434,6 @@ impl Client { for relay_node in self.relay_map.nodes().take(MAX_RELAYS) { if if_state.have_v4 { - debug!(?relay_node.url, "v4 QAD probe"); let relay_node = relay_node.clone(); let dns_resolver = self.socket_state.dns_resolver.clone(); let quic_client = quic_client.clone(); @@ -444,12 +445,11 @@ impl Client { PROBES_TIMEOUT, run_probe_v4(relay_node, quic_client, dns_resolver), )) - .instrument(warn_span!("QAD-IPv4", %relay_url)), + .instrument(warn_span!("QADv4", %relay_url)), ); } if if_state.have_v6 { - debug!(?relay_node.url, "v6 QAD probe"); let relay_node = relay_node.clone(); let dns_resolver = self.socket_state.dns_resolver.clone(); let quic_client = quic_client.clone(); @@ -461,7 +461,7 @@ impl Client { PROBES_TIMEOUT, run_probe_v6(relay_node, quic_client, dns_resolver), )) - .instrument(warn_span!("QAD-IPv6", %relay_url)), + .instrument(warn_span!("QADv6", %relay_url)), ); } } @@ -478,6 +478,7 @@ impl Client { loop { // We early-abort the tasks once we have at least `enough_relays` reports, // and at least one ipv4 and one ipv6 report completed (if they were started, see comment above). + if reports.len() >= enough_relays && !ipv4_pending && !ipv6_pending { debug!("enough probes: {}", reports.len()); cancel_v4.cancel(); @@ -489,12 +490,14 @@ impl Client { biased; val = v4_buf.join_next(), if !v4_buf.is_empty() => { + let span = warn_span!("QADv4"); + let _guard = span.enter(); ipv4_pending = false; match val { Some(Ok(Some(Ok(res)))) => { match res { Ok((r, conn)) => { - debug!(?r, "got v4 QAD conn"); + debug!(?r, "probe report"); let url = r.node.clone(); reports.push(ProbeReport::QadIpv4(r)); if self.qad_conns.v4.is_none() { @@ -504,32 +507,34 @@ impl Client { } } Err(err) => { - debug!("probe v4 QAD failed: {err:?}"); + debug!("probe failed: {err:#}"); } } } Some(Err(err)) => { if err.is_panic() { - panic!("probe v4 QAD panicked: {err:?}"); + panic!("probe panicked: {err:#}"); } - warn!("probe v4 QAD failed: {err:?}"); + warn!("probe failed: {err:#}"); } Some(Ok(None)) => { - debug!("probe v4 QAD canceled"); + debug!("probe canceled"); } Some(Ok(Some(Err(time::Elapsed { .. })))) => { - debug!("probe v4 QAD timed out"); + debug!("probe timed out"); } None => {} } } val = v6_buf.join_next(), if !v6_buf.is_empty() => { + let span = warn_span!("QADv6"); + let _guard = span.enter(); ipv6_pending = false; match val { Some(Ok(Some(Ok(res)))) => { match res { Ok((r, conn)) => { - debug!(?r, "got v6 QAD conn"); + debug!(?r, "probe report"); let url = r.node.clone(); reports.push(ProbeReport::QadIpv6(r)); if self.qad_conns.v6.is_none() { @@ -539,21 +544,21 @@ impl Client { } } Err(err) => { - debug!("probe v6 QAD failed: {err:?}"); + debug!("probe failed: {err:#}"); } } } Some(Err(err)) => { if err.is_panic() { - panic!("probe v6 QAD panicked: {err:?}"); + panic!("probe panicked: {err:#}"); } - warn!("probe v6 QAD failed: {err:?}"); + warn!("probe failed: {err:#}"); } Some(Ok(None)) => { - debug!("probe v6 QAD canceled"); + debug!("probe canceled"); } Some(Ok(Some(Err(time::Elapsed { .. })))) => { - debug!("probe v6 QAD timed out"); + debug!("probe timed out"); } None => {} } @@ -683,7 +688,7 @@ async fn run_probe_v4( let relay_addr = reportgen::get_relay_addr_ipv4(&dns_resolver, &relay_node).await?; - debug!(?relay_addr, "relay addr v4"); + trace!(?relay_addr, "resolved relay server address"); let host = relay_node.url.host_str().context("missing host url")?; let conn = quic_client.create_conn(relay_addr.into(), host).await?; let mut receiver = conn.observed_external_addr(); @@ -712,7 +717,6 @@ async fn run_probe_v4( // that is ivp6 then the address is an [IPv4-Mapped IPv6 Addresses](https://doc.rust-lang.org/beta/std/net/struct.Ipv6Addr.html#ipv4-mapped-ipv6-addresses) let val = val.map(|val| SocketAddr::new(val.ip().to_canonical(), val.port())); let latency = conn.rtt(); - trace!(?val, ?relay_addr, ?latency, "got addr V4"); observer .set(val.map(|addr| QadProbeReport { node: node.clone(), @@ -747,7 +751,7 @@ async fn run_probe_v6( use n0_snafu::ResultExt; let relay_addr = reportgen::get_relay_addr_ipv6(&dns_resolver, &relay_node).await?; - debug!(?relay_addr, "relay addr v6"); + trace!(?relay_addr, "resolved relay server address"); let host = relay_node.url.host_str().context("missing host url")?; let conn = quic_client.create_conn(relay_addr.into(), host).await?; let mut receiver = conn.observed_external_addr(); @@ -776,7 +780,6 @@ async fn run_probe_v6( // that is ivp6 then the address is an [IPv4-Mapped IPv6 Addresses](https://doc.rust-lang.org/beta/std/net/struct.Ipv6Addr.html#ipv4-mapped-ipv6-addresses) let val = val.map(|val| SocketAddr::new(val.ip().to_canonical(), val.port())); let latency = conn.rtt(); - trace!(?val, ?relay_addr, ?latency, "got addr V6"); observer .set(val.map(|addr| QadProbeReport { node: node.clone(), diff --git a/iroh/src/net_report/report.rs b/iroh/src/net_report/report.rs index ada3cb5b6a2..8abb8462f3f 100644 --- a/iroh/src/net_report/report.rs +++ b/iroh/src/net_report/report.rs @@ -7,7 +7,7 @@ use std::{ use iroh_base::RelayUrl; use serde::{Deserialize, Serialize}; -use tracing::warn; +use tracing::{trace, warn}; use super::{ProbeReport, probes::Probe}; @@ -82,7 +82,6 @@ impl Report { self.udp_v4 = true; - tracing::debug!(?self.global_v4, ?self.mapping_varies_by_dest_ipv4, %ipp,"got"); if let Some(global) = self.global_v4 { if global == ipp { if self.mapping_varies_by_dest_ipv4.is_none() { @@ -95,6 +94,7 @@ impl Report { } else { self.global_v4 = Some(ipp); } + trace!(?self.global_v4, ?self.mapping_varies_by_dest_ipv4, %ipp, "stored report"); } #[cfg(not(wasm_browser))] ProbeReport::QadIpv6(report) => { @@ -109,7 +109,6 @@ impl Report { }; self.udp_v6 = true; - tracing::debug!(?self.global_v6, ?self.mapping_varies_by_dest_ipv6, %ipp,"got"); if let Some(global) = self.global_v6 { if global == ipp { if self.mapping_varies_by_dest_ipv6.is_none() { @@ -122,6 +121,7 @@ impl Report { } else { self.global_v6 = Some(ipp); } + trace!(?self.global_v6, ?self.mapping_varies_by_dest_ipv6, %ipp, "stored report"); } } } diff --git a/iroh/src/net_report/reportgen.rs b/iroh/src/net_report/reportgen.rs index 8b65e09454f..15bf6387acd 100644 --- a/iroh/src/net_report/reportgen.rs +++ b/iroh/src/net_report/reportgen.rs @@ -196,7 +196,7 @@ pub(super) enum ProbeFinished { impl Actor { async fn run(self) { match time::timeout(OVERALL_REPORT_TIMEOUT, self.run_inner()).await { - Ok(()) => debug!("reportgen actor finished"), + Ok(()) => trace!("reportgen actor finished"), Err(time::Elapsed { .. }) => { warn!("reportgen timed out"); } @@ -215,7 +215,7 @@ impl Actor { /// - Updates the report, cancels unneeded futures. /// - Sends the report to the net_report actor. async fn run_inner(self) { - debug!("reportstate actor starting"); + trace!("reportgen actor starting"); let mut probes = JoinSet::default(); @@ -346,7 +346,7 @@ impl Actor { if_state: IfStateDetails, probes: &mut JoinSet, ) -> CancellationToken { - debug!(?if_state, "local interface details"); + trace!(?if_state, "local interface details"); let plan = match self.last_report { Some(ref report) => { ProbePlan::with_last_report(&self.relay_map, report, &self.protocols) @@ -637,8 +637,11 @@ fn get_quic_port(relay_node: &RelayNode) -> Option { pub enum GetRelayAddrError { #[snafu(display("No valid hostname in the relay URL"))] InvalidHostname, - #[snafu(display("No suitable relay address found"))] - NoAddrFound, + #[snafu(display("No suitable relay address found for {url} ({addr_type})"))] + NoAddrFound { + url: RelayUrl, + addr_type: &'static str, + }, #[snafu(display("DNS lookup failed"))] DnsLookup { source: StaggeredError }, #[snafu(display("Relay node is not suitable for non-STUN probes"))] @@ -691,12 +694,22 @@ async fn relay_lookup_ipv4_staggered( IpAddr::V4(ip) => SocketAddrV4::new(ip, port), IpAddr::V6(_) => unreachable!("bad DNS lookup: {:?}", addr), }) - .ok_or(get_relay_addr_error::NoAddrFoundSnafu.build()), + .ok_or( + get_relay_addr_error::NoAddrFoundSnafu { + url: relay.url.clone(), + addr_type: "A", + } + .build(), + ), Err(err) => Err(get_relay_addr_error::DnsLookupSnafu.into_error(err)), } } Some(url::Host::Ipv4(addr)) => Ok(SocketAddrV4::new(addr, port)), - Some(url::Host::Ipv6(_addr)) => Err(get_relay_addr_error::NoAddrFoundSnafu.build()), + Some(url::Host::Ipv6(_addr)) => Err(get_relay_addr_error::NoAddrFoundSnafu { + url: relay.url.clone(), + addr_type: "A", + } + .build()), None => Err(get_relay_addr_error::InvalidHostnameSnafu.build()), } } @@ -723,11 +736,21 @@ async fn relay_lookup_ipv6_staggered( IpAddr::V4(_) => unreachable!("bad DNS lookup: {:?}", addr), IpAddr::V6(ip) => SocketAddrV6::new(ip, port, 0, 0), }) - .ok_or(get_relay_addr_error::NoAddrFoundSnafu.build()), + .ok_or( + get_relay_addr_error::NoAddrFoundSnafu { + url: relay.url.clone(), + addr_type: "AAAA", + } + .build(), + ), Err(err) => Err(get_relay_addr_error::DnsLookupSnafu.into_error(err)), } } - Some(url::Host::Ipv4(_addr)) => Err(get_relay_addr_error::NoAddrFoundSnafu.build()), + Some(url::Host::Ipv4(_addr)) => Err(get_relay_addr_error::NoAddrFoundSnafu { + url: relay.url.clone(), + addr_type: "AAAA", + } + .build()), Some(url::Host::Ipv6(addr)) => Ok(SocketAddrV6::new(addr, port, 0, 0)), None => Err(get_relay_addr_error::InvalidHostnameSnafu.build()), } From 660c39a0b2427d1b6fcd71b8b4012f6a6722e97c Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Oct 2025 16:31:09 +0200 Subject: [PATCH 084/164] random lost import --- iroh/src/net_report.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/iroh/src/net_report.rs b/iroh/src/net_report.rs index ca79c6f47f1..9e241a98153 100644 --- a/iroh/src/net_report.rs +++ b/iroh/src/net_report.rs @@ -39,8 +39,6 @@ use n0_future::{ use n0_watcher::{Watchable, Watcher}; use tokio::task::JoinSet; use tokio_util::sync::CancellationToken; -#[cfg(not(wasm_browser))] -use tracing::instrument; use tracing::{debug, trace, warn}; #[cfg(not(wasm_browser))] From 9da6c1c326d12df2530554443f1176dfb9bc8097 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 8 Oct 2025 16:57:32 +0200 Subject: [PATCH 085/164] keep reducing redundant logging --- iroh-relay/src/client.rs | 4 ++-- iroh-relay/src/client/conn.rs | 6 +++--- iroh/src/magicsock/transports/relay.rs | 30 +++++++------------------- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/iroh-relay/src/client.rs b/iroh-relay/src/client.rs index 9fbad400b2d..16059b7090b 100644 --- a/iroh-relay/src/client.rs +++ b/iroh-relay/src/client.rs @@ -281,7 +281,7 @@ impl ClientBuilder { let conn = Conn::new(conn, self.key_cache.clone(), &self.secret_key).await?; event!( - target: "events.net.relay.connected", + target: "iroh::_events::net::relay::connected", Level::DEBUG, url = %self.url, ); @@ -337,7 +337,7 @@ impl ClientBuilder { let conn = Conn::new(ws_stream, self.key_cache.clone(), &self.secret_key).await?; event!( - target: "events.net.relay.connected", + target: "iroh::_events::net::relay::connected", Level::DEBUG, url = %self.url, ); diff --git a/iroh-relay/src/client/conn.rs b/iroh-relay/src/client/conn.rs index 7a76bcff21a..6e0a68e813a 100644 --- a/iroh-relay/src/client/conn.rs +++ b/iroh-relay/src/client/conn.rs @@ -11,7 +11,7 @@ use iroh_base::SecretKey; use n0_future::{Sink, Stream}; use nested_enum_utils::common_fields; use snafu::{Backtrace, Snafu}; -use tracing::debug; +use tracing::{debug, trace}; use super::KeyCache; #[cfg(not(wasm_browser))] @@ -99,9 +99,9 @@ impl Conn { let mut conn = WsBytesFramed { io }; // exchange information with the server - debug!("server_handshake: started"); + trace!("server_handshake: started"); handshake::clientside(&mut conn, secret_key).await?; - debug!("server_handshake: done"); + trace!("server_handshake: done"); Ok(Self { conn, key_cache }) } diff --git a/iroh/src/magicsock/transports/relay.rs b/iroh/src/magicsock/transports/relay.rs index a1902221d50..41b557b9a49 100644 --- a/iroh/src/magicsock/transports/relay.rs +++ b/iroh/src/magicsock/transports/relay.rs @@ -290,38 +290,24 @@ impl RelaySender { ) -> Poll> { match ready!(self.sender.poll_reserve(cx)) { Ok(()) => { - trace!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message queued"); - let contents = datagrams_from_transmit(transmit); let item = RelaySendItem { remote_node: dest_node, url: dest_url.clone(), datagrams: contents, }; - let dest_node = item.remote_node; - let dest_url = item.url.clone(); - match self.sender.send_item(item) { Ok(()) => Poll::Ready(Ok(())), - Err(_err) => { - error!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message dropped, channel to actor is closed"); - Poll::Ready(Err(io::Error::new( - io::ErrorKind::ConnectionReset, - "channel to actor is closed", - ))) - } + Err(_err) => Poll::Ready(Err(io::Error::new( + io::ErrorKind::ConnectionReset, + "channel to actor is closed", + ))), } } - Err(_err) => { - error!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message dropped, channel to actor is closed"); - Poll::Ready(Err(io::Error::new( - io::ErrorKind::ConnectionReset, - "channel to actor is closed", - ))) - } + Err(_err) => Poll::Ready(Err(io::Error::new( + io::ErrorKind::ConnectionReset, + "channel to actor is closed", + ))), } } } From 85cedca028f9e44e875e9c3d2bad37659eebfe24 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 12:30:19 +0200 Subject: [PATCH 086/164] Make transports Addrs use the canonical form --- iroh/src/magicsock/transports/ip.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index dfcd62ab791..4a4d83c188f 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -51,16 +51,21 @@ impl IpTransport { match self.socket.poll_recv_quinn(cx, bufs, metas) { Poll::Pending => Poll::Pending, Poll::Ready(Ok(n)) => { - for (addr, el) in source_addrs.iter_mut().zip(metas.iter_mut()).take(n) { - if el.addr.is_ipv4() { - // We always used IPv6 addresses in our AsyncUdpSocket. - let v6_ip = match el.addr.ip() { + for (source_addr, meta) in source_addrs.iter_mut().zip(metas.iter_mut()).take(n) { + if meta.addr.is_ipv4() { + // The AsyncUdpSocket is an AF_INET6 socket and needs to show this + // as coming from an IPv4-mapped IPv6 addresses, since Quinn will + // use those when sending on an INET6 socket. + let v6_ip = match meta.addr.ip() { IpAddr::V4(ipv4_addr) => ipv4_addr.to_ipv6_mapped(), IpAddr::V6(ipv6_addr) => ipv6_addr, }; - el.addr = SocketAddr::new(v6_ip.into(), el.addr.port()); + meta.addr = SocketAddr::new(v6_ip.into(), meta.addr.port()); } - *addr = el.addr.into(); + // The transport addresses are internal to iroh and we always want those + // to remain the canonical address. + *source_addr = + SocketAddr::new(meta.addr.ip().to_canonical(), meta.addr.port()).into(); } Poll::Ready(Ok(n)) } From 7ee963fb64def5c22bb211e66d4026b181b10e21 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 12:31:03 +0200 Subject: [PATCH 087/164] insert PathId::ZERO in the path_id_map --- iroh/src/magicsock/node_map/node_state.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 65aa2a657a0..880d573862a 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -258,13 +258,15 @@ impl NodeStateActor { // This is a good time to clean up connections. self.cleanup_connections(); - let stable_id = conn.stable_id(); + let conn_id = conn.stable_id(); let events = BroadcastStream::new(conn.path_events()); - let stream = events.map(move |evt| (stable_id, evt)); + let stream = events.map(move |evt| (conn_id, evt)); self.path_events.push(Box::pin(stream)); - self.connections.insert(stable_id, handle.clone()); + self.connections.insert(conn_id, handle.clone()); if let Some(conn) = handle.upgrade() { if let Some(addr) = self.path_transports_addr(&conn, PathId::ZERO) { + self.path_id_map + .insert((conn_id, PathId::ZERO), addr.clone()); self.paths .entry(addr) .or_default() From 9de1055ae6d1edf223d20f5b5ffd58860b176d05 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 12:32:04 +0200 Subject: [PATCH 088/164] fix addr selection for holepunching --- iroh/src/magicsock/node_map/node_state.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 880d573862a..25cbc4a31c8 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -450,6 +450,7 @@ impl NodeStateActor { .map(|last_hp| { // Addrs are allowed to disappear, but if there are new ones we need to // holepunch again. + trace!(?last_hp, ?local_addrs, ?remote_addrs, "addrs to holepunch?"); !remote_addrs.is_subset(&last_hp.remote_addrs) || !local_addrs.is_subset(&last_hp.local_addrs) }) @@ -482,12 +483,12 @@ impl NodeStateActor { if state .sources .get(&Source::CallMeMaybe) - .map(|when| when.elapsed() >= CALL_ME_MAYBE_VALIDITY) + .map(|when| when.elapsed() <= CALL_ME_MAYBE_VALIDITY) .unwrap_or_default() || state .sources .get(&Source::Ping) - .map(|when| when.elapsed() >= CALL_ME_MAYBE_VALIDITY) + .map(|when| when.elapsed() <= CALL_ME_MAYBE_VALIDITY) .unwrap_or_default() { Some(*addr) From ae07d688efa45db327f90479c31873b0995644cd Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 12:32:41 +0200 Subject: [PATCH 089/164] bunch of logging imporvements --- iroh/src/magicsock.rs | 1 - iroh/src/magicsock/node_map/node_state.rs | 23 ++++++++++++----------- iroh/src/magicsock/node_map/path_state.rs | 4 +--- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 4cdaf6a4de4..dcb87dcff59 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -661,7 +661,6 @@ impl MagicSock { /// Handles a discovery message. #[instrument("disco_in", skip_all, fields(node = %sender.fmt_short(), ?src))] fn handle_disco_message(&self, sender: PublicKey, sealed_box: &[u8], src: &transports::Addr) { - trace!("handle_disco_message start"); if self.is_closed() { return; } diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 25cbc4a31c8..6549af18898 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -294,7 +294,7 @@ impl NodeStateActor { } NodeStateMessage::CallMeMaybeReceived(msg) => { event!( - target: "iroh::_events::call-me-maybe::recv", + target: "iroh::_events::call_me_maybe::recv", Level::DEBUG, remote_node = %self.node_id.fmt_short(), addrs = ?msg.my_numbers, @@ -306,7 +306,7 @@ impl NodeStateActor { let path = self.paths.entry(dst.clone()).or_default(); path.sources.insert(Source::CallMeMaybe, now); - path.ping_sent = Some(ping.clone()); + path.ping_sent = Some(ping.tx_id); event!( target: "iroh::_events::ping::sent", @@ -352,12 +352,11 @@ impl NodeStateActor { } NodeStateMessage::PongReceived(pong, src) => { let Some(state) = self.paths.get(&src) else { - warn!(path = ?src, "ignoring DISCO Pong for unknown path"); + warn!(path = ?src, ?self.paths, "ignoring DISCO Pong for unknown path"); return Ok(()); }; - let ping_tx = state.ping_sent.as_ref().map(|ping| ping.tx_id); - if ping_tx != Some(pong.tx_id) { - debug!(path = ?src, ?ping_tx, pong_tx = ?pong.tx_id, + if state.ping_sent != Some(pong.tx_id) { + debug!(path = ?src, ?state.ping_sent, pong_tx = ?pong.tx_id, "ignoring unknown DISCO Pong for path"); return Ok(()); } @@ -458,8 +457,9 @@ impl NodeStateActor { if !new_addrs { if let Some(ref last_hp) = self.last_holepunch { let next_hp = last_hp.when + HOLEPUNCH_ATTEMPTS_INTERVAL; - if next_hp > Instant::now() { - trace!(scheduled_in = ?next_hp, "not holepunching: no new addresses"); + let now = Instant::now(); + if next_hp > now { + trace!(scheduled_in = ?(now - next_hp), "not holepunching: no new addresses"); self.scheduled_holepunch = Some(next_hp); return; } @@ -504,8 +504,8 @@ impl NodeStateActor { /// - DISCO pings will be sent to addresses recently advertised in a call-me-maybe /// message. /// - A DISCO call-me-maybe message advertising our own addresses will be sent. + #[instrument(skip_all)] async fn do_holepunching(&mut self) { - trace!("holepunching"); let Some(relay_addr) = self .paths .iter() @@ -532,7 +532,7 @@ impl NodeStateActor { txn = ?msg.tx_id, ); let addr = transports::Addr::Ip(*dst); - self.paths.entry(addr.clone()).or_default().ping_sent = Some(msg.clone()); + self.paths.entry(addr.clone()).or_default().ping_sent = Some(msg.tx_id); self.send_disco_message(addr, disco::Message::Ping(msg)) .await; } @@ -547,7 +547,7 @@ impl NodeStateActor { let local_addrs: BTreeSet = my_numbers.iter().copied().collect(); let msg = disco::CallMeMaybe { my_numbers }; event!( - target: "iroh::_events::call-me-maybe::sent", + target: "iroh::_events::call_me_maybe::sent", Level::DEBUG, remote_node = %self.node_id.fmt_short(), dst = ?relay_addr, @@ -726,6 +726,7 @@ impl NodeStateActor { /// /// The selected path is added to any connections which do not yet have it. Any unused /// direct paths are close from all connections. + #[instrument(skip_all)] fn select_path(&mut self) { // Find the lowest RTT across all connections for each open path. The long way, so // we get to trace-log *all* RTTs. diff --git a/iroh/src/magicsock/node_map/path_state.rs b/iroh/src/magicsock/node_map/path_state.rs index bbd5bf8a21e..3622cd3c910 100644 --- a/iroh/src/magicsock/node_map/path_state.rs +++ b/iroh/src/magicsock/node_map/path_state.rs @@ -4,8 +4,6 @@ use std::collections::HashMap; use n0_future::time::Instant; -use crate::disco; - use super::Source; /// The state of a single path to the remote endpoint. @@ -22,5 +20,5 @@ pub(super) struct PathState { /// of the map of sources down to one entry per type of source. pub(super) sources: HashMap, /// The last ping sent on this path. - pub(super) ping_sent: Option, + pub(super) ping_sent: Option, } From 0ce64c90c41189d6ce1c7c55c6c5209d45030fe1 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 17:43:13 +0200 Subject: [PATCH 090/164] remove redundant logging --- iroh/src/magicsock/transports/ip.rs | 2 +- iroh/src/magicsock/transports/relay.rs | 18 +++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index 4a4d83c188f..a1c2fabad16 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -1,6 +1,6 @@ use std::{ io, - net::{IpAddr, SocketAddr, SocketAddrV6}, + net::{IpAddr, SocketAddr}, pin::Pin, sync::Arc, task::{Context, Poll}, diff --git a/iroh/src/magicsock/transports/relay.rs b/iroh/src/magicsock/transports/relay.rs index 41b557b9a49..ba42eaac5dc 100644 --- a/iroh/src/magicsock/transports/relay.rs +++ b/iroh/src/magicsock/transports/relay.rs @@ -265,19 +265,11 @@ impl RelaySender { return Err(io::Error::other("channel closed")); }; match sender.send(item).await { - Ok(_) => { - trace!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message queued"); - Ok(()) - } - Err(mpsc::error::SendError(_)) => { - error!(node = %dest_node.fmt_short(), relay_url = %dest_url, - "send relay: message dropped, channel to actor is closed"); - Err(io::Error::new( - io::ErrorKind::ConnectionReset, - "channel to actor is closed", - )) - } + Ok(_) => Ok(()), + Err(mpsc::error::SendError(_)) => Err(io::Error::new( + io::ErrorKind::ConnectionReset, + "channel to actor is closed", + )), } } From 9a592cecbf740f5753beb9fa3f0370e3543e85f9 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 20:50:22 +0200 Subject: [PATCH 091/164] plug through a minimal PathInfo I'm really not that happy with info or implementation. This should be improved. --- iroh/src/discovery.rs | 4 - iroh/src/endpoint.rs | 55 +++++- iroh/src/magicsock.rs | 21 ++- iroh/src/magicsock/node_map.rs | 11 +- iroh/src/magicsock/node_map/node_state.rs | 212 +++++++++++++++++----- iroh/src/magicsock/transports/relay.rs | 4 +- 6 files changed, 238 insertions(+), 69 deletions(-) diff --git a/iroh/src/discovery.rs b/iroh/src/discovery.rs index d16cfb12486..4eb212afc30 100644 --- a/iroh/src/discovery.rs +++ b/iroh/src/discovery.rs @@ -497,10 +497,6 @@ impl Discovery for ConcurrentDiscovery { } } -/// Maximum duration since the last control or data message received from an endpoint to make us -/// start a discovery task. -const MAX_AGE: Duration = Duration::from_secs(10); - /// A wrapper around a tokio task which runs a node discovery. pub(super) struct DiscoveryTask { on_first_rx: oneshot::Receiver>, diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 15c16d7f5f3..1b45e0e3935 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -27,6 +27,7 @@ use n0_future::time::Duration; use n0_watcher::Watcher; use nested_enum_utils::common_fields; use pin_project::pin_project; +use quinn_proto::PathId; use snafu::{ResultExt, Snafu, ensure}; use tracing::{debug, instrument, trace, warn}; use url::Url; @@ -41,8 +42,9 @@ use crate::{ UserData, pkarr::PkarrPublisher, }, magicsock::{ - self, Handle, OwnAddressSnafu, - mapped_addrs::{MappedAddr, NodeIdMappedAddr}, + self, Handle, OwnAddressSnafu, PathInfo, + mapped_addrs::{MappedAddr, MultipathMappedAddr, NodeIdMappedAddr}, + node_map::TransportType, }, metrics::EndpointMetrics, net_report::Report, @@ -1692,6 +1694,7 @@ impl Future for ZeroRttAccepted { #[derive(Debug, Clone)] pub struct Connection { inner: quinn::Connection, + paths_info: n0_watcher::Direct>, } #[allow(missing_docs)] @@ -1703,13 +1706,28 @@ pub struct RemoteNodeIdError { impl Connection { fn new(inner: quinn::Connection, remote_id: Option, ep: &Endpoint) -> Self { - let conn = Connection { inner }; + let mut paths_info = Vec::with_capacity(1); + if let Some(path0) = inner.path(PathId::ZERO) { + // This all is supposed to be infallible, but anyway. + if let Ok(remote) = path0.remote_address() { + let mapped = MultipathMappedAddr::from(remote); + let transport = TransportType::from(mapped); + paths_info.push(PathInfo { transport }); + } + } + let paths_info_watcher = n0_watcher::Watchable::new(paths_info); + let conn = Connection { + inner, + paths_info: paths_info_watcher.watch(), + }; // Grab the remote identity and register this connection if let Some(remote) = remote_id { - ep.msock.register_connection(remote, &conn.inner); + ep.msock + .register_connection(remote, &conn.inner, paths_info_watcher); } else if let Ok(remote) = conn.remote_node_id() { - ep.msock.register_connection(remote, &conn.inner); + ep.msock + .register_connection(remote, &conn.inner, paths_info_watcher); } else { warn!("unable to determine node id for the remote"); } @@ -1970,6 +1988,15 @@ impl Connection { self.inner.stable_id() } + /// Returns information about the network paths in use by this connection. + /// + /// A connection can have several network paths to the remote endpoint, commonly there + /// will be a path via the relay server and a holepunched path. This returns all the + /// paths in use by this connection. + pub fn paths_info(&self) -> impl Watcher> { + self.paths_info.clone() + } + /// Derives keying material from this connection's TLS session secrets. /// /// When both peers call this method with the same `label` and `context` @@ -2127,6 +2154,7 @@ mod tests { RelayMode, discovery::static_provider::StaticProvider, endpoint::{ConnectOptions, Connection, ConnectionType}, + magicsock::node_map::TransportType, protocol::{AcceptError, ProtocolHandler, Router}, test_utils::{run_relay_server, run_relay_server_with}, }; @@ -2508,6 +2536,16 @@ mod tests { let conn = ep.connect(dst, TEST_ALPN).await?; let mut send = conn.open_uni().await.e()?; send.write_all(b"hello").await.e()?; + let mut paths = conn.paths_info().stream(); + info!("Waiting for direct connection"); + while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + if infos.iter().any(|info| info.transport == TransportType::Ip) { + break; + } + } + info!("Have direct connection"); + send.write_all(b"close please").await.e()?; send.finish().e()?; Ok(conn.closed().await) } @@ -2519,8 +2557,13 @@ mod tests { let node_id = conn.remote_node_id()?; assert_eq!(node_id, src); let mut recv = conn.accept_uni().await.e()?; + let mut msg = [0u8; 5]; + recv.read_exact(&mut msg).await.e()?; + assert_eq!(&msg, b"hello"); + info!("received hello"); let msg = recv.read_to_end(100).await.e()?; - assert_eq!(msg, b"hello"); + assert_eq!(msg, b"close please"); + info!("received 'close please'"); // Dropping the connection closes it just fine. Ok(()) } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index dcb87dcff59..856e87c7790 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -82,11 +82,15 @@ pub(crate) mod transports; use mapped_addrs::{MappedAddr, NodeIdMappedAddr}; -pub use self::{metrics::Metrics, node_map::ConnectionType}; +pub use self::{ + metrics::Metrics, + node_map::{ConnectionType, PathInfo}, +}; -/// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically -/// expire at 30 seconds, so this is a few seconds shy of that. -const ENDPOINTS_FRESH_ENOUGH_DURATION: Duration = Duration::from_secs(27); +// TODO: Use this +// /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically +// /// expire at 30 seconds, so this is a few seconds shy of that. +// const ENDPOINTS_FRESH_ENOUGH_DURATION: Duration = Duration::from_secs(27); /// The duration in which we send keep-alives. /// @@ -276,7 +280,12 @@ impl MagicSock { /// connection. /// /// [`NodeStateActor`]: crate::magicsock::node_map::node_state::NodeStateActor - pub(crate) fn register_connection(&self, remote: NodeId, conn: &quinn::Connection) { + pub(crate) fn register_connection( + &self, + remote: NodeId, + conn: &quinn::Connection, + paths_info: n0_watcher::Watchable>, + ) { // TODO: Spawning tasks like this is obviously bad. But it is solvable: // - This is only called from inside Connection::new. // - Connection::new is called from: @@ -294,7 +303,7 @@ impl MagicSock { // do with this connection inside of the NodeStateActor anyway. let weak_handle = conn.weak_handle(); let node_state = self.node_map.node_state_actor(remote); - let msg = NodeStateMessage::AddConnection(weak_handle); + let msg = NodeStateMessage::AddConnection(weak_handle, paths_info); tokio::task::spawn(async move { node_state.send(msg).await.ok(); diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 8cc8cf4d7a4..46c8ca6b283 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -32,13 +32,14 @@ use super::{ mod node_state; mod path_state; -pub(super) use node_state::NodeStateMessage; +pub use node_state::{ConnectionType, PathInfo, TransportType}; -pub use node_state::ConnectionType; +pub(super) use node_state::NodeStateMessage; -/// Number of nodes that are inactive for which we keep info about. This limit is enforced -/// periodically via [`NodeMap::prune_inactive`]. -const MAX_INACTIVE_NODES: usize = 30; +// TODO: use this +// /// Number of nodes that are inactive for which we keep info about. This limit is enforced +// /// periodically via [`NodeMap::prune_inactive`]. +// const MAX_INACTIVE_NODES: usize = 30; /// Map of the [`NodeState`] information for all the known nodes. /// diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 6549af18898..145f4f2c08d 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -6,7 +6,7 @@ use n0_future::{ task::AbortOnDropHandle, time::{Duration, Instant}, }; -use n0_watcher::Watcher; +use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; use quinn_proto::{PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; @@ -16,8 +16,9 @@ use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; -#[cfg(any(test, feature = "test-utils"))] -use crate::endpoint::PathSelection; +// TODO: Use this +// #[cfg(any(test, feature = "test-utils"))] +// use crate::endpoint::PathSelection; use crate::{ disco::{self}, endpoint::DirectAddr, @@ -31,26 +32,31 @@ use crate::{ use super::{Source, TransportsSenderMessage, path_state::PathState}; -/// Number of addresses that are not active that we keep around per node. -/// -/// See [`NodeState::prune_direct_addresses`]. -pub(super) const MAX_INACTIVE_DIRECT_ADDRESSES: usize = 20; +// TODO: use this +// /// Number of addresses that are not active that we keep around per node. +// /// +// /// See [`NodeState::prune_direct_addresses`]. +// pub(super) const MAX_INACTIVE_DIRECT_ADDRESSES: usize = 20; -/// How long since an endpoint path was last alive before it might be pruned. -const LAST_ALIVE_PRUNE_DURATION: Duration = Duration::from_secs(120); +// TODO: use this +// /// How long since an endpoint path was last alive before it might be pruned. +// const LAST_ALIVE_PRUNE_DURATION: Duration = Duration::from_secs(120); -/// The latency at or under which we don't try to upgrade to a better path. -const GOOD_ENOUGH_LATENCY: Duration = Duration::from_millis(5); +// TODO: use this +// /// The latency at or under which we don't try to upgrade to a better path. +// const GOOD_ENOUGH_LATENCY: Duration = Duration::from_millis(5); -/// How long since the last activity we try to keep an established endpoint peering alive. -/// -/// It's also the idle time at which we stop doing QAD queries to keep NAT mappings alive. -pub(super) const SESSION_ACTIVE_TIMEOUT: Duration = Duration::from_secs(45); +// TODO: use this +// /// How long since the last activity we try to keep an established endpoint peering alive. +// /// +// /// It's also the idle time at which we stop doing QAD queries to keep NAT mappings alive. +// pub(super) const SESSION_ACTIVE_TIMEOUT: Duration = Duration::from_secs(45); -/// How often we try to upgrade to a better path. -/// -/// Even if we have some non-relay route that works. -const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); +// TODO: use this +// /// How often we try to upgrade to a better path. +// /// +// /// Even if we have some non-relay route that works. +// const UPGRADE_INTERVAL: Duration = Duration::from_secs(60); /// The value which we close paths. // TODO: Quinn should just do this. Also, I made this value up. @@ -88,9 +94,8 @@ pub(super) struct NodeStateActor { /// All connections we have to this remote node. /// /// The key is the [`quinn::Connection::stable_id`]. - connections: FxHashMap, + connections: FxHashMap, /// Events emitted by Quinn about path changes. - // path_events: MergeUnbounded>, path_events: MergeUnbounded< Pin< Box< @@ -209,7 +214,7 @@ impl NodeStateActor { } } Some((id, evt)) = self.path_events.next() => { - self.handle_path_event(id, evt).await; + self.handle_path_event(id, evt); } _ = self.local_addrs.updated() => { trace!("local addrs updated, triggering holepunching"); @@ -248,7 +253,7 @@ impl NodeStateActor { // holepunching when AddConnection is received. } } - NodeStateMessage::AddConnection(handle) => { + NodeStateMessage::AddConnection(handle, paths_info) => { if let Some(conn) = handle.upgrade() { // Remove any conflicting stable_ids from the local state. let stable_id = conn.stable_id(); @@ -262,7 +267,13 @@ impl NodeStateActor { let events = BroadcastStream::new(conn.path_events()); let stream = events.map(move |evt| (conn_id, evt)); self.path_events.push(Box::pin(stream)); - self.connections.insert(conn_id, handle.clone()); + self.connections.insert( + conn_id, + ConnectionState { + handle: handle.clone(), + paths_info, + }, + ); if let Some(conn) = handle.upgrade() { if let Some(addr) = self.path_transports_addr(&conn, PathId::ZERO) { self.path_id_map @@ -384,7 +395,7 @@ impl NodeStateActor { if let Some(conn) = self .connections .get(conn_id) - .and_then(|handle| handle.upgrade()) + .and_then(|c| c.handle.upgrade()) { if let Some(path_stats) = conn.stats().paths.get(path_id) { return Some(path_stats.rtt); @@ -459,7 +470,7 @@ impl NodeStateActor { let next_hp = last_hp.when + HOLEPUNCH_ATTEMPTS_INTERVAL; let now = Instant::now(); if next_hp > now { - trace!(scheduled_in = ?(now - next_hp), "not holepunching: no new addresses"); + trace!(scheduled_in = ?(next_hp - now), "not holepunching: no new addresses"); self.scheduled_holepunch = Some(next_hp); return; } @@ -617,23 +628,26 @@ impl NodeStateActor { .connections .iter() .filter_map(|(conn_id, handle)| (!conns_with_path.contains(conn_id)).then_some(handle)) - .filter_map(|handle| handle.upgrade()) + .filter_map(|c| c.handle.upgrade()) .filter(|conn| conn.side().is_client()) { - match conn.open_path_ensure(quic_addr, path_status).path_id() { + let fut = conn.open_path_ensure(quic_addr, path_status); + match fut.path_id() { Some(path_id) => { + trace!(conn_id = conn.stable_id(), ?path_id, "opening new path"); self.path_id_map .insert((conn.stable_id(), path_id), open_addr.clone()); } None => { - warn!("Opening path failed"); + let ret = poll_once(fut); + warn!(?ret, "Opening path failed"); } } } } #[instrument(skip(self))] - async fn handle_path_event( + fn handle_path_event( &mut self, conn_id: usize, event: Result, @@ -644,11 +658,11 @@ impl NodeStateActor { // state of the connection and it's paths are? return; }; - let Some(handle) = self.connections.get(&conn_id) else { + let Some(conn_state) = self.connections.get(&conn_id) else { trace!("event for removed connection"); return; }; - let Some(conn) = handle.upgrade() else { + let Some(conn) = conn_state.handle.upgrade() else { trace!("event for closed connection"); return; }; @@ -662,6 +676,28 @@ impl NodeStateActor { path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); + if let Some(addr) = self.path_transports_addr(&conn, path_id) { + event!( + target: "iroh::_events::path::open", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + ?addr, + conn_id, + ?path_id, + ); + self.path_id_map.insert((conn_id, path_id), addr.clone()); + self.paths + .entry(addr.clone()) + .or_default() + .sources + .insert(Source::Connection, Instant::now()); + let mut paths = conn_state.paths_info.get(); + paths.push(PathInfo { + transport: addr.into(), + }); + conn_state.paths_info.set(paths).ok(); + } + self.select_path(); } PathEvent::Abandoned { id, path_stats } => { @@ -670,11 +706,35 @@ impl NodeStateActor { self.path_id_map.remove(&(conn_id, id)); } PathEvent::Closed { id, .. } | PathEvent::LocallyClosed { id, .. } => { - // If one connection closes this path, close it on all connections. let Some(addr) = self.path_id_map.get(&(conn_id, id)) else { debug!("path not in path_id_map"); return; }; + event!( + target: "iroh::_events::path::closed", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + ?addr, + conn_id, + path_id = ?id, + ); + // Remove this from the public PathInfo. + if let Some(state) = self.connections.get(&conn_id) { + let mut path_info = state.paths_info.get(); + let transport = TransportType::from(addr); + let mut done = false; + path_info.retain(|info| { + if !done && info.transport == transport { + done = true; + false + } else { + true + } + }); + state.paths_info.set(path_info).ok(); + } + + // If one connection closes this path, close it on all connections. for (conn_id, path_id) in self .path_id_map .iter() @@ -684,8 +744,7 @@ impl NodeStateActor { if let Some(conn) = self .connections .get(&conn_id) - .map(|handle| handle.upgrade()) - .flatten() + .and_then(|c| c.handle.upgrade()) { if let Some(path) = conn.path(*path_id) { trace!(?addr, ?conn_id, ?path_id, "closing path"); @@ -705,12 +764,12 @@ impl NodeStateActor { /// Clean up connections which no longer exist. // TODO: Call this on a schedule. fn cleanup_connections(&mut self) { - self.connections - .retain(|_, handle| handle.upgrade().is_some()); + self.connections.retain(|_, c| c.handle.upgrade().is_some()); let mut stable_ids = BTreeSet::new(); - for handle in self.connections.values() { - handle + for state in self.connections.values() { + state + .handle .upgrade() .map(|conn| stable_ids.insert(conn.stable_id())); } @@ -734,7 +793,7 @@ impl NodeStateActor { for (conn_id, conn) in self .connections .iter() - .filter_map(|(id, handle)| handle.upgrade().map(|conn| (*id, conn))) + .filter_map(|(id, c)| c.handle.upgrade().map(|conn| (*id, conn))) { let stats = conn.stats(); for (path_id, stats) in stats.paths { @@ -828,7 +887,7 @@ impl NodeStateActor { if let Some(conn) = self .connections .get(conn_id) - .map(|handle| handle.upgrade()) + .map(|c| c.handle.upgrade()) .flatten() { trace!(?addr, ?conn_id, ?path_id, "closing direct path"); @@ -890,15 +949,15 @@ pub(crate) enum NodeStateMessage { /// This is not acceptable to use on the normal send path, as it is an async send /// operation with a bunch more copying. So it should only be used for sending QUIC /// Initial packets. - #[debug("SendDatagram(OwnedTransmit)")] + #[debug("SendDatagram(..)")] SendDatagram(OwnedTransmit), /// Adds an active connection to this remote node. /// /// The connection will now be managed by this actor. Holepunching will happen when /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. - #[debug("AddConnection(WeakConnectionHandle)")] - AddConnection(WeakConnectionHandle), + #[debug("AddConnection(..)")] + AddConnection(WeakConnectionHandle, Watchable>), /// Adds a [`NodeAddr`] with locations where the node might be reachable. AddNodeAddr(NodeAddr, Source), /// Process a received DISCO CallMeMaybe message. @@ -910,11 +969,12 @@ pub(crate) enum NodeStateMessage { /// Asks if there is any possible path that could be used. /// /// This does not mean there is any guarantee that the remote endpoint is reachable. - #[debug("CanSend(onseshot::Sender)")] + #[debug("CanSend(..)")] CanSend(oneshot::Sender), /// Returns the current latency to the remote endpoint. /// /// TODO: This is more of a placeholder message currently. Check MagicSock::latency. + #[debug("Latency(..)")] Latency(oneshot::Sender>), } @@ -966,3 +1026,65 @@ pub enum ConnectionType { #[display("none")] None, } + +#[derive(Debug)] +struct ConnectionState { + handle: WeakConnectionHandle, + paths_info: Watchable>, +} + +/// Information about a network path used by a [`Connection`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PathInfo { + /// The kind of transport this network path is using. + pub transport: TransportType, +} + +/// Different kinds of transports a [`Connection`] can use. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TransportType { + /// A transport via a relay server. + Relay, + /// A transport via an IP connection. + Ip, +} + +impl From for TransportType { + fn from(source: MultipathMappedAddr) -> Self { + match source { + MultipathMappedAddr::Mixed(_) => { + error!("paths should not use mixed addrs"); + TransportType::Relay + } + MultipathMappedAddr::Relay(_) => TransportType::Relay, + MultipathMappedAddr::Ip(_) => TransportType::Ip, + } + } +} + +impl From for TransportType { + fn from(source: transports::Addr) -> Self { + match source { + transports::Addr::Ip(_) => Self::Ip, + transports::Addr::Relay(_, _) => Self::Relay, + } + } +} + +impl From<&transports::Addr> for TransportType { + fn from(source: &transports::Addr) -> Self { + match source { + transports::Addr::Ip(_) => Self::Ip, + transports::Addr::Relay(_, _) => Self::Relay, + } + } +} + +/// Poll a future once, like n0_future::future::poll_once but sync. +fn poll_once>(fut: F) -> Option { + let fut = std::pin::pin!(fut); + match fut.poll(&mut std::task::Context::from_waker(std::task::Waker::noop())) { + std::task::Poll::Ready(res) => Some(res), + std::task::Poll::Pending => None, + } +} diff --git a/iroh/src/magicsock/transports/relay.rs b/iroh/src/magicsock/transports/relay.rs index ba42eaac5dc..344e6efb7c6 100644 --- a/iroh/src/magicsock/transports/relay.rs +++ b/iroh/src/magicsock/transports/relay.rs @@ -14,7 +14,7 @@ use n0_future::{ use n0_watcher::{Watchable, Watcher as _}; use tokio::sync::mpsc; use tokio_util::sync::PollSender; -use tracing::{Instrument, error, info_span, trace, warn}; +use tracing::{Instrument, error, info_span, warn}; use super::{Addr, Transmit}; @@ -259,8 +259,6 @@ impl RelaySender { datagrams: contents, }; - let dest_node = item.remote_node; - let dest_url = item.url.clone(); let Some(sender) = self.sender.get_ref() else { return Err(io::Error::other("channel closed")); }; From eb92bb721efb93efbbe84bb94ba70bb8e1748e41 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 20:53:37 +0200 Subject: [PATCH 092/164] We don't pend CallMeMaybe anymore The refresh interval and time when we went to pending was about the same anyway. Instead we need this info to just be up to date. This queuing mechanism was the wrong approach. --- iroh/src/magicsock.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 856e87c7790..d4e67ecb78b 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -820,7 +820,6 @@ enum UpdateReason { /// Initial state #[default] None, - RefreshForPeering, Periodic, PortmapUpdated, LinkChangeMajor, @@ -1335,7 +1334,6 @@ enum DiscoBoxError { #[derive(Debug)] enum ActorMessage { NetworkChange, - ScheduleDirectAddrUpdate(UpdateReason, Option<(NodeId, RelayUrl)>), #[cfg(test)] ForceNetworkChange(bool), } @@ -1576,14 +1574,6 @@ impl Actor { ActorMessage::NetworkChange => { self.network_monitor.network_change().await.ok(); } - ActorMessage::ScheduleDirectAddrUpdate(why, data) => { - if let Some((node, url)) = data { - self.pending_call_me_maybes.insert(node, url); - } - let state = self.netmon_watcher.get(); - self.direct_addr_update_state - .schedule_run(why, state.into()); - } #[cfg(test)] ActorMessage::ForceNetworkChange(is_major) => { self.handle_network_change(is_major).await; From 6e31e4d187b478afd172cbe6479ebae781c88d00 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 20:58:55 +0200 Subject: [PATCH 093/164] clippy, at last some code quality --- iroh/src/endpoint.rs | 4 +-- iroh/src/magicsock/mapped_addrs.rs | 2 +- iroh/src/magicsock/node_map.rs | 30 ++++++++--------- iroh/src/magicsock/node_map/node_state.rs | 40 ++++++++++------------- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 1b45e0e3935..a03b7abde1e 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1509,7 +1509,7 @@ impl Future for IncomingFuture { Poll::Pending => Poll::Pending, Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Ready(Ok(inner)) => { - let conn = Connection::new(inner, None, &this.ep); + let conn = Connection::new(inner, None, this.ep); Poll::Ready(Ok(conn)) } } @@ -1646,7 +1646,7 @@ impl Future for Connecting { Poll::Pending => Poll::Pending, Poll::Ready(Err(err)) => Poll::Ready(Err(err)), Poll::Ready(Ok(inner)) => { - let conn = Connection::new(inner, *this.remote_node_id, &this.ep); + let conn = Connection::new(inner, *this.remote_node_id, this.ep); Poll::Ready(Ok(conn)) } } diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 1446194444c..dc91670e8e5 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -249,7 +249,7 @@ where /// Returns the [`MappedAddr`], generating one if needed. pub(super) fn get(&self, key: &K) -> V { let mut inner = self.inner.lock().expect("poisoned"); - match inner.addrs.get(&key) { + match inner.addrs.get(key) { Some(addr) => *addr, None => { let addr = V::generate(); diff --git a/iroh/src/magicsock/node_map.rs b/iroh/src/magicsock/node_map.rs index 46c8ca6b283..41e7f4142ae 100644 --- a/iroh/src/magicsock/node_map.rs +++ b/iroh/src/magicsock/node_map.rs @@ -378,23 +378,19 @@ impl TransportsSenderActor { async fn run(self, mut inbox: mpsc::Receiver) { use TransportsSenderMessage::SendDatagram; - loop { - if let Some(SendDatagram(dst, owned_transmit)) = inbox.recv().await { - let transmit = transports::Transmit { - ecn: owned_transmit.ecn, - contents: owned_transmit.contents.as_ref(), - segment_size: owned_transmit.segment_size, - }; - let len = transmit.contents.len(); - match self.sender.send(&dst, None, &transmit).await { - Ok(()) => {} - Err(err) => { - trace!(?dst, %len, "transmit failed to send: {err:#}"); - } - }; - } else { - break; - } + while let Some(SendDatagram(dst, owned_transmit)) = inbox.recv().await { + let transmit = transports::Transmit { + ecn: owned_transmit.ecn, + contents: owned_transmit.contents.as_ref(), + segment_size: owned_transmit.segment_size, + }; + let len = transmit.contents.len(); + match self.sender.send(&dst, None, &transmit).await { + Ok(()) => {} + Err(err) => { + trace!(?dst, %len, "transmit failed to send: {err:#}"); + } + }; } trace!("actor terminating"); } diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 145f4f2c08d..8b19c959189 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -96,6 +96,7 @@ pub(super) struct NodeStateActor { /// The key is the [`quinn::Connection::stable_id`]. connections: FxHashMap, /// Events emitted by Quinn about path changes. + #[allow(clippy::type_complexity)] path_events: MergeUnbounded< Pin< Box< @@ -743,7 +744,7 @@ impl NodeStateActor { { if let Some(conn) = self .connections - .get(&conn_id) + .get(conn_id) .and_then(|c| c.handle.upgrade()) { if let Some(path) = conn.path(*path_id) { @@ -851,7 +852,6 @@ impl NodeStateActor { } self.open_path(&addr); self.close_redundant_paths(&addr); - return; } } @@ -887,8 +887,7 @@ impl NodeStateActor { if let Some(conn) = self .connections .get(conn_id) - .map(|c| c.handle.upgrade()) - .flatten() + .and_then(|c| c.handle.upgrade()) { trace!(?addr, ?conn_id, ?path_id, "closing direct path"); if let Some(path) = conn.path(*path_id) { @@ -913,28 +912,25 @@ impl NodeStateActor { conn: &quinn::Connection, path_id: PathId, ) -> Option { - conn.path(path_id) - .map(|path| { - path.remote_address().map_or(None, |remote| { - match MultipathMappedAddr::from(remote) { - MultipathMappedAddr::Mixed(_) => { - error!("Mixed addr in use for path"); - None - } - MultipathMappedAddr::Relay(mapped) => { - match self.relay_mapped_addrs.lookup(&mapped) { - Some(parts) => Some(transports::Addr::from(parts)), - None => { - error!("Unknown RelayMappedAddr in path"); - None - } + conn.path(path_id).and_then(|path| { + path.remote_address() + .map_or(None, |remote| match MultipathMappedAddr::from(remote) { + MultipathMappedAddr::Mixed(_) => { + error!("Mixed addr in use for path"); + None + } + MultipathMappedAddr::Relay(mapped) => { + match self.relay_mapped_addrs.lookup(&mapped) { + Some(parts) => Some(transports::Addr::from(parts)), + None => { + error!("Unknown RelayMappedAddr in path"); + None } } - MultipathMappedAddr::Ip(addr) => Some(addr.into()), } + MultipathMappedAddr::Ip(addr) => Some(addr.into()), }) - }) - .flatten() + }) } } From 008648c773c593262ae8d730b7deffe53638c20c Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 9 Oct 2025 21:28:32 +0200 Subject: [PATCH 094/164] care to enable multipath? --- iroh/src/endpoint.rs | 20 +++++++++++++++----- iroh/src/magicsock.rs | 9 +++++++-- iroh/src/magicsock/node_map/node_state.rs | 4 ++-- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index a03b7abde1e..006372f407b 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -42,7 +42,8 @@ use crate::{ UserData, pkarr::PkarrPublisher, }, magicsock::{ - self, Handle, OwnAddressSnafu, PathInfo, + self, HEARTBEAT_INTERVAL, Handle, MAX_MULTIPATH_PATHS, OwnAddressSnafu, + PATH_MAX_IDLE_TIMEOUT, PathInfo, mapped_addrs::{MappedAddr, MultipathMappedAddr, NodeIdMappedAddr}, node_map::TransportType, }, @@ -118,13 +119,11 @@ pub struct Builder { impl Default for Builder { fn default() -> Self { - let mut transport_config = quinn::TransportConfig::default(); - transport_config.keep_alive_interval(Some(Duration::from_secs(1))); Self { secret_key: Default::default(), relay_mode: default_relay_mode(), alpn_protocols: Default::default(), - transport_config, + transport_config: quinn::TransportConfig::default(), keylog: Default::default(), discovery: Default::default(), discovery_user_data: Default::default(), @@ -149,12 +148,23 @@ impl Builder { // # The final constructor that everyone needs. /// Binds the magic endpoint. - pub async fn bind(self) -> Result { + pub async fn bind(mut self) -> Result { let mut rng = rand::rng(); let relay_map = self.relay_mode.relay_map(); let secret_key = self .secret_key .unwrap_or_else(move || SecretKey::generate(&mut rng)); + + // Override some transport config settings. + self.transport_config + .keep_alive_interval(Some(HEARTBEAT_INTERVAL)); + self.transport_config + .default_path_keep_alive_interval(Some(HEARTBEAT_INTERVAL)); + self.transport_config + .default_path_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)); + self.transport_config + .max_concurrent_multipath_paths(MAX_MULTIPATH_PATHS); + let static_config = StaticConfig { transport_config: Arc::new(self.transport_config), tls_config: tls::TlsConfig::new(secret_key.clone(), self.max_tls_tickets), diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index d4e67ecb78b..b72e4cf94a9 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -96,13 +96,18 @@ pub use self::{ /// /// If a path is idle for this long, a PING frame will be sent to keep the connection /// alive. -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); +pub(crate) const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); /// The maximum time a path can stay idle before being closed. /// /// This is [`HEARTBEAT_INTERVAL`] + 1.5s. This gives us a chance to send a PING frame and /// some retries. -const MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(6500); +pub(crate) const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(6500); + +/// Maximum number of concurrent QUIC multipath paths per connection. +/// +/// Pretty arbitrary and high right now. +pub(crate) const MAX_MULTIPATH_PATHS: u32 = 32; /// Contains options for `MagicSock::listen`. #[derive(derive_more::Debug)] diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 8b19c959189..2954202262b 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -23,7 +23,7 @@ use crate::{ disco::{self}, endpoint::DirectAddr, magicsock::{ - DiscoState, HEARTBEAT_INTERVAL, MAX_IDLE_TIMEOUT, MagicsockMetrics, + DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, mapped_addrs::{AddrMap, MappedAddr, MultipathMappedAddr, RelayMappedAddr}, transports::{self, OwnedTransmit}, }, @@ -675,7 +675,7 @@ impl NodeStateActor { return; }; path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); - path.set_max_idle_timeout(Some(MAX_IDLE_TIMEOUT)).ok(); + path.set_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)).ok(); if let Some(addr) = self.path_transports_addr(&conn, path_id) { event!( From cdc0f90fef87c444eeb0f89a5d8ad61ae1d45469 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 10 Oct 2025 11:55:59 +0200 Subject: [PATCH 095/164] refactor into more methods, just mechanical --- iroh/src/magicsock/node_map/node_state.rs | 379 +++++++++++++--------- 1 file changed, 223 insertions(+), 156 deletions(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 2954202262b..46965b8a2d2 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -198,6 +198,11 @@ impl NodeStateActor { } } + /// Runs the main loop of the actor. + /// + /// Note that the actor uses async handlers for tasks from the main loop. The actor is + /// not processing items from the inbox while waiting on any async calls. So some + /// dicipline is needed to not turn pending for a long time. async fn run(&mut self, mut inbox: mpsc::Receiver) -> Result<(), Whatever> { trace!("actor started"); loop { @@ -232,185 +237,247 @@ impl NodeStateActor { Ok(()) } + /// Handles an actor message. + /// + /// Error returns are fatal and kill the actor. #[instrument(skip(self))] async fn handle_message(&mut self, msg: NodeStateMessage) -> Result<(), Whatever> { - trace!("handling message"); + // trace!("handling message"); match msg { NodeStateMessage::SendDatagram(transmit) => { - if let Some(ref addr) = self.selected_path { - self.transports_sender - .send((addr.clone(), transmit).into()) - .await - .whatever_context("TransportSenderActor stopped")?; - } else { - for addr in self.paths.keys() { - self.transports_sender - .send((addr.clone(), transmit.clone()).into()) - .await - .whatever_context("TransportSenerActor stopped")?; - } - // This message is received *before* a connection is added. So we do - // not yet have a connection to holepunch. Instead we trigger - // holepunching when AddConnection is received. - } + self.handle_msg_send_datagram(transmit).await?; } NodeStateMessage::AddConnection(handle, paths_info) => { - if let Some(conn) = handle.upgrade() { - // Remove any conflicting stable_ids from the local state. - let stable_id = conn.stable_id(); - self.connections.remove(&stable_id); - self.path_id_map.retain(|(id, _), _| *id != stable_id); - - // This is a good time to clean up connections. - self.cleanup_connections(); - - let conn_id = conn.stable_id(); - let events = BroadcastStream::new(conn.path_events()); - let stream = events.map(move |evt| (conn_id, evt)); - self.path_events.push(Box::pin(stream)); - self.connections.insert( - conn_id, - ConnectionState { - handle: handle.clone(), - paths_info, - }, - ); - if let Some(conn) = handle.upgrade() { - if let Some(addr) = self.path_transports_addr(&conn, PathId::ZERO) { - self.path_id_map - .insert((conn_id, PathId::ZERO), addr.clone()); - self.paths - .entry(addr) - .or_default() - .sources - .insert(Source::Connection, Instant::now()); - self.select_path(); - } - // TODO: Make sure we are adding the relay path if we're on a direct - // path. - self.trigger_holepunching().await; - } - } + self.handle_msg_add_connection(handle, paths_info).await; } NodeStateMessage::AddNodeAddr(node_addr, source) => { - for sockaddr in node_addr.direct_addresses { - let addr = transports::Addr::from(sockaddr); - let path = self.paths.entry(addr).or_default(); - path.sources.insert(source.clone(), Instant::now()); - } - if let Some(relay_url) = node_addr.relay_url { - let addr = transports::Addr::from((relay_url, self.node_id)); - let path = self.paths.entry(addr).or_default(); - path.sources.insert(source, Instant::now()); - } + self.handle_msg_add_node_addr(node_addr, source); } NodeStateMessage::CallMeMaybeReceived(msg) => { - event!( - target: "iroh::_events::call_me_maybe::recv", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - addrs = ?msg.my_numbers, - ); - let now = Instant::now(); - for addr in msg.my_numbers { - let dst = transports::Addr::Ip(addr); - let ping = disco::Ping::new(self.local_node_id); - - let path = self.paths.entry(dst.clone()).or_default(); - path.sources.insert(Source::CallMeMaybe, now); - path.ping_sent = Some(ping.tx_id); - - event!( - target: "iroh::_events::ping::sent", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - ?dst, - ); - self.send_disco_message(dst, disco::Message::Ping(ping)) - .await; - } + self.handle_msg_call_me_maybe_received(msg).await; } NodeStateMessage::PingReceived(ping, src) => { - let transports::Addr::Ip(addr) = src else { - warn!("received ping via relay transport, ignored"); - return Ok(()); - }; - event!( - target: "iroh::_events::ping::recv", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - ?src, - txn = ?ping.tx_id, - ); - let pong = disco::Pong { - tx_id: ping.tx_id, - ping_observed_addr: addr.into(), - }; - event!( - target: "iroh::_events::pong::sent", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - dst = ?src, - txn = ?pong.tx_id, - ); - self.send_disco_message(src.clone(), disco::Message::Pong(pong)) - .await; - - let path = self.paths.entry(src).or_default(); - path.sources.insert(Source::Ping, Instant::now()); - - trace!("ping received, triggering holepunching"); - self.trigger_holepunching().await; + self.handle_msg_ping_received(ping, src).await; } NodeStateMessage::PongReceived(pong, src) => { - let Some(state) = self.paths.get(&src) else { - warn!(path = ?src, ?self.paths, "ignoring DISCO Pong for unknown path"); - return Ok(()); - }; - if state.ping_sent != Some(pong.tx_id) { - debug!(path = ?src, ?state.ping_sent, pong_tx = ?pong.tx_id, - "ignoring unknown DISCO Pong for path"); - return Ok(()); - } - event!( - target: "iroh::_events::pong::recv", - Level::DEBUG, - remote_node = %self.node_id.fmt_short(), - ?src, - txn = ?pong.tx_id, - ); - - self.open_path(&src); + self.handle_msg_pong_received(pong, src); } NodeStateMessage::CanSend(tx) => { - let can_send = !self.paths.is_empty(); - tx.send(can_send).ok(); + self.handle_msg_can_send(tx); } NodeStateMessage::Latency(tx) => { - let rtt = self.selected_path.as_ref().and_then(|addr| { - for (conn_id, path_id) in self - .path_id_map - .iter() - .filter_map(|(key, path)| (path == addr).then_some(key)) - { - if let Some(conn) = self - .connections - .get(conn_id) - .and_then(|c| c.handle.upgrade()) - { - if let Some(path_stats) = conn.stats().paths.get(path_id) { - return Some(path_stats.rtt); - } - } - } - None - }); - tx.send(rtt).ok(); + self.handle_msg_latency(tx); } } Ok(()) } + /// Handles [`NodeStateMessage::SendDatagram`]. + /// + /// Error returns are fatal and kill the actor. + async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) -> Result<(), Whatever> { + if let Some(ref addr) = self.selected_path { + trace!(?addr, "sending datagram to selected path"); + self.transports_sender + .send((addr.clone(), transmit).into()) + .await + .whatever_context("TransportSenderActor stopped")?; + } else { + trace!( + paths = ?self.paths.keys().collect::>(), + "sending datagram to all known paths", + ); + for addr in self.paths.keys() { + self.transports_sender + .send((addr.clone(), transmit.clone()).into()) + .await + .whatever_context("TransportSenerActor stopped")?; + } + // This message is received *before* a connection is added. So we do + // not yet have a connection to holepunch. Instead we trigger + // holepunching when AddConnection is received. + } + Ok(()) + } + + /// Handles [`NodeStateMessage::AddConnection`]. + /// + /// Error returns are fatal and kill the actor. + async fn handle_msg_add_connection( + &mut self, + handle: WeakConnectionHandle, + paths_info: Watchable>, + ) { + if let Some(conn) = handle.upgrade() { + // Remove any conflicting stable_ids from the local state. + let stable_id = conn.stable_id(); + self.connections.remove(&stable_id); + self.path_id_map.retain(|(id, _), _| *id != stable_id); + + // This is a good time to clean up connections. + self.cleanup_connections(); + + // Store the connection and hook up paths events stream. + let conn_id = conn.stable_id(); + let events = BroadcastStream::new(conn.path_events()); + let stream = events.map(move |evt| (conn_id, evt)); + self.path_events.push(Box::pin(stream)); + self.connections.insert( + conn_id, + ConnectionState { + handle: handle.clone(), + paths_info, + }, + ); + + // Store PathId(0) and select best path, check if holepunching is needed. + if let Some(conn) = handle.upgrade() { + if let Some(path_remote) = self.path_transports_addr(&conn, PathId::ZERO) { + trace!(?path_remote, "added new connection"); + self.path_id_map + .insert((conn_id, PathId::ZERO), path_remote.clone()); + self.paths + .entry(path_remote) + .or_default() + .sources + .insert(Source::Connection, Instant::now()); + self.select_path(); + } + // TODO: Make sure we are adding the relay path if we're on a direct + // path. + self.trigger_holepunching().await; + } + } + } + + /// Handles [`NodeStateMessage::AddNodeAddr`]. + fn handle_msg_add_node_addr(&mut self, node_addr: NodeAddr, source: Source) { + for sockaddr in node_addr.direct_addresses { + let addr = transports::Addr::from(sockaddr); + let path = self.paths.entry(addr).or_default(); + path.sources.insert(source.clone(), Instant::now()); + } + if let Some(relay_url) = node_addr.relay_url { + let addr = transports::Addr::from((relay_url, self.node_id)); + let path = self.paths.entry(addr).or_default(); + path.sources.insert(source, Instant::now()); + } + trace!("added addressing information"); + } + + /// Handles [`NodeStateMessage::CallMeMaybeReceived`]. + async fn handle_msg_call_me_maybe_received(&mut self, msg: disco::CallMeMaybe) { + event!( + target: "iroh::_events::call_me_maybe::recv", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + addrs = ?msg.my_numbers, + ); + let now = Instant::now(); + for addr in msg.my_numbers { + let dst = transports::Addr::Ip(addr); + let ping = disco::Ping::new(self.local_node_id); + + let path = self.paths.entry(dst.clone()).or_default(); + path.sources.insert(Source::CallMeMaybe, now); + path.ping_sent = Some(ping.tx_id); + + event!( + target: "iroh::_events::ping::sent", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + ?dst, + ); + self.send_disco_message(dst, disco::Message::Ping(ping)) + .await; + } + } + + /// Handles [`NodeStateMessage::PingReceived`]. + async fn handle_msg_ping_received(&mut self, ping: disco::Ping, src: transports::Addr) { + let transports::Addr::Ip(addr) = src else { + warn!("received ping via relay transport, ignored"); + return; + }; + event!( + target: "iroh::_events::ping::recv", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + ?src, + txn = ?ping.tx_id, + ); + let pong = disco::Pong { + tx_id: ping.tx_id, + ping_observed_addr: addr.into(), + }; + event!( + target: "iroh::_events::pong::sent", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + dst = ?src, + txn = ?pong.tx_id, + ); + self.send_disco_message(src.clone(), disco::Message::Pong(pong)) + .await; + + let path = self.paths.entry(src).or_default(); + path.sources.insert(Source::Ping, Instant::now()); + + trace!("ping received, triggering holepunching"); + self.trigger_holepunching().await; + } + + /// Handles [`NodeStateMessage::PongReceived`]. + fn handle_msg_pong_received(&mut self, pong: disco::Pong, src: transports::Addr) { + let Some(state) = self.paths.get(&src) else { + warn!(path = ?src, ?self.paths, "ignoring DISCO Pong for unknown path"); + return; + }; + if state.ping_sent != Some(pong.tx_id) { + debug!(path = ?src, ?state.ping_sent, pong_tx = ?pong.tx_id, + "ignoring unknown DISCO Pong for path"); + return; + } + event!( + target: "iroh::_events::pong::recv", + Level::DEBUG, + remote_node = %self.node_id.fmt_short(), + ?src, + txn = ?pong.tx_id, + ); + + self.open_path(&src); + } + + /// Handles [`NodeStateMessage::CanSend`]. + fn handle_msg_can_send(&self, tx: oneshot::Sender) { + let can_send = !self.paths.is_empty(); + tx.send(can_send).ok(); + } + + /// Handles [`NodeStateMessage::Latency`]. + fn handle_msg_latency(&self, tx: oneshot::Sender>) { + let rtt = self.selected_path.as_ref().and_then(|addr| { + for (conn_id, path_id) in self + .path_id_map + .iter() + .filter_map(|(key, path)| (path == addr).then_some(key)) + { + if let Some(conn) = self + .connections + .get(conn_id) + .and_then(|c| c.handle.upgrade()) + { + if let Some(path_stats) = conn.stats().paths.get(path_id) { + return Some(path_stats.rtt); + } + } + } + None + }); + tx.send(rtt).ok(); + } + /// Triggers holepunching to the remote node. /// /// This will manage the entire process of holepunching with the remote node. From b80de1a567c6a713f9e6f1a74b6ef80920364a2e Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 14 Oct 2025 17:34:47 +0200 Subject: [PATCH 096/164] Log transports::Addr a bit more compactly --- iroh/src/magicsock/transports.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 6ec2750e641..e0c70b0ce98 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -1,4 +1,5 @@ use std::{ + fmt, io::{self, IoSliceMut}, net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, pin::Pin, @@ -324,12 +325,21 @@ impl From<&quinn_udp::Transmit<'_>> for OwnedTransmit { } } -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum Addr { Ip(SocketAddr), Relay(RelayUrl, NodeId), } +impl fmt::Debug for Addr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Addr::Ip(addr) => write!(f, "Ip({addr})"), + Addr::Relay(url, node_id) => write!(f, "Relay({url}, {})", node_id.fmt_short()), + } + } +} + impl Default for Addr { fn default() -> Self { Self::Ip(SocketAddr::V6(SocketAddrV6::new( From 42000596296df00a55b951041b74d80c662575f4 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 14 Oct 2025 17:35:15 +0200 Subject: [PATCH 097/164] some minimal docs --- iroh/src/magicsock/transports.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index e0c70b0ce98..809574521a5 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -325,9 +325,12 @@ impl From<&quinn_udp::Transmit<'_>> for OwnedTransmit { } } +/// Transports address. #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum Addr { + /// An IP address, should always be stored in its canonical form. Ip(SocketAddr), + /// A relay address. Relay(RelayUrl, NodeId), } From 5988e944f98bcf09533ce587aaf90eefe62fc21e Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 14 Oct 2025 17:36:15 +0200 Subject: [PATCH 098/164] rework the test a little to have easier spans --- iroh/src/endpoint.rs | 68 +++++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 006372f407b..e87995230b8 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2156,12 +2156,13 @@ mod tests { use n0_watcher::Watcher; use quinn::ConnectionError; use rand::SeedableRng; + use tokio::sync::oneshot; use tracing::{Instrument, error_span, info, info_span, instrument}; use tracing_test::traced_test; use super::Endpoint; use crate::{ - RelayMode, + RelayMap, RelayMode, discovery::static_provider::StaticProvider, endpoint::{ConnectOptions, Connection, ConnectionType}, magicsock::node_map::TransportType, @@ -2514,35 +2515,26 @@ mod tests { // Connect two endpoints on the same network, via a relay server, without // discovery. let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; - let server = { - let span = info_span!("server"); - let _guard = span.enter(); - Endpoint::builder() - .alpns(vec![TEST_ALPN.to_vec()]) - .insecure_skip_relay_cert_verify(true) - .relay_mode(RelayMode::Custom(relay_map.clone())) - .bind() - .await? - }; - let client = { - let span = info_span!("client"); - let _guard = span.enter(); - Endpoint::builder() + let (node_addr_tx, node_addr_rx) = oneshot::channel(); + + #[instrument(name = "client", skip_all)] + async fn connect( + relay_map: RelayMap, + node_addr_rx: oneshot::Receiver, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let secret = SecretKey::generate(&mut rng); + let ep = Endpoint::builder() + .secret_key(secret) .alpns(vec![TEST_ALPN.to_vec()]) .insecure_skip_relay_cert_verify(true) .relay_mode(RelayMode::Custom(relay_map)) .bind() - .await? - }; - server.online().await; - let server_node_addr = NodeAddr { - direct_addresses: Default::default(), - ..server.node_addr() - }; - - #[instrument(name = "client", skip_all)] - async fn connect(ep: Endpoint, dst: NodeAddr) -> Result { + .await?; info!(me = %ep.node_id().fmt_short(), "client starting"); + let dst = node_addr_rx.await.e()?; + + info!(me = %ep.node_id().fmt_short(), "client connecting"); let conn = ep.connect(dst, TEST_ALPN).await?; let mut send = conn.open_uni().await.e()?; send.write_all(b"hello").await.e()?; @@ -2561,11 +2553,27 @@ mod tests { } #[instrument(name = "server", skip_all)] - async fn accept(ep: Endpoint, src: NodeId) -> Result { + async fn accept(relay_map: RelayMap, node_addr_tx: oneshot::Sender) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1u64); + let secret = SecretKey::generate(&mut rng); + let ep = Endpoint::builder() + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + ep.online().await; + let node_addr = NodeAddr { + direct_addresses: Default::default(), + ..ep.node_addr() + }; + node_addr_tx.send(node_addr).unwrap(); + info!(me = %ep.node_id().fmt_short(), "server starting"); let conn = ep.accept().await.e()?.await.e()?; - let node_id = conn.remote_node_id()?; - assert_eq!(node_id, src); + // let node_id = conn.remote_node_id()?; + // assert_eq!(node_id, src); let mut recv = conn.accept_uni().await.e()?; let mut msg = [0u8; 5]; recv.read_exact(&mut msg).await.e()?; @@ -2578,8 +2586,8 @@ mod tests { Ok(()) } - let server_task = tokio::spawn(accept(server.clone(), client.node_id())); - let client_task = tokio::spawn(connect(client.clone(), server_node_addr)); + let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx)); + let client_task = tokio::spawn(connect(relay_map, node_addr_rx)); server_task.await.e()??; let conn_closed = dbg!(client_task.await.e()??); From 77dfc3249baf5594b15cb2e9b563703a7611958b Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 14 Oct 2025 17:36:40 +0200 Subject: [PATCH 099/164] log the correct packet lengths --- iroh/src/magicsock.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index b72e4cf94a9..7d66ce94e47 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -605,11 +605,11 @@ impl MagicSock { // relies on quinn::EndpointConfig::grease_quic_bit being set to `false`, // which we do in Endpoint::bind. if let Some((sender, sealed_box)) = disco::source_and_box(datagram) { - trace!(src = ?source_addr, len = %quinn_meta.stride, "UDP recv: DISCO packet"); + trace!(src = ?source_addr, len = datagram.len(), "UDP recv: DISCO packet"); self.handle_disco_message(sender, sealed_box, source_addr); datagram[0] = 0u8; } else { - trace!(src = ?source_addr, len = %quinn_meta.stride, "UDP recv: QUIC packet"); + trace!(src = ?source_addr, len = datagram.len(), "UDP recv: QUIC packet"); match source_addr { transports::Addr::Ip(SocketAddr::V4(..)) => { self.metrics From 8b8321ba74f6066c9a29789a59441134dc955373 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 14 Oct 2025 17:37:12 +0200 Subject: [PATCH 100/164] Set the path status for the initial path --- iroh/src/magicsock/node_map/node_state.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 46965b8a2d2..11f01aaa8db 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -330,10 +330,18 @@ impl NodeStateActor { }, ); - // Store PathId(0) and select best path, check if holepunching is needed. + // Store PathId(0), set path_status and select best path, check if holepunching + // is needed. if let Some(conn) = handle.upgrade() { if let Some(path_remote) = self.path_transports_addr(&conn, PathId::ZERO) { trace!(?path_remote, "added new connection"); + if let Some(path) = conn.path(PathId::ZERO) { + let status = match path_remote { + transports::Addr::Ip(_) => PathStatus::Available, + transports::Addr::Relay(_, _) => PathStatus::Backup, + }; + path.set_status(status).ok(); + } self.path_id_map .insert((conn_id, PathId::ZERO), path_remote.clone()); self.paths From 50f98b89d929e0e83c6c9a7fda37a9d38a1a0631 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 14 Oct 2025 17:37:50 +0200 Subject: [PATCH 101/164] next thing to work on --- iroh/src/magicsock/node_map/node_state.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 11f01aaa8db..3a66aecc3a3 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -863,6 +863,11 @@ impl NodeStateActor { /// direct paths are close from all connections. #[instrument(skip_all)] fn select_path(&mut self) { + // TODO: Only consider paths that are actively open: that is we received the open + // event and have not closed it yet, or have not received a close. Otherwise we + // might select from paths that doen't work. Plus we might not have a + // representative RTT time yet. + // Find the lowest RTT across all connections for each open path. The long way, so // we get to trace-log *all* RTTs. let mut all_path_rtts: FxHashMap> = FxHashMap::default(); From b4114fa6f6381f882ef515a566e6de3b0847c6d5 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Tue, 14 Oct 2025 17:41:40 +0200 Subject: [PATCH 102/164] chore: update git deps --- Cargo.lock | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8715a6bad42..f03f416b86b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,7 +1330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -2172,7 +2172,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.0", "tokio", "tower-service", "tracing", @@ -2630,7 +2630,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#3820a0ae48ce8362d8851295769a9184309830d6" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#ef19d5339195af6a99196e949d7d0c31ba44290f" dependencies = [ "bytes", "cfg_aliases", @@ -2639,7 +2639,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2650,7 +2650,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#3820a0ae48ce8362d8851295769a9184309830d6" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#ef19d5339195af6a99196e949d7d0c31ba44290f" dependencies = [ "bytes", "fastbloom", @@ -2672,14 +2672,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#3820a0ae48ce8362d8851295769a9184309830d6" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#ef19d5339195af6a99196e949d7d0c31ba44290f" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3884,7 +3884,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.17", "tokio", "tracing", @@ -3921,7 +3921,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", "windows-sys 0.60.2", ] @@ -4235,7 +4235,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -4991,7 +4991,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.60.2", ] [[package]] @@ -5875,7 +5875,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.48.0", ] [[package]] From 197699354c078141264688f50c749b3e465524e8 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 15 Oct 2025 15:25:23 +0200 Subject: [PATCH 103/164] improve logging and reduce max concurrent paths to 16 --- iroh/src/magicsock.rs | 2 +- iroh/src/magicsock/node_map/node_state.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index b5b72da4c33..50f0bb7a93c 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -106,7 +106,7 @@ pub(crate) const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(6500); /// Maximum number of concurrent QUIC multipath paths per connection. /// /// Pretty arbitrary and high right now. -pub(crate) const MAX_MULTIPATH_PATHS: u32 = 32; +pub(crate) const MAX_MULTIPATH_PATHS: u32 = 16; /// Contains options for `MagicSock::listen`. #[derive(derive_more::Debug)] diff --git a/iroh/src/magicsock/node_map/node_state.rs b/iroh/src/magicsock/node_map/node_state.rs index 85a2f094a0b..886eea9f976 100644 --- a/iroh/src/magicsock/node_map/node_state.rs +++ b/iroh/src/magicsock/node_map/node_state.rs @@ -1038,8 +1038,10 @@ pub(crate) enum NodeStateMessage { /// Process a received DISCO CallMeMaybe message. CallMeMaybeReceived(disco::CallMeMaybe), /// Process a received DISCO Ping message. + #[debug("PingReceived({:?}, src: {_1:?})", _0.tx_id)] PingReceived(disco::Ping, transports::Addr), /// Process a received DISCO Pong message. + #[debug("PongReceived({:?}, src: {_1:?})", _0.tx_id)] PongReceived(disco::Pong, transports::Addr), /// Asks if there is any possible path that could be used. /// From ce5bdd28820dfe6f03cfc16dffce62831d96b8c6 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 16 Oct 2025 14:45:12 +0200 Subject: [PATCH 104/164] newtype the connection id --- .../magicsock/endpoint_map/endpoint_state.rs | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 990c7b11b80..e762d300099 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -9,7 +9,7 @@ use n0_future::{ use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; use quinn_proto::{PathEvent, PathId, PathStatus}; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Whatever}; use tokio::sync::{mpsc, oneshot}; @@ -91,15 +91,13 @@ pub(super) struct EndpointStateActor { // Internal state - Quinn Connections we are managing. // /// All connections we have to this remote endpoint. - /// - /// The key is the [`quinn::Connection::stable_id`]. - connections: FxHashMap, + connections: FxHashMap, /// Events emitted by Quinn about path changes. #[allow(clippy::type_complexity)] path_events: MergeUnbounded< Pin< Box< - dyn Stream)> + dyn Stream)> + Send + Sync, >, @@ -124,7 +122,7 @@ pub(super) struct EndpointStateActor { /// a previous connection no longer exists. // TODO: We do exhaustive searches through this map to find items based on // transports::Addr. Perhaps a bi-directional map could be considered. - path_id_map: FxHashMap<(usize, PathId), transports::Addr>, + path_id_map: FxHashMap<(ConnId, PathId), transports::Addr>, /// Information about the last holepunching attempt. last_holepunch: Option, /// The path we currently consider the preferred path to the remote endpoint. @@ -312,15 +310,14 @@ impl EndpointStateActor { ) { if let Some(conn) = handle.upgrade() { // Remove any conflicting stable_ids from the local state. - let stable_id = conn.stable_id(); - self.connections.remove(&stable_id); - self.path_id_map.retain(|(id, _), _| *id != stable_id); + let conn_id = ConnId(conn.stable_id()); + self.connections.remove(&conn_id); + self.path_id_map.retain(|(id, _), _| *id != conn_id); // This is a good time to clean up connections. self.cleanup_connections(); // Store the connection and hook up paths events stream. - let conn_id = conn.stable_id(); let events = BroadcastStream::new(conn.path_events()); let stream = events.map(move |evt| (conn_id, evt)); self.path_events.push(Box::pin(stream)); @@ -695,29 +692,29 @@ impl EndpointStateActor { }; // The connections that already have this path. - let mut conns_with_path = BTreeSet::new(); + let mut conns_with_path = FxHashSet::default(); for ((conn_id, _), addr) in self.path_id_map.iter() { if addr == open_addr { conns_with_path.insert(*conn_id); } } - for conn in self + for (conn_id, conn) in self .connections .iter() - .filter_map(|(conn_id, handle)| (!conns_with_path.contains(conn_id)).then_some(handle)) - .filter_map(|c| c.handle.upgrade()) - .filter(|conn| conn.side().is_client()) + .filter(|(conn_id, _)| !conns_with_path.contains(conn_id)) + .filter_map(|(id, c)| c.handle.upgrade().and_then(|c| Some((*id, c)))) + .filter(|(_, conn)| conn.side().is_client()) { let fut = conn.open_path_ensure(quic_addr, path_status); match fut.path_id() { Some(path_id) => { - trace!(conn_id = conn.stable_id(), ?path_id, "opening new path"); + trace!(?conn_id, ?path_id, "opening new path"); self.path_id_map - .insert((conn.stable_id(), path_id), open_addr.clone()); + .insert((conn_id, path_id), open_addr.clone()); } None => { - let ret = poll_once(fut); + let ret = now_or_never(fut); warn!(?ret, "Opening path failed"); } } @@ -727,7 +724,7 @@ impl EndpointStateActor { #[instrument(skip(self))] fn handle_path_event( &mut self, - conn_id: usize, + conn_id: ConnId, event: Result, ) { let Ok(event) = event else { @@ -760,7 +757,7 @@ impl EndpointStateActor { Level::DEBUG, remote = %self.endpoint_id.fmt_short(), ?addr, - conn_id, + ?conn_id, ?path_id, ); self.path_id_map.insert((conn_id, path_id), addr.clone()); @@ -793,7 +790,7 @@ impl EndpointStateActor { Level::DEBUG, remote = %self.endpoint_id.fmt_short(), ?addr, - conn_id, + ?conn_id, path_id = ?id, ); // Remove this from the public PathInfo. @@ -844,16 +841,12 @@ impl EndpointStateActor { fn cleanup_connections(&mut self) { self.connections.retain(|_, c| c.handle.upgrade().is_some()); - let mut stable_ids = BTreeSet::new(); - for state in self.connections.values() { - state - .handle - .upgrade() - .map(|conn| stable_ids.insert(conn.stable_id())); + let mut conn_ids = FxHashSet::default(); + for (id, state) in self.connections.iter() { + state.handle.upgrade().map(|_| conn_ids.insert(id)); } - self.path_id_map - .retain(|(stable_id, _), _| stable_ids.contains(stable_id)); + self.path_id_map.retain(|(id, _), _| conn_ids.contains(id)); } /// Selects the path with the lowest RTT, prefers direct paths. @@ -862,7 +855,7 @@ impl EndpointStateActor { /// there are only relay paths, the relay path with the lowest RTT is chosen. /// /// The selected path is added to any connections which do not yet have it. Any unused - /// direct paths are close from all connections. + /// direct paths are closed for all connections. #[instrument(skip_all)] fn select_path(&mut self) { // TODO: Only consider paths that are actively open: that is we received the open @@ -945,7 +938,7 @@ impl EndpointStateActor { debug_assert_eq!(self.selected_path.as_ref(), Some(selected_path)); // We create this to make sure we do not close the last direct path. - let mut paths_per_conn: FxHashMap> = FxHashMap::default(); + let mut paths_per_conn: FxHashMap> = FxHashMap::default(); for ((conn_id, path_id), addr) in self.path_id_map.iter() { if !addr.is_ip() { continue; @@ -1109,7 +1102,9 @@ pub enum ConnectionType { #[derive(Debug)] struct ConnectionState { + /// Weak handle to the connection. handle: WeakConnectionHandle, + /// The information we publish to users about the paths used in this connection. paths_info: Watchable>, } @@ -1160,8 +1155,15 @@ impl From<&transports::Addr> for TransportType { } } +/// Newtype to track Connections. +/// +/// The wrapped value is the [`Connection::stable_id`] value, and is thus only valid for +/// active connections. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ConnId(usize); + /// Poll a future once, like n0_future::future::poll_once but sync. -fn poll_once>(fut: F) -> Option { +fn now_or_never>(fut: F) -> Option { let fut = std::pin::pin!(fut); match fut.poll(&mut std::task::Context::from_waker(std::task::Waker::noop())) { std::task::Poll::Ready(res) => Some(res), From 7839a4c0d0cd89065278f6602340e678885a871a Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 17 Oct 2025 17:13:35 +0200 Subject: [PATCH 105/164] make sure that the transport addrs use canonical addresses --- iroh/src/magicsock/transports.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index b9a01e77bee..91abc31fe75 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -354,7 +354,12 @@ impl Default for Addr { impl From for Addr { fn from(value: SocketAddr) -> Self { - Self::Ip(value) + match value { + SocketAddr::V4(_) => Self::Ip(value), + SocketAddr::V6(addr) => { + Self::Ip(SocketAddr::new(addr.ip().to_canonical(), addr.port())) + } + } } } From fcaea95c5937055049f7004b2cbf08029575d289 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sat, 18 Oct 2025 14:53:13 +0200 Subject: [PATCH 106/164] No longer need to patch rustls --- Cargo.lock | 23 ++++++++++++----------- Cargo.toml | 1 - 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52b591ff951..dcd550c1b19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2172,7 +2172,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2632,7 +2632,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#ef19d5339195af6a99196e949d7d0c31ba44290f" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a4597406bf649a8eb38a5f8a1861979b6cee2ef4" dependencies = [ "bytes", "cfg_aliases", @@ -2652,7 +2652,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#ef19d5339195af6a99196e949d7d0c31ba44290f" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a4597406bf649a8eb38a5f8a1861979b6cee2ef4" dependencies = [ "bytes", "fastbloom", @@ -2674,14 +2674,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#ef19d5339195af6a99196e949d7d0c31ba44290f" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a4597406bf649a8eb38a5f8a1861979b6cee2ef4" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3211,7 +3211,7 @@ dependencies = [ [[package]] name = "netwatch" version = "0.9.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#2d53e93285ebd6be1f53de84443351ea00b6e056" +source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#62a495bfc48ae85ae4acf69fa0402a34e2828051" dependencies = [ "atomic-waker", "bytes", @@ -3907,7 +3907,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.0", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -3944,7 +3944,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -4263,8 +4263,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.29" -source = "git+https://github.com/n0-computer/rustls?rev=da7b54c6621fdeb2defff38242c5b56ef4c5a920#da7b54c6621fdeb2defff38242c5b56ef4c5a920" +version = "0.23.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" dependencies = [ "log", "once_cell", @@ -5898,7 +5899,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d21abccad84..048c9667157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ unused-async = "warn" [patch.crates-io] -rustls = { git = "https://github.com/n0-computer/rustls", rev = "da7b54c6621fdeb2defff38242c5b56ef4c5a920" } netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-multipath" } [patch."https://github.com/n0-computer/quinn"] From 7d86efff2343d87bff2444f8b8d68703a125aa36 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 20 Oct 2025 19:27:42 +0200 Subject: [PATCH 107/164] Only select the path based on open paths This selects paths only from open paths. This avoids paths that haven't been opened yet showing up with 333ms as RTT. To achieve that somewhat reasonably this refactors by moving the path_id_map to be per-connection. The bi-directional map needed becomes a lot simpler to manage, all lookups a lot more sane. And keeping track of open paths becomes easier. --- iroh/src/magicsock.rs | 1 + .../magicsock/endpoint_map/endpoint_state.rs | 377 +++++++++--------- iroh/src/magicsock/mapped_addrs.rs | 27 +- 3 files changed, 211 insertions(+), 194 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index e25e1c14eb1..110faab1d3a 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1358,6 +1358,7 @@ enum DiscoBoxError { } #[derive(Debug)] +#[allow(clippy::enum_variant_names)] enum ActorMessage { NetworkChange, RelayMapChange, diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index e762d300099..55d25daa872 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -9,7 +9,7 @@ use n0_future::{ use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; use quinn_proto::{PathEvent, PathId, PathStatus}; -use rustc_hash::{FxHashMap, FxHashSet}; +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Whatever}; use tokio::sync::{mpsc, oneshot}; @@ -111,18 +111,6 @@ pub(super) struct EndpointStateActor { /// These paths might be entirely impossible to use, since they are added by discovery /// mechanisms. The are only potentially usable. paths: FxHashMap, - /// Maps connections and path IDs to the transport addr. - /// - /// The [`transports::Addr`] can be looked up in [`Self::paths`]. - /// - /// The `usize` is the [`Connection::stable_id`] of a connection. It is important that - /// this map is cleared of the stable ID of a new connection received from - /// [`EndpointStateMessage::AddConnection`], because this ID is only unique within - /// *currently active* connections. So there could be conflicts if we did not yet know - /// a previous connection no longer exists. - // TODO: We do exhaustive searches through this map to find items based on - // transports::Addr. Perhaps a bi-directional map could be considered. - path_id_map: FxHashMap<(ConnId, PathId), transports::Addr>, /// Information about the last holepunching attempt. last_holepunch: Option, /// The path we currently consider the preferred path to the remote endpoint. @@ -159,7 +147,6 @@ impl EndpointStateActor { connections: FxHashMap::default(), path_events: Default::default(), paths: FxHashMap::default(), - path_id_map: FxHashMap::default(), last_holepunch: None, selected_path: None, scheduled_holepunch: None, @@ -312,7 +299,6 @@ impl EndpointStateActor { // Remove any conflicting stable_ids from the local state. let conn_id = ConnId(conn.stable_id()); self.connections.remove(&conn_id); - self.path_id_map.retain(|(id, _), _| *id != conn_id); // This is a good time to clean up connections. self.cleanup_connections(); @@ -325,30 +311,38 @@ impl EndpointStateActor { conn_id, ConnectionState { handle: handle.clone(), - paths_info, + pub_path_info: paths_info, + paths: Default::default(), + open_paths: Default::default(), + path_ids: Default::default(), }, ); // Store PathId(0), set path_status and select best path, check if holepunching // is needed. if let Some(conn) = handle.upgrade() { - if let Some(path_remote) = self.path_transports_addr(&conn, PathId::ZERO) { - trace!(?path_remote, "added new connection"); - if let Some(path) = conn.path(PathId::ZERO) { + if let Some(path) = conn.path(PathId::ZERO) { + if let Some(path_remote) = path + .remote_address() + .map_or(None, |remote| Some(MultipathMappedAddr::from(remote))) + .and_then(|mmaddr| mmaddr.to_transport_addr(&self.relay_mapped_addrs)) + { + trace!(?path_remote, "added new connection"); let status = match path_remote { transports::Addr::Ip(_) => PathStatus::Available, transports::Addr::Relay(_, _) => PathStatus::Backup, }; path.set_status(status).ok(); + let conn_state = + self.connections.get_mut(&conn_id).expect("inserted above"); + conn_state.add_open_path(path_remote.clone(), PathId::ZERO); + self.paths + .entry(path_remote) + .or_default() + .sources + .insert(Source::Connection, Instant::now()); + self.select_path(); } - self.path_id_map - .insert((conn_id, PathId::ZERO), path_remote.clone()); - self.paths - .entry(path_remote) - .or_default() - .sources - .insert(Source::Connection, Instant::now()); - self.select_path(); } // TODO: Make sure we are adding the relay path if we're on a direct // path. @@ -361,13 +355,19 @@ impl EndpointStateActor { fn handle_msg_add_endpoint_addr(&mut self, addr: EndpointAddr, source: Source) { for sockaddr in addr.direct_addresses { let addr = transports::Addr::from(sockaddr); - let path = self.paths.entry(addr).or_default(); - path.sources.insert(source.clone(), Instant::now()); + self.paths + .entry(addr) + .or_default() + .sources + .insert(source.clone(), Instant::now()); } if let Some(relay_url) = addr.relay_url { let addr = transports::Addr::from((relay_url, self.endpoint_id)); - let path = self.paths.entry(addr).or_default(); - path.sources.insert(source, Instant::now()); + self.paths + .entry(addr) + .or_default() + .sources + .insert(source, Instant::now()); } trace!("added addressing information"); } @@ -465,19 +465,19 @@ impl EndpointStateActor { /// Handles [`EndpointStateMessage::Latency`]. fn handle_msg_latency(&self, tx: oneshot::Sender>) { let rtt = self.selected_path.as_ref().and_then(|addr| { - for (conn_id, path_id) in self - .path_id_map - .iter() - .filter_map(|(key, path)| (path == addr).then_some(key)) - { - if let Some(conn) = self - .connections - .get(conn_id) - .and_then(|c| c.handle.upgrade()) + for conn_state in self.connections.values() { + let Some(path_id) = conn_state.path_ids.get(addr) else { + continue; + }; + if !conn_state.open_paths.contains_key(path_id) { + continue; + } + if let Some(stats) = conn_state + .handle + .upgrade() + .and_then(|conn| conn.stats().paths.get(path_id).copied()) { - if let Some(path_stats) = conn.stats().paths.get(path_id) { - return Some(path_stats.rtt); - } + return Some(stats.rtt); } } None @@ -649,7 +649,7 @@ impl EndpointStateActor { }); } - /// Sends a DISCO message to *this* remote endpoint. + /// Sends a DISCO message to the remote endpoint this actor manages. #[instrument(skip(self), fields(remote = %self.endpoint_id.fmt_short()))] async fn send_disco_message(&self, dst: transports::Addr, msg: disco::Message) { let pkt = self.disco.encode_and_seal(self.endpoint_id, &msg); @@ -691,27 +691,21 @@ impl EndpointStateActor { .private_socket_addr(), }; - // The connections that already have this path. - let mut conns_with_path = FxHashSet::default(); - for ((conn_id, _), addr) in self.path_id_map.iter() { - if addr == open_addr { - conns_with_path.insert(*conn_id); + for (conn_id, conn_state) in self.connections.iter_mut() { + if conn_state.path_ids.contains_key(open_addr) { + continue; + } + let Some(conn) = conn_state.handle.upgrade() else { + continue; + }; + if conn.side().is_server() { + continue; } - } - - for (conn_id, conn) in self - .connections - .iter() - .filter(|(conn_id, _)| !conns_with_path.contains(conn_id)) - .filter_map(|(id, c)| c.handle.upgrade().and_then(|c| Some((*id, c)))) - .filter(|(_, conn)| conn.side().is_client()) - { let fut = conn.open_path_ensure(quic_addr, path_status); match fut.path_id() { Some(path_id) => { trace!(?conn_id, ?path_id, "opening new path"); - self.path_id_map - .insert((conn_id, path_id), open_addr.clone()); + conn_state.add_path(open_addr.clone(), path_id); } None => { let ret = now_or_never(fut); @@ -733,7 +727,7 @@ impl EndpointStateActor { // state of the connection and it's paths are? return; }; - let Some(conn_state) = self.connections.get(&conn_id) else { + let Some(conn_state) = self.connections.get_mut(&conn_id) else { trace!("event for removed connection"); return; }; @@ -748,29 +742,35 @@ impl EndpointStateActor { trace!("path open event for unknown path"); return; }; + // TODO: We configure this as defaults when we setup the endpoint, do we + // really need to duplicate this? path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); path.set_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)).ok(); - if let Some(addr) = self.path_transports_addr(&conn, path_id) { + if let Some(path_remote) = path + .remote_address() + .map_or(None, |remote| Some(MultipathMappedAddr::from(remote))) + .and_then(|mmaddr| mmaddr.to_transport_addr(&self.relay_mapped_addrs)) + { event!( target: "iroh::_events::path::open", Level::DEBUG, remote = %self.endpoint_id.fmt_short(), - ?addr, + ?path_remote, ?conn_id, ?path_id, ); - self.path_id_map.insert((conn_id, path_id), addr.clone()); + conn_state.add_open_path(path_remote.clone(), path_id); self.paths - .entry(addr.clone()) + .entry(path_remote.clone()) .or_default() .sources .insert(Source::Connection, Instant::now()); - let mut paths = conn_state.paths_info.get(); + let mut paths = conn_state.pub_path_info.get(); paths.push(PathInfo { - transport: addr.into(), + transport: path_remote.into(), }); - conn_state.paths_info.set(paths).ok(); + conn_state.pub_path_info.set(paths).ok(); } self.select_path(); @@ -778,10 +778,10 @@ impl EndpointStateActor { PathEvent::Abandoned { id, path_stats } => { trace!(?path_stats, "path abandoned"); // This is the last event for this path. - self.path_id_map.remove(&(conn_id, id)); + conn_state.remove_path(&id); } PathEvent::Closed { id, .. } | PathEvent::LocallyClosed { id, .. } => { - let Some(addr) = self.path_id_map.get(&(conn_id, id)) else { + let Some(path_remote) = conn_state.paths.get(&id).cloned() else { debug!("path not in path_id_map"); return; }; @@ -789,14 +789,15 @@ impl EndpointStateActor { target: "iroh::_events::path::closed", Level::DEBUG, remote = %self.endpoint_id.fmt_short(), - ?addr, + ?path_remote, ?conn_id, path_id = ?id, ); + conn_state.remove_open_path(&id); // Remove this from the public PathInfo. if let Some(state) = self.connections.get(&conn_id) { - let mut path_info = state.paths_info.get(); - let transport = TransportType::from(addr); + let mut path_info = state.pub_path_info.get(); + let transport = TransportType::from(&path_remote); let mut done = false; path_info.retain(|info| { if !done && info.transport == transport { @@ -806,26 +807,26 @@ impl EndpointStateActor { true } }); - state.paths_info.set(path_info).ok(); + state.pub_path_info.set(path_info).ok(); } // If one connection closes this path, close it on all connections. - for (conn_id, path_id) in self - .path_id_map - .iter() - .filter(|(_, path_addr)| *path_addr == addr) - .map(|(key, _)| key) - { - if let Some(conn) = self - .connections - .get(conn_id) - .and_then(|c| c.handle.upgrade()) - { - if let Some(path) = conn.path(*path_id) { - trace!(?addr, ?conn_id, ?path_id, "closing path"); - if let Err(err) = path.close(APPLICATION_ABANDON_PATH.into()) { - trace!(?addr, ?conn_id, ?path_id, "path close failed: {err:#}"); - } + for (conn_id, conn_state) in self.connections.iter_mut() { + let Some(path_id) = conn_state.path_ids.get(&path_remote) else { + continue; + }; + let Some(conn) = conn_state.handle.upgrade() else { + continue; + }; + if let Some(path) = conn.path(*path_id) { + trace!(?path_remote, ?conn_id, ?path_id, "closing path"); + if let Err(err) = path.close(APPLICATION_ABANDON_PATH.into()) { + trace!( + ?path_remote, + ?conn_id, + ?path_id, + "path close failed: {err:#}" + ); } } } @@ -840,13 +841,6 @@ impl EndpointStateActor { // TODO: Call this on a schedule. fn cleanup_connections(&mut self) { self.connections.retain(|_, c| c.handle.upgrade().is_some()); - - let mut conn_ids = FxHashSet::default(); - for (id, state) in self.connections.iter() { - state.handle.upgrade().map(|_| conn_ids.insert(id)); - } - - self.path_id_map.retain(|(id, _), _| conn_ids.contains(id)); } /// Selects the path with the lowest RTT, prefers direct paths. @@ -864,22 +858,21 @@ impl EndpointStateActor { // representative RTT time yet. // Find the lowest RTT across all connections for each open path. The long way, so - // we get to trace-log *all* RTTs. + // we get to log *all* RTTs. let mut all_path_rtts: FxHashMap> = FxHashMap::default(); - for (conn_id, conn) in self - .connections - .iter() - .filter_map(|(id, c)| c.handle.upgrade().map(|conn| (*id, conn))) - { + for (conn_id, conn_state) in self.connections.iter() { + let Some(conn) = conn_state.handle.upgrade() else { + continue; + }; let stats = conn.stats(); for (path_id, stats) in stats.paths { - if let Some(addr) = self.path_id_map.get(&(conn_id, path_id)) { + if let Some(addr) = conn_state.open_paths.get(&path_id) { all_path_rtts .entry(addr.clone()) .or_default() .push(stats.rtt); } else { - trace!(?path_id, "unknown PathId in ConnectionStats"); + trace!(?conn_id, ?path_id, "unknown PathId in ConnectionStats"); } } } @@ -889,7 +882,7 @@ impl EndpointStateActor { .filter_map(|(addr, rtts)| rtts.into_iter().min().map(|rtt| (addr, rtt))) .collect(); - // Find the fastest direct path. + // Find the fastest direct or relay path. const IPV6_RTT_ADVANTAGE: Duration = Duration::from_millis(3); let direct_path = path_rtts .iter() @@ -903,27 +896,19 @@ impl EndpointStateActor { }) .min() .map(|(_rtt, addr)| addr.clone()); - if let Some(addr) = direct_path { - let prev = self.selected_path.replace(addr.clone()); - if prev.as_ref() != Some(&addr) { - debug!(?addr, ?prev, "selected new direct path"); - } - self.open_path(&addr); - self.close_redundant_paths(&addr); - return; - } - - // Still here? Find the fastest relay path. - let relay_path = path_rtts - .iter() - .filter(|(addr, _rtt)| addr.is_relay()) - .map(|(addr, rtt)| (rtt, addr)) - .min() - .map(|(_rtt, addr)| addr.clone()); - if let Some(addr) = relay_path { + let selected_path = direct_path.or_else(|| { + // Find the fasted relay path. + path_rtts + .iter() + .filter(|(addr, _rtt)| addr.is_relay()) + .map(|(addr, rtt)| (rtt, addr)) + .min() + .map(|(_rtt, addr)| addr.clone()) + }); + if let Some(addr) = selected_path { let prev = self.selected_path.replace(addr.clone()); if prev.as_ref() != Some(&addr) { - debug!(?addr, ?prev, "selected new relay path"); + debug!(?addr, ?prev, "selected new path"); } self.open_path(&addr); self.close_redundant_paths(&addr); @@ -934,78 +919,43 @@ impl EndpointStateActor { /// /// Makes sure not to close the last direct path. Relay paths are never closed /// currently, because we only have one relay path at this time. + // TODO: Need to handle this on a timer as well probably. In .select_path() we open new + // paths and immediately call this. But the new paths are probably not yet open on + // all connections. fn close_redundant_paths(&mut self, selected_path: &transports::Addr) { debug_assert_eq!(self.selected_path.as_ref(), Some(selected_path)); - // We create this to make sure we do not close the last direct path. - let mut paths_per_conn: FxHashMap> = FxHashMap::default(); - for ((conn_id, path_id), addr) in self.path_id_map.iter() { - if !addr.is_ip() { - continue; - } - paths_per_conn.entry(*conn_id).or_default().push(*path_id); - } - - self.path_id_map.retain(|(conn_id, path_id), addr| { - if !addr.is_ip() || addr == selected_path { - // This not a direct path or is the selected path. - return true; - } - if paths_per_conn - .get(conn_id) - .map(|paths| paths.len() == 1) - .unwrap_or_default() - { - // This is the only direct path on this connection. - return true; - } - if let Some(conn) = self - .connections - .get(conn_id) - .and_then(|c| c.handle.upgrade()) - { - trace!(?addr, ?conn_id, ?path_id, "closing direct path"); - if let Some(path) = conn.path(*path_id) { + for (conn_id, conn_state) in self.connections.iter() { + for (path_id, path_remote) in conn_state.paths.iter() { + if path_remote.is_relay() { + continue; + } + if path_remote == selected_path { + continue; // Do not close the selected path. + } + if conn_state.open_paths.contains_key(path_id) && conn_state.open_paths.len() <= 1 { + continue; // Do not close the last direct path. + } + if let Some(path) = conn_state + .handle + .upgrade() + .and_then(|conn| conn.path(*path_id)) + { + trace!(?path_remote, ?conn_id, ?path_id, "closing direct path"); match path.close(APPLICATION_ABANDON_PATH.into()) { Err(quinn_proto::ClosePathError::LastOpenPath) => { error!("could not close last open path"); } - Err(quinn_proto::ClosePathError::ClosedPath) => (), + Err(quinn_proto::ClosePathError::ClosedPath) => { + // We already closed this. + } Ok(_fut) => { - // TODO: Should investigate if we care about this future. + // We will handle the event in Self::handle_path_events. } } } } - false - }); - } - - /// Returns the remote [`transports::Addr`] for a path. - fn path_transports_addr( - &self, - conn: &quinn::Connection, - path_id: PathId, - ) -> Option { - conn.path(path_id).and_then(|path| { - path.remote_address() - .map_or(None, |remote| match MultipathMappedAddr::from(remote) { - MultipathMappedAddr::Mixed(_) => { - error!("Mixed addr in use for path"); - None - } - MultipathMappedAddr::Relay(mapped) => { - match self.relay_mapped_addrs.lookup(&mapped) { - Some(parts) => Some(transports::Addr::from(parts)), - None => { - error!("Unknown RelayMappedAddr in path"); - None - } - } - } - MultipathMappedAddr::Ip(addr) => Some(addr.into()), - }) - }) + } } } @@ -1100,12 +1050,60 @@ pub enum ConnectionType { None, } +/// Newtype to track Connections. +/// +/// The wrapped value is the [`Connection::stable_id`] value, and is thus only valid for +/// active connections. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct ConnId(usize); + +/// State about one connection. #[derive(Debug)] struct ConnectionState { /// Weak handle to the connection. handle: WeakConnectionHandle, /// The information we publish to users about the paths used in this connection. - paths_info: Watchable>, + // TODO: Improve this. Use a map of TransportAddr once that's merged. Handle the logic + // in a method on this struct. + pub_path_info: Watchable>, + /// The paths that exist on this connection. + /// + /// This could be in any state, e.g. while still validating the path or already closed + /// but not yet fully removed from the connection. This exists as long as Quinn knows + /// about the [`PathId`]. + paths: FxHashMap, + /// The open paths on this connection, a subset of [`Self::paths`]. + open_paths: FxHashMap, + /// Reverse map of [`Self::paths]. + path_ids: FxHashMap, +} + +impl ConnectionState { + /// Tracks a path for the connection. + fn add_path(&mut self, remote: transports::Addr, path_id: PathId) { + self.paths.insert(path_id, remote.clone()); + self.path_ids.insert(remote, path_id); + } + + /// Tracks an open path for the connection. + fn add_open_path(&mut self, remote: transports::Addr, path_id: PathId) { + self.paths.insert(path_id, remote.clone()); + self.open_paths.insert(path_id, remote.clone()); + self.path_ids.insert(remote, path_id); + } + + /// Completely removes a path from this connection. + fn remove_path(&mut self, path_id: &PathId) { + if let Some(addr) = self.paths.remove(path_id) { + self.path_ids.remove(&addr); + } + self.open_paths.remove(path_id); + } + + /// Removes the path from the open paths. + fn remove_open_path(&mut self, path_id: &PathId) { + self.open_paths.remove(path_id); + } } /// Information about a network path used by a [`Connection`]. @@ -1155,13 +1153,6 @@ impl From<&transports::Addr> for TransportType { } } -/// Newtype to track Connections. -/// -/// The wrapped value is the [`Connection::stable_id`] value, and is thus only valid for -/// active connections. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct ConnId(usize); - /// Poll a future once, like n0_future::future::poll_once but sync. fn now_or_never>(fut: F) -> Option { let fut = std::pin::pin!(fut); diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 4132f3b2d99..77a87b9c890 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -14,9 +14,12 @@ use std::{ }, }; +use iroh_base::{EndpointId, RelayUrl}; use rustc_hash::FxHashMap; use snafu::Snafu; -use tracing::trace; +use tracing::{error, trace}; + +use super::transports; /// The Prefix/L of all Unique Local Addresses. const ADDR_PREFIXL: u8 = 0xfd; @@ -91,6 +94,28 @@ impl From for MultipathMappedAddr { } } +impl MultipathMappedAddr { + pub(super) fn to_transport_addr( + &self, + relay_mapped_addrs: &AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, + ) -> Option { + match self { + Self::Mixed(_) => { + error!("Mixed addr has not transports::Addr"); + None + } + Self::Relay(mapped) => match relay_mapped_addrs.lookup(mapped) { + Some(parts) => Some(transports::Addr::from(parts)), + None => { + error!("Unknown RelayMappedAddr"); + None + } + }, + Self::Ip(addr) => Some(transports::Addr::from(*addr)), + } + } +} + /// An address used to address a endpoint on any or all paths. /// /// This is only used for initially connecting to a remote endpoint. We instruct Quinn to From 203204c2a75a0313282e4e39e30dafa46886529d Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Mon, 20 Oct 2025 20:21:18 +0200 Subject: [PATCH 108/164] some fixes to path selection --- .../magicsock/endpoint_map/endpoint_state.rs | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 55d25daa872..6a3ec3f5913 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -852,15 +852,10 @@ impl EndpointStateActor { /// direct paths are closed for all connections. #[instrument(skip_all)] fn select_path(&mut self) { - // TODO: Only consider paths that are actively open: that is we received the open - // event and have not closed it yet, or have not received a close. Otherwise we - // might select from paths that doen't work. Plus we might not have a - // representative RTT time yet. - // Find the lowest RTT across all connections for each open path. The long way, so // we get to log *all* RTTs. let mut all_path_rtts: FxHashMap> = FxHashMap::default(); - for (conn_id, conn_state) in self.connections.iter() { + for conn_state in self.connections.values() { let Some(conn) = conn_state.handle.upgrade() else { continue; }; @@ -871,8 +866,6 @@ impl EndpointStateActor { .entry(addr.clone()) .or_default() .push(stats.rtt); - } else { - trace!(?conn_id, ?path_id, "unknown PathId in ConnectionStats"); } } } @@ -894,18 +887,16 @@ impl EndpointStateActor { (*rtt, addr) } }) - .min() - .map(|(_rtt, addr)| addr.clone()); + .min(); let selected_path = direct_path.or_else(|| { // Find the fasted relay path. path_rtts .iter() .filter(|(addr, _rtt)| addr.is_relay()) - .map(|(addr, rtt)| (rtt, addr)) + .map(|(addr, rtt)| (*rtt, addr)) .min() - .map(|(_rtt, addr)| addr.clone()) }); - if let Some(addr) = selected_path { + if let Some((rtt, addr)) = selected_path { let prev = self.selected_path.replace(addr.clone()); if prev.as_ref() != Some(&addr) { debug!(?addr, ?prev, "selected new path"); @@ -926,14 +917,13 @@ impl EndpointStateActor { debug_assert_eq!(self.selected_path.as_ref(), Some(selected_path)); for (conn_id, conn_state) in self.connections.iter() { - for (path_id, path_remote) in conn_state.paths.iter() { - if path_remote.is_relay() { - continue; - } - if path_remote == selected_path { - continue; // Do not close the selected path. - } - if conn_state.open_paths.contains_key(path_id) && conn_state.open_paths.len() <= 1 { + for (path_id, path_remote) in conn_state + .open_paths + .iter() + .filter(|(_, addr)| addr.is_ip()) + .filter(|(_, addr)| *addr != selected_path) + { + if conn_state.open_paths.values().filter(|a| a.is_ip()).count() <= 1 { continue; // Do not close the last direct path. } if let Some(path) = conn_state From de2074ec9e7e09e1a9efa010c6519963d6e3a6db Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 21 Oct 2025 12:44:48 +0200 Subject: [PATCH 109/164] small cleanups, review comments --- .../magicsock/endpoint_map/endpoint_state.rs | 31 ++++++++++--------- iroh/src/magicsock/transports.rs | 11 ------- 2 files changed, 16 insertions(+), 26 deletions(-) diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 6a3ec3f5913..5ab035dbac6 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -61,6 +61,16 @@ use crate::{ // TODO: Quinn should just do this. Also, I made this value up. const APPLICATION_ABANDON_PATH: u8 = 30; +/// A stream of events from all paths for all connections. +/// +/// The connection is identified using [`ConnId`]. The event `Err` variant happens when the +/// actor has lagged processing the events, which is rather critical for us. +type PathEvents = MergeUnbounded< + Pin< + Box)> + Send + Sync>, + >, +>; + /// The state we need to know about a single remote endpoint. /// /// This actor manages all connections to the remote endpoint. It will trigger holepunching @@ -92,17 +102,8 @@ pub(super) struct EndpointStateActor { // /// All connections we have to this remote endpoint. connections: FxHashMap, - /// Events emitted by Quinn about path changes. - #[allow(clippy::type_complexity)] - path_events: MergeUnbounded< - Pin< - Box< - dyn Stream)> - + Send - + Sync, - >, - >, - >, + /// Events emitted by Quinn about path changes, for all paths, all connections. + path_events: PathEvents, // Internal state - Holepunching and path state. // @@ -898,11 +899,11 @@ impl EndpointStateActor { }); if let Some((rtt, addr)) = selected_path { let prev = self.selected_path.replace(addr.clone()); - if prev.as_ref() != Some(&addr) { - debug!(?addr, ?prev, "selected new path"); + if prev.as_ref() != Some(addr) { + debug!(?addr, ?rtt, ?prev, "selected new path"); } - self.open_path(&addr); - self.close_redundant_paths(&addr); + self.open_path(addr); + self.close_redundant_paths(addr); } } diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 91abc31fe75..df79ccdb31c 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -602,18 +602,7 @@ impl MagicSender { /// IPv6 Unique Local Address ranges. This extracts the transport addresses out of the /// transmit's destination. fn mapped_addr(&self, transmit: &quinn_udp::Transmit) -> io::Result { - self.msock - .metrics - .magicsock - .send_data - .inc_by(transmit.contents.len() as _); - if self.msock.is_closed() { - self.msock - .metrics - .magicsock - .send_data_network_down - .inc_by(transmit.contents.len() as _); return Err(io::Error::new( io::ErrorKind::NotConnected, "connection closed", From 9fcdd719c3fc7f15b87c681812e95e8089459a17 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Tue, 21 Oct 2025 18:31:26 +0200 Subject: [PATCH 110/164] Use TransportAddr to improve PathInfo exposed The nice thing is that the setting of PathInfo is now a lot more sane. The bad thing is that we are making random things in the Endpoint and MagicSock pub(crate). --- iroh/src/endpoint.rs | 28 ++-- iroh/src/magicsock.rs | 4 +- iroh/src/magicsock/endpoint_map.rs | 25 +++- .../magicsock/endpoint_map/endpoint_state.rs | 122 ++++++++---------- iroh/src/magicsock/mapped_addrs.rs | 2 +- iroh/src/magicsock/transports.rs | 11 +- 6 files changed, 103 insertions(+), 89 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index df797d2de3c..71c372f5bbd 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -13,6 +13,7 @@ use std::{ any::Any, + collections::HashMap, future::{Future, IntoFuture}, net::{IpAddr, SocketAddr, SocketAddrV4, SocketAddrV6}, pin::Pin, @@ -61,8 +62,8 @@ use crate::{ magicsock::{ self, HEARTBEAT_INTERVAL, Handle, MAX_MULTIPATH_PATHS, OwnAddressSnafu, PATH_MAX_IDLE_TIMEOUT, PathInfo, - endpoint_map::{Source, TransportType}, - mapped_addrs::{EndpointIdMappedAddr, MappedAddr, MultipathMappedAddr}, + endpoint_map::Source, + mapped_addrs::{EndpointIdMappedAddr, MappedAddr}, }, metrics::EndpointMetrics, net_report::Report, @@ -484,7 +485,7 @@ impl StaticConfig { #[derive(Clone, Debug)] pub struct Endpoint { /// Handle to the magicsocket/actor - msock: Handle, + pub(crate) msock: Handle, /// Configuration structs for quinn, holds the transport config, certificate setup, secret key etc. static_config: Arc, } @@ -1670,7 +1671,7 @@ impl Future for ZeroRttAccepted { #[derive(Debug, Clone)] pub struct Connection { inner: quinn::Connection, - paths_info: n0_watcher::Direct>, + paths_info: n0_watcher::Direct>, } #[allow(missing_docs)] @@ -1682,13 +1683,19 @@ pub struct RemoteEndpointIdError { impl Connection { fn new(inner: quinn::Connection, remote_id: Option, ep: &Endpoint) -> Self { - let mut paths_info = Vec::with_capacity(1); + let mut paths_info = HashMap::with_capacity(5); if let Some(path0) = inner.path(PathId::ZERO) { // This all is supposed to be infallible, but anyway. if let Ok(remote) = path0.remote_address() { - let mapped = MultipathMappedAddr::from(remote); - let transport = TransportType::from(mapped); - paths_info.push(PathInfo { transport }); + if let Some(remote) = ep.msock.endpoint_map.transport_addr_from_mapped(remote) { + paths_info.insert( + remote.clone(), + PathInfo { + remote, + path_id: PathId::ZERO, + }, + ); + } } } let paths_info_watcher = n0_watcher::Watchable::new(paths_info); @@ -1971,7 +1978,7 @@ impl Connection { /// A connection can have several network paths to the remote endpoint, commonly there /// will be a path via the relay server and a holepunched path. This returns all the /// paths in use by this connection. - pub fn paths_info(&self) -> impl Watcher> { + pub fn paths_info(&self) -> impl Watcher> { self.paths_info.clone() } @@ -2133,7 +2140,6 @@ mod tests { RelayMap, RelayMode, discovery::static_provider::StaticProvider, endpoint::{ConnectOptions, Connection, ConnectionType}, - magicsock::endpoint_map::TransportType, protocol::{AcceptError, ProtocolHandler, Router}, test_utils::{run_relay_server, run_relay_server_with}, }; @@ -2610,7 +2616,7 @@ mod tests { info!("Waiting for direct connection"); while let Some(infos) = paths.next().await { info!(?infos, "new PathInfos"); - if infos.iter().any(|info| info.transport == TransportType::Ip) { + if infos.keys().any(|addr| addr.is_ip()) { break; } } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 365cc9fc190..9ac643050fc 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -197,7 +197,7 @@ pub(crate) struct MagicSock { /// If the last net_report report, reports IPv6 to be available. ipv6_reported: Arc, /// Tracks the networkmap endpoint entity for each endpoint discovery key. - endpoint_map: EndpointMap, + pub(crate) endpoint_map: EndpointMap, /// Local addresses local_addrs_watch: LocalAddrsWatch, @@ -282,7 +282,7 @@ impl MagicSock { &self, remote: EndpointId, conn: &quinn::Connection, - paths_info: n0_watcher::Watchable>, + paths_info: n0_watcher::Watchable>, ) { // TODO: Spawning tasks like this is obviously bad. But it is solvable: // - This is only called from inside Connection::new. diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 8ec555fa124..411d2a89e42 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -5,11 +5,11 @@ use std::{ sync::{Arc, Mutex}, }; -use iroh_base::{EndpointAddr, EndpointId, RelayUrl}; +use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; use n0_future::task::AbortOnDropHandle; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::{Instrument, info_span, trace, warn}; +use tracing::{Instrument, error, info_span, trace, warn}; #[cfg(any(test, feature = "test-utils"))] use super::transports::TransportsSender; @@ -17,7 +17,7 @@ use super::transports::TransportsSender; use super::transports::TransportsSender; use super::{ DirectAddr, DiscoState, MagicsockMetrics, - mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, + mapped_addrs::{AddrMap, EndpointIdMappedAddr, MultipathMappedAddr, RelayMappedAddr}, transports::{self, OwnedTransmit}, }; use crate::disco::{self}; @@ -28,7 +28,7 @@ mod endpoint_state; mod path_state; pub(super) use endpoint_state::EndpointStateMessage; -pub use endpoint_state::{ConnectionType, PathInfo, TransportType}; +pub use endpoint_state::{ConnectionType, PathInfo}; use endpoint_state::{EndpointStateActor, EndpointStateHandle}; // TODO: use this @@ -38,7 +38,7 @@ use endpoint_state::{EndpointStateActor, EndpointStateHandle}; /// Map of the [`EndpointState`] information for all the known endpoints. #[derive(Debug)] -pub(super) struct EndpointMap { +pub(crate) struct EndpointMap { /// The endpoint ID of the local endpoint. local_endpoint_id: EndpointId, inner: Mutex, @@ -158,6 +158,21 @@ impl EndpointMap { self.endpoint_mapped_addrs.get(&eid) } + /// Converts a mapped address as we use them inside Quinn. + pub(crate) fn transport_addr_from_mapped(&self, mapped: SocketAddr) -> Option { + match MultipathMappedAddr::from(mapped) { + MultipathMappedAddr::Mixed(_) => None, + MultipathMappedAddr::Relay(addr) => match self.relay_mapped_addrs.lookup(&addr) { + Some((url, _)) => Some(TransportAddr::Relay(url)), + None => { + error!("Unknown RelayMappedAddr"); + None + } + }, + MultipathMappedAddr::Ip(addr) => Some(TransportAddr::Ip(addr)), + } + } + /// Returns a [`n0_watcher::Direct`] for given endpoint's [`ConnectionType`]. /// /// # Errors diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 1a7d8a91c74..6071362d311 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -1,6 +1,11 @@ -use std::{collections::BTreeSet, net::SocketAddr, pin::Pin, sync::Arc}; +use std::{ + collections::{BTreeSet, HashMap}, + net::SocketAddr, + pin::Pin, + sync::Arc, +}; -use iroh_base::{EndpointAddr, EndpointId, RelayUrl}; +use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; use n0_future::{ MergeUnbounded, Stream, StreamExt, task::AbortOnDropHandle, @@ -294,7 +299,7 @@ impl EndpointStateActor { async fn handle_msg_add_connection( &mut self, handle: WeakConnectionHandle, - paths_info: Watchable>, + paths_info: Watchable>, ) { if let Some(conn) = handle.upgrade() { // Remove any conflicting stable_ids from the local state. @@ -767,11 +772,6 @@ impl EndpointStateActor { .or_default() .sources .insert(Source::Connection, Instant::now()); - let mut paths = conn_state.pub_path_info.get(); - paths.push(PathInfo { - transport: path_remote.into(), - }); - conn_state.pub_path_info.set(paths).ok(); } self.select_path(); @@ -795,21 +795,6 @@ impl EndpointStateActor { path_id = ?id, ); conn_state.remove_open_path(&id); - // Remove this from the public PathInfo. - if let Some(state) = self.connections.get(&conn_id) { - let mut path_info = state.pub_path_info.get(); - let transport = TransportType::from(&path_remote); - let mut done = false; - path_info.retain(|info| { - if !done && info.transport == transport { - done = true; - false - } else { - true - } - }); - state.pub_path_info.set(path_info).ok(); - } // If one connection closes this path, close it on all connections. for (conn_id, conn_state) in self.connections.iter_mut() { @@ -969,7 +954,10 @@ pub(crate) enum EndpointStateMessage { /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. #[debug("AddConnection(..)")] - AddConnection(WeakConnectionHandle, Watchable>), + AddConnection( + WeakConnectionHandle, + Watchable>, + ), /// Adds a [`EndpointAddr`] with locations where the endpoint might be reachable. AddEndpointAddr(EndpointAddr, Source), /// Process a received DISCO CallMeMaybe message. @@ -1054,9 +1042,7 @@ struct ConnectionState { /// Weak handle to the connection. handle: WeakConnectionHandle, /// The information we publish to users about the paths used in this connection. - // TODO: Improve this. Use a map of TransportAddr once that's merged. Handle the logic - // in a method on this struct. - pub_path_info: Watchable>, + pub_path_info: Watchable>, /// The paths that exist on this connection. /// /// This could be in any state, e.g. while still validating the path or already closed @@ -1081,6 +1067,8 @@ impl ConnectionState { self.paths.insert(path_id, remote.clone()); self.open_paths.insert(path_id, remote.clone()); self.path_ids.insert(remote, path_id); + + self.update_pub_path_info(); } /// Completely removes a path from this connection. @@ -1094,54 +1082,50 @@ impl ConnectionState { /// Removes the path from the open paths. fn remove_open_path(&mut self, path_id: &PathId) { self.open_paths.remove(path_id); - } -} -/// Information about a network path used by a [`Connection`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PathInfo { - /// The kind of transport this network path is using. - pub transport: TransportType, -} - -/// Different kinds of transports a [`Connection`] can use. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TransportType { - /// A transport via a relay server. - Relay, - /// A transport via an IP connection. - Ip, -} - -impl From for TransportType { - fn from(source: MultipathMappedAddr) -> Self { - match source { - MultipathMappedAddr::Mixed(_) => { - error!("paths should not use mixed addrs"); - TransportType::Relay - } - MultipathMappedAddr::Relay(_) => TransportType::Relay, - MultipathMappedAddr::Ip(_) => TransportType::Ip, - } + self.update_pub_path_info(); } -} -impl From for TransportType { - fn from(source: transports::Addr) -> Self { - match source { - transports::Addr::Ip(_) => Self::Ip, - transports::Addr::Relay(_, _) => Self::Relay, - } + /// Sets the new [`PathInfo`] structs for the public [`Connection`]. + fn update_pub_path_info(&self) { + let new = self + .open_paths + .iter() + .map(|(path_id, remote)| { + let remote = TransportAddr::from(remote.clone()); + ( + remote.clone(), + PathInfo { + remote: remote.clone(), + path_id: *path_id, + }, + ) + }) + .collect::>(); + + self.pub_path_info.set(new).ok(); } } -impl From<&transports::Addr> for TransportType { - fn from(source: &transports::Addr) -> Self { - match source { - transports::Addr::Ip(_) => Self::Ip, - transports::Addr::Relay(_, _) => Self::Relay, - } - } +/// Information about a network path used by a [`Connection`]. +/// +/// [`Connection`]: crate::endpoint::Connection +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PathInfo { + /// The remote transport address used by this network path. + pub remote: TransportAddr, + /// The internal path identifier for the [`Connection`] + /// + /// This is unique for the lifetime of the connection. Can be used to look up the path + /// statistics in the [`ConnectionStats::paths`], returned by [`Connection::stats`]. + /// + /// [`Connection`]: crate::endpoint::Connection + /// [`Connection::stats`]: crate::endpoint::Connection::stats + /// [`ConnectionStats::paths`]: crate::endpoint::ConnectionStats::paths + // TODO: Decide if exposing this is a good idea. Maybe we should just hide this + // entirely try to provide Self::stats(). But that would mean this needs to have a + // WeakConnectionHandle. + pub path_id: PathId, } /// Poll a future once, like n0_future::future::poll_once but sync. diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 77a87b9c890..0f8b473bc28 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -101,7 +101,7 @@ impl MultipathMappedAddr { ) -> Option { match self { Self::Mixed(_) => { - error!("Mixed addr has not transports::Addr"); + error!("Mixed addr has no transports::Addr"); None } Self::Relay(mapped) => match relay_mapped_addrs.lookup(mapped) { diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 4a4e148590d..677bced59ce 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -8,7 +8,7 @@ use std::{ }; use bytes::Bytes; -use iroh_base::{EndpointId, RelayUrl}; +use iroh_base::{EndpointId, RelayUrl, TransportAddr}; use n0_watcher::Watcher; use relay::{RelayNetworkChangeSender, RelaySender}; use tracing::{debug, error, instrument, trace, warn}; @@ -380,6 +380,15 @@ impl From<(RelayUrl, EndpointId)> for Addr { } } +impl From for TransportAddr { + fn from(value: Addr) -> Self { + match value { + Addr::Ip(addr) => TransportAddr::Ip(addr), + Addr::Relay(url, _) => TransportAddr::Relay(url), + } + } +} + impl Addr { pub(crate) fn is_relay(&self) -> bool { matches!(self, Self::Relay(..)) From de6eefcf8f0ce14e89bcb9c94c7816dec1c586a9 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 22 Oct 2025 12:27:24 +0200 Subject: [PATCH 111/164] compile on wasm again --- iroh/src/magicsock.rs | 1 - iroh/src/magicsock/endpoint_map.rs | 4 ++-- iroh/src/magicsock/endpoint_map/endpoint_state.rs | 8 ++++---- iroh/src/magicsock/mapped_addrs.rs | 11 ++++++----- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 9ac643050fc..d67513f4f9f 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1115,7 +1115,6 @@ impl Handle { let net_reporter = net_report::Client::new( #[cfg(not(wasm_browser))] dns_resolver, - #[cfg(not(wasm_browser))] relay_map.clone(), net_report_config, metrics.net_report.clone(), diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 411d2a89e42..4ba29ef3277 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -6,7 +6,7 @@ use std::{ }; use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; -use n0_future::task::AbortOnDropHandle; +use n0_future::task::{self, AbortOnDropHandle}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{Instrument, error, info_span, trace, warn}; @@ -363,7 +363,7 @@ impl TransportsSenderActor { // can. No need to introduce extra buffering. let (tx, rx) = mpsc::channel(1); - let task = tokio::spawn( + let task = task::spawn( async move { self.run(rx).await; } diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 6071362d311..84d5764cd42 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -8,8 +8,8 @@ use std::{ use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; use n0_future::{ MergeUnbounded, Stream, StreamExt, - task::AbortOnDropHandle, - time::{Duration, Instant}, + task::{self, AbortOnDropHandle}, + time::{self, Duration, Instant}, }; use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; @@ -169,7 +169,7 @@ impl EndpointStateActor { // we don't explicitly set a span we get the spans from whatever call happens to // first create the actor, which is often very confusing as it then keeps those // spans for all logging of the actor. - let task = tokio::spawn( + let task = task::spawn( async move { if let Err(err) = self.run(rx).await { error!("actor failed: {err:#}"); @@ -200,7 +200,7 @@ impl EndpointStateActor { trace!("actor started"); loop { let scheduled_hp = match self.scheduled_holepunch { - Some(when) => MaybeFuture::Some(tokio::time::sleep_until(when)), + Some(when) => MaybeFuture::Some(time::sleep_until(when)), None => MaybeFuture::None, }; let mut scheduled_hp = std::pin::pin!(scheduled_hp); diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 0f8b473bc28..16e36ae66ea 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -62,9 +62,12 @@ pub(crate) trait MappedAddr { /// An enum encompassing all the mapped and unmapped addresses. /// -/// This can consistently convert a socket address as we use them in Quinn and return a real -/// socket address or a mapped address. Note that this does not mean that the mapped -/// address exists, only that it is semantically a valid mapped address. +/// This is essentially a slightly-stronger typed version of the IPv6 mapped addresses that +/// we use on the Quinn side. It categorises the addressed in what kind of mapped or +/// unmapped addresses they are. +/// +/// It does not guarantee that a mapped address exists in the mapping. Or that a particular +/// address is even supported on this platform. Hence no wasm exceptions here. #[derive(Clone, Debug)] pub(crate) enum MultipathMappedAddr { /// An address for a [`EndpointId`], via one or more paths. @@ -72,7 +75,6 @@ pub(crate) enum MultipathMappedAddr { /// An address for a particular [`EndpointId`] via a particular relay. Relay(RelayMappedAddr), /// An IP based transport address. - #[cfg(not(wasm_browser))] Ip(SocketAddr), } @@ -87,7 +89,6 @@ impl From for MultipathMappedAddr { if let Ok(addr) = RelayMappedAddr::try_from(addr) { return Self::Relay(addr); } - #[cfg(not(wasm_browser))] Self::Ip(value) } } From c2b131f90c8b993b9bc41449b9d40bdb86f5a588 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sat, 25 Oct 2025 18:21:10 +0200 Subject: [PATCH 112/164] add relay to a new connection that is direct (#3569) When a new connection arrives that is direct, we should add the relay connection. Because the initial connection was probably racing direct and relay and direct won. --- Cargo.lock | 18 +- iroh/src/endpoint.rs | 271 ++++++++++++------ .../magicsock/endpoint_map/endpoint_state.rs | 54 +++- 3 files changed, 236 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d304c0bcfe..8497b76c579 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1977,7 +1977,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2428,7 +2428,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a4597406bf649a8eb38a5f8a1861979b6cee2ef4" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e74c803219cb83f1780f59dcc6823f422f244703" dependencies = [ "bytes", "cfg_aliases", @@ -2437,7 +2437,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.6.0", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2448,7 +2448,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a4597406bf649a8eb38a5f8a1861979b6cee2ef4" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e74c803219cb83f1780f59dcc6823f422f244703" dependencies = [ "bytes", "fastbloom", @@ -2470,12 +2470,12 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#a4597406bf649a8eb38a5f8a1861979b6cee2ef4" +source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#e74c803219cb83f1780f59dcc6823f422f244703" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -3575,7 +3575,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.0", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -3612,7 +3612,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -5484,7 +5484,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.48.0", ] [[package]] diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 71c372f5bbd..fa5f109e92f 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2126,7 +2126,7 @@ mod tests { use std::time::{Duration, Instant}; use iroh_base::{EndpointAddr, EndpointId, SecretKey, TransportAddr}; - use n0_future::{BufferedStreamExt, StreamExt, stream, task::AbortOnDropHandle}; + use n0_future::{BufferedStreamExt, StreamExt, stream, task::AbortOnDropHandle, time}; use n0_snafu::{Error, Result, ResultExt}; use n0_watcher::Watcher; use quinn::ConnectionError; @@ -2474,6 +2474,184 @@ mod tests { Ok(()) } + #[tokio::test] + #[traced_test] + async fn endpoint_two_relay_only() -> Result { + // Connect two endpoints on the same network, via a relay server, without + // discovery. + let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; + let (node_addr_tx, node_addr_rx) = oneshot::channel(); + + #[instrument(name = "client", skip_all)] + async fn connect( + relay_map: RelayMap, + node_addr_rx: oneshot::Receiver, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let secret = SecretKey::generate(&mut rng); + let ep = Endpoint::builder() + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + info!(me = %ep.id().fmt_short(), "client starting"); + let dst = node_addr_rx.await.e()?; + + info!(me = %ep.id().fmt_short(), "client connecting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + let mut send = conn.open_uni().await.e()?; + send.write_all(b"hello").await.e()?; + let mut paths = conn.paths_info().stream(); + info!("Waiting for direct connection"); + while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + if infos.keys().any(|addr| addr.is_ip()) { + break; + } + } + info!("Have direct connection"); + send.write_all(b"close please").await.e()?; + send.finish().e()?; + Ok(conn.closed().await) + } + + #[instrument(name = "server", skip_all)] + async fn accept( + relay_map: RelayMap, + node_addr_tx: oneshot::Sender, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1u64); + let secret = SecretKey::generate(&mut rng); + let ep = Endpoint::builder() + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + ep.online().await; + let mut node_addr = ep.addr(); + node_addr.addrs.retain(|addr| addr.is_relay()); + node_addr_tx.send(node_addr).unwrap(); + + info!(me = %ep.id().fmt_short(), "server starting"); + let conn = ep.accept().await.e()?.await.e()?; + // let node_id = conn.remote_node_id()?; + // assert_eq!(node_id, src); + let mut recv = conn.accept_uni().await.e()?; + let mut msg = [0u8; 5]; + recv.read_exact(&mut msg).await.e()?; + assert_eq!(&msg, b"hello"); + info!("received hello"); + let msg = recv.read_to_end(100).await.e()?; + assert_eq!(msg, b"close please"); + info!("received 'close please'"); + // Dropping the connection closes it just fine. + Ok(()) + } + + let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx)); + let client_task = tokio::spawn(connect(relay_map, node_addr_rx)); + + server_task.await.e()??; + let conn_closed = dbg!(client_task.await.e()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(quinn::ApplicationClose { .. }) + )); + + Ok(()) + } + + #[tokio::test] + #[traced_test] + async fn endpoint_two_direct_add_relay() -> Result { + // Connect two endpoints on the same network, without relay server and without + // discovery. Add a relay connection later. + let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; + let (node_addr_tx, node_addr_rx) = oneshot::channel(); + + #[instrument(name = "client", skip_all)] + async fn connect( + relay_map: RelayMap, + node_addr_rx: oneshot::Receiver, + ) -> Result<()> { + let secret = SecretKey::from([0u8; 32]); + let ep = Endpoint::builder() + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + info!(me = %ep.id().fmt_short(), "client starting"); + let dst = node_addr_rx.await.e()?; + + info!(me = %ep.id().fmt_short(), "client connecting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + + // We should be connected via IP, because it is faster than the relay server. + // TODO: Maybe not panic if this is not true? + let path_info = conn.paths_info().get(); + assert_eq!(path_info.len(), 1); + assert!(path_info.keys().next().unwrap().is_ip()); + + let mut paths = conn.paths_info().stream(); + time::timeout(Duration::from_secs(5), async move { + let mut have_relay = false; + while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + have_relay = infos.keys().any(|a| a.is_relay()); + if have_relay { + break; + } + } + have_relay + }) + .await + .e()?; + conn.close(0u8.into(), b""); + ep.close().await; + Ok(()) + } + + #[instrument(name = "server", skip_all)] + async fn accept( + relay_map: RelayMap, + node_addr_tx: oneshot::Sender, + ) -> Result { + let secret = SecretKey::from([1u8; 32]); + let ep = Endpoint::builder() + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .bind() + .await?; + ep.online().await; + let node_addr = ep.addr(); + node_addr_tx.send(node_addr).unwrap(); + + info!(me = %ep.id().fmt_short(), "server starting"); + let conn = ep.accept().await.e()?.await.e()?; + Ok(conn.closed().await) + } + + let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx)); + let client_task = tokio::spawn(connect(relay_map, node_addr_rx)); + + client_task.await.e()??; + let conn_closed = dbg!(server_task.await.e()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(quinn::ApplicationClose { .. }) + )); + + Ok(()) + } + #[tokio::test] #[traced_test] async fn endpoint_relay_map_change() -> Result { @@ -2583,97 +2761,6 @@ mod tests { Ok(()) } - #[tokio::test] - #[traced_test] - async fn endpoint_two_relay_only() -> Result { - // Connect two endpoints on the same network, via a relay server, without - // discovery. - let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; - let (node_addr_tx, node_addr_rx) = oneshot::channel(); - - #[instrument(name = "client", skip_all)] - async fn connect( - relay_map: RelayMap, - node_addr_rx: oneshot::Receiver, - ) -> Result { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let secret = SecretKey::generate(&mut rng); - let ep = Endpoint::builder() - .secret_key(secret) - .alpns(vec![TEST_ALPN.to_vec()]) - .insecure_skip_relay_cert_verify(true) - .relay_mode(RelayMode::Custom(relay_map)) - .bind() - .await?; - info!(me = %ep.id().fmt_short(), "client starting"); - let dst = node_addr_rx.await.e()?; - - info!(me = %ep.id().fmt_short(), "client connecting"); - let conn = ep.connect(dst, TEST_ALPN).await?; - let mut send = conn.open_uni().await.e()?; - send.write_all(b"hello").await.e()?; - let mut paths = conn.paths_info().stream(); - info!("Waiting for direct connection"); - while let Some(infos) = paths.next().await { - info!(?infos, "new PathInfos"); - if infos.keys().any(|addr| addr.is_ip()) { - break; - } - } - info!("Have direct connection"); - send.write_all(b"close please").await.e()?; - send.finish().e()?; - Ok(conn.closed().await) - } - - #[instrument(name = "server", skip_all)] - async fn accept( - relay_map: RelayMap, - node_addr_tx: oneshot::Sender, - ) -> Result { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1u64); - let secret = SecretKey::generate(&mut rng); - let ep = Endpoint::builder() - .secret_key(secret) - .alpns(vec![TEST_ALPN.to_vec()]) - .insecure_skip_relay_cert_verify(true) - .relay_mode(RelayMode::Custom(relay_map)) - .bind() - .await?; - ep.online().await; - let mut node_addr = ep.addr(); - node_addr.addrs.retain(|addr| addr.is_relay()); - node_addr_tx.send(node_addr).unwrap(); - - info!(me = %ep.id().fmt_short(), "server starting"); - let conn = ep.accept().await.e()?.await.e()?; - // let node_id = conn.remote_node_id()?; - // assert_eq!(node_id, src); - let mut recv = conn.accept_uni().await.e()?; - let mut msg = [0u8; 5]; - recv.read_exact(&mut msg).await.e()?; - assert_eq!(&msg, b"hello"); - info!("received hello"); - let msg = recv.read_to_end(100).await.e()?; - assert_eq!(msg, b"close please"); - info!("received 'close please'"); - // Dropping the connection closes it just fine. - Ok(()) - } - - let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx)); - let client_task = tokio::spawn(connect(relay_map, node_addr_rx)); - - server_task.await.e()??; - let conn_closed = dbg!(client_task.await.e()??); - assert!(matches!( - conn_closed, - ConnectionError::ApplicationClosed(quinn::ApplicationClose { .. }) - )); - - Ok(()) - } - #[tokio::test] #[traced_test] async fn endpoint_bidi_send_recv() -> Result { diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 84d5764cd42..08ba5951fb8 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeSet, HashMap, VecDeque}, net::SocketAddr, pin::Pin, sync::Arc, @@ -13,7 +13,7 @@ use n0_future::{ }; use n0_watcher::{Watchable, Watcher}; use quinn::WeakConnectionHandle; -use quinn_proto::{PathEvent, PathId, PathStatus}; +use quinn_proto::{PathError, PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use snafu::{ResultExt, Whatever}; @@ -130,6 +130,12 @@ pub(super) struct EndpointStateActor { selected_path: Option, /// Time at which we should schedule the next holepunch attempt. scheduled_holepunch: Option, + /// When to next attempt opening paths in [`Self::pending_open_paths`]. + scheduled_open_path: Option, + /// Paths which we still need to open. + /// + /// They failed to open because we did not have enough CIDs issued by the remote. + pending_open_paths: VecDeque, } impl EndpointStateActor { @@ -156,6 +162,8 @@ impl EndpointStateActor { last_holepunch: None, selected_path: None, scheduled_holepunch: None, + scheduled_open_path: None, + pending_open_paths: VecDeque::new(), } } @@ -199,11 +207,16 @@ impl EndpointStateActor { ) -> Result<(), Whatever> { trace!("actor started"); loop { + let scheduled_path_open = match self.scheduled_open_path { + Some(when) => MaybeFuture::Some(time::sleep_until(when)), + None => MaybeFuture::None, + }; + n0_future::pin!(scheduled_path_open); let scheduled_hp = match self.scheduled_holepunch { Some(when) => MaybeFuture::Some(time::sleep_until(when)), None => MaybeFuture::None, }; - let mut scheduled_hp = std::pin::pin!(scheduled_hp); + n0_future::pin!(scheduled_hp); tokio::select! { biased; msg = inbox.recv() => { @@ -219,6 +232,14 @@ impl EndpointStateActor { trace!("local addrs updated, triggering holepunching"); self.trigger_holepunching().await; } + _ = &mut scheduled_path_open => { + trace!("triggering scheduled path_open"); + self.scheduled_open_path = None; + let mut addrs = std::mem::take(&mut self.pending_open_paths); + while let Some(addr) = addrs.pop_front() { + self.open_path(&addr); + } + } _ = &mut scheduled_hp => { trace!("triggering scheduled holepunching"); self.scheduled_holepunch = None; @@ -334,6 +355,7 @@ impl EndpointStateActor { .and_then(|mmaddr| mmaddr.to_transport_addr(&self.relay_mapped_addrs)) { trace!(?path_remote, "added new connection"); + let path_remote_is_ip = path_remote.is_ip(); let status = match path_remote { transports::Addr::Ip(_) => PathStatus::Available, transports::Addr::Relay(_, _) => PathStatus::Backup, @@ -348,10 +370,22 @@ impl EndpointStateActor { .sources .insert(Source::Connection, Instant::now()); self.select_path(); + + if path_remote_is_ip { + // We may have raced this with a relay address. Try and add any + // relay addresses we have back. + let relays = self + .paths + .keys() + .filter(|a| a.is_relay()) + .cloned() + .collect::>(); + for remote in relays { + self.open_path(&remote); + } + } } } - // TODO: Make sure we are adding the relay path if we're on a direct - // path. self.trigger_holepunching().await; } } @@ -715,7 +749,15 @@ impl EndpointStateActor { } None => { let ret = now_or_never(fut); - warn!(?ret, "Opening path failed"); + match ret { + Some(Err(PathError::RemoteCidsExhausted)) => { + self.scheduled_open_path = + Some(Instant::now() + Duration::from_millis(333)); + self.pending_open_paths.push_back(open_addr.clone()); + trace!(?open_addr, "scheduling open_path"); + } + _ => warn!(?ret, "Opening path failed"), + } } } } From a0edf73d9e5ca4a414970405965f67db1da4184e Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 29 Oct 2025 14:03:03 +0100 Subject: [PATCH 113/164] target main-iroh branch --- Cargo.lock | 10 +++++----- iroh-relay/Cargo.toml | 4 ++-- iroh/Cargo.toml | 8 ++++---- iroh/bench/Cargo.toml | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2ffd409799c..c23f82e2819 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2387,7 +2387,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#20e462d8103000b8b5cf29aa8b76f263bd627f83" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#20e462d8103000b8b5cf29aa8b76f263bd627f83" dependencies = [ "bytes", "cfg_aliases", @@ -2407,7 +2407,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#20e462d8103000b8b5cf29aa8b76f263bd627f83" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#20e462d8103000b8b5cf29aa8b76f263bd627f83" dependencies = [ "bytes", "fastbloom", @@ -2429,7 +2429,7 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=multipath-quinn-0.11.x#20e462d8103000b8b5cf29aa8b76f263bd627f83" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#20e462d8103000b8b5cf29aa8b76f263bd627f83" dependencies = [ "cfg_aliases", "libc", @@ -2886,7 +2886,7 @@ dependencies = [ [[package]] name = "netwatch" version = "0.11.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#7fb79f7fe82a9ac8bea2ca5eca195e7448a6087e" +source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#f27b7f011837048dc1245e63a68b1b00e5dedf97" dependencies = [ "atomic-waker", "bytes", @@ -3271,7 +3271,7 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portmapper" version = "0.11.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#7fb79f7fe82a9ac8bea2ca5eca195e7448a6087e" +source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#f27b7f011837048dc1245e63a68b1b00e5dedf97" dependencies = [ "base64", "bytes", diff --git a/iroh-relay/Cargo.toml b/iroh-relay/Cargo.toml index 14295d684d0..5ce4e0564f0 100644 --- a/iroh-relay/Cargo.toml +++ b/iroh-relay/Cargo.toml @@ -42,8 +42,8 @@ postcard = { version = "1", default-features = false, features = [ "use-std", "experimental-derive", ] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index 6841904275c..d59007a536c 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -45,9 +45,9 @@ nested_enum_utils = "0.2.1" netwatch = { version = "0.11" } pin-project = "1" pkarr = { version = "5", default-features = false, features = ["relays"] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } -quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", @@ -92,7 +92,7 @@ hickory-resolver = "0.25.1" igd-next = { version = "0.16", features = ["aio_tokio"] } netdev = { version = "0.38.1" } portmapper = { version = "0.11", default-features = false } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x", default-features = false, features = ["runtime-tokio", "rustls-ring"] } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["runtime-tokio", "rustls-ring"] } tokio = { version = "1", features = [ "io-util", "macros", diff --git a/iroh/bench/Cargo.toml b/iroh/bench/Cargo.toml index 0df51d37e77..e757811d4f8 100644 --- a/iroh/bench/Cargo.toml +++ b/iroh/bench/Cargo.toml @@ -12,7 +12,7 @@ iroh = { path = ".." } iroh-metrics = "0.36" n0-future = "0.3.0" n0-snafu = "0.2.0" -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "multipath-quinn-0.11.x" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } rand = "0.9.2" rcgen = "0.14" rustls = { version = "0.23.33", default-features = false, features = ["ring"] } From e5ad6812b69099906608123cf2eca28914c8b0c6 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 31 Oct 2025 13:35:51 +0100 Subject: [PATCH 114/164] test improvements --- iroh/src/endpoint.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 4a6a813ba76..c1648e44ba4 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2595,18 +2595,21 @@ mod tests { let mut paths = conn.paths_info().stream(); time::timeout(Duration::from_secs(5), async move { - let mut have_relay = false; while let Some(infos) = paths.next().await { info!(?infos, "new PathInfos"); - have_relay = infos.keys().any(|a| a.is_relay()); - if have_relay { + if infos.keys().any(|a| a.is_relay()) { break; } } - have_relay }) .await .e()?; + + // wait for the server to signal it has the relay connection + let mut stream = conn.accept_uni().await.e()?; + stream.read_to_end(100).await.e()?; + + info!("client closing"); conn.close(0u8.into(), b""); ep.close().await; Ok(()) @@ -2631,6 +2634,25 @@ mod tests { info!(me = %ep.id().fmt_short(), "server starting"); let conn = ep.accept().await.e()?.await.e()?; + + // Wait for a relay connection to be added. Client does all the asserting here, + // we just want to wait so we get to see all the mechanics of the connection + // being added on this side too. + let mut paths = conn.paths_info().stream(); + time::timeout(Duration::from_secs(5), async move { + while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + if infos.keys().any(|a| a.is_relay()) { + break; + } + } + }) + .await + .e()?; + + let mut stream = conn.open_uni().await.e()?; + stream.write_all(b"have relay").await.e()?; + Ok(conn.closed().await) } From a5ca3652272e8bb9ee4b516f16c8ffc8bcdd54f9 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 3 Nov 2025 12:57:07 +0100 Subject: [PATCH 115/164] chore: fix typos --- .github/workflows/ci.yml | 2 +- iroh/src/magicsock.rs | 2 +- iroh/src/magicsock/endpoint_map/endpoint_state.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ea4d92fef5..4d6126a9e77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -338,4 +338,4 @@ jobs: steps: - uses: actions/checkout@v5 - run: pip install --user codespell[toml] - - run: codespell --ignore-words-list=ans,atmost,crate,inout,ratatui,ser,stayin,swarmin,worl --skip=CHANGELOG.md + - run: codespell --ignore-words-list=ans,atmost,crate,inout,ratatui,ser,stayin,swarmin,worl,keep-alives --skip=CHANGELOG.md diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 42b6d1ba19a..55f1a671f2f 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1872,7 +1872,7 @@ fn disco_message_sent(msg: &disco::Message, metrics: &MagicsockMetrics) { /// iroh endpoint, see [`DirectAddrType`] for the several kinds of sources. /// /// This is essentially a combination of our local addresses combined with any reflexive -/// transport addresses we disovered using QAD. +/// transport addresses we discovered using QAD. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct DirectAddr { /// The address. diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 08ba5951fb8..9688707ad76 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -200,7 +200,7 @@ impl EndpointStateActor { /// /// Note that the actor uses async handlers for tasks from the main loop. The actor is /// not processing items from the inbox while waiting on any async calls. So some - /// dicipline is needed to not turn pending for a long time. + /// discipline is needed to not turn pending for a long time. async fn run( &mut self, mut inbox: mpsc::Receiver, From a710f9d9d30bdf8811b4cc1e623444884f953731 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 3 Nov 2025 13:08:26 +0100 Subject: [PATCH 116/164] cleanup docs and references --- iroh-relay/src/client/conn.rs | 2 +- iroh/src/endpoint.rs | 5 ++-- iroh/src/magicsock.rs | 24 +++++++++---------- iroh/src/magicsock/endpoint_map.rs | 13 +++++----- iroh/src/magicsock/endpoint_map/path_state.rs | 4 ++-- iroh/src/magicsock/transports.rs | 4 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/iroh-relay/src/client/conn.rs b/iroh-relay/src/client/conn.rs index 6e0a68e813a..91bdb2204d6 100644 --- a/iroh-relay/src/client/conn.rs +++ b/iroh-relay/src/client/conn.rs @@ -11,7 +11,7 @@ use iroh_base::SecretKey; use n0_future::{Sink, Stream}; use nested_enum_utils::common_fields; use snafu::{Backtrace, Snafu}; -use tracing::{debug, trace}; +use tracing::trace; use super::KeyCache; #[cfg(not(wasm_browser))] diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 139d0258376..2e6dad7566d 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -49,7 +49,9 @@ use snafu::{ResultExt, Snafu, ensure}; use tracing::{debug, instrument, trace, warn}; use url::Url; -pub use super::magicsock::{AddEndpointAddrError, ConnectionType, DirectAddr, DirectAddrType}; +pub use super::magicsock::{ + AddEndpointAddrError, ConnectionType, DirectAddr, DirectAddrType, endpoint_map::Source, +}; #[cfg(wasm_browser)] use crate::discovery::pkarr::PkarrResolver; #[cfg(not(wasm_browser))] @@ -62,7 +64,6 @@ use crate::{ magicsock::{ self, HEARTBEAT_INTERVAL, Handle, MAX_MULTIPATH_PATHS, OwnAddressSnafu, PATH_MAX_IDLE_TIMEOUT, PathInfo, - endpoint_map::Source, mapped_addrs::{EndpointIdMappedAddr, MappedAddr}, }, metrics::EndpointMetrics, diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 55f1a671f2f..6ddce5cdc00 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -272,12 +272,12 @@ impl MagicSock { self.local_addrs_watch.clone().get() } - /// Registers the connection in the [`NodeStateActor`]. + /// Registers the connection in the [`EndpointStateActor`]. /// /// The actor is responsible for holepunching and opening additional paths to this /// connection. /// - /// [`NodeStateActor`]: crate::magicsock::node_map::node_state::NodeStateActor + /// [`EndpointStateActor`]: crate::magicsock::endpoint_map::endpoint_state::EndpointStateActor pub(crate) fn register_connection( &self, remote: EndpointId, @@ -298,13 +298,13 @@ impl MagicSock { // we'll end up changing Connecting::into_0rtt() to return a ZrttConnection. Then // have a ZrttConnection::into_connection() function which can be async and actually // send this. Before the handshake has completed we don't have anything useful to - // do with this connection inside of the NodeStateActor anyway. + // do with this connection inside of the EndpointStateActor anyway. let weak_handle = conn.weak_handle(); - let node_state = self.endpoint_map.endpoint_state_actor(remote); + let endpoint_state = self.endpoint_map.endpoint_state_actor(remote); let msg = EndpointStateMessage::AddConnection(weak_handle, paths_info); tokio::task::spawn(async move { - node_state.send(msg).await.ok(); + endpoint_state.send(msg).await.ok(); }); } @@ -438,10 +438,10 @@ impl MagicSock { self.endpoint_map.endpoint_mapped_addr(eid) } - /// Add potential addresses for a node to the [`EndpointStateActor`]. + /// Add potential addresses for a endpoint to the [`EndpointStateActor`]. /// - /// This is used to add possible paths that the remote node might be reachable on. They - /// will be used when there is no active connection to the node to attempt to establish + /// This is used to add possible paths that the remote endpoint might be reachable on. They + /// will be used when there is no active connection to the endpoint to attempt to establish /// a connection. #[instrument(skip_all)] pub(crate) async fn add_endpoint_addr( @@ -649,11 +649,11 @@ impl MagicSock { transports::Addr::Ip(_addr) => { quic_packets_total += quic_datagram_count; } - transports::Addr::Relay(src_url, src_node) => { + transports::Addr::Relay(src_url, src_endpoint) => { let mapped_addr = self .endpoint_map .relay_mapped_addrs - .get(&(src_url.clone(), *src_node)); + .get(&(src_url.clone(), *src_endpoint)); quinn_meta.addr = mapped_addr.private_socket_addr(); } } @@ -1245,7 +1245,7 @@ fn default_quic_client_config() -> rustls::ClientConfig { #[derive(Debug, Clone)] struct DiscoState { - /// The EndpointId/PublikeKey of this node. + /// The EndpointId/PublikeKey of this endpoint. this_id: EndpointId, /// Encryption key for this endpoint. secret_encryption_key: Arc, @@ -2125,7 +2125,7 @@ mod tests { /// Returns a pair of endpoints with a shared [`StaticDiscovery`]. /// /// The endpoints do not use a relay server but can connect to each other via local - /// addresses. Dialing by [`NodeId`] is possible, and the addresses get updated even if + /// addresses. Dialing by [`EndpointId`] is possible, and the addresses get updated even if /// the endpoints rebind. async fn endpoint_pair() -> (AbortOnDropHandle<()>, Endpoint, Endpoint) { let discovery = StaticProvider::new(); diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 4ba29ef3277..5bc2a5a51ce 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -102,10 +102,9 @@ pub enum Source { /// We established a connection on this address. /// /// Currently this means the path was in uses as [`PathId::ZERO`] when the a connection - /// was added to the [`NodeStateActor`]. + /// was added to the `EndpointStateActor`. /// /// [`PathId::ZERO`]: quinn_proto::PathId::ZERO - /// [`NodeStateActor`]: self::node_state::NodeStateActor Connection, } @@ -137,7 +136,7 @@ impl EndpointMap { } } - /// Adds addresses where a node might be contactable. + /// Adds addresses where a endpoint might be contactable. pub(super) async fn add_endpoint_addr(&self, endpoint_addr: EndpointAddr, source: Source) { for url in endpoint_addr.relay_urls() { // Ensure we have a RelayMappedAddress. @@ -146,7 +145,7 @@ impl EndpointMap { } let actor = self.endpoint_state_actor(endpoint_addr.id); - // This only fails if the sender is closed. That means the NodeStateActor has + // This only fails if the sender is closed. That means the EndpointStateActor has // stopped, which only happens during shutdown. actor .send(EndpointStateMessage::AddEndpointAddr(endpoint_addr, source)) @@ -342,11 +341,11 @@ impl IpPort { /// An actor that can send datagrams onto iroh transports. /// -/// The [`NodeStateActor`]s want to be able to send datagrams. Because we can not create -/// [`TransportsSender`]s on demand we must share one for the entire [`NodeMap`], which +/// The [`EndpointStateActor`]s want to be able to send datagrams. Because we can not create +/// [`TransportsSender`]s on demand we must share one for the entire [`EndpointMap`], which /// lives in this actor. /// -/// [`NodeStateActor`]: node_state::NodeStateActor +/// [`EndpointStateActor`]: endpoint_state::EndpointStateActor #[derive(Debug)] struct TransportsSenderActor { sender: TransportsSender, diff --git a/iroh/src/magicsock/endpoint_map/path_state.rs b/iroh/src/magicsock/endpoint_map/path_state.rs index 44fdef65b93..cfe92f3ef94 100644 --- a/iroh/src/magicsock/endpoint_map/path_state.rs +++ b/iroh/src/magicsock/endpoint_map/path_state.rs @@ -10,9 +10,9 @@ use crate::disco::TransactionId; /// The state of a single path to the remote endpoint. /// /// Each path is identified by the destination [`transports::Addr`] and they are stored in -/// the [`NodeStateActor::paths`] map. +/// the [`EndpointStateActor::paths`] map. /// -/// [`NodeStateActor::paths`]: super::node_state::NodeStateActor +/// [`EndpointStateActor::paths`]: super::endpoint_state::EndpointStateActor #[derive(Debug, Default)] pub(super) struct PathState { /// How we learned about this path, and when. diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index efe09baabea..4d9da61b387 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -677,7 +677,7 @@ impl quinn::UdpSender for MagicSender { // a lost datagram. // TODO: Revisit this: we might want to do something better. debug!(dst = ?mapped_addr, dst_node = %node_id.fmt_short(), - "NodeStateActor inbox {err:#}, dropped transmit"); + "EndpointStateActor inbox {err:#}, dropped transmit"); Poll::Ready(Ok(())) } }; @@ -689,7 +689,7 @@ impl quinn::UdpSender for MagicSender { .relay_mapped_addrs .lookup(&relay_mapped_addr) { - Some((relay_url, node_id)) => Addr::Relay(relay_url, node_id), + Some((relay_url, endpoint_id)) => Addr::Relay(relay_url, endpoint_id), None => { error!("unknown RelayMappedAddr, dropped transmit"); return Poll::Ready(Ok(())); From a08af734b29ee8ecc29e34d6f00741e50181892b Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Mon, 3 Nov 2025 13:12:49 +0100 Subject: [PATCH 117/164] wasm fixes --- iroh-base/src/endpoint_addr.rs | 4 +--- iroh/src/magicsock.rs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/iroh-base/src/endpoint_addr.rs b/iroh-base/src/endpoint_addr.rs index 56ffbcc5d38..76548518dd2 100644 --- a/iroh-base/src/endpoint_addr.rs +++ b/iroh-base/src/endpoint_addr.rs @@ -36,9 +36,7 @@ use crate::{EndpointId, PublicKey, RelayUrl}; /// [discovery]: https://docs.rs/iroh/*/iroh/index.html#endpoint-discovery /// [home relay]: https://docs.rs/iroh/*/iroh/relay/index.html /// [Relay server]: https://docs.rs/iroh/*/iroh/index.html#relay-servers -#[derive( - derive_more::Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, -)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct EndpointAddr { /// The endpoint's identifier. pub id: EndpointId, diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 6ddce5cdc00..93e8551b221 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -303,7 +303,7 @@ impl MagicSock { let endpoint_state = self.endpoint_map.endpoint_state_actor(remote); let msg = EndpointStateMessage::AddConnection(weak_handle, paths_info); - tokio::task::spawn(async move { + task::spawn(async move { endpoint_state.send(msg).await.ok(); }); } From 6bec028a526495d6f2ae1ea8e19dc3a2ce931656 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 5 Nov 2025 16:11:01 +0100 Subject: [PATCH 118/164] Remove obsolete test This is replaced by the endpoint_two_relay_only_becomes_direct test. --- iroh/src/endpoint.rs | 94 ++------------------------------------------ 1 file changed, 4 insertions(+), 90 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 0d84d427f31..63367203178 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1426,7 +1426,7 @@ mod tests { use iroh_base::{EndpointAddr, EndpointId, SecretKey, TransportAddr}; use n0_error::{AnyError as Error, Result, StdResultExt}; - use n0_future::{BufferedStreamExt, StreamExt, stream, task::AbortOnDropHandle, time}; + use n0_future::{BufferedStreamExt, StreamExt, stream, time}; use n0_watcher::Watcher; use quinn::ConnectionError; use rand::SeedableRng; @@ -1438,7 +1438,7 @@ mod tests { use crate::{ RelayMap, RelayMode, discovery::static_provider::StaticProvider, - endpoint::{ConnectOptions, Connection, ConnectionType}, + endpoint::{ConnectOptions, Connection}, protocol::{AcceptError, ProtocolHandler, Router}, test_utils::{run_relay_server, run_relay_server_with}, }; @@ -1775,9 +1775,9 @@ mod tests { #[tokio::test] #[traced_test] - async fn endpoint_two_relay_only() -> Result { + async fn endpoint_two_relay_only_becomes_direct() -> Result { // Connect two endpoints on the same network, via a relay server, without - // discovery. + // discovery. Wait until there is a direct connection. let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; let (node_addr_tx, node_addr_rx) = oneshot::channel(); @@ -2180,92 +2180,6 @@ mod tests { Ok(()) } - #[tokio::test] - #[traced_test] - async fn endpoint_conn_type_becomes_direct() -> Result { - const TIMEOUT: Duration = std::time::Duration::from_secs(15); - let (relay_map, _relay_url, _relay_guard) = run_relay_server().await?; - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(42); - let ep1_secret_key = SecretKey::generate(&mut rng); - let ep2_secret_key = SecretKey::generate(&mut rng); - let ep1 = Endpoint::empty_builder(RelayMode::Custom(relay_map.clone())) - .secret_key(ep1_secret_key) - .insecure_skip_relay_cert_verify(true) - .alpns(vec![TEST_ALPN.to_vec()]) - .bind() - .await?; - let ep2 = Endpoint::empty_builder(RelayMode::Custom(relay_map)) - .secret_key(ep2_secret_key) - .insecure_skip_relay_cert_verify(true) - .alpns(vec![TEST_ALPN.to_vec()]) - .bind() - .await?; - - async fn wait_for_conn_type_direct(ep: &Endpoint, endpoint_id: EndpointId) -> Result { - let mut stream = ep - .conn_type(endpoint_id) - .expect("connection exists") - .stream(); - let src = ep.id().fmt_short(); - let dst = endpoint_id.fmt_short(); - while let Some(conn_type) = stream.next().await { - tracing::info!(me = %src, dst = %dst, conn_type = ?conn_type); - if matches!(conn_type, ConnectionType::Direct(_)) { - return Ok(()); - } - } - n0_error::bail_any!("conn_type stream ended before `ConnectionType::Direct`"); - } - - async fn accept(ep: &Endpoint) -> Result { - let incoming = ep.accept().await.expect("ep closed"); - let conn = incoming.await.anyerr()?; - let endpoint_id = conn.remote_id(); - tracing::info!(endpoint_id=%endpoint_id.fmt_short(), "accepted connection"); - Ok(conn) - } - - let ep1_endpointid = ep1.id(); - let ep2_endpointid = ep2.id(); - - let ep1_endpointaddr = ep1.addr(); - tracing::info!( - "endpoint id 1 {ep1_endpointid}, relay URL {:?}", - ep1_endpointaddr.relay_urls().next() - ); - tracing::info!("endpoint id 2 {ep2_endpointid}"); - - let ep1_side = tokio::time::timeout(TIMEOUT, async move { - let conn = accept(&ep1).await?; - let mut send = conn.open_uni().await.anyerr()?; - wait_for_conn_type_direct(&ep1, ep2_endpointid).await?; - send.write_all(b"Conn is direct").await.anyerr()?; - send.finish().anyerr()?; - conn.closed().await; - Ok::<(), Error>(()) - }); - - let ep2_side = tokio::time::timeout(TIMEOUT, async move { - let conn = ep2.connect(ep1_endpointaddr, TEST_ALPN).await?; - let mut recv = conn.accept_uni().await.anyerr()?; - wait_for_conn_type_direct(&ep2, ep1_endpointid).await?; - let read = recv.read_to_end(100).await.anyerr()?; - assert_eq!(read, b"Conn is direct".to_vec()); - conn.close(0u32.into(), b"done"); - conn.closed().await; - Ok::<(), Error>(()) - }); - - let res_ep1 = AbortOnDropHandle::new(tokio::spawn(ep1_side)); - let res_ep2 = AbortOnDropHandle::new(tokio::spawn(ep2_side)); - - let (r1, r2) = tokio::try_join!(res_ep1, res_ep2).anyerr()?; - r1.anyerr()??; - r2.anyerr()??; - - Ok(()) - } - #[tokio::test] #[traced_test] async fn test_direct_addresses_no_qad_relay() -> Result { From dc46e18f8989abcb12f4ee337f6b9122fdb11e70 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 5 Nov 2025 16:11:51 +0100 Subject: [PATCH 119/164] fix doc links and clippy --- iroh/src/endpoint/connection.rs | 6 +++--- iroh/src/magicsock.rs | 6 ++---- iroh/src/magicsock/endpoint_map.rs | 7 ++++++- iroh/src/magicsock/endpoint_map/endpoint_state.rs | 6 ++++-- iroh/src/magicsock/endpoint_map/path_state.rs | 1 + iroh/src/magicsock/mapped_addrs.rs | 9 ++++----- 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index eabe19f6878..2d2531ccdeb 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -179,7 +179,7 @@ impl Future for IncomingFuture { Poll::Pending => Poll::Pending, Poll::Ready(Err(err)) => Poll::Ready(Err(err.into())), Poll::Ready(Ok(inner)) => { - let conn = match conn_from_quinn_conn(inner, &this.ep) { + let conn = match conn_from_quinn_conn(inner, this.ep) { Ok(conn) => conn, Err(err) => return Poll::Ready(Err(err.into())), }; @@ -468,7 +468,7 @@ impl Future for Connecting { Poll::Pending => Poll::Pending, Poll::Ready(Err(err)) => Poll::Ready(Err(err.into())), Poll::Ready(Ok(inner)) => { - let conn = match conn_from_quinn_conn(inner, &this.ep) { + let conn = match conn_from_quinn_conn(inner, this.ep) { Ok(conn) => conn, Err(err) => { return Poll::Ready(Err(err.into())); @@ -548,7 +548,7 @@ impl Future for Accepting { Poll::Pending => Poll::Pending, Poll::Ready(Err(err)) => Poll::Ready(Err(err.into())), Poll::Ready(Ok(inner)) => { - let conn = match conn_from_quinn_conn(inner, &this.ep) { + let conn = match conn_from_quinn_conn(inner, this.ep) { Ok(conn) => conn, Err(err) => return Poll::Ready(Err(err.into())), }; diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index e3819d089c4..8a18184c721 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -265,12 +265,10 @@ impl MagicSock { self.local_addrs_watch.clone().get() } - /// Registers the connection in the [`EndpointStateActor`]. + /// Registers the connection in the `EndpointStateActor`. /// /// The actor is responsible for holepunching and opening additional paths to this /// connection. - /// - /// [`EndpointStateActor`]: crate::magicsock::endpoint_map::endpoint_state::EndpointStateActor pub(crate) fn register_connection( &self, remote: EndpointId, @@ -431,7 +429,7 @@ impl MagicSock { self.endpoint_map.endpoint_mapped_addr(eid) } - /// Add potential addresses for a endpoint to the [`EndpointStateActor`]. + /// Add potential addresses for a endpoint to the `EndpointStateActor`. /// /// This is used to add possible paths that the remote endpoint might be reachable on. They /// will be used when there is no active connection to the endpoint to attempt to establish diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 20c368ec830..8e51925e317 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -36,7 +36,12 @@ use endpoint_state::{EndpointStateActor, EndpointStateHandle}; // /// periodically via [`NodeMap::prune_inactive`]. // const MAX_INACTIVE_NODES: usize = 30; -/// Map of the [`EndpointState`] information for all the known endpoints. +/// Map containing all the state for endpoints. +/// +/// - Has actors which each manage all the connection state for a remote endpoint. +/// +/// - Has the mapped addresses we use to refer to non-IP transports destinations into IPv6 +/// addressing space that is used by Quinn. #[derive(Debug)] pub(crate) struct EndpointMap { /// The endpoint ID of the local endpoint. diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index a80235a5122..8800f997d66 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -1073,8 +1073,8 @@ pub enum ConnectionType { /// Newtype to track Connections. /// -/// The wrapped value is the [`Connection::stable_id`] value, and is thus only valid for -/// active connections. +/// The wrapped value is the [`quinn::Connection::stable_id`] value, and is thus only valid +/// for active connections. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct ConnId(usize); @@ -1129,6 +1129,8 @@ impl ConnectionState { } /// Sets the new [`PathInfo`] structs for the public [`Connection`]. + /// + /// [`Connection`]: crate::endpoint::Connection fn update_pub_path_info(&self) { let new = self .open_paths diff --git a/iroh/src/magicsock/endpoint_map/path_state.rs b/iroh/src/magicsock/endpoint_map/path_state.rs index cfe92f3ef94..8a097a5edc7 100644 --- a/iroh/src/magicsock/endpoint_map/path_state.rs +++ b/iroh/src/magicsock/endpoint_map/path_state.rs @@ -12,6 +12,7 @@ use crate::disco::TransactionId; /// Each path is identified by the destination [`transports::Addr`] and they are stored in /// the [`EndpointStateActor::paths`] map. /// +/// [`transports::Addr`]: super::transports::Addr /// [`EndpointStateActor::paths`]: super::endpoint_state::EndpointStateActor #[derive(Debug, Default)] pub(super) struct PathState { diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 24185e6c536..49030e07147 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -152,7 +152,7 @@ impl MappedAddr for EndpointIdMappedAddr { /// Returns a consistent [`SocketAddr`] for the [`EndpointIdMappedAddr`]. /// - /// This socket address does not have a routable IP address. + /// This socket address does not have a routable IP address and port. /// /// This uses a made-up port number, since the port does not play a role in the /// addressing. This socket address is only to be used to pass into Quinn. @@ -215,11 +215,10 @@ impl MappedAddr for RelayMappedAddr { /// Returns a consistent [`SocketAddr`] for the [`RelayMappedAddr`]. /// - /// This does not have a routable IP address. + /// This socket address does not have a routable IP address and port. /// - /// This uses a made-up, but fixed port number. The [`RelayAddrMap`] creates a unique - /// [`RelayMappedAddr`] for each `(EndpointId, RelayUrl)` pair and thus does not use the - /// port to map back to the original [`SocketAddr`]. + /// This uses a made-up port number, since the port does not play a role in the + /// addressing. This socket address is only to be used to pass into Quinn. fn private_socket_addr(&self) -> SocketAddr { SocketAddr::new(IpAddr::from(self.0), MAPPED_PORT) } From a59f62f2cef1ea1727e4c23dff47c5338f50862b Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 5 Nov 2025 17:46:23 +0100 Subject: [PATCH 120/164] Remove EndpointStateMapInner This was a weird relic from the past. The state it maintains is much clearer without the inner. This also comments out the unused PathSelection for now. We do need to bring that back somehow though. --- iroh/src/endpoint.rs | 4 +- iroh/src/magicsock.rs | 28 +++-- iroh/src/magicsock/endpoint_map.rs | 196 +++++++++++------------------ 3 files changed, 91 insertions(+), 137 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 63367203178..b9695673afa 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -205,8 +205,8 @@ impl Builder { server_config, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: self.insecure_skip_relay_cert_verify, - #[cfg(any(test, feature = "test-utils"))] - path_selection: self.path_selection, + // #[cfg(any(test, feature = "test-utils"))] + // path_selection: self.path_selection, metrics, }; diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 8a18184c721..f93026e2ea7 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -55,8 +55,8 @@ use self::{ }; #[cfg(not(wasm_browser))] use crate::dns::DnsResolver; -#[cfg(any(test, feature = "test-utils"))] -use crate::endpoint::PathSelection; +// #[cfg(any(test, feature = "test-utils"))] +// use crate::endpoint::PathSelection; #[cfg(not(wasm_browser))] use crate::net_report::QuicConfig; use crate::{ @@ -143,10 +143,9 @@ pub(crate) struct Options { #[cfg(any(test, feature = "test-utils"))] pub(crate) insecure_skip_relay_cert_verify: bool, - /// Configuration for what path selection to use - #[cfg(any(test, feature = "test-utils"))] - pub(crate) path_selection: PathSelection, - + // /// Configuration for what path selection to use + // #[cfg(any(test, feature = "test-utils"))] + // pub(crate) path_selection: PathSelection, pub(crate) metrics: EndpointMetrics, } @@ -968,7 +967,7 @@ impl Handle { #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify, #[cfg(any(test, feature = "test-utils"))] - path_selection, + // path_selection, metrics, } = opts; @@ -1018,8 +1017,8 @@ impl Handle { let sender = transports.create_sender(); EndpointMap::new( secret_key.public(), - #[cfg(any(test, feature = "test-utils"))] - path_selection, + // #[cfg(any(test, feature = "test-utils"))] + // path_selection, metrics.magicsock.clone(), sender, direct_addrs.addrs.watch(), @@ -1917,10 +1916,13 @@ mod tests { use super::{EndpointIdMappedAddr, Options, endpoint_map::Source, mapped_addrs::MappedAddr}; use crate::{ - Endpoint, RelayMap, RelayMode, SecretKey, + Endpoint, + RelayMap, + RelayMode, + SecretKey, discovery::static_provider::StaticProvider, dns::DnsResolver, - endpoint::PathSelection, + // endpoint::PathSelection, magicsock::{Handle, MagicSock}, tls::{self, DEFAULT_MAX_TLS_TICKETS}, }; @@ -1941,7 +1943,7 @@ mod tests { #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection::default(), + // path_selection: PathSelection::default(), discovery_user_data: None, metrics: Default::default(), } @@ -2374,7 +2376,7 @@ mod tests { proxy_url: None, server_config, insecure_skip_relay_cert_verify: false, - path_selection: PathSelection::default(), + // path_selection: PathSelection::default(), metrics: Default::default(), }; let msock = MagicSock::spawn(opts).await?; diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 8e51925e317..ea33e4770ca 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeSet, HashMap}, + collections::BTreeSet, hash::Hash, net::{IpAddr, SocketAddr}, sync::{Arc, Mutex}, @@ -7,22 +7,19 @@ use std::{ use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; use n0_future::task::{self, AbortOnDropHandle}; +use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::{Instrument, error, info_span, trace, warn}; -#[cfg(any(test, feature = "test-utils"))] -use super::transports::TransportsSender; -#[cfg(not(any(test, feature = "test-utils")))] -use super::transports::TransportsSender; use super::{ DirectAddr, DiscoState, MagicsockMetrics, mapped_addrs::{AddrMap, EndpointIdMappedAddr, MultipathMappedAddr, RelayMappedAddr}, - transports::{self, OwnedTransmit}, + transports::{self, OwnedTransmit, TransportsSender}, }; use crate::disco::{self}; -#[cfg(any(test, feature = "test-utils"))] -use crate::endpoint::PathSelection; +// #[cfg(any(test, feature = "test-utils"))] +// use crate::endpoint::PathSelection; mod endpoint_state; mod path_state; @@ -44,104 +41,52 @@ use endpoint_state::{EndpointStateActor, EndpointStateHandle}; /// addressing space that is used by Quinn. #[derive(Debug)] pub(crate) struct EndpointMap { - /// The endpoint ID of the local endpoint. - local_endpoint_id: EndpointId, - inner: Mutex, + // + // State we keep about remote endpoints. + // + /// The actors tracking each remote endpoint. + actor_handles: Mutex>, /// The mapping between [`EndpointId`]s and [`EndpointIdMappedAddr`]s. pub(super) endpoint_mapped_addrs: AddrMap, /// The mapping between endpoints via a relay and their [`RelayMappedAddr`]s. pub(super) relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, -} -#[derive(Debug)] -pub(super) struct EndpointMapInner { + // + // State needed to start a new EndpointStateHandle. + // + /// The endpoint ID of the local endpoint. + local_endpoint_id: EndpointId, metrics: Arc, /// Handle to an actor that can send over the transports. transports_handle: TransportsSenderHandle, local_addrs: n0_watcher::Direct>, disco: DiscoState, - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection, - /// The [`EndpointStateActor`] for each remote endpoint. - /// - /// [`EndpointStateActor`]: endpoint_state::EndpointStateActor - endpoint_states: HashMap, -} - -/// The origin or *source* through which an address associated with a remote endpoint -/// was discovered. -/// -/// An aggregate of the [`Source`]s of all the addresses of an endpoint describe the -/// [`Source`]s of the endpoint itself. -/// -/// A [`Source`] helps track how and where an address was learned. Multiple -/// sources can be associated with a single address, if we have discovered this -/// address through multiple means. -#[derive(Serialize, Deserialize, strum::Display, Debug, Clone, Eq, PartialEq, Hash)] -#[strum(serialize_all = "kebab-case")] -pub enum Source { - /// Address was loaded from the fs. - Saved, - /// An endpoint communicated with us first via UDP. - Udp, - /// An endpoint communicated with us first via relay. - Relay, - /// Application layer added the address directly. - App, - /// The address was discovered by a discovery service. - #[strum(serialize = "{name}")] - Discovery { - /// The name of the discovery service that discovered the address. - name: String, - }, - /// Application layer with a specific name added the endpoint directly. - #[strum(serialize = "{name}")] - NamedApp { - /// The name of the application that added the endpoint - name: String, - }, - /// The address was advertised by a call-me-maybe DISCO message. - CallMeMaybe, - /// We received a ping on the path. - Ping, - /// We established a connection on this address. - /// - /// Currently this means the path was in uses as [`PathId::ZERO`] when the a connection - /// was added to the `EndpointStateActor`. - /// - /// [`PathId::ZERO`]: quinn_proto::PathId::ZERO - Connection, } impl EndpointMap { /// Creates a new [`EndpointMap`]. pub(super) fn new( local_endpoint_id: EndpointId, - #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, + // TODO: + // #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, metrics: Arc, sender: TransportsSender, local_addrs: n0_watcher::Direct>, disco: DiscoState, ) -> Self { - #[cfg(not(any(test, feature = "test-utils")))] - let inner = EndpointMapInner::new(metrics, sender, local_addrs, disco); - - #[cfg(any(test, feature = "test-utils"))] - let inner = { - let mut inner = EndpointMapInner::new(metrics, sender, local_addrs, disco); - inner.path_selection = path_selection; - inner - }; - Self { - local_endpoint_id, - inner: Mutex::new(inner), + actor_handles: Mutex::new(FxHashMap::default()), endpoint_mapped_addrs: Default::default(), relay_mapped_addrs: Default::default(), + local_endpoint_id, + metrics, + transports_handle: TransportsSenderActor::new(sender).start(), + local_addrs, + disco, } } - /// Adds addresses where a endpoint might be contactable. + /// Adds addresses where an endpoint might be contactable. pub(super) async fn add_endpoint_addr(&self, endpoint_addr: EndpointAddr, source: Source) { for url in endpoint_addr.relay_urls() { // Ensure we have a RelayMappedAddress. @@ -185,9 +130,9 @@ impl EndpointMap { /// the `endpoint_id` pub(super) fn conn_type( &self, - endpoint_id: EndpointId, + _endpoint_id: EndpointId, ) -> Option> { - self.inner.lock().expect("poisoned").conn_type(endpoint_id) + todo!(); } /// Returns the sender for the [`EndpointStateActor`]. @@ -199,15 +144,15 @@ impl EndpointMap { &self, eid: EndpointId, ) -> mpsc::Sender { - let mut inner = self.inner.lock().expect("poisoned"); - match inner.endpoint_states.get(&eid) { + let mut handles = self.actor_handles.lock().expect("poisoned"); + match handles.get(&eid) { Some(handle) => handle.sender.clone(), None => { // Create a new EndpointStateActor and insert it into the endpoint map. - let sender = inner.transports_handle.inbox.clone(); - let local_addrs = inner.local_addrs.clone(); - let disco = inner.disco.clone(); - let metrics = inner.metrics.clone(); + let sender = self.transports_handle.inbox.clone(); + let local_addrs = self.local_addrs.clone(); + let disco = self.disco.clone(); + let metrics = self.metrics.clone(); let actor = EndpointStateActor::new( eid, self.local_endpoint_id, @@ -219,7 +164,7 @@ impl EndpointMap { ); let handle = actor.start(); let sender = handle.sender.clone(); - inner.endpoint_states.insert(eid, handle); + handles.insert(eid, handle); // Ensure there is a EndpointMappedAddr for this EndpointId. self.endpoint_mapped_addrs.get(&eid); @@ -269,42 +214,49 @@ impl EndpointMap { } } -impl EndpointMapInner { - fn new( - metrics: Arc, - sender: TransportsSender, - local_addrs: n0_watcher::Direct>, - disco: DiscoState, - ) -> Self { - let transports_handle = Self::start_transports_sender(sender); - Self { - metrics, - transports_handle, - local_addrs, - disco, - #[cfg(any(test, feature = "test-utils"))] - path_selection: Default::default(), - endpoint_states: Default::default(), - } - } - - fn start_transports_sender(sender: TransportsSender) -> TransportsSenderHandle { - let actor = TransportsSenderActor::new(sender); - actor.start() - } - - /// Returns a stream of [`ConnectionType`]. - /// - /// Sends the current [`ConnectionType`] whenever any changes to the - /// connection type for `public_key` has occurred. +/// The origin or *source* through which an address associated with a remote endpoint +/// was discovered. +/// +/// An aggregate of the [`Source`]s of all the addresses of an endpoint describe the +/// [`Source`]s of the endpoint itself. +/// +/// A [`Source`] helps track how and where an address was learned. Multiple +/// sources can be associated with a single address, if we have discovered this +/// address through multiple means. +#[derive(Serialize, Deserialize, strum::Display, Debug, Clone, Eq, PartialEq, Hash)] +#[strum(serialize_all = "kebab-case")] +pub enum Source { + /// Address was loaded from the fs. + Saved, + /// An endpoint communicated with us first via UDP. + Udp, + /// An endpoint communicated with us first via relay. + Relay, + /// Application layer added the address directly. + App, + /// The address was discovered by a discovery service. + #[strum(serialize = "{name}")] + Discovery { + /// The name of the discovery service that discovered the address. + name: String, + }, + /// Application layer with a specific name added the endpoint directly. + #[strum(serialize = "{name}")] + NamedApp { + /// The name of the application that added the endpoint + name: String, + }, + /// The address was advertised by a call-me-maybe DISCO message. + CallMeMaybe, + /// We received a ping on the path. + Ping, + /// We established a connection on this address. /// - /// # Errors + /// Currently this means the path was in uses as [`PathId::ZERO`] when the a connection + /// was added to the `EndpointStateActor`. /// - /// Will return `None` if there is not an entry in the [`EndpointMap`] for - /// the `public_key` - fn conn_type(&self, _eid: EndpointId) -> Option> { - todo!(); - } + /// [`PathId::ZERO`]: quinn_proto::PathId::ZERO + Connection, } /// An (Ip, Port) pair. From 8f1cb977358e29142be557443bfa2192d2732085 Mon Sep 17 00:00:00 2001 From: Frando Date: Thu, 6 Nov 2025 09:07:24 +0100 Subject: [PATCH 121/164] fix: removal of path selection missed a cfg attribute --- iroh/src/magicsock.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index f93026e2ea7..b43f3905d93 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -966,7 +966,7 @@ impl Handle { server_config, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify, - #[cfg(any(test, feature = "test-utils"))] + // #[cfg(any(test, feature = "test-utils"))] // path_selection, metrics, } = opts; From 9921a35dc100b933b372141f4a0f76540d3589e1 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Thu, 6 Nov 2025 12:26:13 +0100 Subject: [PATCH 122/164] bump quinn branch --- Cargo.lock | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce0dff73233..8c50d17f26e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1896,7 +1896,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2007,6 +2007,12 @@ dependencies = [ "zerovec", ] +[[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" @@ -2322,7 +2328,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#bc99406e75d2b56db4de3c1bbe301203a625277f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#817a1b565db5d48a3d04f0f51dd108c1d75aad56" dependencies = [ "bytes", "cfg_aliases", @@ -2331,7 +2337,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2342,11 +2348,12 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#bc99406e75d2b56db4de3c1bbe301203a625277f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#817a1b565db5d48a3d04f0f51dd108c1d75aad56" dependencies = [ "bytes", "fastbloom", "getrandom 0.3.4", + "identity-hash", "lru-slab", "rand", "ring", @@ -2364,14 +2371,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#bc99406e75d2b56db4de3c1bbe301203a625277f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#817a1b565db5d48a3d04f0f51dd108c1d75aad56" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2881,7 +2888,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3349,7 +3356,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3386,9 +3393,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3657,7 +3664,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3780,7 +3787,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.3", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4343,7 +4350,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5115,7 +5122,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] From 1a5c4dd58166feb2439564e25f6b945860b9d18e Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Thu, 6 Nov 2025 12:46:00 +0100 Subject: [PATCH 123/164] chore: only patch quinn directly --- Cargo.lock | 22 ++++++++++++---------- Cargo.toml | 6 +++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c50d17f26e..e327102e00a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2378,7 +2378,7 @@ dependencies = [ "once_cell", "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2815,7 +2815,8 @@ dependencies = [ [[package]] name = "netwatch" version = "0.12.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#a7c39abc893c27e2e1411f75196cd4f3134fb3c8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f2acd376ef48b6c326abf3ba23c449e0cb8aa5c2511d189dd8a8a3bfac889b" dependencies = [ "atomic-waker", "bytes", @@ -2888,7 +2889,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3190,7 +3191,8 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portmapper" version = "0.12.0" -source = "git+https://github.com/n0-computer/net-tools?branch=feat-multipath#a7c39abc893c27e2e1411f75196cd4f3134fb3c8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b575f975dcf03e258b0c7ab3f81497d7124f508884c37da66a7314aa2a8d467" dependencies = [ "base64", "bytes", @@ -3395,7 +3397,7 @@ dependencies = [ "once_cell", "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3664,7 +3666,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3787,7 +3789,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.3", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4350,7 +4352,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -5122,7 +5124,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e98239d4c9a..a1faa46daa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,10 +43,10 @@ unused-async = "warn" [patch.crates-io] -netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "feat-multipath" } -portmapper = { git = "https://github.com/n0-computer/net-tools", branch = "feat-multipath" } +iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } -[patch."https://github.com/n0-computer/quinn"] # iroh-quinn = { path = "../iroh-quinn/quinn" } # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } From 147e6bb248fa4fc9f364e9d9ffef62242e69f679 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Fri, 7 Nov 2025 16:15:28 +0100 Subject: [PATCH 124/164] bench: add ipv6 option and metrics feature --- iroh/bench/Cargo.toml | 7 ++++--- iroh/bench/src/bin/bulk.rs | 5 +++++ iroh/bench/src/iroh.rs | 7 +++---- iroh/bench/src/lib.rs | 2 ++ iroh/bench/src/quinn.rs | 31 +++++++++++++++++++------------ 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/iroh/bench/Cargo.toml b/iroh/bench/Cargo.toml index edf45686fe1..086d1ebc31a 100644 --- a/iroh/bench/Cargo.toml +++ b/iroh/bench/Cargo.toml @@ -8,8 +8,8 @@ publish = false [dependencies] bytes = "1.7" hdrhistogram = { version = "7.2", default-features = false } -iroh = { path = ".." } -iroh-metrics = "0.37" +iroh = { path = "..", default-features = false } +iroh-metrics = { version = "0.37", optional = true } n0-future = "0.3.0" n0-error = "0.1.0" quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } @@ -28,5 +28,6 @@ tracing-subscriber = { version = "0.3.0", default-features = false, features = [ ] } [features] -default = [] +default = ["metrics"] +metrics = ["iroh/metrics", "iroh-metrics"] local-relay = ["iroh/test-utils"] diff --git a/iroh/bench/src/bin/bulk.rs b/iroh/bench/src/bin/bulk.rs index b38d87e02f3..acc0cf8ce40 100644 --- a/iroh/bench/src/bin/bulk.rs +++ b/iroh/bench/src/bin/bulk.rs @@ -1,9 +1,11 @@ +#[cfg(feature = "metrics")] use std::collections::BTreeMap; use clap::Parser; #[cfg(not(any(target_os = "freebsd", target_os = "openbsd", target_os = "netbsd")))] use iroh_bench::quinn; use iroh_bench::{Commands, Opt, configure_tracing_subscriber, iroh, rt, s2n}; +#[cfg(feature = "metrics")] use iroh_metrics::{MetricValue, MetricsGroup}; use n0_error::Result; @@ -52,6 +54,7 @@ pub fn run_iroh(opt: Opt) -> Result<()> { iroh::server_endpoint(&runtime, &relay_url, &opt) }; + #[cfg(feature = "metrics")] let endpoint_metrics = endpoint.metrics().clone(); let server_thread = std::thread::spawn(move || { @@ -86,6 +89,7 @@ pub fn run_iroh(opt: Opt) -> Result<()> { } } + #[cfg(feature = "metrics")] if opt.metrics { // print metrics println!("\nMetrics:"); @@ -158,6 +162,7 @@ pub fn run_s2n(_opt: s2n::Opt) -> Result<()> { unimplemented!() } +#[cfg(feature = "metrics")] fn collect_and_print(category: &'static str, metrics: &dyn MetricsGroup) { let mut map = BTreeMap::new(); for item in metrics.iter() { diff --git a/iroh/bench/src/iroh.rs b/iroh/bench/src/iroh.rs index 2f1430873ee..8e323b1f805 100644 --- a/iroh/bench/src/iroh.rs +++ b/iroh/bench/src/iroh.rs @@ -133,10 +133,9 @@ pub fn transport_config(max_streams: usize, initial_mtu: u16) -> TransportConfig config.max_concurrent_uni_streams(max_streams.try_into().unwrap()); config.initial_mtu(initial_mtu); - // TODO: re-enable when we upgrade quinn version - // let mut acks = quinn::AckFrequencyConfig::default(); - // acks.ack_eliciting_threshold(10u32.into()); - // config.ack_frequency_config(Some(acks)); + let mut acks = quinn::AckFrequencyConfig::default(); + acks.ack_eliciting_threshold(10u32.into()); + config.ack_frequency_config(Some(acks)); config } diff --git a/iroh/bench/src/lib.rs b/iroh/bench/src/lib.rs index 0e6669b38a6..e313009ce5d 100644 --- a/iroh/bench/src/lib.rs +++ b/iroh/bench/src/lib.rs @@ -74,6 +74,8 @@ pub struct Opt { #[cfg(feature = "local-relay")] #[clap(long, default_value_t = false)] pub only_relay: bool, + #[clap(long, default_value_t = false)] + pub use_ipv6: bool, } pub enum EndpointSelector { diff --git a/iroh/bench/src/quinn.rs b/iroh/bench/src/quinn.rs index cd97729ecd5..01aed0bf9c6 100644 --- a/iroh/bench/src/quinn.rs +++ b/iroh/bench/src/quinn.rs @@ -1,5 +1,5 @@ use std::{ - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, sync::Arc, time::{Duration, Instant}, }; @@ -32,13 +32,15 @@ pub fn server_endpoint( let mut server_config = quinn::ServerConfig::with_single_cert(cert_chain, key).unwrap(); server_config.transport = Arc::new(transport_config(opt.max_streams, opt.initial_mtu)); + let addr = if opt.use_ipv6 { + IpAddr::V6(Ipv6Addr::LOCALHOST) + } else { + IpAddr::V4(Ipv4Addr::LOCALHOST) + }; + let endpoint = { let _guard = rt.enter(); - quinn::Endpoint::server( - server_config, - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0), - ) - .unwrap() + quinn::Endpoint::server(server_config, SocketAddr::new(addr, 0)).unwrap() }; let server_addr = endpoint.local_addr().unwrap(); (server_addr, endpoint) @@ -69,8 +71,13 @@ pub async fn connect_client( server_cert: CertificateDer<'_>, opt: Opt, ) -> Result<(::quinn::Endpoint, Connection)> { - let endpoint = - quinn::Endpoint::client(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).unwrap(); + let addr = if opt.use_ipv6 { + IpAddr::V6(Ipv6Addr::LOCALHOST) + } else { + IpAddr::V4(Ipv4Addr::LOCALHOST) + }; + + let endpoint = quinn::Endpoint::client(SocketAddr::new(addr, 0)).unwrap(); let mut roots = RootCertStore::empty(); roots.add(server_cert).anyerr()?; @@ -103,11 +110,11 @@ pub fn transport_config(max_streams: usize, initial_mtu: u16) -> TransportConfig let mut config = TransportConfig::default(); config.max_concurrent_uni_streams(max_streams.try_into().unwrap()); config.initial_mtu(initial_mtu); + config.max_concurrent_multipath_paths(16); - // TODO: re-enable when we upgrade quinn version - // let mut acks = quinn::AckFrequencyConfig::default(); - // acks.ack_eliciting_threshold(10u32.into()); - // config.ack_frequency_config(Some(acks)); + let mut acks = quinn::AckFrequencyConfig::default(); + acks.ack_eliciting_threshold(10u32.into()); + config.ack_frequency_config(Some(acks)); config } From d1c1dab01eb3963a901bb34510e2423cf877ffb9 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Fri, 7 Nov 2025 16:57:25 +0100 Subject: [PATCH 125/164] refactor: improve path watching, add path stats (#3622) ## Description This improves how we expose paths and path stats for connections, and also updates feat-multipath to use https://github.com/n0-computer/quinn/pull/168. * The watcher for open paths internally uses a SmallVec to not allocate in the common case of not-too-many paths * The path info for a path now includes a boolean whether this is the currently selected primary transmission path * We no longer expose PathIds to users * We expose stats for paths from `Connection::path_stats` ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --------- Co-authored-by: Floris Bruynooghe --- Cargo.lock | 32 +-- iroh/Cargo.toml | 9 +- iroh/src/endpoint.rs | 28 +-- iroh/src/endpoint/connection.rs | 52 ++--- iroh/src/magicsock.rs | 42 +++- iroh/src/magicsock/endpoint_map.rs | 40 +++- .../magicsock/endpoint_map/endpoint_state.rs | 220 ++++++++++++++---- 7 files changed, 288 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e327102e00a..9cf9dca565d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -1896,7 +1896,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2328,7 +2328,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#817a1b565db5d48a3d04f0f51dd108c1d75aad56" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#dced59ee0ae406fa4445136c33c598448e0e738f" dependencies = [ "bytes", "cfg_aliases", @@ -2337,7 +2337,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#817a1b565db5d48a3d04f0f51dd108c1d75aad56" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#dced59ee0ae406fa4445136c33c598448e0e738f" dependencies = [ "bytes", "fastbloom", @@ -2371,14 +2371,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#817a1b565db5d48a3d04f0f51dd108c1d75aad56" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#dced59ee0ae406fa4445136c33c598448e0e738f" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2889,7 +2889,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3358,7 +3358,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -3395,9 +3395,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3666,7 +3666,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3789,7 +3789,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.3", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4352,7 +4352,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5124,7 +5124,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index 5a69320a9d5..558ff278d00 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -26,14 +26,7 @@ backon = { version = "1.4" } bytes = "1.7" crypto_box = { version = "0.10.0-pre.0", features = ["serde", "chacha20"] } data-encoding = "2.2" -derive_more = { version = "2.0.1", features = [ - "debug", - "display", - "from", - "try_into", - "deref", - "from_str" -] } +derive_more = { version = "2.0.1", features = ["debug", "display", "from", "try_into", "deref", "from_str", "into_iterator"] } ed25519-dalek = { version = "3.0.0-pre.1", features = ["serde", "rand_core", "zeroize", "pkcs8", "pem"] } http = "1" iroh-base = { version = "0.95.1", default-features = false, features = ["key", "relay"], path = "../iroh-base" } diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index b9695673afa..9ec41b57248 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -25,8 +25,8 @@ use tracing::{debug, instrument, trace, warn}; use url::Url; pub use super::magicsock::{ - AddEndpointAddrError, ConnectionType, DirectAddr, DirectAddrType, PathInfo, PathsInfo, - endpoint_map::Source, + AddEndpointAddrError, ConnectionType, DirectAddr, DirectAddrType, PathInfo, + endpoint_map::{PathInfoList, Source}, }; #[cfg(wasm_browser)] use crate::discovery::pkarr::PkarrResolver; @@ -54,12 +54,12 @@ pub mod presets; pub use quinn::{ AcceptBi, AcceptUni, AckFrequencyConfig, ApplicationClose, Chunk, ClosedStream, ConnectionClose, ConnectionError, ConnectionStats, MtuDiscoveryConfig, OpenBi, OpenUni, - ReadDatagram, ReadError, ReadExactError, ReadToEndError, RecvStream, ResetError, RetryError, - SendDatagramError, SendStream, ServerConfig, StoppedError, StreamId, TransportConfig, VarInt, - WeakConnectionHandle, WriteError, + PathStats, ReadDatagram, ReadError, ReadExactError, ReadToEndError, RecvStream, ResetError, + RetryError, SendDatagramError, SendStream, ServerConfig, StoppedError, StreamId, + TransportConfig, VarInt, WeakConnectionHandle, WriteError, }; pub use quinn_proto::{ - FrameStats, PathStats, TransportError, TransportErrorCode, UdpStats, Written, + FrameStats, TransportError, TransportErrorCode, UdpStats, Written, congestion::{Controller, ControllerFactory}, crypto::{ AeadKey, CryptoError, ExportKeyingMaterialError, HandshakeTokenKey, @@ -1802,11 +1802,11 @@ mod tests { let conn = ep.connect(dst, TEST_ALPN).await?; let mut send = conn.open_uni().await.anyerr()?; send.write_all(b"hello").await.anyerr()?; - let mut paths = conn.paths_info().stream(); + let mut paths = conn.paths().stream(); info!("Waiting for direct connection"); while let Some(infos) = paths.next().await { info!(?infos, "new PathInfos"); - if infos.keys().any(|addr| addr.is_ip()) { + if infos.iter().any(|info| info.is_ip()) { break; } } @@ -1893,15 +1893,15 @@ mod tests { // We should be connected via IP, because it is faster than the relay server. // TODO: Maybe not panic if this is not true? - let path_info = conn.paths_info().get(); + let path_info = conn.paths().get(); assert_eq!(path_info.len(), 1); - assert!(path_info.keys().next().unwrap().is_ip()); + assert!(path_info.iter().next().unwrap().is_ip()); - let mut paths = conn.paths_info().stream(); + let mut paths = conn.paths().stream(); time::timeout(Duration::from_secs(5), async move { while let Some(infos) = paths.next().await { info!(?infos, "new PathInfos"); - if infos.keys().any(|a| a.is_relay()) { + if infos.iter().any(|info| info.is_relay()) { break; } } @@ -1942,11 +1942,11 @@ mod tests { // Wait for a relay connection to be added. Client does all the asserting here, // we just want to wait so we get to see all the mechanics of the connection // being added on this side too. - let mut paths = conn.paths_info().stream(); + let mut paths = conn.paths().stream(); time::timeout(Duration::from_secs(5), async move { while let Some(infos) = paths.next().await { info!(?infos, "new PathInfos"); - if infos.keys().any(|a| a.is_relay()) { + if infos.iter().any(|path| path.is_relay()) { break; } } diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index e1e46d34b6f..f1ba60fd280 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -18,7 +18,6 @@ //! [module docs]: crate use std::{ any::Any, - collections::HashMap, future::{Future, IntoFuture}, net::{IpAddr, SocketAddr}, pin::Pin, @@ -31,19 +30,18 @@ use futures_util::{FutureExt, future::Shared}; use iroh_base::EndpointId; use n0_error::{e, stack_error}; use n0_future::time::Duration; -use n0_watcher::{Watchable, Watcher}; +use n0_watcher::Watcher; use pin_project::pin_project; use quinn::{ AcceptBi, AcceptUni, ConnectionError, ConnectionStats, OpenBi, OpenUni, ReadDatagram, RetryError, SendDatagramError, ServerConfig, VarInt, }; -use quinn_proto::PathId; use tracing::warn; use crate::{ Endpoint, discovery::DiscoveryTask, - magicsock::{PathInfo, PathsInfo}, + magicsock::endpoint_map::{PathInfoList, PathsWatchable}, }; /// Future produced by [`Endpoint::accept`]. @@ -239,38 +237,16 @@ fn conn_from_quinn_conn( } let remote_id = remote_id_from_quinn_conn(&conn)?; let alpn = alpn_from_quinn_conn(&conn).ok_or_else(|| e!(AuthenticationError::NoAlpn))?; - let paths_info_watchable = init_paths_info_watcher(&conn, ep); - let paths_info = paths_info_watchable.watch(); // Register this connection with the magicsock. - ep.msock - .register_connection(remote_id, &conn, paths_info_watchable.clone()); + let paths = ep.msock.register_connection(remote_id, &conn); Ok(Connection { remote_id, alpn, inner: conn, - paths_info, + paths, }) } -fn init_paths_info_watcher(conn: &quinn::Connection, ep: &Endpoint) -> Watchable { - let mut paths_info = HashMap::with_capacity(5); - if let Some(path0) = conn.path(PathId::ZERO) { - // This all is supposed to be infallible, but anyway. - if let Ok(remote) = path0.remote_address() { - if let Some(remote) = ep.msock.endpoint_map.transport_addr_from_mapped(remote) { - paths_info.insert( - remote.clone(), - PathInfo { - remote, - path_id: PathId::ZERO, - }, - ); - } - } - } - n0_watcher::Watchable::new(paths_info) -} - /// Returns the [`EndpointId`] from the peer's TLS certificate. /// /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is @@ -1245,7 +1221,7 @@ pub struct Connection { inner: quinn::Connection, remote_id: EndpointId, alpn: Vec, - paths_info: n0_watcher::Direct, + paths: PathsWatchable, } #[allow(missing_docs)] @@ -1480,13 +1456,21 @@ impl Connection { self.inner.stable_id() } - /// Returns information about the network paths in use by this connection. + /// Returns a [`Watcher`] for the network paths of this connection. /// /// A connection can have several network paths to the remote endpoint, commonly there - /// will be a path via the relay server and a holepunched path. This returns all the - /// paths in use by this connection. - pub fn paths_info(&self) -> impl Watcher { - self.paths_info.clone() + /// will be a path via the relay server and a holepunched path. + /// + /// The watcher is updated whenever a path is opened or closed, or when the path selected + /// for transmission changes (see [`PathInfo::is_selected`]). + /// + /// The [`PathInfoList`] returned from the watcher contains a [`PathInfo`] for each + /// transmission path. + /// + /// [`PathInfo::is_selected`]: crate::magicsock::PathInfo::is_selected + /// [`PathInfo`]: crate::magicsock::PathInfo + pub fn paths(&self) -> impl Watcher { + self.paths.watch(self.inner.weak_handle()) } /// Derives keying material from this connection's TLS session secrets. diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index b43f3905d93..c1b6c372e76 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -39,6 +39,7 @@ use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; use quinn::ServerConfig; +use quinn_proto::PathId; use rand::Rng; use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot}; use tokio_util::sync::CancellationToken; @@ -64,6 +65,7 @@ use crate::{ disco::{self, SendAddr}, discovery::{ConcurrentDiscovery, Discovery, EndpointData, UserData}, key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, + magicsock::endpoint_map::{PathAddrList, PathsWatchable}, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, }; @@ -77,7 +79,7 @@ pub(crate) mod transports; use mapped_addrs::{EndpointIdMappedAddr, MappedAddr}; pub use self::{ - endpoint_map::{ConnectionType, PathInfo, PathsInfo}, + endpoint_map::{ConnectionType, PathInfo}, metrics::Metrics, }; @@ -272,8 +274,19 @@ impl MagicSock { &self, remote: EndpointId, conn: &quinn::Connection, - paths_info: n0_watcher::Watchable, - ) { + ) -> PathsWatchable { + // Init the open paths watchable. + let mut open_paths: PathAddrList = Default::default(); + if let Some(path0) = conn.path(PathId::ZERO) { + // This all is supposed to be infallible, but anyway. + if let Ok(remote) = path0.remote_address() { + if let Some(remote) = self.endpoint_map.transport_addr_from_mapped(remote) { + open_paths.push((remote.clone(), PathId::ZERO)); + } + } + } + let open_paths = n0_watcher::Watchable::new(open_paths); + // TODO: Spawning tasks like this is obviously bad. But it is solvable: // - This is only called from inside Connection::new. // - Connection::new is called from: @@ -290,12 +303,15 @@ impl MagicSock { // send this. Before the handshake has completed we don't have anything useful to // do with this connection inside of the EndpointStateActor anyway. let weak_handle = conn.weak_handle(); - let endpoint_state = self.endpoint_map.endpoint_state_actor(remote); - let msg = EndpointStateMessage::AddConnection(weak_handle, paths_info); + let (sender, selected_path) = self + .endpoint_map + .endpoint_state_actor_with_selected_path(remote); + let msg = EndpointStateMessage::AddConnection(weak_handle, open_paths.clone()); task::spawn(async move { - endpoint_state.send(msg).await.ok(); + sender.send(msg).await.ok(); }); + PathsWatchable::new(open_paths, selected_path) } #[cfg(not(wasm_browser))] @@ -1988,10 +2004,11 @@ mod tests { info!("stats: {:#?}", stats); // TODO: ensure panics in this function are reported ok if matches!(loss, ExpectedLoss::AlmostNone) { - for (id, path) in &stats.paths { + for info in conn.paths().get().iter() { assert!( - path.lost_packets < 10, - "[receiver] path {id:?} should not loose many packets", + info.stats().lost_packets < 10, + "[receiver] path {:?} should not loose many packets", + info.remote_addr() ); } } @@ -2041,10 +2058,11 @@ mod tests { let stats = conn.stats(); info!("stats: {:#?}", stats); if matches!(loss, ExpectedLoss::AlmostNone) { - for (id, path) in &stats.paths { + for info in conn.paths().get() { assert!( - path.lost_packets < 10, - "[sender] path {id:?} should not loose many packets", + info.stats().lost_packets < 10, + "[sender] path {:?} should not loose many packets", + info.remote_addr() ); } } diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index ea33e4770ca..82a9852131c 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -7,6 +7,7 @@ use std::{ use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; use n0_future::task::{self, AbortOnDropHandle}; +use n0_watcher::Watchable; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; @@ -25,8 +26,9 @@ mod endpoint_state; mod path_state; pub(super) use endpoint_state::EndpointStateMessage; -pub use endpoint_state::{ConnectionType, PathInfo, PathsInfo}; +pub use endpoint_state::{ConnectionType, PathInfo, PathInfoList}; use endpoint_state::{EndpointStateActor, EndpointStateHandle}; +pub(crate) use endpoint_state::{PathAddrList, PathsWatchable}; // TODO: use this // /// Number of endpoints that are inactive for which we keep info about. This limit is enforced @@ -144,9 +146,39 @@ impl EndpointMap { &self, eid: EndpointId, ) -> mpsc::Sender { + self.endpoint_state_actor_inner(eid, |handle| handle.sender.clone()) + } + + /// Returns the sender and selected path watchable for the [`EndpointStateActor`]. + /// + /// If needed a new actor is started on demand. + /// + /// [`EndpointStateActor`]: endpoint_state::EndpointStateActor + pub(super) fn endpoint_state_actor_with_selected_path( + &self, + eid: EndpointId, + ) -> ( + mpsc::Sender, + Watchable>, + ) { + self.endpoint_state_actor_inner(eid, |handle| { + (handle.sender.clone(), handle.selected_path.clone()) + }) + } + + /// Returns data from the handle to an [`EndpointStateActor`]. + /// + /// If needed a new actor is started on demand. + /// + /// The callback gets a [`EndpointStateHandle`] and can clone out the data to be returned. + fn endpoint_state_actor_inner( + &self, + eid: EndpointId, + f: impl FnOnce(&EndpointStateHandle) -> R, + ) -> R { let mut handles = self.actor_handles.lock().expect("poisoned"); match handles.get(&eid) { - Some(handle) => handle.sender.clone(), + Some(handle) => f(handle), None => { // Create a new EndpointStateActor and insert it into the endpoint map. let sender = self.transports_handle.inbox.clone(); @@ -163,12 +195,12 @@ impl EndpointMap { metrics, ); let handle = actor.start(); - let sender = handle.sender.clone(); + let ret = f(&handle); handles.insert(eid, handle); // Ensure there is a EndpointMappedAddr for this EndpointId. self.endpoint_mapped_addrs.get(&eid); - sender + ret } } } diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 8800f997d66..e7aa0c243a9 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -1,5 +1,5 @@ use std::{ - collections::{BTreeSet, HashMap, VecDeque}, + collections::{BTreeSet, VecDeque}, net::SocketAddr, pin::Pin, sync::Arc, @@ -13,10 +13,11 @@ use n0_future::{ time::{self, Duration, Instant}, }; use n0_watcher::{Watchable, Watcher}; -use quinn::WeakConnectionHandle; +use quinn::{PathStats, WeakConnectionHandle}; use quinn_proto::{PathError, PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; @@ -76,8 +77,8 @@ type PathEvents = MergeUnbounded< >, >; -/// Information about currently open paths. -pub type PathsInfo = HashMap; +/// List of addrs and path ids for open paths in a connection. +pub(crate) type PathAddrList = SmallVec<[(TransportAddr, PathId); 4]>; /// The state we need to know about a single remote endpoint. /// @@ -130,7 +131,7 @@ pub(super) struct EndpointStateActor { /// holepunching regularly. /// /// We only select a path once the path is functional in Quinn. - selected_path: Option, + selected_path: Watchable>, /// Time at which we should schedule the next holepunch attempt. scheduled_holepunch: Option, /// When to next attempt opening paths in [`Self::pending_open_paths`]. @@ -163,7 +164,7 @@ impl EndpointStateActor { path_events: Default::default(), paths: FxHashMap::default(), last_holepunch: None, - selected_path: None, + selected_path: Default::default(), scheduled_holepunch: None, scheduled_open_path: None, pending_open_paths: VecDeque::new(), @@ -174,6 +175,7 @@ impl EndpointStateActor { let (tx, rx) = mpsc::channel(16); let me = self.local_endpoint_id; let endpoint_id = self.endpoint_id; + let selected_path = self.selected_path.clone(); // Ideally we'd use the endpoint span as parent. We'd have to plug that span into // here somehow. Instead we have no parent and explicitly set the me attribute. If @@ -195,6 +197,7 @@ impl EndpointStateActor { ); EndpointStateHandle { sender: tx, + selected_path, _task: AbortOnDropHandle::new(task), } } @@ -264,8 +267,9 @@ impl EndpointStateActor { EndpointStateMessage::SendDatagram(transmit) => { self.handle_msg_send_datagram(transmit).await?; } - EndpointStateMessage::AddConnection(handle, paths_info) => { - self.handle_msg_add_connection(handle, paths_info).await; + EndpointStateMessage::AddConnection(handle, paths_watchable) => { + self.handle_msg_add_connection(handle, paths_watchable) + .await; } EndpointStateMessage::AddEndpointAddr(addr, source) => { self.handle_msg_add_endpoint_addr(addr, source); @@ -293,10 +297,10 @@ impl EndpointStateActor { /// /// Error returns are fatal and kill the actor. async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) -> n0_error::Result<()> { - if let Some(ref addr) = self.selected_path { + if let Some(addr) = self.selected_path.get() { trace!(?addr, "sending datagram to selected path"); self.transports_sender - .send((addr.clone(), transmit).into()) + .send((addr, transmit).into()) .await .std_context("TransportSenderActor stopped")?; } else { @@ -323,7 +327,7 @@ impl EndpointStateActor { async fn handle_msg_add_connection( &mut self, handle: WeakConnectionHandle, - paths_info: Watchable, + paths_watchable: Watchable, ) { if let Some(conn) = handle.upgrade() { // Remove any conflicting stable_ids from the local state. @@ -341,7 +345,7 @@ impl EndpointStateActor { conn_id, ConnectionState { handle: handle.clone(), - pub_path_info: paths_info, + pub_open_paths: paths_watchable, paths: Default::default(), open_paths: Default::default(), path_ids: Default::default(), @@ -507,20 +511,20 @@ impl EndpointStateActor { /// Handles [`EndpointStateMessage::Latency`]. fn handle_msg_latency(&self, tx: oneshot::Sender>) { - let rtt = self.selected_path.as_ref().and_then(|addr| { + let rtt = self.selected_path.get().and_then(|addr| { for conn_state in self.connections.values() { - let Some(path_id) = conn_state.path_ids.get(addr) else { + let Some(path_id) = conn_state.path_ids.get(&addr) else { continue; }; if !conn_state.open_paths.contains_key(path_id) { continue; } - if let Some(stats) = conn_state + if let Some(rtt) = conn_state .handle .upgrade() - .and_then(|conn| conn.stats().paths.get(path_id).copied()) + .and_then(|conn| conn.path_stats(*path_id).map(|stats| stats.rtt)) { - return Some(stats.rtt); + return Some(rtt); } } None @@ -554,7 +558,7 @@ impl EndpointStateActor { if self .selected_path - .as_ref() + .get() .map(|addr| addr.is_ip()) .unwrap_or_default() { @@ -890,9 +894,8 @@ impl EndpointStateActor { let Some(conn) = conn_state.handle.upgrade() else { continue; }; - let stats = conn.stats(); - for (path_id, stats) in stats.paths { - if let Some(addr) = conn_state.open_paths.get(&path_id) { + for (path_id, addr) in conn_state.open_paths.iter() { + if let Some(stats) = conn.path_stats(*path_id) { all_path_rtts .entry(addr.clone()) .or_default() @@ -928,8 +931,8 @@ impl EndpointStateActor { .min() }); if let Some((rtt, addr)) = selected_path { - let prev = self.selected_path.replace(addr.clone()); - if prev.as_ref() != Some(addr) { + let prev = self.selected_path.set(Some(addr.clone())); + if prev.is_ok() { debug!(?addr, ?rtt, ?prev, "selected new path"); } self.open_path(addr); @@ -945,7 +948,7 @@ impl EndpointStateActor { // paths and immediately call this. But the new paths are probably not yet open on // all connections. fn close_redundant_paths(&mut self, selected_path: &transports::Addr) { - debug_assert_eq!(self.selected_path.as_ref(), Some(selected_path)); + debug_assert_eq!(self.selected_path.get().as_ref(), Some(selected_path),); for (conn_id, conn_state) in self.connections.iter() { for (path_id, path_remote) in conn_state @@ -999,7 +1002,7 @@ pub(crate) enum EndpointStateMessage { /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. #[debug("AddConnection(..)")] - AddConnection(WeakConnectionHandle, Watchable), + AddConnection(WeakConnectionHandle, Watchable), /// Adds a [`EndpointAddr`] with locations where the endpoint might be reachable. AddEndpointAddr(EndpointAddr, Source), /// Process a received DISCO CallMeMaybe message. @@ -1027,7 +1030,12 @@ pub(crate) enum EndpointStateMessage { /// Dropping this will stop the actor. #[derive(Debug)] pub(super) struct EndpointStateHandle { + /// Sender for the channel into the [`EndpointStateActor`]. pub(super) sender: mpsc::Sender, + /// Watchable for the selected transmission path. + /// + /// This should only be read or watched. It is set from within the actro. + pub(super) selected_path: Watchable>, _task: AbortOnDropHandle<()>, } @@ -1084,7 +1092,7 @@ struct ConnectionState { /// Weak handle to the connection. handle: WeakConnectionHandle, /// The information we publish to users about the paths used in this connection. - pub_path_info: Watchable, + pub_open_paths: Watchable, /// The paths that exist on this connection. /// /// This could be in any state, e.g. while still validating the path or already closed @@ -1137,39 +1145,157 @@ impl ConnectionState { .iter() .map(|(path_id, remote)| { let remote = TransportAddr::from(remote.clone()); - ( - remote.clone(), - PathInfo { - remote: remote.clone(), - path_id: *path_id, - }, - ) + (remote, *path_id) }) - .collect::>(); + .collect::(); - self.pub_path_info.set(new).ok(); + self.pub_open_paths.set(new).ok(); + } +} + +/// Watchables for the open paths and selected transmission path in a connection. +/// +/// This is stored in the [`Connection`], and the watchables are set from within the endpoint state actor. +#[derive(Debug, Default, Clone)] +pub(crate) struct PathsWatchable { + /// Watchable for the open paths (in this connection). + open_paths: Watchable, + /// Watchable for the selected transmission path (global for this remote endpoint). + selected_path: Watchable>, +} + +impl PathsWatchable { + pub(crate) fn new( + open_paths: Watchable, + selected_path: Watchable>, + ) -> Self { + Self { + open_paths, + selected_path, + } + } + + pub(crate) fn watch( + &self, + conn_handle: WeakConnectionHandle, + ) -> impl Watcher { + let joined_watcher = (self.open_paths.watch(), self.selected_path.watch()); + joined_watcher.map(move |(open_paths, selected_path)| { + let selected_path: Option = selected_path.map(Into::into); + let Some(conn) = conn_handle.upgrade() else { + return PathInfoList(Default::default()); + }; + let list = open_paths + .into_iter() + .flat_map(move |(remote, path_id)| { + PathInfo::new(path_id, &conn, remote, selected_path.as_ref()) + }) + .collect(); + PathInfoList(list) + }) + } +} + +/// List of [`PathInfo`] for the network paths of a [`Connection`]. +/// +/// This struct implements [`IntoIterator`]. +/// +/// [`Connection`]: crate::endpoint::Connection +#[derive(derive_more::Debug, derive_more::IntoIterator, Eq, PartialEq, Clone)] +#[debug("{_0:?}")] +pub struct PathInfoList(SmallVec<[PathInfo; 4]>); + +impl PathInfoList { + /// Returns an iterator over the path infos. + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Returns `true` if the list is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the number of paths. + pub fn len(&self) -> usize { + self.0.len() } } /// Information about a network path used by a [`Connection`]. /// /// [`Connection`]: crate::endpoint::Connection -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(derive_more::Debug, Clone)] pub struct PathInfo { + path_id: PathId, + #[debug(skip)] + handle: WeakConnectionHandle, + stats: PathStats, + remote: TransportAddr, + is_selected: bool, +} + +impl PartialEq for PathInfo { + fn eq(&self, other: &Self) -> bool { + self.path_id == other.path_id + && self.remote == other.remote + && self.is_selected == other.is_selected + } +} + +impl Eq for PathInfo {} + +impl PathInfo { + fn new( + path_id: PathId, + conn: &quinn::Connection, + remote: TransportAddr, + selected_path: Option<&TransportAddr>, + ) -> Option { + let stats = conn.path_stats(path_id)?; + Some(Self { + path_id, + handle: conn.weak_handle(), + is_selected: Some(&remote) == selected_path, + remote, + stats, + }) + } + /// The remote transport address used by this network path. - pub remote: TransportAddr, - /// The internal path identifier for the [`Connection`] - /// - /// This is unique for the lifetime of the connection. Can be used to look up the path - /// statistics in the [`ConnectionStats::paths`], returned by [`Connection::stats`]. + pub fn remote_addr(&self) -> &TransportAddr { + &self.remote + } + + /// Returns `true` if this path is currently the main transmission path for this [`Connection`]. /// /// [`Connection`]: crate::endpoint::Connection - /// [`Connection::stats`]: crate::endpoint::Connection::stats - /// [`ConnectionStats::paths`]: crate::endpoint::ConnectionStats::paths - // TODO: Decide if exposing this is a good idea. Maybe we should just hide this - // entirely try to provide Self::stats(). But that would mean this needs to have a - // WeakConnectionHandle. - pub path_id: PathId, + pub fn is_selected(&self) -> bool { + self.is_selected + } + + /// Whether this is an IP transport address. + pub fn is_ip(&self) -> bool { + self.remote.is_ip() + } + + /// Whether this is a transport address via a relay server. + pub fn is_relay(&self) -> bool { + self.remote.is_relay() + } + + /// Returns stats for this transmission path. + pub fn stats(&self) -> PathStats { + self.handle + .upgrade() + .and_then(|conn| conn.path_stats(self.path_id)) + .unwrap_or(self.stats) + } + + /// Current best estimate of this paths's latency (round-trip-time) + pub fn rtt(&self) -> Duration { + self.stats().rtt + } } /// Poll a future once, like n0_future::future::poll_once but sync. From 7887fb599541ce5268471cb89879d83f449e9202 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Mon, 10 Nov 2025 10:25:43 +0100 Subject: [PATCH 126/164] refactor: minor cleanups in endpoint state (#3626) ## Description This has a few minor cleanups without any functional changes in the endpoint state actor: * Remove double handle upgrade * Add helper function `to_transport_addr` on the relay mapped addr map * Use hash map `entry` API instead of `get` and `expect` * Use `if let` chains to remove a level of indentation ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- .../magicsock/endpoint_map/endpoint_state.rs | 90 +++++++++---------- iroh/src/magicsock/mapped_addrs.rs | 62 ++++++++----- 2 files changed, 82 insertions(+), 70 deletions(-) diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index e7aa0c243a9..72e1bca8079 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -31,7 +31,7 @@ use crate::{ endpoint::DirectAddr, magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, - mapped_addrs::{AddrMap, MappedAddr, MultipathMappedAddr, RelayMappedAddr}, + mapped_addrs::{AddrMap, MappedAddr, RelayMappedAddr}, transports::{self, OwnedTransmit}, }, util::MaybeFuture, @@ -341,60 +341,54 @@ impl EndpointStateActor { let events = BroadcastStream::new(conn.path_events()); let stream = events.map(move |evt| (conn_id, evt)); self.path_events.push(Box::pin(stream)); - self.connections.insert( - conn_id, - ConnectionState { + let conn_state = self + .connections + .entry(conn_id) + .insert_entry(ConnectionState { handle: handle.clone(), pub_open_paths: paths_watchable, paths: Default::default(), open_paths: Default::default(), path_ids: Default::default(), - }, - ); + }) + .into_mut(); // Store PathId(0), set path_status and select best path, check if holepunching // is needed. - if let Some(conn) = handle.upgrade() { - if let Some(path) = conn.path(PathId::ZERO) { - if let Some(path_remote) = path - .remote_address() - .map_or(None, |remote| Some(MultipathMappedAddr::from(remote))) - .and_then(|mmaddr| mmaddr.to_transport_addr(&self.relay_mapped_addrs)) - { - trace!(?path_remote, "added new connection"); - let path_remote_is_ip = path_remote.is_ip(); - let status = match path_remote { - transports::Addr::Ip(_) => PathStatus::Available, - transports::Addr::Relay(_, _) => PathStatus::Backup, - }; - path.set_status(status).ok(); - let conn_state = - self.connections.get_mut(&conn_id).expect("inserted above"); - conn_state.add_open_path(path_remote.clone(), PathId::ZERO); - self.paths - .entry(path_remote) - .or_default() - .sources - .insert(Source::Connection, Instant::now()); - self.select_path(); - - if path_remote_is_ip { - // We may have raced this with a relay address. Try and add any - // relay addresses we have back. - let relays = self - .paths - .keys() - .filter(|a| a.is_relay()) - .cloned() - .collect::>(); - for remote in relays { - self.open_path(&remote); - } - } + if let Some(path) = conn.path(PathId::ZERO) + && let Ok(socketaddr) = path.remote_address() + && let Some(path_remote) = self.relay_mapped_addrs.to_transport_addr(socketaddr) + { + trace!(?path_remote, "added new connection"); + let path_remote_is_ip = path_remote.is_ip(); + let status = match path_remote { + transports::Addr::Ip(_) => PathStatus::Available, + transports::Addr::Relay(_, _) => PathStatus::Backup, + }; + path.set_status(status).ok(); + conn_state.add_open_path(path_remote.clone(), PathId::ZERO); + self.paths + .entry(path_remote) + .or_default() + .sources + .insert(Source::Connection, Instant::now()); + self.select_path(); + + if path_remote_is_ip { + // We may have raced this with a relay address. Try and add any + // relay addresses we have back. + let relays = self + .paths + .keys() + .filter(|a| a.is_relay()) + .cloned() + .collect::>(); + for remote in relays { + self.open_path(&remote); } } - self.trigger_holepunching().await; } + self.trigger_holepunching().await; } } @@ -802,10 +796,8 @@ impl EndpointStateActor { path.set_keep_alive_interval(Some(HEARTBEAT_INTERVAL)).ok(); path.set_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)).ok(); - if let Some(path_remote) = path - .remote_address() - .map_or(None, |remote| Some(MultipathMappedAddr::from(remote))) - .and_then(|mmaddr| mmaddr.to_transport_addr(&self.relay_mapped_addrs)) + if let Ok(socketaddr) = path.remote_address() + && let Some(path_remote) = self.relay_mapped_addrs.to_transport_addr(socketaddr) { event!( target: "iroh::_events::path::open", @@ -1156,6 +1148,8 @@ impl ConnectionState { /// Watchables for the open paths and selected transmission path in a connection. /// /// This is stored in the [`Connection`], and the watchables are set from within the endpoint state actor. +/// +/// [`Connection`]: crate::endpoint::Connection #[derive(Debug, Default, Clone)] pub(crate) struct PathsWatchable { /// Watchable for the open paths (in this connection). diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 49030e07147..53e5b67b9d4 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -95,28 +95,6 @@ impl From for MultipathMappedAddr { } } -impl MultipathMappedAddr { - pub(super) fn to_transport_addr( - &self, - relay_mapped_addrs: &AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, - ) -> Option { - match self { - Self::Mixed(_) => { - error!("Mixed addr has no transports::Addr"); - None - } - Self::Relay(mapped) => match relay_mapped_addrs.lookup(mapped) { - Some(parts) => Some(transports::Addr::from(parts)), - None => { - error!("Unknown RelayMappedAddr"); - None - } - }, - Self::Ip(addr) => Some(transports::Addr::from(*addr)), - } - } -} - /// An address used to address a endpoint on any or all paths. /// /// This is only used for initially connecting to a remote endpoint. We instruct Quinn to @@ -307,3 +285,43 @@ impl Default for AddrMapInner { } } } + +/// Functions for the relay mapped address map. +impl AddrMap<(RelayUrl, EndpointId), RelayMappedAddr> { + /// Converts a mapped socket address to a transport address. + /// + /// This takes a socket address, converts it into a [`MultipathMappedAddr`] and then tries + /// to convert the mapped address into a [`transports::Addr`]. + /// + /// Returns `Some` with the transport address for IP mapped addresses and for relay mapped + /// addresses if an entry for the mapped address exists in `self`. + /// + /// Returns `None` and emits an error log if the mapped address is a [`MultipathMappedAddr::Mixed`], + /// or if the mapped address is a [`MultipathMappedAddr::Relay`] and `self` does not contain the + /// mapped address. + pub(crate) fn to_transport_addr( + &self, + addr: impl Into, + ) -> Option { + match addr.into() { + MultipathMappedAddr::Mixed(_) => { + error!( + "Failed to convert addr to transport addr: Mixed mapped addr has no transport address" + ); + None + } + MultipathMappedAddr::Relay(relay_mapped_addr) => { + match self.lookup(&relay_mapped_addr) { + Some(parts) => Some(transports::Addr::from(parts)), + None => { + error!( + "Failed to convert addr to transport addr: Unknown relay mapped addr" + ); + None + } + } + } + MultipathMappedAddr::Ip(addr) => Some(transports::Addr::from(addr)), + } + } +} From 1d5937ca8624077d5e346f1198339e7712ea2406 Mon Sep 17 00:00:00 2001 From: Frando Date: Mon, 10 Nov 2025 10:58:55 +0100 Subject: [PATCH 127/164] refactor: use Connection::on_closed in endpoint state actor (#3627) In the endpoint state actor, this uses the `Connection::on_closed` future added in https://github.com/n0-computer/quinn/pull/153 to remove connections once they are closed instead of relying on manual cleanup. --- .../magicsock/endpoint_map/endpoint_state.rs | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 72e1bca8079..54aec93536d 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -3,12 +3,13 @@ use std::{ net::SocketAddr, pin::Pin, sync::Arc, + task::Poll, }; use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; use n0_error::StdResultExt; use n0_future::{ - MergeUnbounded, Stream, StreamExt, + FuturesUnordered, MergeUnbounded, Stream, StreamExt, task::{self, AbortOnDropHandle}, time::{self, Duration, Instant}, }; @@ -111,6 +112,8 @@ pub(super) struct EndpointStateActor { // /// All connections we have to this remote endpoint. connections: FxHashMap, + /// Notifications when connections are closed. + connections_close: FuturesUnordered, /// Events emitted by Quinn about path changes, for all paths, all connections. path_events: PathEvents, @@ -161,6 +164,7 @@ impl EndpointStateActor { relay_mapped_addrs, disco, connections: FxHashMap::default(), + connections_close: Default::default(), path_events: Default::default(), paths: FxHashMap::default(), last_holepunch: None, @@ -234,6 +238,9 @@ impl EndpointStateActor { Some((id, evt)) = self.path_events.next() => { self.handle_path_event(id, evt); } + Some(conn_id) = self.connections_close.next(), if !self.connections_close.is_empty() => { + self.connections.remove(&conn_id); + } _ = self.local_addrs.updated() => { trace!("local addrs updated, triggering holepunching"); self.trigger_holepunching().await; @@ -334,13 +341,11 @@ impl EndpointStateActor { let conn_id = ConnId(conn.stable_id()); self.connections.remove(&conn_id); - // This is a good time to clean up connections. - self.cleanup_connections(); - // Store the connection and hook up paths events stream. let events = BroadcastStream::new(conn.path_events()); let stream = events.map(move |evt| (conn_id, evt)); self.path_events.push(Box::pin(stream)); + self.connections_close.push(OnClosed::new(&conn)); let conn_state = self .connections .entry(conn_id) @@ -864,12 +869,6 @@ impl EndpointStateActor { } } - /// Clean up connections which no longer exist. - // TODO: Call this on a schedule. - fn cleanup_connections(&mut self) { - self.connections.retain(|_, c| c.handle.upgrade().is_some()); - } - /// Selects the path with the lowest RTT, prefers direct paths. /// /// If there are direct paths, this selects the direct path with the lowest RTT. If @@ -1296,7 +1295,34 @@ impl PathInfo { fn now_or_never>(fut: F) -> Option { let fut = std::pin::pin!(fut); match fut.poll(&mut std::task::Context::from_waker(std::task::Waker::noop())) { - std::task::Poll::Ready(res) => Some(res), - std::task::Poll::Pending => None, + Poll::Ready(res) => Some(res), + Poll::Pending => None, + } +} + +/// Future that resolves to the `conn_id` once a connection is closed. +/// +/// This uses [`quinn::Connection::on_closed`], which does not keep the connection alive +/// while awaiting the future. +struct OnClosed { + conn_id: ConnId, + inner: quinn::OnClosed, +} + +impl OnClosed { + fn new(conn: &quinn::Connection) -> Self { + Self { + conn_id: ConnId(conn.stable_id()), + inner: conn.on_closed(), + } + } +} + +impl Future for OnClosed { + type Output = ConnId; + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + let (_close_reason, _stats) = std::task::ready!(Pin::new(&mut self.inner).poll(cx)); + Poll::Ready(self.conn_id) } } From e0f10ce7f2d645bded18621ef5b7ca31ff1ad721 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Mon, 10 Nov 2025 13:55:18 +0100 Subject: [PATCH 128/164] refactor(multipath): Make registering connections with the magicsock async (#3629) ## Description Currently, `Magicsock::register_connection` is a sync function, but needs to send over an async channel to notify the endpoint state actor about the new connection. It currently employs a hack to achieve that: it spawns a tokio task for sending the message. This PR cleans this up by making `regsiter_connection` return a future, and awaits this future at the various sites where we go from quinn::Connection to iroh Connection. Luckily, all these call sites already are in async contexts. * When going from `Connecting` or `Accepting` to `Connection`, we await the registration after having the `quinn::Connecting` completes. The future is stored in an option instead of using a state enum as you would usually, because we need unconditional access to the `quinn::Connecting` in the functions on `Connecting`/`Accepting`. * For the `(Incoming|Outgoing)ZeroRttConnection`, we store a future that first awaits the handshake and then registers the connection. So we need only a single future here. With `register_connection` being async, we can also clean up some of the not-so-nice things introduced in #3622: Because we now have an async function, we can let the endpoint state actor return a reply. This makes it much more straightforward because we can have the endpoint state actor initialize a watcher for the paths and return it instead of having to do a weird dance with parts of the state being initialized or stored outside of the endpoint state actor to satisfy the sync function constraints. This is much nicer now IMO. ## Breaking Changes ## Notes & open questions This adds a boxed future into the process of going from a `Connecting` to a `Connection`. If we really wanted, we could use a manually implemented future instead. However, I don't think one boxed future *per connection* is an issue, so I'd prefer to leave it like this (implementing a manual future for `tokio::mpsc::Sender::send` is cumbersome). ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- iroh/src/endpoint/connection.rs | 253 +++++++++--------- iroh/src/magicsock.rs | 63 ++--- iroh/src/magicsock/endpoint_map.rs | 60 +---- .../magicsock/endpoint_map/endpoint_state.rs | 33 +-- 4 files changed, 163 insertions(+), 246 deletions(-) diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index f1ba60fd280..dc269a3a551 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -29,7 +29,7 @@ use ed25519_dalek::{VerifyingKey, pkcs8::DecodePublicKey}; use futures_util::{FutureExt, future::Shared}; use iroh_base::EndpointId; use n0_error::{e, stack_error}; -use n0_future::time::Duration; +use n0_future::{TryFutureExt, future::Boxed as BoxFuture, time::Duration}; use n0_watcher::Watcher; use pin_project::pin_project; use quinn::{ @@ -41,7 +41,10 @@ use tracing::warn; use crate::{ Endpoint, discovery::DiscoveryTask, - magicsock::endpoint_map::{PathInfoList, PathsWatchable}, + magicsock::{ + EndpointStateActorStoppedError, + endpoint_map::{PathInfoList, PathsWatchable}, + }, }; /// Future produced by [`Endpoint::accept`]. @@ -153,40 +156,27 @@ impl IntoFuture for Incoming { type IntoFuture = IncomingFuture; fn into_future(self) -> Self::IntoFuture { - IncomingFuture { - inner: self.inner.into_future(), - ep: self.ep, - } + IncomingFuture(Box::pin(async move { + let quinn_conn = self.inner.into_future().await?; + let conn = conn_from_quinn_conn(quinn_conn, &self.ep)?.await?; + Ok(conn) + })) } } /// Adaptor to let [`Incoming`] be `await`ed like a [`Connecting`]. -#[derive(Debug)] -#[pin_project] -pub struct IncomingFuture { - #[pin] - inner: quinn::IncomingFuture, - ep: Endpoint, -} +#[derive(derive_more::Debug)] +#[debug("IncomingFuture")] +pub struct IncomingFuture(BoxFuture>); impl Future for IncomingFuture { type Output = Result; - fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - let this = self.project(); - match this.inner.poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err.into())), - Poll::Ready(Ok(inner)) => { - let conn = match conn_from_quinn_conn(inner, this.ep) { - Ok(conn) => conn, - Err(err) => return Poll::Ready(Err(err.into())), - }; - Poll::Ready(Ok(conn)) - } - } + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + self.0.poll_unpin(cx) } } + /// Extracts the ALPN protocol from the peer's handshake data. fn alpn_from_quinn_conn(conn: &quinn::Connection) -> Option> { let data = conn.handshake_data()?; @@ -210,6 +200,7 @@ async fn alpn_from_quinn_connecting(conn: &mut quinn::Connecting) -> Result for AuthenticationError { + #[track_caller] + fn from(_value: EndpointStateActorStoppedError) -> Self { + e!(Self::InternalConsistencyError) + } +} + +impl From for ConnectingError { + #[track_caller] + fn from(_value: EndpointStateActorStoppedError) -> Self { + e!(AuthenticationError::InternalConsistencyError).into() + } } /// Converts a `quinn::Connection` to a `Connection`. /// -/// ## Errors +/// Returns a [`AuthenticationError`] if the handshake data has not completed, +/// or if no alpn was set by the remote node. /// -/// Returns a [`AuthenticationError`] if the handshake data has -/// not completed, or if no alpn was set by the remote node. +/// Otherwise returns a future that completes once the connection has been registered with the +/// magicsock. This future can return an [`EndpointStateActorStoppedError`], which will only be +/// emitted if the endpoint is closing. +/// +/// The returned future is `'static`, so it can be stored without being lifetime-bound on `&ep`. fn conn_from_quinn_conn( conn: quinn::Connection, ep: &Endpoint, -) -> Result { +) -> Result< + impl Future> + Send + 'static, + AuthenticationError, +> { if let Some(reason) = conn.close_reason() { return Err(e!(AuthenticationError::ConnectionError { source: reason })); } let remote_id = remote_id_from_quinn_conn(&conn)?; let alpn = alpn_from_quinn_conn(&conn).ok_or_else(|| e!(AuthenticationError::NoAlpn))?; // Register this connection with the magicsock. - let paths = ep.msock.register_connection(remote_id, &conn); - Ok(Connection { - remote_id, - alpn, - inner: conn, - paths, + let fut = ep.msock.register_connection(remote_id, conn.weak_handle()); + Ok(async move { + let paths = fut.await?; + Ok(Connection { + paths, + remote_id, + alpn, + inner: conn, + }) }) } @@ -293,10 +310,14 @@ fn remote_id_from_quinn_conn( /// /// This future resolves to a [`Connection`] once the handshake completes. #[derive(derive_more::Debug)] -#[pin_project] pub struct Connecting { - #[pin] inner: quinn::Connecting, + /// Future to register the connection with the magicsock. + /// + /// This is set and polled after `inner` completes. We are using an option instead of an enum + /// because we need infallible access to `inner` in some methods. + #[debug("{}", register_with_magicsock.as_ref().map(|_| "Some(RegisterWithMagicsockFut)").unwrap_or("None"))] + register_with_magicsock: Option, ep: Endpoint, /// `Some(remote_id)` if this is an outgoing connection, `None` if this is an incoming conn remote_endpoint_id: EndpointId, @@ -305,12 +326,18 @@ pub struct Connecting { _discovery_drop_guard: Option, } +type RegisterWithMagicsockFut = BoxFuture>; + /// In-progress connection attempt future #[derive(derive_more::Debug)] -#[pin_project] pub struct Accepting { - #[pin] inner: quinn::Connecting, + /// Future to register the connection with the magicsock. + /// + /// This is set and polled after `inner` completes. We are using an option instead of an enum + /// because we need infallible access to `inner` in some methods. + #[debug("{}", register_with_magicsock.as_ref().map(|_| "Some(RegisterWithMagicsockFut)").unwrap_or("None"))] + register_with_magicsock: Option, ep: Endpoint, } @@ -353,6 +380,7 @@ impl Connecting { inner, ep, remote_endpoint_id, + register_with_magicsock: None, _discovery_drop_guard, } } @@ -395,29 +423,25 @@ impl Connecting { #[allow(clippy::result_large_err)] pub fn into_0rtt(self) -> Result { match self.inner.into_0rtt() { - Ok((inner, zrtt_accepted)) => { - // This call is why `self.remote_endpoint_id` was introduced. - // When we `Connecting::into_0rtt`, then we don't yet have `handshake_data` - // in our `Connection`, thus `we won't be able to pick up - // `Connection::remote_endpoint_id`. - // Instead, we provide `self.remote_endpoint_id` here - we know it in advance, - // after all. - Ok(OutgoingZeroRttConnection { - inner, - ep: self.ep, - accepted: ZeroRttAccepted { - inner: zrtt_accepted, - _discovery_drop_guard: self._discovery_drop_guard, + Ok((quinn_conn, zrtt_accepted)) => { + let handshake_completed_fut: BoxFuture<_> = Box::pin({ + let quinn_conn = quinn_conn.clone(); + async move { + let accepted = zrtt_accepted.await; + let conn = conn_from_quinn_conn(quinn_conn, &self.ep)?.await?; + drop(self._discovery_drop_guard); + Ok(match accepted { + true => ZeroRttStatus::Accepted(conn), + false => ZeroRttStatus::Rejected(conn), + }) } - .shared(), + }); + Ok(OutgoingZeroRttConnection { + inner: quinn_conn, + handshake_completed_fut: handshake_completed_fut.shared(), }) } - Err(inner) => Err(Self { - inner, - ep: self.ep, - remote_endpoint_id: self.remote_endpoint_id, - _discovery_drop_guard: self._discovery_drop_guard, - }), + Err(inner) => Err(Self { inner, ..self }), } } @@ -440,20 +464,16 @@ impl Connecting { impl Future for Connecting { type Output = Result; - fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - let this = self.project(); - match this.inner.poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err.into())), - Poll::Ready(Ok(inner)) => { - let conn = match conn_from_quinn_conn(inner, this.ep) { - Ok(conn) => conn, - Err(err) => { - return Poll::Ready(Err(err.into())); - } + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + loop { + if let Some(fut) = &mut self.register_with_magicsock { + return fut.poll_unpin(cx).map_err(Into::into); + } else { + let quinn_conn = std::task::ready!(self.inner.poll_unpin(cx)?); + match conn_from_quinn_conn(quinn_conn, &self.ep) { + Err(err) => return Poll::Ready(Err(err.into())), + Ok(fut) => self.register_with_magicsock = Some(Box::pin(fut.err_into())), }; - - Poll::Ready(Ok(conn)) } } } @@ -461,7 +481,11 @@ impl Future for Connecting { impl Accepting { pub(crate) fn new(inner: quinn::Connecting, ep: Endpoint) -> Self { - Self { inner, ep } + Self { + inner, + ep, + register_with_magicsock: None, + } } /// Converts this [`Accepting`] into a 0-RTT or 0.5-RTT connection at the cost of weakened @@ -492,17 +516,21 @@ impl Accepting { /// /// [`RecvStream::is_0rtt`]: quinn::RecvStream::is_0rtt pub fn into_0rtt(self) -> IncomingZeroRttConnection { - let (inner, accepted) = self + let (quinn_conn, zrtt_accepted) = self .inner .into_0rtt() .expect("incoming connections can always be converted to 0-RTT"); + let handshake_completed_fut: BoxFuture<_> = Box::pin({ + let quinn_conn = quinn_conn.clone(); + async move { + zrtt_accepted.await; + let conn = conn_from_quinn_conn(quinn_conn, &self.ep)?.await?; + Ok(conn) + } + }); IncomingZeroRttConnection { - accepted: ZeroRttAccepted { - inner: accepted, - _discovery_drop_guard: None, - }, - inner, - ep: self.ep, + inner: quinn_conn, + handshake_completed_fut: handshake_completed_fut.shared(), } } @@ -520,48 +548,21 @@ impl Accepting { impl Future for Accepting { type Output = Result; - fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - let this = self.project(); - match this.inner.poll(cx) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => Poll::Ready(Err(err.into())), - Poll::Ready(Ok(inner)) => { - let conn = match conn_from_quinn_conn(inner, this.ep) { - Ok(conn) => conn, + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + loop { + if let Some(fut) = &mut self.register_with_magicsock { + return fut.poll_unpin(cx).map_err(Into::into); + } else { + let quinn_conn = std::task::ready!(self.inner.poll_unpin(cx)?); + match conn_from_quinn_conn(quinn_conn, &self.ep) { Err(err) => return Poll::Ready(Err(err.into())), + Ok(fut) => self.register_with_magicsock = Some(Box::pin(fut.err_into())), }; - - Poll::Ready(Ok(conn)) } } } } -/// Future that completes when a connection is fully established. -/// -/// For clients, the resulting value indicates if 0-RTT was accepted. For servers, the resulting -/// value is meaningless. -#[derive(derive_more::Debug)] -#[debug("ZeroRttAccepted")] -struct ZeroRttAccepted { - inner: quinn::ZeroRttAccepted, - /// When we call `Connecting::into_0rtt`, we don't want to stop discovery, so we transfer the task - /// to this future. - /// When `quinn::ZeroRttAccepted` resolves, we've successfully received data from the remote. - /// Thus, that's the right time to drop discovery to preserve the behaviour similar to - /// `Connecting` -> `Connection` without 0-RTT. - /// Should we eventually decide to keep the discovery task alive for the duration of the whole - /// `Connection`, then this task should be transferred to the `Connection` instead of here. - _discovery_drop_guard: Option, -} - -impl Future for ZeroRttAccepted { - type Output = bool; - fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - Pin::new(&mut self.inner).poll(cx) - } -} - /// The client side of a 0-RTT connection. /// /// This is created using [`Connecting::into_0rtt`]. @@ -576,12 +577,11 @@ impl Future for ZeroRttAccepted { #[derive(Debug, Clone)] pub struct OutgoingZeroRttConnection { inner: quinn::Connection, - accepted: Shared, - ep: Endpoint, + handshake_completed_fut: Shared>>, } /// Returned from [`OutgoingZeroRttConnection::handshake_completed`]. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum ZeroRttStatus { /// If the 0-RTT data was accepted, you can continue to use any streams /// that were created before the handshake was completed. @@ -613,13 +613,7 @@ impl OutgoingZeroRttConnection { /// Thus, those errors should only occur if someone connects to you with a /// modified iroh endpoint or with a plain QUIC client. pub async fn handshake_completed(self) -> Result { - let accepted = self.accepted.clone().await; - let conn = conn_from_quinn_conn(self.inner.clone(), &self.ep)?; - - Ok(match accepted { - true => ZeroRttStatus::Accepted(conn), - false => ZeroRttStatus::Rejected(conn), - }) + self.handshake_completed_fut.await } /// Initiates a new outgoing unidirectional stream. @@ -912,8 +906,7 @@ impl OutgoingZeroRttConnection { #[derive(Debug)] pub struct IncomingZeroRttConnection { inner: quinn::Connection, - accepted: ZeroRttAccepted, - ep: Endpoint, + handshake_completed_fut: Shared>>, } impl IncomingZeroRttConnection { @@ -930,8 +923,7 @@ impl IncomingZeroRttConnection { /// Thus, those errors should only occur if someone connects to you with a /// modified iroh endpoint or with a plain QUIC client. pub async fn handshake_completed(self) -> Result { - self.accepted.await; - conn_from_quinn_conn(self.inner, &self.ep) + self.handshake_completed_fut.await } /// Initiates a new outgoing unidirectional stream. @@ -1227,6 +1219,7 @@ pub struct Connection { #[allow(missing_docs)] #[stack_error(add_meta, derive)] #[error("Protocol error: no remote id available")] +#[derive(Clone)] pub struct RemoteEndpointIdError; impl Connection { diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index c1b6c372e76..3f417f5b617 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -38,8 +38,7 @@ use n0_watcher::{self, Watchable, Watcher}; use netwatch::netmon; #[cfg(not(wasm_browser))] use netwatch::{UdpSocket, ip::LocalAddresses}; -use quinn::ServerConfig; -use quinn_proto::PathId; +use quinn::{ServerConfig, WeakConnectionHandle}; use rand::Rng; use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot}; use tokio_util::sync::CancellationToken; @@ -65,7 +64,7 @@ use crate::{ disco::{self, SendAddr}, discovery::{ConcurrentDiscovery, Discovery, EndpointData, UserData}, key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, - magicsock::endpoint_map::{PathAddrList, PathsWatchable}, + magicsock::endpoint_map::PathsWatchable, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, }; @@ -105,6 +104,11 @@ pub(crate) const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(6500); /// Pretty arbitrary and high right now. pub(crate) const MAX_MULTIPATH_PATHS: u32 = 16; +/// Error returned when the endpoint state actor stopped while waiting for a reply. +#[stack_error(derive)] +#[error("endpoint state actor stopped")] +pub(crate) struct EndpointStateActorStoppedError; + /// Contains options for `MagicSock::listen`. #[derive(derive_more::Debug)] pub(crate) struct Options { @@ -270,48 +274,25 @@ impl MagicSock { /// /// The actor is responsible for holepunching and opening additional paths to this /// connection. + /// + /// Returns a future that resolves to [`PathsWatchable`]. + /// + /// The returned future is `'static`, so it can be stored without being liftetime-bound to `&self`. pub(crate) fn register_connection( &self, remote: EndpointId, - conn: &quinn::Connection, - ) -> PathsWatchable { - // Init the open paths watchable. - let mut open_paths: PathAddrList = Default::default(); - if let Some(path0) = conn.path(PathId::ZERO) { - // This all is supposed to be infallible, but anyway. - if let Ok(remote) = path0.remote_address() { - if let Some(remote) = self.endpoint_map.transport_addr_from_mapped(remote) { - open_paths.push((remote.clone(), PathId::ZERO)); - } - } + conn: WeakConnectionHandle, + ) -> impl Future> + Send + 'static + { + let (tx, rx) = oneshot::channel(); + let sender = self.endpoint_map.endpoint_state_actor(remote); + async move { + sender + .send(EndpointStateMessage::AddConnection(conn, tx)) + .await + .map_err(|_| EndpointStateActorStoppedError)?; + rx.await.map_err(|_| EndpointStateActorStoppedError) } - let open_paths = n0_watcher::Watchable::new(open_paths); - - // TODO: Spawning tasks like this is obviously bad. But it is solvable: - // - This is only called from inside Connection::new. - // - Connection::new is called from: - // - impl Future for IncomingFuture - // - impl Future for Connecting - // - Connecting::into_0rtt() - // - // The first two can keep returning Pending until this message is also sent. It'll - // require storing the pinned future but it'll work. - // - // The last one is trickier. But we can make that function async. Or more likely - // we'll end up changing Connecting::into_0rtt() to return a ZrttConnection. Then - // have a ZrttConnection::into_connection() function which can be async and actually - // send this. Before the handshake has completed we don't have anything useful to - // do with this connection inside of the EndpointStateActor anyway. - let weak_handle = conn.weak_handle(); - let (sender, selected_path) = self - .endpoint_map - .endpoint_state_actor_with_selected_path(remote); - let msg = EndpointStateMessage::AddConnection(weak_handle, open_paths.clone()); - - task::spawn(async move { - sender.send(msg).await.ok(); - }); - PathsWatchable::new(open_paths, selected_path) } #[cfg(not(wasm_browser))] diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 82a9852131c..7c4b80d4fc6 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -5,17 +5,16 @@ use std::{ sync::{Arc, Mutex}, }; -use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; +use iroh_base::{EndpointAddr, EndpointId, RelayUrl}; use n0_future::task::{self, AbortOnDropHandle}; -use n0_watcher::Watchable; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::{Instrument, error, info_span, trace, warn}; +use tracing::{Instrument, info_span, trace, warn}; use super::{ DirectAddr, DiscoState, MagicsockMetrics, - mapped_addrs::{AddrMap, EndpointIdMappedAddr, MultipathMappedAddr, RelayMappedAddr}, + mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, transports::{self, OwnedTransmit, TransportsSender}, }; use crate::disco::{self}; @@ -26,9 +25,9 @@ mod endpoint_state; mod path_state; pub(super) use endpoint_state::EndpointStateMessage; +pub(crate) use endpoint_state::PathsWatchable; pub use endpoint_state::{ConnectionType, PathInfo, PathInfoList}; use endpoint_state::{EndpointStateActor, EndpointStateHandle}; -pub(crate) use endpoint_state::{PathAddrList, PathsWatchable}; // TODO: use this // /// Number of endpoints that are inactive for which we keep info about. This limit is enforced @@ -109,21 +108,6 @@ impl EndpointMap { self.endpoint_mapped_addrs.get(&eid) } - /// Converts a mapped address as we use them inside Quinn. - pub(crate) fn transport_addr_from_mapped(&self, mapped: SocketAddr) -> Option { - match MultipathMappedAddr::from(mapped) { - MultipathMappedAddr::Mixed(_) => None, - MultipathMappedAddr::Relay(addr) => match self.relay_mapped_addrs.lookup(&addr) { - Some((url, _)) => Some(TransportAddr::Relay(url)), - None => { - error!("Unknown RelayMappedAddr"); - None - } - }, - MultipathMappedAddr::Ip(addr) => Some(TransportAddr::Ip(addr)), - } - } - /// Returns a [`n0_watcher::Direct`] for given endpoint's [`ConnectionType`]. /// /// # Errors @@ -146,39 +130,9 @@ impl EndpointMap { &self, eid: EndpointId, ) -> mpsc::Sender { - self.endpoint_state_actor_inner(eid, |handle| handle.sender.clone()) - } - - /// Returns the sender and selected path watchable for the [`EndpointStateActor`]. - /// - /// If needed a new actor is started on demand. - /// - /// [`EndpointStateActor`]: endpoint_state::EndpointStateActor - pub(super) fn endpoint_state_actor_with_selected_path( - &self, - eid: EndpointId, - ) -> ( - mpsc::Sender, - Watchable>, - ) { - self.endpoint_state_actor_inner(eid, |handle| { - (handle.sender.clone(), handle.selected_path.clone()) - }) - } - - /// Returns data from the handle to an [`EndpointStateActor`]. - /// - /// If needed a new actor is started on demand. - /// - /// The callback gets a [`EndpointStateHandle`] and can clone out the data to be returned. - fn endpoint_state_actor_inner( - &self, - eid: EndpointId, - f: impl FnOnce(&EndpointStateHandle) -> R, - ) -> R { let mut handles = self.actor_handles.lock().expect("poisoned"); match handles.get(&eid) { - Some(handle) => f(handle), + Some(handle) => handle.sender.clone(), None => { // Create a new EndpointStateActor and insert it into the endpoint map. let sender = self.transports_handle.inbox.clone(); @@ -195,12 +149,12 @@ impl EndpointMap { metrics, ); let handle = actor.start(); - let ret = f(&handle); + let sender = handle.sender.clone(); handles.insert(eid, handle); // Ensure there is a EndpointMappedAddr for this EndpointId. self.endpoint_mapped_addrs.get(&eid); - ret + sender } } } diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 54aec93536d..e5cb923979a 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -179,7 +179,6 @@ impl EndpointStateActor { let (tx, rx) = mpsc::channel(16); let me = self.local_endpoint_id; let endpoint_id = self.endpoint_id; - let selected_path = self.selected_path.clone(); // Ideally we'd use the endpoint span as parent. We'd have to plug that span into // here somehow. Instead we have no parent and explicitly set the me attribute. If @@ -201,7 +200,6 @@ impl EndpointStateActor { ); EndpointStateHandle { sender: tx, - selected_path, _task: AbortOnDropHandle::new(task), } } @@ -274,9 +272,8 @@ impl EndpointStateActor { EndpointStateMessage::SendDatagram(transmit) => { self.handle_msg_send_datagram(transmit).await?; } - EndpointStateMessage::AddConnection(handle, paths_watchable) => { - self.handle_msg_add_connection(handle, paths_watchable) - .await; + EndpointStateMessage::AddConnection(handle, tx) => { + self.handle_msg_add_connection(handle, tx).await; } EndpointStateMessage::AddEndpointAddr(addr, source) => { self.handle_msg_add_endpoint_addr(addr, source); @@ -334,8 +331,9 @@ impl EndpointStateActor { async fn handle_msg_add_connection( &mut self, handle: WeakConnectionHandle, - paths_watchable: Watchable, + tx: oneshot::Sender, ) { + let pub_open_paths = Watchable::default(); if let Some(conn) = handle.upgrade() { // Remove any conflicting stable_ids from the local state. let conn_id = ConnId(conn.stable_id()); @@ -351,7 +349,7 @@ impl EndpointStateActor { .entry(conn_id) .insert_entry(ConnectionState { handle: handle.clone(), - pub_open_paths: paths_watchable, + pub_open_paths: pub_open_paths.clone(), paths: Default::default(), open_paths: Default::default(), path_ids: Default::default(), @@ -395,6 +393,11 @@ impl EndpointStateActor { } self.trigger_holepunching().await; } + tx.send(PathsWatchable { + open_paths: pub_open_paths, + selected_path: self.selected_path.clone(), + }) + .ok(); } /// Handles [`EndpointStateMessage::AddEndpointAddr`]. @@ -993,7 +996,7 @@ pub(crate) enum EndpointStateMessage { /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. #[debug("AddConnection(..)")] - AddConnection(WeakConnectionHandle, Watchable), + AddConnection(WeakConnectionHandle, oneshot::Sender), /// Adds a [`EndpointAddr`] with locations where the endpoint might be reachable. AddEndpointAddr(EndpointAddr, Source), /// Process a received DISCO CallMeMaybe message. @@ -1023,10 +1026,6 @@ pub(crate) enum EndpointStateMessage { pub(super) struct EndpointStateHandle { /// Sender for the channel into the [`EndpointStateActor`]. pub(super) sender: mpsc::Sender, - /// Watchable for the selected transmission path. - /// - /// This should only be read or watched. It is set from within the actro. - pub(super) selected_path: Watchable>, _task: AbortOnDropHandle<()>, } @@ -1158,16 +1157,6 @@ pub(crate) struct PathsWatchable { } impl PathsWatchable { - pub(crate) fn new( - open_paths: Watchable, - selected_path: Watchable>, - ) -> Self { - Self { - open_paths, - selected_path, - } - } - pub(crate) fn watch( &self, conn_handle: WeakConnectionHandle, From 638024621cf2c414f0f3775afb44c6e446263c55 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Tue, 11 Nov 2025 14:05:02 +0100 Subject: [PATCH 129/164] refactor: remove the TransportsSenderActor First step for #3641, the rest can be done once quic holepunching has landed Unfortunately `send_disco_message` also needs the sender, so it can't be fully removed from the `EndpointState` --- Cargo.lock | 29 +++--- Cargo.toml | 2 + iroh/src/magicsock.rs | 3 +- iroh/src/magicsock/endpoint_map.rs | 89 ++----------------- .../magicsock/endpoint_map/endpoint_state.rs | 40 +++++---- iroh/src/magicsock/transports.rs | 4 +- iroh/src/magicsock/transports/ip.rs | 2 +- 7 files changed, 48 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9cf9dca565d..dae53d970a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1896,7 +1896,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2337,7 +2337,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2376,9 +2376,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2815,8 +2815,7 @@ dependencies = [ [[package]] name = "netwatch" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26f2acd376ef48b6c326abf3ba23c449e0cb8aa5c2511d189dd8a8a3bfac889b" +source = "git+https://github.com/n0-computer/net-tools?branch=main#2708e3d7b0a6e1bf3322f71033a4b2002ec2339d" dependencies = [ "atomic-waker", "bytes", @@ -2889,7 +2888,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3358,7 +3357,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3395,9 +3394,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3666,7 +3665,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3789,7 +3788,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.3", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4352,7 +4351,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5124,7 +5123,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a1faa46daa3..7de2e7463e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "main" } + # iroh-quinn = { path = "../iroh-quinn/quinn" } # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 3f417f5b617..11c673c088c 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1011,15 +1011,14 @@ impl Handle { let (disco, disco_receiver) = DiscoState::new(&secret_key); let endpoint_map = { - let sender = transports.create_sender(); EndpointMap::new( secret_key.public(), // #[cfg(any(test, feature = "test-utils"))] // path_selection, metrics.magicsock.clone(), - sender, direct_addrs.addrs.watch(), disco.clone(), + transports.create_sender(), ) }; diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 7c4b80d4fc6..c018e04b70f 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -6,16 +6,15 @@ use std::{ }; use iroh_base::{EndpointAddr, EndpointId, RelayUrl}; -use n0_future::task::{self, AbortOnDropHandle}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::{Instrument, info_span, trace, warn}; +use tracing::warn; use super::{ DirectAddr, DiscoState, MagicsockMetrics, mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, - transports::{self, OwnedTransmit, TransportsSender}, + transports::{self, TransportsSender}, }; use crate::disco::{self}; // #[cfg(any(test, feature = "test-utils"))] @@ -58,10 +57,9 @@ pub(crate) struct EndpointMap { /// The endpoint ID of the local endpoint. local_endpoint_id: EndpointId, metrics: Arc, - /// Handle to an actor that can send over the transports. - transports_handle: TransportsSenderHandle, local_addrs: n0_watcher::Direct>, disco: DiscoState, + sender: TransportsSender, } impl EndpointMap { @@ -71,9 +69,10 @@ impl EndpointMap { // TODO: // #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, metrics: Arc, - sender: TransportsSender, + local_addrs: n0_watcher::Direct>, disco: DiscoState, + sender: TransportsSender, ) -> Self { Self { actor_handles: Mutex::new(FxHashMap::default()), @@ -81,9 +80,9 @@ impl EndpointMap { relay_mapped_addrs: Default::default(), local_endpoint_id, metrics, - transports_handle: TransportsSenderActor::new(sender).start(), local_addrs, disco, + sender, } } @@ -135,18 +134,17 @@ impl EndpointMap { Some(handle) => handle.sender.clone(), None => { // Create a new EndpointStateActor and insert it into the endpoint map. - let sender = self.transports_handle.inbox.clone(); let local_addrs = self.local_addrs.clone(); let disco = self.disco.clone(); let metrics = self.metrics.clone(); let actor = EndpointStateActor::new( eid, self.local_endpoint_id, - sender, local_addrs, disco, self.relay_mapped_addrs.clone(), metrics, + self.sender.clone(), ); let handle = actor.start(); let sender = handle.sender.clone(); @@ -282,79 +280,6 @@ impl IpPort { } } -/// An actor that can send datagrams onto iroh transports. -/// -/// The [`EndpointStateActor`]s want to be able to send datagrams. Because we can not create -/// [`TransportsSender`]s on demand we must share one for the entire [`EndpointMap`], which -/// lives in this actor. -/// -/// [`EndpointStateActor`]: endpoint_state::EndpointStateActor -#[derive(Debug)] -struct TransportsSenderActor { - sender: TransportsSender, -} - -impl TransportsSenderActor { - fn new(sender: TransportsSender) -> Self { - Self { sender } - } - - fn start(self) -> TransportsSenderHandle { - // This actor gets an inbox size of exactly 1. This is the same as if they had the - // underlying sender directly: either you can send or not, or you await until you - // can. No need to introduce extra buffering. - let (tx, rx) = mpsc::channel(1); - - let task = task::spawn( - async move { - self.run(rx).await; - } - .instrument(info_span!("TransportsSenderActor")), - ); - TransportsSenderHandle { - inbox: tx, - _task: AbortOnDropHandle::new(task), - } - } - - async fn run(self, mut inbox: mpsc::Receiver) { - use TransportsSenderMessage::SendDatagram; - - while let Some(SendDatagram(dst, owned_transmit)) = inbox.recv().await { - let transmit = transports::Transmit { - ecn: owned_transmit.ecn, - contents: owned_transmit.contents.as_ref(), - segment_size: owned_transmit.segment_size, - }; - let len = transmit.contents.len(); - match self.sender.send(&dst, None, &transmit).await { - Ok(()) => {} - Err(err) => { - trace!(?dst, %len, "transmit failed to send: {err:#}"); - } - }; - } - trace!("actor terminating"); - } -} - -#[derive(Debug)] -struct TransportsSenderHandle { - inbox: mpsc::Sender, - _task: AbortOnDropHandle<()>, -} - -#[derive(Debug)] -enum TransportsSenderMessage { - SendDatagram(transports::Addr, OwnedTransmit), -} - -impl From<(transports::Addr, OwnedTransmit)> for TransportsSenderMessage { - fn from(source: (transports::Addr, OwnedTransmit)) -> Self { - Self::SendDatagram(source.0, source.1) - } -} - #[cfg(test)] mod tests { diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index e5cb923979a..6eb8030d457 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -7,7 +7,6 @@ use std::{ }; use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; -use n0_error::StdResultExt; use n0_future::{ FuturesUnordered, MergeUnbounded, Stream, StreamExt, task::{self, AbortOnDropHandle}, @@ -23,7 +22,7 @@ use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; -use super::{Source, TransportsSenderMessage, path_state::PathState}; +use super::{Source, path_state::PathState}; // TODO: Use this // #[cfg(any(test, feature = "test-utils"))] // use crate::endpoint::PathSelection; @@ -33,7 +32,7 @@ use crate::{ magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, mapped_addrs::{AddrMap, MappedAddr, RelayMappedAddr}, - transports::{self, OwnedTransmit}, + transports::{self, OwnedTransmit, TransportsSender}, }, util::MaybeFuture, }; @@ -95,10 +94,7 @@ pub(super) struct EndpointStateActor { // /// Metrics. metrics: Arc, - /// Allowing us to directly send datagrams. - /// - /// Used for handling [`EndpointStateMessage::SendDatagram`] messages. - transports_sender: mpsc::Sender, + sender: TransportsSender, /// Our local addresses. /// /// These are our local addresses and any reflexive transport addresses. @@ -149,17 +145,16 @@ impl EndpointStateActor { pub(super) fn new( endpoint_id: EndpointId, local_endpoint_id: EndpointId, - transports_sender: mpsc::Sender, local_addrs: n0_watcher::Direct>, disco: DiscoState, relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, metrics: Arc, + sender: TransportsSender, ) -> Self { Self { endpoint_id, local_endpoint_id, metrics, - transports_sender, local_addrs, relay_mapped_addrs, disco, @@ -172,6 +167,7 @@ impl EndpointStateActor { scheduled_holepunch: None, scheduled_open_path: None, pending_open_paths: VecDeque::new(), + sender, } } @@ -297,26 +293,34 @@ impl EndpointStateActor { Ok(()) } + async fn send_datagram( + &self, + dst: transports::Addr, + owned_transmit: OwnedTransmit, + ) -> n0_error::Result<()> { + let transmit = transports::Transmit { + ecn: owned_transmit.ecn, + contents: owned_transmit.contents.as_ref(), + segment_size: owned_transmit.segment_size, + }; + self.sender.send(&dst, None, &transmit).await?; + Ok(()) + } + /// Handles [`EndpointStateMessage::SendDatagram`]. /// /// Error returns are fatal and kill the actor. async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) -> n0_error::Result<()> { if let Some(addr) = self.selected_path.get() { trace!(?addr, "sending datagram to selected path"); - self.transports_sender - .send((addr, transmit).into()) - .await - .std_context("TransportSenderActor stopped")?; + self.send_datagram(addr, transmit).await?; } else { trace!( paths = ?self.paths.keys().collect::>(), "sending datagram to all known paths", ); for addr in self.paths.keys() { - self.transports_sender - .send((addr.clone(), transmit.clone()).into()) - .await - .std_context("TransportSenerActor stopped")?; + self.send_datagram(addr.clone(), transmit.clone()).await?; } // This message is received *before* a connection is added. So we do // not yet have a connection to holepunch. Instead we trigger @@ -711,7 +715,7 @@ impl EndpointStateActor { transports::Addr::Ip(_) => &self.metrics.send_disco_udp, transports::Addr::Relay(_, _) => &self.metrics.send_disco_relay, }; - match self.transports_sender.send((dst, transmit).into()).await { + match self.send_datagram(dst, transmit).await { Ok(()) => { trace!("sent"); counter.inc(); diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 8cc031109f9..d9be7bbfa97 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -412,7 +412,7 @@ impl Addr { } /// A sender that sends to all our transports. -#[derive(Debug)] +#[derive(Debug, Clone)] pub(crate) struct TransportsSender { #[cfg(not(wasm_browser))] ip: Vec, @@ -602,8 +602,6 @@ impl quinn::AsyncUdpSocket for MagicTransport { /// This is special in that it handles [`MultipathMappedAddr::Mixed`] by delegating to the /// [`MagicSock`] which expands it back to one or more [`Addr`]s and sends it /// using the underlying [`Transports`]. -// TODO: Can I just send the TransportsSender along in the NodeStateMessage::SendDatagram -// message?? That way you don't have to hook up the sender into the NodeMap! #[derive(Debug)] #[pin_project::pin_project] pub(crate) struct MagicSender { diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index a1c2fabad16..147cfcec022 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -132,7 +132,7 @@ impl IpNetworkChangeSender { } } -#[derive(Debug)] +#[derive(Debug, Clone)] #[pin_project] pub(super) struct IpSender { bind_addr: SocketAddr, From d0707e88fada54e6d6e63c8a4b1e2868909bd7ff Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Tue, 11 Nov 2025 14:26:10 +0100 Subject: [PATCH 130/164] chore: fixup deny --- deny.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index cc23a1d1440..1e96919b93e 100644 --- a/deny.toml +++ b/deny.toml @@ -27,4 +27,7 @@ ignore = [ ] [sources] -allow-git = [] +allow-git = [ + "https://github.com/n0-computer/quinn", + "https://github.com/n0-computer/net-tools" +] From 50fdda393a9a58c352cec05c859d603edbbae030 Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Tue, 11 Nov 2025 14:36:22 +0100 Subject: [PATCH 131/164] refactor: disallow certain Source variants to be constructed externally Fixes #3639 --- iroh/src/magicsock/endpoint_map.rs | 25 +++++++++++++++---- .../magicsock/endpoint_map/endpoint_state.rs | 15 ++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index c018e04b70f..a35f3e0aa79 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -209,9 +209,8 @@ impl EndpointMap { /// address through multiple means. #[derive(Serialize, Deserialize, strum::Display, Debug, Clone, Eq, PartialEq, Hash)] #[strum(serialize_all = "kebab-case")] +#[allow(private_interfaces)] pub enum Source { - /// Address was loaded from the fs. - Saved, /// An endpoint communicated with us first via UDP. Udp, /// An endpoint communicated with us first via relay. @@ -231,18 +230,34 @@ pub enum Source { name: String, }, /// The address was advertised by a call-me-maybe DISCO message. - CallMeMaybe, + #[strum(serialize = "CallMeMaybe")] + CallMeMaybe { + /// private marker + _0: Private, + }, /// We received a ping on the path. - Ping, + #[strum(serialize = "Ping")] + Ping { + /// private marker + _0: Private, + }, /// We established a connection on this address. /// /// Currently this means the path was in uses as [`PathId::ZERO`] when the a connection /// was added to the `EndpointStateActor`. /// /// [`PathId::ZERO`]: quinn_proto::PathId::ZERO - Connection, + #[strum(serialize = "Connection")] + Connection { + /// private marker + _0: Private, + }, } +/// Helper to ensure certain `Source` variants can not be constructed externally. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq, Hash)] +struct Private; + /// An (Ip, Port) pair. /// /// NOTE: storing an [`IpPort`] is safer than storing a [`SocketAddr`] because for IPv6 socket diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 6eb8030d457..5c176477c47 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -31,6 +31,7 @@ use crate::{ endpoint::DirectAddr, magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, + endpoint_map::Private, mapped_addrs::{AddrMap, MappedAddr, RelayMappedAddr}, transports::{self, OwnedTransmit, TransportsSender}, }, @@ -378,7 +379,7 @@ impl EndpointStateActor { .entry(path_remote) .or_default() .sources - .insert(Source::Connection, Instant::now()); + .insert(Source::Connection { _0: Private }, Instant::now()); self.select_path(); if path_remote_is_ip { @@ -439,7 +440,8 @@ impl EndpointStateActor { let ping = disco::Ping::new(self.local_endpoint_id); let path = self.paths.entry(dst.clone()).or_default(); - path.sources.insert(Source::CallMeMaybe, now); + path.sources + .insert(Source::CallMeMaybe { _0: Private }, now); path.ping_sent = Some(ping.tx_id); event!( @@ -481,7 +483,8 @@ impl EndpointStateActor { .await; let path = self.paths.entry(src).or_default(); - path.sources.insert(Source::Ping, Instant::now()); + path.sources + .insert(Source::Ping { _0: Private }, Instant::now()); trace!("ping received, triggering holepunching"); self.trigger_holepunching().await; @@ -621,12 +624,12 @@ impl EndpointStateActor { .filter_map(|(addr, state)| { if state .sources - .get(&Source::CallMeMaybe) + .get(&Source::CallMeMaybe { _0: Private }) .map(|when| when.elapsed() <= CALL_ME_MAYBE_VALIDITY) .unwrap_or_default() || state .sources - .get(&Source::Ping) + .get(&Source::Ping { _0: Private }) .map(|when| when.elapsed() <= CALL_ME_MAYBE_VALIDITY) .unwrap_or_default() { @@ -824,7 +827,7 @@ impl EndpointStateActor { .entry(path_remote.clone()) .or_default() .sources - .insert(Source::Connection, Instant::now()); + .insert(Source::Connection { _0: Private }, Instant::now()); } self.select_path(); From 2f924d90f903e029045432d7762db976934ccd8c Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 11 Nov 2025 14:40:34 +0100 Subject: [PATCH 132/164] refactor: remove Endpoint::conn_type (#3647) ## Description * Remove `Endpoint::conn_type` * Update `transfer.rs` example to use `Connection::paths` instead * Change return type of `Connection::paths` to have more guarantees on the return type (Send, Unpin, 'static) ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- iroh/examples/transfer.rs | 45 ++++++++++++++----- iroh/src/endpoint.rs | 29 +----------- iroh/src/endpoint/connection.rs | 2 +- iroh/src/magicsock.rs | 19 +------- iroh/src/magicsock/endpoint_map.rs | 15 +------ .../magicsock/endpoint_map/endpoint_state.rs | 24 +--------- 6 files changed, 38 insertions(+), 96 deletions(-) diff --git a/iroh/examples/transfer.rs b/iroh/examples/transfer.rs index 00cb52c9588..3f76e522bc3 100644 --- a/iroh/examples/transfer.rs +++ b/iroh/examples/transfer.rs @@ -10,16 +10,16 @@ use data_encoding::HEXLOWER; use indicatif::HumanBytes; use iroh::{ Endpoint, EndpointAddr, EndpointId, RelayMap, RelayMode, RelayUrl, SecretKey, TransportAddr, + Watcher, discovery::{ dns::DnsDiscovery, pkarr::{N0_DNS_PKARR_RELAY_PROD, N0_DNS_PKARR_RELAY_STAGING, PkarrPublisher}, }, dns::{DnsResolver, N0_DNS_ENDPOINT_ORIGIN_PROD, N0_DNS_ENDPOINT_ORIGIN_STAGING}, - endpoint::ConnectionError, + endpoint::{ConnectionError, PathInfoList}, }; use n0_error::{Result, StackResultExt, StdResultExt}; use n0_future::task::AbortOnDropHandle; -use n0_watcher::Watcher as _; use tokio_stream::StreamExt; use tracing::{info, warn}; use url::Url; @@ -337,7 +337,6 @@ async fn provide(endpoint: Endpoint, size: u64) -> Result<()> { } }; // spawn a task to handle reading and writing off of the connection - let endpoint_clone = endpoint.clone(); tokio::spawn(async move { let conn = accepting.await.anyerr()?; let endpoint_id = conn.remote_id(); @@ -350,7 +349,7 @@ async fn provide(endpoint: Endpoint, size: u64) -> Result<()> { println!("[{remote}] Connected"); // Spawn a background task that prints connection type changes. Will be aborted on drop. - let _guard = watch_conn_type(&endpoint_clone, endpoint_id); + let _guard = watch_conn_type(conn.remote_id(), conn.paths()); // accept a bi-directional QUIC connection // use the `quinn` APIs to send and recv content @@ -404,7 +403,7 @@ async fn fetch(endpoint: Endpoint, remote_addr: EndpointAddr) -> Result<()> { let conn = endpoint.connect(remote_addr, TRANSFER_ALPN).await?; println!("Connected to {}", remote_id); // Spawn a background task that prints connection type changes. Will be aborted on drop. - let _guard = watch_conn_type(&endpoint, remote_id); + let _guard = watch_conn_type(conn.remote_id(), conn.paths()); // Use the Quinn API to send and recv content. let (mut send, mut recv) = conn.open_bi().await.anyerr()?; @@ -521,14 +520,36 @@ fn parse_byte_size(s: &str) -> std::result::Result { cfg.parse_size(s) } -fn watch_conn_type(endpoint: &Endpoint, endpoint_id: EndpointId) -> AbortOnDropHandle<()> { - let mut stream = endpoint.conn_type(endpoint_id).unwrap().stream(); +fn watch_conn_type( + endpoint_id: EndpointId, + paths_watcher: impl Watcher + Send + Unpin + 'static, +) -> AbortOnDropHandle<()> { + let id = endpoint_id.fmt_short(); let task = tokio::task::spawn(async move { - while let Some(conn_type) = stream.next().await { - println!( - "[{}] Connection type changed to: {conn_type}", - endpoint_id.fmt_short() - ); + let mut stream = paths_watcher.stream(); + let mut previous = None; + while let Some(paths) = stream.next().await { + if let Some(path) = paths.iter().find(|p| p.is_selected()) { + // We can get path updates without the selected path changing. We don't want to log again in that case. + if Some(path) == previous.as_ref() { + continue; + } + println!( + "[{id}] Connection type changed to: {:?} (RTT: {:?})", + path.remote_addr(), + path.rtt() + ); + previous = Some(path.clone()); + } else if !paths.is_empty() { + println!( + "[{id}] Connection type changed to: mixed ({} paths)", + paths.len() + ); + previous = None; + } else { + println!("[{id}] Connection type changed to none (no active transmission paths)",); + previous = None; + } } }); AbortOnDropHandle::new(task) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index d0d55ff5705..d7491aafccb 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -25,7 +25,7 @@ use tracing::{debug, instrument, trace, warn}; use url::Url; pub use super::magicsock::{ - AddEndpointAddrError, ConnectionType, DirectAddr, DirectAddrType, PathInfo, + AddEndpointAddrError, DirectAddr, DirectAddrType, PathInfo, endpoint_map::{PathInfoList, Source}, }; #[cfg(wasm_browser)] @@ -963,33 +963,6 @@ impl Endpoint { // // Partially they return things passed into the builder. - /// Returns a [`Watcher`] that reports the current connection type and any changes for - /// given remote endpoint. - /// - /// This watcher allows observing a stream of [`ConnectionType`] items by calling - /// [`Watcher::stream()`]. If the underlying connection to a remote endpoint changes, it will - /// yield a new item. These connection changes are when the connection switches between - /// using the Relay server and a direct connection. - /// - /// Note that this does not guarantee each connection change is yielded in the stream. - /// If the connection type changes several times before this stream is polled, only the - /// last recorded state is returned. This can be observed e.g. right at the start of a - /// connection when the switch from a relayed to a direct connection can be so fast that - /// the relayed state is never exposed. - /// - /// If there is currently a connection with the remote endpoint, then using [`Watcher::get`] - /// will immediately return either [`ConnectionType::Relay`], [`ConnectionType::Direct`] - /// or [`ConnectionType::Mixed`]. - /// - /// It is possible for the connection type to be [`ConnectionType::None`] if you've - /// recently connected to this endpoint id but previous methods of reaching the endpoint have - /// become inaccessible. - /// - /// Will return `None` if we do not have any address information for the given `endpoint_id`. - pub fn conn_type(&self, endpoint_id: EndpointId) -> Option> { - self.msock.conn_type(endpoint_id) - } - /// Returns the currently lowest latency for this endpoint. /// /// Will return `None` if we do not have any address information for the given `endpoint_id`. diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index dc269a3a551..0998650be61 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -1462,7 +1462,7 @@ impl Connection { /// /// [`PathInfo::is_selected`]: crate::magicsock::PathInfo::is_selected /// [`PathInfo`]: crate::magicsock::PathInfo - pub fn paths(&self) -> impl Watcher { + pub fn paths(&self) -> impl Watcher + Unpin + Send + Sync + 'static { self.paths.watch(self.inner.weak_handle()) } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 11c673c088c..6085aa6f564 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -77,10 +77,7 @@ pub(crate) mod transports; use mapped_addrs::{EndpointIdMappedAddr, MappedAddr}; -pub use self::{ - endpoint_map::{ConnectionType, PathInfo}, - metrics::Metrics, -}; +pub use self::{endpoint_map::PathInfo, metrics::Metrics}; // TODO: Use this // /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically @@ -394,20 +391,6 @@ impl MagicSock { }) } - /// Returns a [`n0_watcher::Direct`] that reports the [`ConnectionType`] we have to the - /// given `endpoint_id`. - /// - /// This gets us a copy of the [`n0_watcher::Direct`] for the [`Watchable`] with a - /// [`ConnectionType`] that the `EndpointMap` stores for each `endpoint_id`'s endpoint. - /// - /// # Errors - /// - /// Will return `None` if there is no address information known about the - /// given `endpoint_id`. - pub(crate) fn conn_type(&self, eid: EndpointId) -> Option> { - self.endpoint_map.conn_type(eid) - } - // TODO: Build better info to expose to the user about remote nodes. We probably want // to expose this as part of path information instead. pub(crate) async fn latency(&self, eid: EndpointId) -> Option { diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index a35f3e0aa79..a0fc9a35bbc 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -25,8 +25,8 @@ mod path_state; pub(super) use endpoint_state::EndpointStateMessage; pub(crate) use endpoint_state::PathsWatchable; -pub use endpoint_state::{ConnectionType, PathInfo, PathInfoList}; use endpoint_state::{EndpointStateActor, EndpointStateHandle}; +pub use endpoint_state::{PathInfo, PathInfoList}; // TODO: use this // /// Number of endpoints that are inactive for which we keep info about. This limit is enforced @@ -107,19 +107,6 @@ impl EndpointMap { self.endpoint_mapped_addrs.get(&eid) } - /// Returns a [`n0_watcher::Direct`] for given endpoint's [`ConnectionType`]. - /// - /// # Errors - /// - /// Will return `None` if there is not an entry in the [`EndpointMap`] for - /// the `endpoint_id` - pub(super) fn conn_type( - &self, - _endpoint_id: EndpointId, - ) -> Option> { - todo!(); - } - /// Returns the sender for the [`EndpointStateActor`]. /// /// If needed a new actor is started on demand. diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 5c176477c47..8f9d129228a 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -16,7 +16,6 @@ use n0_watcher::{Watchable, Watcher}; use quinn::{PathStats, WeakConnectionHandle}; use quinn_proto::{PathError, PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; @@ -1055,27 +1054,6 @@ struct HolepunchAttempt { remote_addrs: BTreeSet, } -/// The type of connection we have to the endpoint. -#[derive(derive_more::Display, Default, Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] -pub enum ConnectionType { - /// Direct UDP connection - #[display("direct({_0})")] - Direct(SocketAddr), - /// Relay connection over relay - #[display("relay({_0})")] - Relay(RelayUrl), - /// Both a UDP and a relay connection are used. - /// - /// This is the case if we do have a UDP address, but are missing a recent confirmation that - /// the address works. - #[display("mixed(udp: {_0}, relay: {_1})")] - Mixed(SocketAddr, RelayUrl), - /// We have no verified connection to this PublicKey - #[default] - #[display("none")] - None, -} - /// Newtype to track Connections. /// /// The wrapped value is the [`quinn::Connection::stable_id`] value, and is thus only valid @@ -1167,7 +1145,7 @@ impl PathsWatchable { pub(crate) fn watch( &self, conn_handle: WeakConnectionHandle, - ) -> impl Watcher { + ) -> impl Watcher + Unpin + Send + Sync + 'static { let joined_watcher = (self.open_paths.watch(), self.selected_path.watch()); joined_watcher.map(move |(open_paths, selected_path)| { let selected_path: Option = selected_path.map(Into::into); From 492b74ea4d43d6245b6b5c822265ec008e38ea22 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 11 Nov 2025 17:49:06 +0100 Subject: [PATCH 133/164] refactor: use boxed watcher, not watchable, on connection (#3632) ## Description Alternative to https://github.com/n0-computer/iroh/pull/3631 Replaces the `Watchable`s for path changes on the `Connection` with a boxed `Watcher`. The watcher is boxed because it would increase the `Connection` struct size significantly otherwise because the mapped-and-joined watcher with a `SmallVec` of `PathInfo` inside is ~600 bytes atm. The benefit of storing a `Watcher` and not a `Watchable` is that the watcher streams now close once the EndpointStateActor drops the state for the connection, which it does after the connection is closed. Also adds a test for path watching, including testing that the streams now close when the connection closes. ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- iroh/src/endpoint/connection.rs | 105 +++++++++++++++++- iroh/src/magicsock.rs | 6 +- iroh/src/magicsock/endpoint_map.rs | 15 ++- .../magicsock/endpoint_map/endpoint_state.rs | 95 ++++++++++------ 4 files changed, 174 insertions(+), 47 deletions(-) diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index 0998650be61..84ea5ef646c 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -43,7 +43,7 @@ use crate::{ discovery::DiscoveryTask, magicsock::{ EndpointStateActorStoppedError, - endpoint_map::{PathInfoList, PathsWatchable}, + endpoint_map::{PathInfoList, PathsWatcher}, }, }; @@ -1213,7 +1213,7 @@ pub struct Connection { inner: quinn::Connection, remote_id: EndpointId, alpn: Vec, - paths: PathsWatchable, + paths: PathsWatcher, } #[allow(missing_docs)] @@ -1463,7 +1463,7 @@ impl Connection { /// [`PathInfo::is_selected`]: crate::magicsock::PathInfo::is_selected /// [`PathInfo`]: crate::magicsock::PathInfo pub fn paths(&self) -> impl Watcher + Unpin + Send + Sync + 'static { - self.paths.watch(self.inner.weak_handle()) + self.paths.clone() } /// Derives keying material from this connection's TLS session secrets. @@ -1511,16 +1511,21 @@ impl Connection { #[cfg(test)] mod tests { + use std::time::Duration; + use iroh_base::{EndpointAddr, SecretKey}; use n0_error::{Result, StackResultExt, StdResultExt}; + use n0_future::StreamExt; + use n0_watcher::Watcher; use rand::SeedableRng; - use tracing::{Instrument, info_span, trace_span}; + use tracing::{Instrument, error_span, info_span, trace_span}; use tracing_test::traced_test; use super::Endpoint; use crate::{ RelayMode, - endpoint::{ConnectOptions, Incoming, ZeroRttStatus}, + endpoint::{ConnectOptions, Incoming, PathInfo, PathInfoList, ZeroRttStatus}, + test_utils::run_relay_server, }; const TEST_ALPN: &[u8] = b"n0/iroh/test"; @@ -1730,4 +1735,94 @@ mod tests { tokio::join!(client.close(), server.close()); Ok(()) } + + #[tokio::test] + async fn test_paths_watcher() -> Result { + tracing_subscriber::fmt::init(); + const ALPN: &[u8] = b"test"; + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let (relay_map, _relay_map, _guard) = run_relay_server().await?; + let server = Endpoint::empty_builder(RelayMode::Custom(relay_map.clone())) + .secret_key(SecretKey::generate(&mut rng)) + .insecure_skip_relay_cert_verify(true) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await?; + + let client = Endpoint::empty_builder(RelayMode::Custom(relay_map.clone())) + .secret_key(SecretKey::generate(&mut rng)) + .insecure_skip_relay_cert_verify(true) + .bind() + .await?; + + server.online().await; + let server_addr = server.addr(); + tracing::info!("server addr: {server_addr:?}"); + + let (conn_client, conn_server) = tokio::join!( + async { client.connect(server_addr, ALPN).await.unwrap() }, + async { server.accept().await.unwrap().await.unwrap() } + ); + tracing::info!("connected"); + let mut paths_client = conn_client.paths().stream(); + let mut paths_server = conn_server.paths().stream(); + + /// Advances the path stream until at least one IP and one relay paths are available. + /// + /// Panics if the path stream finishes before that happens. + async fn wait_for_paths( + stream: &mut n0_watcher::Stream + Unpin>, + ) { + loop { + let paths = stream.next().await.expect("paths stream ended"); + tracing::info!(?paths, "paths"); + if paths.len() >= 2 + && paths.iter().any(PathInfo::is_relay) + && paths.iter().any(PathInfo::is_ip) + { + tracing::info!("break"); + return; + } + } + } + + // Verify that both connections are notified of path changes and get an IP and a relay path. + tokio::join!( + async { + tokio::time::timeout(Duration::from_secs(1), wait_for_paths(&mut paths_server)) + .instrument(error_span!("paths-server")) + .await + .unwrap() + }, + async { + tokio::time::timeout(Duration::from_secs(1), wait_for_paths(&mut paths_client)) + .instrument(error_span!("paths-client")) + .await + .unwrap() + } + ); + + // Close the client connection. + tracing::info!("close client conn"); + conn_client.close(0u32.into(), b""); + + // Verify that the path watch streams close. + assert_eq!( + tokio::time::timeout(Duration::from_secs(1), paths_client.next()) + .await + .unwrap(), + None + ); + assert_eq!( + tokio::time::timeout(Duration::from_secs(1), paths_server.next()) + .await + .unwrap(), + None + ); + + server.close().await; + client.close().await; + + Ok(()) + } } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 6085aa6f564..bc9d5dd9376 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -64,7 +64,7 @@ use crate::{ disco::{self, SendAddr}, discovery::{ConcurrentDiscovery, Discovery, EndpointData, UserData}, key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, - magicsock::endpoint_map::PathsWatchable, + magicsock::endpoint_map::PathsWatcher, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, }; @@ -272,14 +272,14 @@ impl MagicSock { /// The actor is responsible for holepunching and opening additional paths to this /// connection. /// - /// Returns a future that resolves to [`PathsWatchable`]. + /// Returns a future that resolves to [`PathsWatcher`]. /// /// The returned future is `'static`, so it can be stored without being liftetime-bound to `&self`. pub(crate) fn register_connection( &self, remote: EndpointId, conn: WeakConnectionHandle, - ) -> impl Future> + Send + 'static + ) -> impl Future> + Send + 'static { let (tx, rx) = oneshot::channel(); let sender = self.endpoint_map.endpoint_state_actor(remote); diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index a0fc9a35bbc..1e782fab714 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -11,23 +11,22 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::warn; +// #[cfg(any(test, feature = "test-utils"))] +// use crate::endpoint::PathSelection; +pub(super) use self::endpoint_state::EndpointStateMessage; +pub(crate) use self::endpoint_state::PathsWatcher; +use self::endpoint_state::{EndpointStateActor, EndpointStateHandle}; +pub use self::endpoint_state::{PathInfo, PathInfoList}; use super::{ DirectAddr, DiscoState, MagicsockMetrics, mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, transports::{self, TransportsSender}, }; -use crate::disco::{self}; -// #[cfg(any(test, feature = "test-utils"))] -// use crate::endpoint::PathSelection; +use crate::disco; mod endpoint_state; mod path_state; -pub(super) use endpoint_state::EndpointStateMessage; -pub(crate) use endpoint_state::PathsWatchable; -use endpoint_state::{EndpointStateActor, EndpointStateHandle}; -pub use endpoint_state::{PathInfo, PathInfoList}; - // TODO: use this // /// Number of endpoints that are inactive for which we keep info about. This limit is enforced // /// periodically via [`NodeMap::prune_inactive`]. diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index 8f9d129228a..a3aabfd5f93 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -335,7 +335,7 @@ impl EndpointStateActor { async fn handle_msg_add_connection( &mut self, handle: WeakConnectionHandle, - tx: oneshot::Sender, + tx: oneshot::Sender, ) { let pub_open_paths = Watchable::default(); if let Some(conn) = handle.upgrade() { @@ -397,10 +397,11 @@ impl EndpointStateActor { } self.trigger_holepunching().await; } - tx.send(PathsWatchable { - open_paths: pub_open_paths, - selected_path: self.selected_path.clone(), - }) + tx.send(PathsWatcher::new( + pub_open_paths.watch(), + self.selected_path.watch(), + handle, + )) .ok(); } @@ -1002,7 +1003,7 @@ pub(crate) enum EndpointStateMessage { /// needed, any new paths discovered via holepunching will be added. And closed paths /// will be removed etc. #[debug("AddConnection(..)")] - AddConnection(WeakConnectionHandle, oneshot::Sender), + AddConnection(WeakConnectionHandle, oneshot::Sender), /// Adds a [`EndpointAddr`] with locations where the endpoint might be reachable. AddEndpointAddr(EndpointAddr, Source), /// Process a received DISCO CallMeMaybe message. @@ -1128,38 +1129,70 @@ impl ConnectionState { } } -/// Watchables for the open paths and selected transmission path in a connection. +/// Watcher for the open paths and selected transmission path in a connection. /// /// This is stored in the [`Connection`], and the watchables are set from within the endpoint state actor. /// +/// Internally, this contains a boxed-mapped-joined watcher over the open paths in the connection and the +/// selected path to the remote endpoint. The watcher is boxed because the mapped-joined watcher with +/// `SmallVec` has a size of over 800 bytes, which we don't want to put upon the [`Connection`]. +/// /// [`Connection`]: crate::endpoint::Connection -#[derive(Debug, Default, Clone)] -pub(crate) struct PathsWatchable { - /// Watchable for the open paths (in this connection). - open_paths: Watchable, - /// Watchable for the selected transmission path (global for this remote endpoint). - selected_path: Watchable>, +#[derive(Clone, derive_more::Debug)] +#[debug("PathsWatcher")] +#[allow(clippy::type_complexity)] +pub(crate) struct PathsWatcher( + Box< + n0_watcher::Map< + ( + n0_watcher::Direct, + n0_watcher::Direct>, + ), + PathInfoList, + >, + >, +); + +impl n0_watcher::Watcher for PathsWatcher { + type Value = PathInfoList; + + fn get(&mut self) -> Self::Value { + self.0.get() + } + + fn is_connected(&self) -> bool { + self.0.is_connected() + } + + fn poll_updated( + &mut self, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + self.0.poll_updated(cx) + } } -impl PathsWatchable { - pub(crate) fn watch( - &self, +impl PathsWatcher { + fn new( + open_paths: n0_watcher::Direct, + selected_path: n0_watcher::Direct>, conn_handle: WeakConnectionHandle, - ) -> impl Watcher + Unpin + Send + Sync + 'static { - let joined_watcher = (self.open_paths.watch(), self.selected_path.watch()); - joined_watcher.map(move |(open_paths, selected_path)| { - let selected_path: Option = selected_path.map(Into::into); - let Some(conn) = conn_handle.upgrade() else { - return PathInfoList(Default::default()); - }; - let list = open_paths - .into_iter() - .flat_map(move |(remote, path_id)| { - PathInfo::new(path_id, &conn, remote, selected_path.as_ref()) - }) - .collect(); - PathInfoList(list) - }) + ) -> Self { + Self(Box::new(open_paths.or(selected_path).map( + move |(open_paths, selected_path)| { + let selected_path: Option = selected_path.map(Into::into); + let Some(conn) = conn_handle.upgrade() else { + return PathInfoList(Default::default()); + }; + let list = open_paths + .into_iter() + .flat_map(move |(remote, path_id)| { + PathInfo::new(path_id, &conn, remote, selected_path.as_ref()) + }) + .collect(); + PathInfoList(list) + }, + ))) } } From 25fe805979806b8102ab5b00e98ce96e6e0ab1c7 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Wed, 12 Nov 2025 08:57:49 +0100 Subject: [PATCH 134/164] refactor(multipath): Stop inactive endpoint actors (#3643) ## Description Fixes #3638 (partially) This is a first, small solution to stop inactive endpoint actors after an idle timeout. I implemented it such that the *actor* decides once to stop, while making sure that we *never* create senders to actors that are shutting down. Logic in the actor: * The actor enters an idle timeout (set to 60 seconds) once it has no active connections, an empty inbox, and no inbox senders * Once the timeout expires, it is rechecked that the idle conditions hold, and if so the actor exits * Once any of the idle conditions don't hold anymore, the idle timeout is deactivated and restarted once the conditions are met again The actor checks if the inbox's sender strong count equals 1, which means that no senders exist apart from the one held in the endpoint map. This check is protected with a mutex, to enter a critical section for closing the inbox while the lock is held in case the conditions are met. This is to ensure that there cannot be a race condition where a sender is cloned out right after the check in the actor returns true, but before the inbox is closed. Logic in the endpoint map: * When handing out senders, we acquire the shared lock, and check that the channel is not closed while the lock is held. This ensures that the actor never closes while a sender is alive. If the actor is closed, we remove the handle to the dead actor and create a new actor. * On regular intervals (set to 60 seconds) the magicsock actor removes handles to dead actors. ## Breaking Changes ## Notes & open questions * I *think* my logic around the critical section and ensuring that we never close the actor while senders exist is sound. However, it needs careful review and tests. I'll do some thinking on how to best test this. * Instead of employing an interval to remove dead actor handles, we could use a channel where the actor informs an outside-task which endpoint actors terminated, so that the outside-task can then lock the endpoint map and remove just those. Not sure if that's worth it. * Another solution here might be to spawn the actor tasks into a join set in the magicsock actor. However this would need further refactoring and would likely make spawning actors async. I think I'd prefer to keep that sync because it makes the surrounding code a lot simpler. * This does not yet implement some of the more advanced reasoning that #3638 proposes. I think we should start with something simple that prevents memory exhaustion and tweak as needed. However, it could also be argued that we should start with a more featureful design right away. ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- iroh/src/magicsock.rs | 6 ++ iroh/src/magicsock/endpoint_map.rs | 74 ++++++++++++------ .../magicsock/endpoint_map/endpoint_state.rs | 51 ++++++++++-- .../endpoint_state/guarded_channel.rs | 78 +++++++++++++++++++ 4 files changed, 178 insertions(+), 31 deletions(-) create mode 100644 iroh/src/magicsock/endpoint_map/endpoint_state/guarded_channel.rs diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index bc9d5dd9376..38eeed85c50 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1388,6 +1388,9 @@ impl Actor { // ensure we are doing an initial publish of our addresses self.msock.publish_my_addr(); + // Interval timer to remove closed `EndpointStateActor` handles from the endpoint map. + let mut endpoint_map_gc = time::interval(endpoint_map::ENDPOINT_MAP_GC_INTERVAL); + loop { self.msock.metrics.magicsock.actor_tick_main.inc(); #[cfg(not(wasm_browser))] @@ -1499,6 +1502,9 @@ impl Actor { warn!(%dst, endpoint = %dst_key.fmt_short(), ?err, "failed to send disco message (UDP)"); } } + _ = endpoint_map_gc.tick() => { + self.msock.endpoint_map.remove_closed_endpoint_state_actors(); + } } } } diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 1e782fab714..30fc8ac4e2f 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -1,8 +1,9 @@ use std::{ - collections::BTreeSet, + collections::{BTreeSet, hash_map}, hash::Hash, net::{IpAddr, SocketAddr}, sync::{Arc, Mutex}, + time::Duration, }; use iroh_base::{EndpointAddr, EndpointId, RelayUrl}; @@ -27,6 +28,9 @@ use crate::disco; mod endpoint_state; mod path_state; +/// Interval in which handles to closed [`EndpointStateActor`]s should be removed. +pub(super) const ENDPOINT_MAP_GC_INTERVAL: Duration = Duration::from_secs(60); + // TODO: use this // /// Number of endpoints that are inactive for which we keep info about. This limit is enforced // /// periodically via [`NodeMap::prune_inactive`]. @@ -106,6 +110,15 @@ impl EndpointMap { self.endpoint_mapped_addrs.get(&eid) } + /// Removes the handles for terminated [`EndpointStateActor`]s from the endpoint map. + /// + /// This should be called periodically to remove handles to endpoint state actors + /// that have shutdown after their idle timeout expired. + pub(super) fn remove_closed_endpoint_state_actors(&self) { + let mut handles = self.actor_handles.lock().expect("poisoned"); + handles.retain(|_eid, handle| !handle.sender.is_closed()) + } + /// Returns the sender for the [`EndpointStateActor`]. /// /// If needed a new actor is started on demand. @@ -116,33 +129,48 @@ impl EndpointMap { eid: EndpointId, ) -> mpsc::Sender { let mut handles = self.actor_handles.lock().expect("poisoned"); - match handles.get(&eid) { - Some(handle) => handle.sender.clone(), - None => { - // Create a new EndpointStateActor and insert it into the endpoint map. - let local_addrs = self.local_addrs.clone(); - let disco = self.disco.clone(); - let metrics = self.metrics.clone(); - let actor = EndpointStateActor::new( - eid, - self.local_endpoint_id, - local_addrs, - disco, - self.relay_mapped_addrs.clone(), - metrics, - self.sender.clone(), - ); - let handle = actor.start(); - let sender = handle.sender.clone(); - handles.insert(eid, handle); - - // Ensure there is a EndpointMappedAddr for this EndpointId. - self.endpoint_mapped_addrs.get(&eid); + match handles.entry(eid) { + hash_map::Entry::Occupied(mut entry) => { + if let Some(sender) = entry.get().sender.get() { + sender + } else { + // The actor is dead: Start a new actor. + let (handle, sender) = self.start_endpoint_state_actor(eid); + entry.insert(handle); + sender + } + } + hash_map::Entry::Vacant(entry) => { + let (handle, sender) = self.start_endpoint_state_actor(eid); + entry.insert(handle); sender } } } + /// Starts a new endpoint state actor and returns a handle and a sender. + /// + /// The handle is not inserted into the endpoint map, this must be done by the caller of this function. + fn start_endpoint_state_actor( + &self, + eid: EndpointId, + ) -> (EndpointStateHandle, mpsc::Sender) { + // Ensure there is a EndpointMappedAddr for this EndpointId. + self.endpoint_mapped_addrs.get(&eid); + let handle = EndpointStateActor::new( + eid, + self.local_endpoint_id, + self.local_addrs.clone(), + self.disco.clone(), + self.relay_mapped_addrs.clone(), + self.metrics.clone(), + self.sender.clone(), + ) + .start(); + let sender = handle.sender.get().expect("just created"); + (handle, sender) + } + pub(super) fn handle_ping(&self, msg: disco::Ping, sender: EndpointId, src: transports::Addr) { if msg.endpoint_key != sender { warn!("DISCO Ping EndpointId mismatch, ignoring ping"); diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index a3aabfd5f93..d40d6fea777 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -17,14 +17,12 @@ use quinn::{PathStats, WeakConnectionHandle}; use quinn_proto::{PathError, PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; use smallvec::SmallVec; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::oneshot; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; +use self::guarded_channel::{GuardedReceiver, GuardedSender, guarded_channel}; use super::{Source, path_state::PathState}; -// TODO: Use this -// #[cfg(any(test, feature = "test-utils"))] -// use crate::endpoint::PathSelection; use crate::{ disco::{self}, endpoint::DirectAddr, @@ -37,6 +35,12 @@ use crate::{ util::MaybeFuture, }; +// TODO: Use this +// #[cfg(any(test, feature = "test-utils"))] +// use crate::endpoint::PathSelection; + +mod guarded_channel; + // TODO: use this // /// Number of addresses that are not active that we keep around per endpoint. // /// @@ -67,6 +71,14 @@ use crate::{ // TODO: Quinn should just do this. Also, I made this value up. const APPLICATION_ABANDON_PATH: u8 = 30; +/// The time after which an idle [`EndpointStateActor`] stops. +/// +/// The actor only enters the idle state if no connections are active and no inbox senders exist +/// apart from the one stored in the endpoint map. Stopping and restarting the actor in this state +/// is not an issue; a timeout here serves the purpose of not stopping-and-recreating actors +/// in a high frequency, and to keep data about previous path around for subsequent connections. +const ACTOR_MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(60); + /// A stream of events from all paths for all connections. /// /// The connection is identified using [`ConnId`]. The event `Err` variant happens when the @@ -172,7 +184,7 @@ impl EndpointStateActor { } pub(super) fn start(mut self) -> EndpointStateHandle { - let (tx, rx) = mpsc::channel(16); + let (tx, rx) = guarded_channel(16); let me = self.local_endpoint_id; let endpoint_id = self.endpoint_id; @@ -207,9 +219,11 @@ impl EndpointStateActor { /// discipline is needed to not turn pending for a long time. async fn run( &mut self, - mut inbox: mpsc::Receiver, + mut inbox: GuardedReceiver, ) -> n0_error::Result<()> { trace!("actor started"); + let idle_timeout = MaybeFuture::None; + tokio::pin!(idle_timeout); loop { let scheduled_path_open = match self.scheduled_open_path { Some(when) => MaybeFuture::Some(time::sleep_until(when)), @@ -252,6 +266,22 @@ impl EndpointStateActor { self.scheduled_holepunch = None; self.trigger_holepunching().await; } + _ = &mut idle_timeout => { + if self.connections.is_empty() && inbox.close_if_idle() { + trace!("idle timeout expired and still idle: terminate actor"); + break; + } + } + } + + if self.connections.is_empty() && inbox.is_idle() && idle_timeout.is_none() { + trace!("start idle timeout"); + idle_timeout + .as_mut() + .set_future(time::sleep(ACTOR_MAX_IDLE_TIMEOUT)); + } else if idle_timeout.is_some() { + trace!("abort idle timeout"); + idle_timeout.as_mut().set_none() } } trace!("actor terminating"); @@ -1028,11 +1058,16 @@ pub(crate) enum EndpointStateMessage { /// A handle to a [`EndpointStateActor`]. /// -/// Dropping this will stop the actor. +/// Dropping this will stop the actor. The actor will also stop after an idle timeout +/// if it has no connections, an empty inbox, and no other senders than the one stored +/// in the endpoint map exist. #[derive(Debug)] pub(super) struct EndpointStateHandle { /// Sender for the channel into the [`EndpointStateActor`]. - pub(super) sender: mpsc::Sender, + /// + /// This is a [`GuardedSender`], from which we can get a sender but only if the receiver + /// hasn't been closed. + pub(super) sender: GuardedSender, _task: AbortOnDropHandle<()>, } diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state/guarded_channel.rs b/iroh/src/magicsock/endpoint_map/endpoint_state/guarded_channel.rs new file mode 100644 index 00000000000..2b3b3b76441 --- /dev/null +++ b/iroh/src/magicsock/endpoint_map/endpoint_state/guarded_channel.rs @@ -0,0 +1,78 @@ +use std::sync::{Arc, Mutex}; + +use tokio::sync::mpsc; + +/// Creates a new [`mpsc`] channel where the receiver can only close if there are no active senders. +pub(super) fn guarded_channel(cap: usize) -> (GuardedSender, GuardedReceiver) { + let (tx, rx) = mpsc::channel(cap); + let tx = Arc::new(Mutex::new(Some(tx))); + (GuardedSender { tx: tx.clone() }, GuardedReceiver { tx, rx }) +} + +#[derive(Debug)] +pub(crate) struct GuardedSender { + tx: Arc>>>, +} + +impl GuardedSender { + /// Returns a sender to the channel. + /// + /// Returns a new sender if the channel is not closed. It is guaranteed that + /// [`GuardedReceiver::close_if_idle`] will not return `true` until the sender is dropped. + /// Returns `None` if the channel has been closed. + pub(crate) fn get(&self) -> Option> { + self.tx.lock().expect("poisoned").clone() + } + + /// Returns `true` if the channel has been closed. + pub(crate) fn is_closed(&self) -> bool { + self.tx.lock().expect("poisoned").is_none() + } +} + +#[derive(Debug)] +pub(super) struct GuardedReceiver { + rx: mpsc::Receiver, + tx: Arc>>>, +} + +impl GuardedReceiver { + /// Receives the next value for this receiver. + /// + /// See [`mpsc::Receiver::recv`]. + pub(super) async fn recv(&mut self) -> Option { + self.rx.recv().await + } + + /// Returns `true` if the inbox is empty and no senders to the inbox exist. + pub(super) fn is_idle(&self) -> bool { + self.rx.is_empty() && self.rx.sender_strong_count() <= 1 + } + + /// Closes the channel if the channel is idle. + /// + /// Returns `true` if the channel is idle and has now been closed, and `false` if the channel + /// is not idle and therefore has not been not closed. + /// + /// Uses a lock internally to make sure that there cannot be a race condition between + /// calling this and a new sender being created. + pub(super) fn close_if_idle(&mut self) -> bool { + let mut guard = self.tx.lock().expect("poisoned"); + if self.is_idle() { + *guard = None; + self.rx.close(); + true + } else { + false + } + } +} + +impl Drop for GuardedReceiver { + fn drop(&mut self) { + let mut guard = self.tx.lock().expect("poisoned"); + *guard = None; + self.rx.close(); + drop(guard) + } +} From e7bf47dfcf49319f1395d984cf3df4fae41f349f Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Wed, 12 Nov 2025 09:49:14 +0100 Subject: [PATCH 135/164] Bump quinn --- Cargo.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dae53d970a9..bf83ac230ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1896,7 +1896,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2328,7 +2328,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#dced59ee0ae406fa4445136c33c598448e0e738f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#3b3a5e8ec6f68cf8242b182f1ae58a6d65aa9449" dependencies = [ "bytes", "cfg_aliases", @@ -2337,7 +2337,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#dced59ee0ae406fa4445136c33c598448e0e738f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#3b3a5e8ec6f68cf8242b182f1ae58a6d65aa9449" dependencies = [ "bytes", "fastbloom", @@ -2371,14 +2371,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#dced59ee0ae406fa4445136c33c598448e0e738f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#3b3a5e8ec6f68cf8242b182f1ae58a6d65aa9449" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2888,7 +2888,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3357,7 +3357,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -3394,9 +3394,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3665,7 +3665,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3788,7 +3788,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.3", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4351,7 +4351,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5123,7 +5123,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] From 4b6824c8a72e5d7efe5f3a14fcc7214a5b27bcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Wed, 12 Nov 2025 13:33:04 +0100 Subject: [PATCH 136/164] fix(iroh): Clear `EndpointStateActor::selected_path` once the last connection closes (#3650) ## Description Addresses #3602 for multipath. This implements the first bullet point from @flub's suggestion in the above issue: We clear the `selected_path` once the last known connection to an endpoint closes. This means that a new connection attempt after that will instead send to all addresses again, and avoids the case where we send on e.g. the old port from a previous restart of the endpoint we're connecting to. This turns the `test_0rtt_after_server_restart` test green. ## Notes & open questions I *think* this will still fail if we have an "open" connection to the server that's in the process of timing out and we open a new connection to the restarted server while that's happening. I'm not sure though. ## Change checklist - [x] Self-review. --- iroh/src/magicsock/endpoint_map/endpoint_state.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index d40d6fea777..aa70a0cae77 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -248,6 +248,10 @@ impl EndpointStateActor { } Some(conn_id) = self.connections_close.next(), if !self.connections_close.is_empty() => { self.connections.remove(&conn_id); + if self.connections.is_empty() { + trace!("last connection closed - clearing selected_path"); + self.selected_path.set(None).ok(); + } } _ = self.local_addrs.updated() => { trace!("local addrs updated, triggering holepunching"); From 8d819f08d1e7fb7ea0dd7d98f0657c42b66d4c87 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Thu, 13 Nov 2025 19:15:30 +0100 Subject: [PATCH 137/164] perf: various improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Philipp Krüger --- Cargo.lock | 6 +-- Cargo.toml | 7 ++++ iroh/Cargo.toml | 2 +- iroh/src/magicsock.rs | 37 ++++++++++--------- .../magicsock/endpoint_map/endpoint_state.rs | 14 ++++--- iroh/src/magicsock/transports.rs | 26 ++++++------- 6 files changed, 52 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf83ac230ea..e46335a8ef5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2738,9 +2738,9 @@ dependencies = [ [[package]] name = "n0-watcher" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38acf13c1ddafc60eb7316d52213467f8ccb70b6f02b65e7d97f7799b1f50be4" +checksum = "ba717c22ceec021ace0ff7674bf8fd60c9394605740a8201678fc1cb3a7398f6" dependencies = [ "derive_more 2.0.1", "n0-error", @@ -2815,7 +2815,7 @@ dependencies = [ [[package]] name = "netwatch" version = "0.12.0" -source = "git+https://github.com/n0-computer/net-tools?branch=main#2708e3d7b0a6e1bf3322f71033a4b2002ec2339d" +source = "git+https://github.com/n0-computer/net-tools?branch=main#0721bbb6a2c4dd487a378d6ef0f56387680649d1" dependencies = [ "atomic-waker", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 7de2e7463e4..89721a6363d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,3 +52,10 @@ netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "main" } # iroh-quinn = { path = "../iroh-quinn/quinn" } # iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } # iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } + + +[patch."https://github.com/n0-computer/quinn"] + +# iroh-quinn = { path = "../iroh-quinn/quinn" } +# iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } +# iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index 558ff278d00..aaa00b80397 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -33,7 +33,7 @@ iroh-base = { version = "0.95.1", default-features = false, features = ["key", " iroh-relay = { version = "0.95", path = "../iroh-relay", default-features = false } n0-future = "0.3.0" n0-error = "0.1.0" -n0-watcher = "0.5" +n0-watcher = "0.6" netwatch = { version = "0.12" } pin-project = "1" pkarr = { version = "5", default-features = false, features = ["relays"] } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 38eeed85c50..2a4f6e66029 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -75,9 +75,11 @@ pub(crate) mod endpoint_map; pub(crate) mod mapped_addrs; pub(crate) mod transports; -use mapped_addrs::{EndpointIdMappedAddr, MappedAddr}; - pub use self::{endpoint_map::PathInfo, metrics::Metrics}; +use self::{ + mapped_addrs::{EndpointIdMappedAddr, MappedAddr}, + transports::Addr, +}; // TODO: Use this // /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically @@ -489,18 +491,18 @@ impl MagicSock { #[cfg_attr(windows, allow(dead_code))] fn normalized_local_addr(&self) -> io::Result { - let addrs = self.local_addrs_watch.clone().get(); + let addrs = self.local_addrs_watch.peek(); let mut ipv4_addr = None; for addr in addrs { - let Some(addr) = addr.into_socket_addr() else { - continue; - }; - if addr.is_ipv6() { - return Ok(addr); - } - if addr.is_ipv4() && ipv4_addr.is_none() { - ipv4_addr.replace(addr); + match addr { + Addr::Ip(addr @ SocketAddr::V6(_)) => { + return Ok(*addr); + } + Addr::Ip(addr @ SocketAddr::V4(_)) if ipv4_addr.is_none() => { + ipv4_addr.replace(*addr); + } + _ => {} } } match ipv4_addr { @@ -550,11 +552,12 @@ impl MagicSock { let mut quic_packets_total = 0; - for ((quinn_meta, buf), source_addr) in metas - .iter_mut() - .zip(bufs.iter_mut()) - .zip(source_addrs.iter()) - { + // zip is slow :( + for i in 0..metas.len() { + let quinn_meta = &mut metas[i]; + let buf = &mut bufs[i]; + let source_addr = &source_addrs[i]; + let mut buf_contains_quic_datagrams = false; let mut quic_datagram_count = 0; if quinn_meta.len > quinn_meta.stride { @@ -578,11 +581,9 @@ impl MagicSock { // relies on quinn::EndpointConfig::grease_quic_bit being set to `false`, // which we do in Endpoint::bind. if let Some((sender, sealed_box)) = disco::source_and_box(datagram) { - trace!(src = ?source_addr, len = datagram.len(), "UDP recv: DISCO packet"); self.handle_disco_message(sender, sealed_box, source_addr); datagram[0] = 0u8; } else { - trace!(src = ?source_addr, len = datagram.len(), "UDP recv: QUIC packet"); match source_addr { transports::Addr::Ip(SocketAddr::V4(..)) => { self.metrics diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index aa70a0cae77..f51d18956c5 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -1183,10 +1183,10 @@ impl ConnectionState { pub(crate) struct PathsWatcher( Box< n0_watcher::Map< - ( + n0_watcher::Tuple< n0_watcher::Direct, n0_watcher::Direct>, - ), + >, PathInfoList, >, >, @@ -1195,8 +1195,12 @@ pub(crate) struct PathsWatcher( impl n0_watcher::Watcher for PathsWatcher { type Value = PathInfoList; - fn get(&mut self) -> Self::Value { - self.0.get() + fn update(&mut self) -> bool { + self.0.update() + } + + fn peek(&self) -> &Self::Value { + self.0.peek() } fn is_connected(&self) -> bool { @@ -1206,7 +1210,7 @@ impl n0_watcher::Watcher for PathsWatcher { fn poll_updated( &mut self, cx: &mut std::task::Context<'_>, - ) -> Poll> { + ) -> Poll> { self.0.poll_updated(cx) } } diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index d9be7bbfa97..5d108b31b85 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -3,7 +3,7 @@ use std::{ io::{self, IoSliceMut}, net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, pin::Pin, - sync::{Arc, atomic::AtomicUsize}, + sync::Arc, task::{Context, Poll}, }; @@ -34,18 +34,20 @@ pub(crate) struct Transports { ip: Vec, relay: Vec, - poll_recv_counter: AtomicUsize, + poll_recv_counter: usize, + /// Cache for source addrs, to speed up access + source_addrs: [Addr; quinn_udp::BATCH_SIZE], } #[cfg(not(wasm_browser))] pub(crate) type LocalAddrsWatch = n0_watcher::Map< - ( + n0_watcher::Tuple< n0_watcher::Join>, n0_watcher::Join< Option<(RelayUrl, EndpointId)>, n0_watcher::Map>, Option<(RelayUrl, EndpointId)>>, >, - ), + >, Vec, >; @@ -69,6 +71,7 @@ impl Transports { ip, relay, poll_recv_counter: Default::default(), + source_addrs: Default::default(), } } @@ -80,15 +83,15 @@ impl Transports { msock: &MagicSock, ) -> Poll> { debug_assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); + debug_assert!(bufs.len() <= quinn_udp::BATCH_SIZE, "too many buffers"); if msock.is_closing() { return Poll::Pending; } - let mut source_addrs = vec![Addr::default(); metas.len()]; - match self.inner_poll_recv(cx, bufs, metas, &mut source_addrs)? { + match self.inner_poll_recv(cx, bufs, metas)? { Poll::Pending | Poll::Ready(0) => Poll::Pending, Poll::Ready(n) => { - msock.process_datagrams(&mut bufs[..n], &mut metas[..n], &source_addrs[..n]); + msock.process_datagrams(&mut bufs[..n], &mut metas[..n], &self.source_addrs[..n]); Poll::Ready(Ok(n)) } } @@ -100,13 +103,12 @@ impl Transports { cx: &mut Context, bufs: &mut [IoSliceMut<'_>], metas: &mut [quinn_udp::RecvMeta], - source_addrs: &mut [Addr], ) -> Poll> { debug_assert_eq!(bufs.len(), metas.len(), "non matching bufs & metas"); macro_rules! poll_transport { ($socket:expr) => { - match $socket.poll_recv(cx, bufs, metas, source_addrs)? { + match $socket.poll_recv(cx, bufs, metas, &mut self.source_addrs)? { Poll::Pending | Poll::Ready(0) => {} Poll::Ready(n) => { return Poll::Ready(Ok(n)); @@ -117,9 +119,7 @@ impl Transports { // To improve fairness, every other call reverses the ordering of polling. - let counter = self - .poll_recv_counter - .fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let counter = self.poll_recv_counter.wrapping_add(1); if counter % 2 == 0 { #[cfg(not(wasm_browser))] @@ -156,7 +156,7 @@ impl Transports { let ips = n0_watcher::Join::new(self.ip.iter().map(|t| t.local_addr_watch())); let relays = n0_watcher::Join::new(self.relay.iter().map(|t| t.local_addr_watch())); - (ips, relays).map(|(ips, relays)| { + ips.or(relays).map(|(ips, relays)| { ips.into_iter() .map(Addr::from) .chain( From 3745e7ec6d3e59cf5ecf9b84711a3a3af44ec08b Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 14 Nov 2025 10:46:04 +0100 Subject: [PATCH 138/164] bump quinn --- Cargo.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e46335a8ef5..cfd0a86e34d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1896,7 +1896,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2328,7 +2328,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#3b3a5e8ec6f68cf8242b182f1ae58a6d65aa9449" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#246779c6f8a704db8a7e291257bb099cbf6a8eb6" dependencies = [ "bytes", "cfg_aliases", @@ -2337,7 +2337,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2348,7 +2348,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#3b3a5e8ec6f68cf8242b182f1ae58a6d65aa9449" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#246779c6f8a704db8a7e291257bb099cbf6a8eb6" dependencies = [ "bytes", "fastbloom", @@ -2371,14 +2371,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#3b3a5e8ec6f68cf8242b182f1ae58a6d65aa9449" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#246779c6f8a704db8a7e291257bb099cbf6a8eb6" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -2888,7 +2888,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3357,7 +3357,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3394,9 +3394,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3665,7 +3665,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3788,7 +3788,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.3", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4351,7 +4351,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5123,7 +5123,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] From dab9d5f229c558a6faf1f331b4a02de3500c3aa2 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 14 Nov 2025 11:29:03 +0100 Subject: [PATCH 139/164] fix(iroh)!: Correct the error structure (#3663) ## Description For some odd reason AuthenticationError was given a branch that had quinn::ConnectionError inside it. But logically the connection error has nothing to do with authentication. That should have been a red flag. AuthenticationError itself is almost always wrapped in ConnectingError, which does correctly have a quinn::ConnectionError branch. And the few places where it was directly returned to the user it arguably **should** have been wrapped in a ConnectingError. The result of this is that before this fix you would get a very confusing authentication error if the remote client closed the connection right at the same time as the handshake completed for it (yes, this is difficult to do at the right time, and it only happens for the client since that completes the handshake one network hop before the server). But this was no authentication error, it is simply a closed connection. The new error structure captures this correctly. Similarly the InternalConsistencyError belongs on the ConnectingError. Though that one should be impossible to produce since it's supposed to be an invariant. ## Breaking Changes - `AuthenticationError` loses the `ConnectionError` and `InternalConsistencyError` branches. Both are on the `ConnectingError` instead. - `OutgoingZeroRttConnection::handshake_completed` and `IncomingZeroRttConnection` now return a `ConnectingError` instead of `AuthenticationError`. ## Notes & open questions While I've managed to trigger this error somewhat occasionally using a program that races the closing with the completed handshake in a very tight loop **before** applying this fix. I'm completely failing to trigger it since applying this fix, so I can admire the beautiful new error reporting this fix should give. It's a bit confusing. **edit**: it **is** confusing. But it is correct. Because my flaky failure does always close the connection *after* it completes the handshake. So ALPN and EndpointId are always available. And then you yield a valid `Connection` when awaiting an `Incoming`, it just is already closed. This fix could also be made against main. I believe the same commit should be able to be cherry-picked and will probably apply fairly clean. Do you think I should make it against main? ## Change checklist - [x] Self-review. - [x] All breaking changes documented. - [x] List all breaking changes in the above "Breaking Changes" section. --- iroh/src/endpoint/connection.rs | 75 +++++++++++++++++---------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index 84ea5ef646c..f4ec9c65b17 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -206,33 +206,19 @@ pub enum AuthenticationError { RemoteId { source: RemoteEndpointIdError }, #[error("no ALPN provided")] NoAlpn {}, - #[error(transparent)] - ConnectionError { - #[error(std_err)] - source: ConnectionError, - }, - #[error("internal consistency error: EndpointStateActor stopped")] - InternalConsistencyError, -} - -impl From for AuthenticationError { - #[track_caller] - fn from(_value: EndpointStateActorStoppedError) -> Self { - e!(Self::InternalConsistencyError) - } } impl From for ConnectingError { #[track_caller] fn from(_value: EndpointStateActorStoppedError) -> Self { - e!(AuthenticationError::InternalConsistencyError).into() + e!(Self::InternalConsistencyError) } } /// Converts a `quinn::Connection` to a `Connection`. /// -/// Returns a [`AuthenticationError`] if the handshake data has not completed, -/// or if no alpn was set by the remote node. +/// Returns an error if there was a connection error, the handshake data has not completed +/// or if the remote did not set an ALPN. /// /// Otherwise returns a future that completes once the connection has been registered with the /// magicsock. This future can return an [`EndpointStateActorStoppedError`], which will only be @@ -244,13 +230,21 @@ fn conn_from_quinn_conn( ep: &Endpoint, ) -> Result< impl Future> + Send + 'static, - AuthenticationError, + ConnectingError, > { - if let Some(reason) = conn.close_reason() { - return Err(e!(AuthenticationError::ConnectionError { source: reason })); - } - let remote_id = remote_id_from_quinn_conn(&conn)?; - let alpn = alpn_from_quinn_conn(&conn).ok_or_else(|| e!(AuthenticationError::NoAlpn))?; + let (remote_id, alpn) = match static_info_from_conn(&conn) { + Ok(val) => val, + Err(auth_err) => { + // If the authentication error raced with a connection error, the connection + // error wins. + if let Some(conn_err) = conn.close_reason() { + return Err(e!(ConnectingError::ConnectionError { source: conn_err })); + } else { + return Err(e!(ConnectingError::HandshakeFailure { source: auth_err })); + } + } + }; + // Register this connection with the magicsock. let fut = ep.msock.register_connection(remote_id, conn.weak_handle()); Ok(async move { @@ -264,6 +258,14 @@ fn conn_from_quinn_conn( }) } +fn static_info_from_conn( + conn: &quinn::Connection, +) -> Result<(EndpointId, Vec), AuthenticationError> { + let remote_id = remote_id_from_quinn_conn(conn)?; + let alpn = alpn_from_quinn_conn(conn).ok_or_else(|| e!(AuthenticationError::NoAlpn))?; + Ok((remote_id, alpn)) +} + /// Returns the [`EndpointId`] from the peer's TLS certificate. /// /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is @@ -359,6 +361,7 @@ pub enum AlpnError { #[stack_error(add_meta, derive, from_sources)] #[allow(missing_docs)] #[non_exhaustive] +#[derive(Clone)] pub enum ConnectingError { #[error(transparent)] ConnectionError { @@ -367,6 +370,8 @@ pub enum ConnectingError { }, #[error("Failure finalizing the handshake")] HandshakeFailure { source: AuthenticationError }, + #[error("internal consistency error: EndpointStateActor stopped")] + InternalConsistencyError, } impl Connecting { @@ -470,10 +475,8 @@ impl Future for Connecting { return fut.poll_unpin(cx).map_err(Into::into); } else { let quinn_conn = std::task::ready!(self.inner.poll_unpin(cx)?); - match conn_from_quinn_conn(quinn_conn, &self.ep) { - Err(err) => return Poll::Ready(Err(err.into())), - Ok(fut) => self.register_with_magicsock = Some(Box::pin(fut.err_into())), - }; + let fut = conn_from_quinn_conn(quinn_conn, &self.ep)?; + self.register_with_magicsock = Some(Box::pin(fut.err_into())); } } } @@ -555,7 +558,7 @@ impl Future for Accepting { } else { let quinn_conn = std::task::ready!(self.inner.poll_unpin(cx)?); match conn_from_quinn_conn(quinn_conn, &self.ep) { - Err(err) => return Poll::Ready(Err(err.into())), + Err(err) => return Poll::Ready(Err(err)), Ok(fut) => self.register_with_magicsock = Some(Box::pin(fut.err_into())), }; } @@ -577,7 +580,7 @@ impl Future for Accepting { #[derive(Debug, Clone)] pub struct OutgoingZeroRttConnection { inner: quinn::Connection, - handshake_completed_fut: Shared>>, + handshake_completed_fut: Shared>>, } /// Returned from [`OutgoingZeroRttConnection::handshake_completed`]. @@ -602,17 +605,17 @@ impl OutgoingZeroRttConnection { /// the handshake will error and any data sent should be re-sent on a /// new stream. /// - /// This may fail with [`AuthenticationError::ConnectionError`], if there was + /// This may fail with [`ConnectingError::ConnectionError`], if there was /// some general failure with the connection, such as a network timeout since /// we initiated the connection. /// - /// This may fail with other [`AuthenticationError`]s, if the other side + /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side /// doesn't use the right TLS authentication, which usually every iroh endpoint /// uses and requires. /// /// Thus, those errors should only occur if someone connects to you with a /// modified iroh endpoint or with a plain QUIC client. - pub async fn handshake_completed(self) -> Result { + pub async fn handshake_completed(self) -> Result { self.handshake_completed_fut.await } @@ -906,23 +909,23 @@ impl OutgoingZeroRttConnection { #[derive(Debug)] pub struct IncomingZeroRttConnection { inner: quinn::Connection, - handshake_completed_fut: Shared>>, + handshake_completed_fut: Shared>>, } impl IncomingZeroRttConnection { /// Waits until the full handshake occurs and then returns a [`Connection`]. /// - /// This may fail with [`AuthenticationError::ConnectionError`], if there was + /// This may fail with [`ConnectingError::ConnectionError`], if there was /// some general failure with the connection, such as a network timeout since /// we accepted the connection. /// - /// This may fail with other [`AuthenticationError`]s, if the other side + /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side /// doesn't use the right TLS authentication, which usually every iroh endpoint /// uses and requires. /// /// Thus, those errors should only occur if someone connects to you with a /// modified iroh endpoint or with a plain QUIC client. - pub async fn handshake_completed(self) -> Result { + pub async fn handshake_completed(self) -> Result { self.handshake_completed_fut.await } From c24c5d4ea1a17a3ca5de606acca1108dceb2ebe8 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 14 Nov 2025 12:43:42 +0100 Subject: [PATCH 140/164] Cleanup some broken tests There are issues for these already --- iroh/src/endpoint.rs | 22 ++--- iroh/src/magicsock/endpoint_map.rs | 152 ----------------------------- 2 files changed, 11 insertions(+), 163 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index d7491aafccb..d106c97bdbd 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2243,17 +2243,17 @@ mod tests { let server = server_task.await.anyerr()??; let m = client.metrics(); - assert_eq!(m.magicsock.num_direct_conns_added.get(), 1); - assert_eq!(m.magicsock.connection_became_direct.get(), 1); - assert_eq!(m.magicsock.connection_handshake_success.get(), 1); - assert_eq!(m.magicsock.endpoints_contacted_directly.get(), 1); + // assert_eq!(m.magicsock.num_direct_conns_added.get(), 1); + // assert_eq!(m.magicsock.connection_became_direct.get(), 1); + // assert_eq!(m.magicsock.connection_handshake_success.get(), 1); + // assert_eq!(m.magicsock.endpoints_contacted_directly.get(), 1); assert!(m.magicsock.recv_datagrams.get() > 0); let m = server.metrics(); - assert_eq!(m.magicsock.num_direct_conns_added.get(), 1); - assert_eq!(m.magicsock.connection_became_direct.get(), 1); - assert_eq!(m.magicsock.endpoints_contacted_directly.get(), 1); - assert_eq!(m.magicsock.connection_handshake_success.get(), 1); + // assert_eq!(m.magicsock.num_direct_conns_added.get(), 1); + // assert_eq!(m.magicsock.connection_became_direct.get(), 1); + // assert_eq!(m.magicsock.endpoints_contacted_directly.get(), 1); + // assert_eq!(m.magicsock.connection_handshake_success.get(), 1); assert!(m.magicsock.recv_datagrams.get() > 0); // test openmetrics encoding with labeled subregistries per endpoint @@ -2265,9 +2265,9 @@ mod tests { let mut registry = Registry::default(); register_endpoint(&mut registry, &client); register_endpoint(&mut registry, &server); - let s = registry.encode_openmetrics_to_string().anyerr()?; - assert!(s.contains(r#"magicsock_endpoints_contacted_directly_total{id="3b6a27bcce"} 1"#)); - assert!(s.contains(r#"magicsock_endpoints_contacted_directly_total{id="8a88e3dd74"} 1"#)); + // let s = registry.encode_openmetrics_to_string().anyerr()?; + // assert!(s.contains(r#"magicsock_endpoints_contacted_directly_total{id="3b6a27bcce"} 1"#)); + // assert!(s.contains(r#"magicsock_endpoints_contacted_directly_total{id="8a88e3dd74"} 1"#)); Ok(()) } diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 30fc8ac4e2f..71a4924d65d 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -308,155 +308,3 @@ impl IpPort { self.port } } - -#[cfg(test)] -mod tests { - - use tracing_test::traced_test; - - // use super::*; - - // impl NodeMap { - // async fn add_test_addr(&self, node_addr: NodeAddr) { - // self.add_node_addr( - // node_addr, - // Source::NamedApp { - // name: "test".into(), - // }, - // ) - // .await; - // } - // } - - // fn addr(port: u16) -> SocketAddr { - // (std::net::IpAddr::V4(Ipv4Addr::LOCALHOST), port).into() - // } - - #[tokio::test] - #[traced_test] - async fn test_prune_direct_addresses() { - panic!("support this again"); - // let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - // let direct_addrs = DiscoveredDirectAddrs::default(); - // let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); - // let (disco, _) = DiscoState::new(&secret_key); - // let node_map = NodeMap::new( - // secret_key.public(), - // Default::default(), - // transports.create_sender(), - // direct_addrs.addrs.watch(), - // disco, - // ); - // let public_key = SecretKey::generate(rand::thread_rng()).public(); - // let id = node_map - // .inner - // .lock() - // .unwrap() - // .insert_node(Options { - // node_id: public_key, - // relay_url: None, - // active: false, - // source: Source::NamedApp { - // name: "test".into(), - // }, - // path_selection: PathSelection::default(), - // }) - // .id(); - - // const LOCALHOST: IpAddr = IpAddr::V4(std::net::Ipv4Addr::LOCALHOST); - - // // add [`MAX_INACTIVE_DIRECT_ADDRESSES`] active direct addresses and double - // // [`MAX_INACTIVE_DIRECT_ADDRESSES`] that are inactive - - // info!("Adding active addresses"); - // for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES { - // let addr = SocketAddr::new(LOCALHOST, 5000 + i as u16); - // let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); - // // add address - // node_map.add_test_addr(node_addr).await; - // // make it active - // node_map.inner.lock().unwrap().receive_udp(addr); - // } - - // info!("Adding offline/inactive addresses"); - // for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES * 2 { - // let addr = SocketAddr::new(LOCALHOST, 6000 + i as u16); - // let node_addr = NodeAddr::new(public_key).with_direct_addresses([addr]); - // node_map.add_test_addr(node_addr).await; - // } - - // let mut node_map_inner = node_map.inner.lock().unwrap(); - // let endpoint = node_map_inner.by_id.get_mut(&id).unwrap(); - - // info!("Adding alive addresses"); - // for i in 0..MAX_INACTIVE_DIRECT_ADDRESSES { - // let addr = SendAddr::Udp(SocketAddr::new(LOCALHOST, 7000 + i as u16)); - // let txid = stun_rs::TransactionId::from([i as u8; 12]); - // // Note that this already invokes .prune_direct_addresses() because these are - // // new UDP paths. - // // endpoint.handle_ping(addr, txid); - // } - - // info!("Pruning addresses"); - // endpoint.prune_direct_addresses(Instant::now()); - - // // Half the offline addresses should have been pruned. All the active and alive - // // addresses should have been kept. - // assert_eq!( - // endpoint.direct_addresses().count(), - // MAX_INACTIVE_DIRECT_ADDRESSES * 3 - // ); - - // // We should have both offline and alive addresses which are not active. - // assert_eq!( - // endpoint - // .direct_address_states() - // .filter(|(_addr, state)| !state.is_active()) - // .count(), - // MAX_INACTIVE_DIRECT_ADDRESSES * 2 - // ) - } - - #[tokio::test] - async fn test_prune_inactive() { - panic!("support this again"); - // let transports = Transports::new(Vec::new(), Vec::new(), Arc::new(1200.into())); - // let direct_addrs = DiscoveredDirectAddrs::default(); - // let secret_key = SecretKey::generate(&mut rand::rngs::OsRng); - // let (disco, _) = DiscoState::new(&secret_key); - // let node_map = NodeMap::new( - // secret_key.public(), - // Default::default(), - // transports.create_sender(), - // direct_addrs.addrs.watch(), - // disco, - // ); - // // add one active node and more than MAX_INACTIVE_NODES inactive nodes - // let active_node = SecretKey::generate(rand::thread_rng()).public(); - // let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 167); - // node_map - // .add_test_addr(NodeAddr::new(active_node).with_direct_addresses([addr])) - // .await; - // node_map - // .inner - // .lock() - // .unwrap() - // .receive_udp(addr) - // .expect("registered"); - - // for _ in 0..MAX_INACTIVE_NODES + 1 { - // let node = SecretKey::generate(rand::thread_rng()).public(); - // node_map.add_test_addr(NodeAddr::new(node)).await; - // } - - // assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 2); - // node_map.prune_inactive(); - // assert_eq!(node_map.node_count(), MAX_INACTIVE_NODES + 1); - // node_map - // .inner - // .lock() - // .unwrap() - // .get(NodeStateKey::NodeId(active_node)) - // .expect("should not be pruned"); - } -} From 103e3c54b7c2262f51d0276c6e3f7d6da7b35db6 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 14 Nov 2025 12:55:25 +0100 Subject: [PATCH 141/164] fix --- iroh/src/endpoint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index d106c97bdbd..767c07b9f71 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -2213,7 +2213,7 @@ mod tests { #[tokio::test] #[traced_test] async fn metrics_smoke() -> Result { - use iroh_metrics::{MetricsSource, Registry}; + use iroh_metrics::Registry; let secret_key = SecretKey::from_bytes(&[0u8; 32]); let client = Endpoint::empty_builder(RelayMode::Disabled) From 13fe7873160b46e36698d9398bb09f89083698ff Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 14 Nov 2025 13:44:17 +0100 Subject: [PATCH 142/164] fix(tests): Also run the tests in isolation in the default profile (#3664) ## Description This means these tests also work when nextest run locally. ## Breaking Changes ## Notes & open questions I'm not sure why this wasn't done when the ci profile override was chosen. What am I missing? ## Change checklist - [x] Self-review. --- .config/nextest.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.config/nextest.toml b/.config/nextest.toml index bd35988a09a..d2ae495e9a4 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -9,5 +9,10 @@ filter = 'test(::run_in_isolation::)' test-group = 'run-in-isolation' threads-required = 32 +[[profile.default.overrides]] +filter = 'test(::run_in_isolation::)' +test-group = 'run-in-isolation' +threads-required = 32 + [profile.default] slow-timeout = { period = "20s", terminate-after = 3 } From 1f2db9ff074bbbcedda876238b199ca26627189a Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 14 Nov 2025 14:21:49 +0100 Subject: [PATCH 143/164] fix test by calling stream.finish() (#3665) --- iroh/src/endpoint.rs | 6 ++++++ iroh/src/endpoint/connection.rs | 14 +++++++------- iroh/src/magicsock.rs | 6 +++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 767c07b9f71..be45b8db164 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -1863,6 +1863,7 @@ mod tests { info!(me = %ep.id().fmt_short(), "client connecting"); let conn = ep.connect(dst, TEST_ALPN).await?; + info!(me = %ep.id().fmt_short(), "client connected"); // We should be connected via IP, because it is faster than the relay server. // TODO: Maybe not panic if this is not true? @@ -1875,6 +1876,7 @@ mod tests { while let Some(infos) = paths.next().await { info!(?infos, "new PathInfos"); if infos.iter().any(|info| info.is_relay()) { + info!("client has a relay path"); break; } } @@ -1911,6 +1913,7 @@ mod tests { info!(me = %ep.id().fmt_short(), "server starting"); let conn = ep.accept().await.anyerr()?.await.anyerr()?; + info!(me = %ep.id().fmt_short(), "server accepted connection"); // Wait for a relay connection to be added. Client does all the asserting here, // we just want to wait so we get to see all the mechanics of the connection @@ -1920,6 +1923,7 @@ mod tests { while let Some(infos) = paths.next().await { info!(?infos, "new PathInfos"); if infos.iter().any(|path| path.is_relay()) { + info!("server has a relay path"); break; } } @@ -1929,6 +1933,8 @@ mod tests { let mut stream = conn.open_uni().await.anyerr()?; stream.write_all(b"have relay").await.anyerr()?; + stream.finish().anyerr()?; + info!("waiting conn.closed()"); Ok(conn.closed().await) } diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index f4ec9c65b17..dfd4203f207 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -1521,7 +1521,7 @@ mod tests { use n0_future::StreamExt; use n0_watcher::Watcher; use rand::SeedableRng; - use tracing::{Instrument, error_span, info_span, trace_span}; + use tracing::{Instrument, error_span, info, info_span, trace_span}; use tracing_test::traced_test; use super::Endpoint; @@ -1740,8 +1740,8 @@ mod tests { } #[tokio::test] + #[traced_test] async fn test_paths_watcher() -> Result { - tracing_subscriber::fmt::init(); const ALPN: &[u8] = b"test"; let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); let (relay_map, _relay_map, _guard) = run_relay_server().await?; @@ -1760,13 +1760,13 @@ mod tests { server.online().await; let server_addr = server.addr(); - tracing::info!("server addr: {server_addr:?}"); + info!("server addr: {server_addr:?}"); let (conn_client, conn_server) = tokio::join!( async { client.connect(server_addr, ALPN).await.unwrap() }, async { server.accept().await.unwrap().await.unwrap() } ); - tracing::info!("connected"); + info!("connected"); let mut paths_client = conn_client.paths().stream(); let mut paths_server = conn_server.paths().stream(); @@ -1778,12 +1778,12 @@ mod tests { ) { loop { let paths = stream.next().await.expect("paths stream ended"); - tracing::info!(?paths, "paths"); + info!(?paths, "paths"); if paths.len() >= 2 && paths.iter().any(PathInfo::is_relay) && paths.iter().any(PathInfo::is_ip) { - tracing::info!("break"); + info!("break"); return; } } @@ -1806,7 +1806,7 @@ mod tests { ); // Close the client connection. - tracing::info!("close client conn"); + info!("close client conn"); conn_client.close(0u32.into(), b""); // Verify that the path watch streams close. diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 2a4f6e66029..14d2f817708 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -2307,11 +2307,11 @@ mod tests { #[traced_test] async fn test_two_devices_setup_teardown() -> Result { for i in 0..10 { - println!("-- round {i}"); - println!("setting up magic stack"); + info!("-- round {i}"); + info!("setting up magic stack"); let (_guard, m1, m2) = endpoint_pair().await; - println!("closing endpoints"); + info!("closing endpoints"); let msock1 = m1.magic_sock(); let msock2 = m2.magic_sock(); m1.close().await; From 6ef582dc32942a008aca6ac350597d61391ebb73 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Mon, 17 Nov 2025 11:31:24 +0100 Subject: [PATCH 144/164] deps(multipath): bump netdev (#3667) ## Description Bumps netwatch and netdev, to remove duplicate dependency on both netdev@0.38 and netdev@0.39. ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- Cargo.lock | 51 +++++++++++++++++++++++++++---------------------- iroh/Cargo.toml | 2 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfd0a86e34d..69d24526694 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,7 +1121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1896,7 +1896,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2337,7 +2337,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2376,9 +2376,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2598,6 +2598,12 @@ 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 = "mainline" version = "6.0.0" @@ -2694,22 +2700,20 @@ dependencies = [ [[package]] name = "n0-error" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a4839a11b62f1fdd75be912ee20634053c734c2240e867ded41c7f50822c549" +checksum = "c7d5969a2f40e9d9ed121a789c415f4114ac2b28e5731c080bdefee217d3b3fb" dependencies = [ - "derive_more 2.0.1", "n0-error-macros", "spez", ] [[package]] name = "n0-error-macros" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed2a7e5ca3cb5729d4a162d7bcab5b338bed299a2fee8457568d7e0a747ed89" +checksum = "9a6908df844696d9af91c7c3950d50e52d67df327d02a95367f95bbf177d6556" dependencies = [ - "heck", "proc-macro2", "quote", "syn", @@ -2717,9 +2721,9 @@ dependencies = [ [[package]] name = "n0-future" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439e746b307c1fd0c08771c3cafcd1746c3ccdb0d9c7b859d3caded366b6da76" +checksum = "8c0709ac8235ce13b82bc4d180ee3c42364b90c1a8a628c3422d991d75a728b5" dependencies = [ "cfg_aliases", "derive_more 1.0.0", @@ -2749,13 +2753,14 @@ dependencies = [ [[package]] name = "netdev" -version = "0.38.2" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ab878b4c90faf36dab10ea51d48c69ae9019bcca47c048a7c9b273d5d7a823" +checksum = "35a703aa1a87cd885b9f674922445a42dbb0c0f4f1b28fef21b227ae32375d21" dependencies = [ "dlopen2", "ipnet", "libc", + "mac-addr", "netlink-packet-core", "netlink-packet-route", "netlink-sys", @@ -2815,7 +2820,7 @@ dependencies = [ [[package]] name = "netwatch" version = "0.12.0" -source = "git+https://github.com/n0-computer/net-tools?branch=main#0721bbb6a2c4dd487a378d6ef0f56387680649d1" +source = "git+https://github.com/n0-computer/net-tools?branch=main#cd7aba545996781786b8168d49b876f0844ad3d7" dependencies = [ "atomic-waker", "bytes", @@ -3357,7 +3362,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -3394,9 +3399,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -3665,7 +3670,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3767,7 +3772,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3788,7 +3793,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.3", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4351,7 +4356,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index aaa00b80397..882cf61e6bb 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -81,7 +81,7 @@ axum = { version = "0.8", optional = true } [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] hickory-resolver = "0.25.1" igd-next = { version = "0.16", features = ["aio_tokio"] } -netdev = { version = "0.38.1" } +netdev = { version = "0.39.0" } portmapper = { version = "0.12", default-features = false } quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["runtime-tokio", "rustls-ring"] } tokio = { version = "1", features = [ From 7f17d983829a90a54a3f09286630448112a391fb Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Mon, 17 Nov 2025 11:51:59 +0100 Subject: [PATCH 145/164] feat: relay only configuration This is a next step into the world of configurable transports. We now allow disabling the IP based transports entirely. Internally this starts to prepare for a world where the user can configure multiple different transports, IP, relay and others in the future. Closes #2957 --- iroh/bench/src/iroh.rs | 6 +- iroh/src/endpoint.rs | 205 ++++++++++++++--- iroh/src/magicsock.rs | 219 ++++++++----------- iroh/src/magicsock/mapped_addrs.rs | 19 +- iroh/src/magicsock/transports.rs | 109 +++++++-- iroh/src/magicsock/transports/ip.rs | 31 ++- iroh/src/magicsock/transports/relay/actor.rs | 2 +- 7 files changed, 413 insertions(+), 178 deletions(-) diff --git a/iroh/bench/src/iroh.rs b/iroh/bench/src/iroh.rs index 8e323b1f805..601a80179ac 100644 --- a/iroh/bench/src/iroh.rs +++ b/iroh/bench/src/iroh.rs @@ -6,7 +6,7 @@ use std::{ use bytes::Bytes; use iroh::{ Endpoint, EndpointAddr, RelayMode, RelayUrl, - endpoint::{Connection, ConnectionError, RecvStream, SendStream, TransportConfig}, + endpoint::{Connection, ConnectionError, QuinnTransportConfig, RecvStream, SendStream}, }; use n0_error::{Result, StackResultExt, StdResultExt}; use tracing::{trace, warn}; @@ -126,10 +126,10 @@ pub async fn connect_client( Ok((endpoint, connection)) } -pub fn transport_config(max_streams: usize, initial_mtu: u16) -> TransportConfig { +pub fn transport_config(max_streams: usize, initial_mtu: u16) -> QuinnTransportConfig { // High stream windows are chosen because the amount of concurrent streams // is configurable as a parameter. - let mut config = TransportConfig::default(); + let mut config = QuinnTransportConfig::default(); config.max_concurrent_uni_streams(max_streams.try_into().unwrap()); config.initial_mtu(initial_mtu); diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index be45b8db164..cd2a1fc8e5d 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -56,7 +56,7 @@ pub use quinn::{ ConnectionClose, ConnectionError, ConnectionStats, MtuDiscoveryConfig, OpenBi, OpenUni, PathStats, ReadDatagram, ReadError, ReadExactError, ReadToEndError, RecvStream, ResetError, RetryError, SendDatagramError, SendStream, ServerConfig, StoppedError, StreamId, - TransportConfig, VarInt, WeakConnectionHandle, WriteError, + TransportConfig as QuinnTransportConfig, VarInt, WeakConnectionHandle, WriteError, }; pub use quinn_proto::{ FrameStats, TransportError, TransportErrorCode, UdpStats, Written, @@ -72,6 +72,7 @@ pub use self::connection::{ Incoming, IncomingZeroRttConnection, OutgoingZeroRttConnection, RemoteEndpointIdError, ZeroRttStatus, }; +pub use crate::magicsock::transports::TransportConfig; /// The delay to fall back to discovery when direct addresses fail. /// @@ -101,7 +102,6 @@ pub enum PathSelection { #[derive(Debug)] pub struct Builder { secret_key: Option, - relay_mode: RelayMode, alpn_protocols: Vec>, transport_config: quinn::TransportConfig, keylog: bool, @@ -112,13 +112,27 @@ pub struct Builder { dns_resolver: Option, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: bool, - addr_v4: Option, - addr_v6: Option, + transports: Vec, #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, max_tls_tickets: usize, } +impl From for Option { + fn from(mode: RelayMode) -> Self { + match mode { + RelayMode::Disabled => None, + RelayMode::Default => Some(TransportConfig::Relay { + relay_map: mode.relay_map(), + }), + RelayMode::Staging => Some(TransportConfig::Relay { + relay_map: mode.relay_map(), + }), + RelayMode::Custom(relay_map) => Some(TransportConfig::Relay { relay_map }), + } + } +} + impl Builder { // The ordering of public methods is reflected directly in the documentation. This is // roughly ordered by what is most commonly needed by users. @@ -140,9 +154,18 @@ impl Builder { pub fn empty(relay_mode: RelayMode) -> Self { let mut transport_config = quinn::TransportConfig::default(); transport_config.keep_alive_interval(Some(Duration::from_secs(1))); + + let mut transports = vec![ + #[cfg(not(wasm_browser))] + TransportConfig::default_ipv4(), + #[cfg(not(wasm_browser))] + TransportConfig::default_ipv6(), + ]; + if let Some(relay) = relay_mode.into() { + transports.push(relay); + } Self { secret_key: Default::default(), - relay_mode, alpn_protocols: Default::default(), transport_config: quinn::TransportConfig::default(), keylog: Default::default(), @@ -153,11 +176,10 @@ impl Builder { dns_resolver: None, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, - addr_v4: None, - addr_v6: None, #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection::default(), max_tls_tickets: DEFAULT_MAX_TLS_TICKETS, + transports, } } @@ -166,7 +188,6 @@ impl Builder { /// Binds the magic endpoint. pub async fn bind(mut self) -> Result { let mut rng = rand::rng(); - let relay_map = self.relay_mode.relay_map(); let secret_key = self .secret_key .unwrap_or_else(move || SecretKey::generate(&mut rng)); @@ -194,10 +215,8 @@ impl Builder { let metrics = EndpointMetrics::default(); let msock_opts = magicsock::Options { - addr_v4: self.addr_v4, - addr_v6: self.addr_v6, + transports: self.transports, secret_key, - relay_map, discovery_user_data: self.discovery_user_data, proxy_url: self.proxy_url, #[cfg(not(wasm_browser))] @@ -230,25 +249,46 @@ impl Builder { // # The very common methods everyone basically needs. - /// Sets the IPv4 bind address. + /// Adds an IP transport, binding to the provided IPv4 address. + /// + /// If you want to remove the default transports, make sure to call `clear_ip` first. /// /// Setting the port to `0` will use a random port. /// If the port specified is already in use, it will fallback to choosing a random port. - /// - /// By default will use `0.0.0.0:0` to bind to. - pub fn bind_addr_v4(mut self, addr: SocketAddrV4) -> Self { - self.addr_v4.replace(addr); + #[cfg(not(wasm_browser))] + pub fn bind_addr_v4(mut self, bind_addr: SocketAddrV4) -> Self { + self.transports.push(TransportConfig::Ip { + bind_addr: bind_addr.into(), + }); self } - /// Sets the IPv6 bind address. + /// Adds an IP transport, binding to the provided IPv6 address. + /// + /// If you want to remove the default transports, make sure to call `clear_ip` first. /// /// Setting the port to `0` will use a random port. /// If the port specified is already in use, it will fallback to choosing a random port. - /// - /// By default will use `[::]:0` to bind to. - pub fn bind_addr_v6(mut self, addr: SocketAddrV6) -> Self { - self.addr_v6.replace(addr); + #[cfg(not(wasm_browser))] + pub fn bind_addr_v6(mut self, bind_addr: SocketAddrV6) -> Self { + self.transports.push(TransportConfig::Ip { + bind_addr: bind_addr.into(), + }); + self + } + + /// Removes all IP based transports + #[cfg(not(wasm_browser))] + pub fn clear_ip_transports(mut self) -> Self { + self.transports + .retain(|t| !matches!(t, TransportConfig::Ip { .. })); + self + } + + /// Removes all relay based transports + pub fn clear_relay_transports(mut self) -> Self { + self.transports + .retain(|t| !matches!(t, TransportConfig::Relay { .. })); self } @@ -294,7 +334,24 @@ impl Builder { /// [crate docs]: crate /// [number 0]: https://n0.computer pub fn relay_mode(mut self, relay_mode: RelayMode) -> Self { - self.relay_mode = relay_mode; + let transport: Option<_> = relay_mode.into(); + match transport { + Some(transport) => { + if let Some(og) = self + .transports + .iter_mut() + .find(|t| matches!(t, TransportConfig::Relay { .. })) + { + *og = transport; + } else { + self.transports.push(transport); + } + } + None => { + self.transports + .retain(|t| !matches!(t, TransportConfig::Relay { .. })); + } + } self } @@ -865,8 +922,6 @@ impl Endpoint { let endpoint_id = self.id(); watch_addrs.or(watch_relay).map(move |(addrs, relays)| { - debug_assert!(!addrs.is_empty(), "direct addresses must never be empty"); - EndpointAddr::from_parts( endpoint_id, relays @@ -1252,7 +1307,7 @@ impl Endpoint { /// Options for the [`Endpoint::connect_with_opts`] function. #[derive(Default, Debug, Clone)] pub struct ConnectOptions { - transport_config: Option>, + transport_config: Option>, additional_alpns: Vec>, } @@ -1266,7 +1321,7 @@ impl ConnectOptions { } /// Sets the QUIC transport config options for this connection. - pub fn with_transport_config(mut self, transport_config: Arc) -> Self { + pub fn with_transport_config(mut self, transport_config: Arc) -> Self { self.transport_config = Some(transport_config); self } @@ -1340,6 +1395,7 @@ fn proxy_url_from_env() -> Option { #[derive(Debug, Clone, PartialEq, Eq)] pub enum RelayMode { /// Disable relay servers completely. + /// This means that neither listening nor dialing relays will be available. Disabled, /// Use the default relay map, with production relay servers from n0. /// @@ -1837,6 +1893,103 @@ mod tests { Ok(()) } + #[tokio::test] + #[traced_test] + async fn endpoint_two_relay_only_no_ip() -> Result { + // Connect two endpoints on the same network, via a relay server, without + // discovery. + let (relay_map, _relay_url, _relay_server_guard) = run_relay_server().await?; + let (node_addr_tx, node_addr_rx) = oneshot::channel(); + + #[instrument(name = "client", skip_all)] + async fn connect( + relay_map: RelayMap, + node_addr_rx: oneshot::Receiver, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let secret = SecretKey::generate(&mut rng); + let ep = Endpoint::builder() + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .clear_ip_transports() // disable direct + .bind() + .await?; + info!(me = %ep.id().fmt_short(), "client starting"); + let dst = node_addr_rx.await.anyerr()?; + + info!(me = %ep.id().fmt_short(), "client connecting"); + let conn = ep.connect(dst, TEST_ALPN).await?; + let mut send = conn.open_uni().await.anyerr()?; + send.write_all(b"hello").await.anyerr()?; + let mut paths = conn.paths().stream(); + info!("Waiting for connection"); + 'outer: while let Some(infos) = paths.next().await { + info!(?infos, "new PathInfos"); + for info in infos { + if info.is_ip() { + panic!("should not happen: {:?}", info); + } + if info.is_relay() { + break 'outer; + } + } + } + info!("Have relay connection"); + send.write_all(b"close please").await.anyerr()?; + send.finish().anyerr()?; + Ok(conn.closed().await) + } + + #[instrument(name = "server", skip_all)] + async fn accept( + relay_map: RelayMap, + node_addr_tx: oneshot::Sender, + ) -> Result { + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(1u64); + let secret = SecretKey::generate(&mut rng); + let ep = Endpoint::builder() + .secret_key(secret) + .alpns(vec![TEST_ALPN.to_vec()]) + .insecure_skip_relay_cert_verify(true) + .relay_mode(RelayMode::Custom(relay_map)) + .clear_ip_transports() + .bind() + .await?; + ep.online().await; + let node_addr = ep.addr(); + node_addr_tx.send(node_addr).unwrap(); + + info!(me = %ep.id().fmt_short(), "server starting"); + let conn = ep.accept().await.anyerr()?.await.anyerr()?; + // let node_id = conn.remote_node_id()?; + // assert_eq!(node_id, src); + let mut recv = conn.accept_uni().await.anyerr()?; + let mut msg = [0u8; 5]; + recv.read_exact(&mut msg).await.anyerr()?; + assert_eq!(&msg, b"hello"); + info!("received hello"); + let msg = recv.read_to_end(100).await.anyerr()?; + assert_eq!(msg, b"close please"); + info!("received 'close please'"); + // Dropping the connection closes it just fine. + Ok(()) + } + + let server_task = tokio::spawn(accept(relay_map.clone(), node_addr_tx)); + let client_task = tokio::spawn(connect(relay_map, node_addr_rx)); + + server_task.await.anyerr()??; + let conn_closed = dbg!(client_task.await.anyerr()??); + assert!(matches!( + conn_closed, + ConnectionError::ApplicationClosed(quinn::ApplicationClose { .. }) + )); + + Ok(()) + } + #[tokio::test] #[traced_test] async fn endpoint_two_direct_add_relay() -> Result { diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 14d2f817708..22ff0efd13d 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -19,7 +19,7 @@ use std::{ collections::{BTreeMap, BTreeSet, HashMap}, fmt::Display, io, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, + net::{IpAddr, SocketAddr}, sync::{ Arc, Mutex, RwLock, atomic::{AtomicBool, Ordering}, @@ -29,29 +29,27 @@ use std::{ use bytes::Bytes; use iroh_base::{EndpointAddr, EndpointId, PublicKey, RelayUrl, SecretKey, TransportAddr}; use iroh_relay::{RelayConfig, RelayMap}; -use n0_error::{e, stack_error}; +use n0_error::{bail, e, stack_error}; use n0_future::{ task::{self, AbortOnDropHandle}, time::{self, Duration, Instant}, }; use n0_watcher::{self, Watchable, Watcher}; -use netwatch::netmon; #[cfg(not(wasm_browser))] -use netwatch::{UdpSocket, ip::LocalAddresses}; +use netwatch::ip::LocalAddresses; +use netwatch::netmon; use quinn::{ServerConfig, WeakConnectionHandle}; use rand::Rng; use tokio::sync::{Mutex as AsyncMutex, mpsc, oneshot}; use tokio_util::sync::CancellationToken; -use tracing::{Instrument, Level, debug, event, info, info_span, instrument, trace, warn}; -use transports::{LocalAddrsWatch, MagicTransport}; +use tracing::{Instrument, Level, debug, event, info_span, instrument, trace, warn}; +use transports::{LocalAddrsWatch, MagicTransport, TransportConfig}; use url::Url; -#[cfg(not(wasm_browser))] -use self::transports::IpTransport; use self::{ endpoint_map::{EndpointMap, EndpointStateMessage}, metrics::Metrics as MagicsockMetrics, - transports::{RelayActorConfig, RelayTransport, Transports, TransportsSender}, + transports::{RelayActorConfig, Transports, TransportsSender}, }; #[cfg(not(wasm_browser))] use crate::dns::DnsResolver; @@ -111,21 +109,12 @@ pub(crate) struct EndpointStateActorStoppedError; /// Contains options for `MagicSock::listen`. #[derive(derive_more::Debug)] pub(crate) struct Options { - /// The IPv4 address to listen on. - /// - /// If set to `None` it will choose a random port and listen on `0.0.0.0:0`. - pub(crate) addr_v4: Option, - /// The IPv6 address to listen on. - /// - /// If set to `None` it will choose a random port and listen on `[::]:0`. - pub(crate) addr_v6: Option, + /// The configuration for the different transports. + pub(crate) transports: Vec, /// Secret key for this endpoint. pub(crate) secret_key: SecretKey, - /// The [`RelayMap`] to use, leave empty to not use a relay server. - pub(crate) relay_map: RelayMap, - /// Optional user-defined discovery data. pub(crate) discovery_user_data: Option, @@ -238,7 +227,7 @@ pub enum AddEndpointAddrError { } impl MagicSock { - /// Creates a magic [`MagicSock`] listening on [`Options::addr_v4`] and [`Options::addr_v6`]. + /// Creates a magic [`MagicSock`] listening. pub(crate) async fn spawn(opts: Options) -> Result { Handle::new(opts).await } @@ -930,16 +919,16 @@ pub enum CreateHandleError { CreateNetmonMonitor { source: netmon::Error }, #[error("Failed to subscribe netmon monitor")] SubscribeNetmonMonitor { source: netmon::Error }, + #[error("Invalid transport configuration")] + InvalidTransportConfig, } impl Handle { - /// Creates a magic [`MagicSock`] listening on [`Options::addr_v4`] and [`Options::addr_v6`]. + /// Creates a magic [`MagicSock`]. async fn new(opts: Options) -> Result { let Options { - addr_v4, - addr_v6, secret_key, - relay_map, + transports: transport_configs, discovery_user_data, #[cfg(not(wasm_browser))] dns_resolver, @@ -953,43 +942,83 @@ impl Handle { } = opts; let discovery = ConcurrentDiscovery::default(); - - let addr_v4 = addr_v4.unwrap_or_else(|| SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)); - #[cfg(not(wasm_browser))] - let (ip_transports, port_mapper) = bind_ip(addr_v4, addr_v6, &metrics) - .map_err(|err| e!(CreateHandleError::BindSockets, err))?; + let port_mapper = + portmapper::Client::with_metrics(Default::default(), metrics.portmapper.clone()); - let (actor_sender, actor_receiver) = mpsc::channel(256); + let relay_transport_configs: Vec<_> = transport_configs + .iter() + .filter(|t| matches!(t, TransportConfig::Relay { .. })) + .collect(); + + // Currently we only support a single relay transport + if relay_transport_configs.len() > 1 { + dbg!(&transport_configs, &relay_transport_configs); + bail!(CreateHandleError::InvalidTransportConfig); + } + let relay_map = relay_transport_configs + .iter() + .filter_map(|t| { + #[allow(irrefutable_let_patterns)] + if let TransportConfig::Relay { relay_map } = t { + Some(relay_map.clone()) + } else { + None + } + }) + .next() + .unwrap_or_else(RelayMap::empty); let my_relay = Watchable::new(None); let ipv6_reported = Arc::new(AtomicBool::new(false)); + let relay_actor_config = RelayActorConfig { + my_relay: my_relay.clone(), + secret_key: secret_key.clone(), + #[cfg(not(wasm_browser))] + dns_resolver: dns_resolver.clone(), + proxy_url: proxy_url.clone(), + ipv6_reported: ipv6_reported.clone(), + #[cfg(any(test, feature = "test-utils"))] + insecure_skip_relay_cert_verify, + metrics: metrics.magicsock.clone(), + }; let shutdown_token = CancellationToken::new(); - let relay_transport = RelayTransport::new( - RelayActorConfig { - my_relay: my_relay.clone(), - secret_key: secret_key.clone(), - #[cfg(not(wasm_browser))] - dns_resolver: dns_resolver.clone(), - proxy_url: proxy_url.clone(), - ipv6_reported: ipv6_reported.clone(), - #[cfg(any(test, feature = "test-utils"))] - insecure_skip_relay_cert_verify, - metrics: metrics.magicsock.clone(), - }, + let transports = Transports::bind( + &transport_configs, + relay_actor_config, + &metrics, shutdown_token.child_token(), - ); - let relay_transports = vec![relay_transport]; + ) + .map_err(|err| e!(CreateHandleError::BindSockets, err))?; #[cfg(not(wasm_browser))] - let ipv6 = ip_transports.iter().any(|t| t.bind_addr().is_ipv6()); + { + if let Some(v4_port) = transports.local_addrs().into_iter().find_map(|t| { + if let transports::Addr::Ip(SocketAddr::V4(addr)) = t { + Some(addr.port()) + } else { + None + } + }) { + // NOTE: we can end up with a zero port if `netwatch::UdpSocket::socket_addr` fails + match v4_port.try_into() { + Ok(non_zero_port) => { + port_mapper.update_local_port(non_zero_port); + } + Err(_zero_port) => debug!("Skipping port mapping with zero local port"), + } + } + } + + let (actor_sender, actor_receiver) = mpsc::channel(256); #[cfg(not(wasm_browser))] - let transports = Transports::new(ip_transports, relay_transports); - #[cfg(wasm_browser)] - let transports = Transports::new(relay_transports); + let ipv6 = transports + .ip_bind_addrs() + .into_iter() + .any(|addr| addr.is_ipv6()); let direct_addrs = DiscoveredDirectAddrs::default(); let (disco, disco_receiver) = DiscoState::new(&secret_key); @@ -1314,56 +1343,6 @@ struct Actor { disco_receiver: mpsc::Receiver<(SendAddr, PublicKey, disco::Message)>, } -#[cfg(not(wasm_browser))] -fn bind_ip( - addr_v4: SocketAddrV4, - addr_v6: Option, - metrics: &EndpointMetrics, -) -> io::Result<(Vec, portmapper::Client)> { - let port_mapper = - portmapper::Client::with_metrics(Default::default(), metrics.portmapper.clone()); - - let v4 = Arc::new(bind_with_fallback(SocketAddr::V4(addr_v4))?); - let ip4_port = v4.local_addr()?.port(); - let ip6_port = ip4_port.checked_add(1).unwrap_or(ip4_port - 1); - - let addr_v6 = - addr_v6.unwrap_or_else(|| SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, ip6_port, 0, 0)); - - let v6 = match bind_with_fallback(SocketAddr::V6(addr_v6)) { - Ok(sock) => Some(Arc::new(sock)), - Err(err) => { - info!("bind ignoring IPv6 bind failure: {:?}", err); - None - } - }; - - let port = v4.local_addr().map_or(0, |p| p.port()); - - let mut ip = vec![IpTransport::new( - addr_v4.into(), - v4, - metrics.magicsock.clone(), - )]; - if let Some(v6) = v6 { - ip.push(IpTransport::new( - addr_v6.into(), - v6, - metrics.magicsock.clone(), - )) - } - - // NOTE: we can end up with a zero port if `netwatch::UdpSocket::socket_addr` fails - match port.try_into() { - Ok(non_zero_port) => { - port_mapper.update_local_port(non_zero_port); - } - Err(_zero_port) => debug!("Skipping port mapping with zero local port"), - } - - Ok((ip, port_mapper)) -} - impl Actor { async fn run( mut self, @@ -1748,31 +1727,6 @@ fn new_re_stun_timer(initial_delay: bool) -> time::Interval { } } -#[cfg(not(wasm_browser))] -fn bind_with_fallback(mut addr: SocketAddr) -> io::Result { - debug!(%addr, "binding"); - - // First try binding a preferred port, if specified - match UdpSocket::bind_full(addr) { - Ok(socket) => { - let local_addr = socket.local_addr()?; - debug!(%addr, %local_addr, "successfully bound"); - return Ok(socket); - } - Err(err) => { - debug!(%addr, "failed to bind: {err:#}"); - // If that was already the fallback port, then error out - if addr.port() == 0 { - return Err(err); - } - } - } - - // Otherwise, try binding with port 0 - addr.set_port(0); - UdpSocket::bind_full(addr) -} - /// The discovered direct addresses of this [`MagicSock`]. /// /// These are all the [`DirectAddr`]s that this [`MagicSock`] is aware of for itself. @@ -1903,13 +1857,12 @@ mod tests { use super::{EndpointIdMappedAddr, Options, endpoint_map::Source, mapped_addrs::MappedAddr}; use crate::{ Endpoint, - RelayMap, RelayMode, SecretKey, discovery::static_provider::StaticProvider, dns::DnsResolver, // endpoint::PathSelection, - magicsock::{Handle, MagicSock}, + magicsock::{Handle, MagicSock, TransportConfig}, tls::{self, DEFAULT_MAX_TLS_TICKETS}, }; @@ -1919,10 +1872,11 @@ mod tests { let secret_key = SecretKey::generate(rng); let server_config = make_default_server_config(&secret_key); Options { - addr_v4: None, - addr_v6: None, + transports: vec![ + TransportConfig::default_ipv4(), + TransportConfig::default_ipv6(), + ], secret_key, - relay_map: RelayMap::empty(), proxy_url: None, dns_resolver: DnsResolver::new(), server_config, @@ -2355,10 +2309,11 @@ mod tests { let dns_resolver = DnsResolver::new(); let opts = Options { - addr_v4: None, - addr_v6: None, + transports: vec![ + TransportConfig::default_ipv4(), + TransportConfig::default_ipv6(), + ], secret_key: secret_key.clone(), - relay_map: RelayMap::empty(), discovery_user_data: None, dns_resolver, proxy_url: None, diff --git a/iroh/src/magicsock/mapped_addrs.rs b/iroh/src/magicsock/mapped_addrs.rs index 53e5b67b9d4..f12482f23f5 100644 --- a/iroh/src/magicsock/mapped_addrs.rs +++ b/iroh/src/magicsock/mapped_addrs.rs @@ -7,7 +7,7 @@ use std::{ fmt, hash::Hash, - net::{IpAddr, Ipv6Addr, SocketAddr}, + net::{IpAddr, Ipv6Addr, SocketAddr, SocketAddrV6}, sync::{ Arc, atomic::{AtomicU64, Ordering}, @@ -33,6 +33,23 @@ const RELAY_MAPPED_SUBNET: [u8; 2] = [0, 1]; /// The Subnet ID for [`EndpointIdMappedAddr`]. const ENDPOINT_ID_SUBNET: [u8; 2] = [0; 2]; +/// A default fake addr, using the maximum addr that the internal fake addrs could be using. +pub const DEFAULT_FAKE_ADDR: SocketAddrV6 = SocketAddrV6::new( + Ipv6Addr::new( + u16::from_be_bytes([ADDR_PREFIXL, 21]), + u16::from_be_bytes([7, 10]), + u16::from_be_bytes([81, 11]), + u16::from_be_bytes([0, 0]), + u16::MAX, + u16::MAX, + u16::MAX, + u16::MAX, + ), + MAPPED_PORT, + 0, + 0, +); + /// The dummy port used for all mapped addresses. /// /// We map each entity, usually an [`EndpointId`], to an IPv6 address. But socket addresses diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 5d108b31b85..5ed652928a2 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -9,12 +9,14 @@ use std::{ use bytes::Bytes; use iroh_base::{EndpointId, RelayUrl, TransportAddr}; +use iroh_relay::RelayMap; use n0_watcher::Watcher; use relay::{RelayNetworkChangeSender, RelaySender}; +use tokio_util::sync::CancellationToken; use tracing::{debug, error, instrument, trace, warn}; use super::{MagicSock, endpoint_map::EndpointStateMessage, mapped_addrs::MultipathMappedAddr}; -use crate::net_report::Report; +use crate::{metrics::EndpointMetrics, net_report::Report}; #[cfg(not(wasm_browser))] mod ip; @@ -60,19 +62,89 @@ pub(crate) type LocalAddrsWatch = n0_watcher::Map< Vec, >; +/// Available transport configurations. +#[derive(Debug, Clone)] +#[non_exhaustive] +pub enum TransportConfig { + /// IP based transport + #[cfg(not(wasm_browser))] + Ip { + /// The address this transport will bind on. + bind_addr: SocketAddr, + }, + /// Relay transport + Relay { + /// The [`RelayMap`] used for this relay. + relay_map: RelayMap, + }, +} +impl TransportConfig { + /// Configures a default IPv4 transport, listening on `0.0.0.0:0`. + #[cfg(not(wasm_browser))] + pub fn default_ipv4() -> Self { + use std::net::{Ipv4Addr, SocketAddrV4}; + + Self::Ip { + bind_addr: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0)), + } + } + + /// Configures a default IPv6 transport, listening on `[::]:0`. + #[cfg(not(wasm_browser))] + pub fn default_ipv6() -> Self { + Self::Ip { + bind_addr: SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0)), + } + } +} + +#[cfg(not(wasm_browser))] +fn bind_ip(configs: &[TransportConfig], metrics: &EndpointMetrics) -> io::Result> { + let mut transports = Vec::new(); + for config in configs { + if let TransportConfig::Ip { bind_addr } = config { + match IpTransport::bind(*bind_addr, metrics.magicsock.clone()) { + Ok(transport) => { + transports.push(transport); + } + Err(err) => { + if bind_addr.is_ipv6() { + tracing::info!("bind ignoring IPv6 bind failure: {:?}", err); + } else { + return Err(err); + } + } + } + } + } + + Ok(transports) +} + impl Transports { - /// Creates a new transports structure. - pub(crate) fn new( - #[cfg(not(wasm_browser))] ip: Vec, - relay: Vec, - ) -> Self { - Self { + /// Binds the transports. + pub(crate) fn bind( + configs: &[TransportConfig], + relay_actor_config: RelayActorConfig, + metrics: &EndpointMetrics, + shutdown_token: CancellationToken, + ) -> io::Result { + #[cfg(not(wasm_browser))] + let ip = bind_ip(configs, metrics)?; + + let relay = configs + .iter() + .filter(|t| matches!(t, TransportConfig::Relay { .. })) + .map(|_c| RelayTransport::new(relay_actor_config.clone(), shutdown_token.child_token())) + .collect(); + + Ok(Self { #[cfg(not(wasm_browser))] ip, relay, poll_recv_counter: Default::default(), source_addrs: Default::default(), - } + }) } pub(crate) fn poll_recv( @@ -560,13 +632,16 @@ impl quinn::AsyncUdpSocket for MagicTransport { #[cfg(not(wasm_browser))] fn local_addr(&self) -> io::Result { - let addrs: Vec<_> = self - .transports - .local_addrs() + let local_addrs = self.transports.local_addrs(); + let addrs: Vec<_> = local_addrs .into_iter() - .filter_map(|addr| { - let addr: SocketAddr = addr.into_socket_addr()?; - Some(addr) + .map(|addr| { + use crate::magicsock::mapped_addrs::DEFAULT_FAKE_ADDR; + + match addr { + Addr::Ip(addr) => addr, + Addr::Relay(..) => DEFAULT_FAKE_ADDR.into(), + } }) .collect(); @@ -579,6 +654,12 @@ impl quinn::AsyncUdpSocket for MagicTransport { return Ok(SocketAddr::new(ip, addr.port())); } + if !self.transports.relay.is_empty() { + // pretend we have an address to make sure things are not too sad during startup + use crate::magicsock::mapped_addrs::DEFAULT_FAKE_ADDR; + + return Ok(DEFAULT_FAKE_ADDR.into()); + } Err(io::Error::other("no valid address available")) } diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index 147cfcec022..e2ee9b5ffc9 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -9,7 +9,7 @@ use std::{ use n0_watcher::Watchable; use netwatch::{UdpSender, UdpSocket}; use pin_project::pin_project; -use tracing::trace; +use tracing::{debug, trace}; use super::{Addr, Transmit}; use crate::metrics::MagicsockMetrics; @@ -22,7 +22,36 @@ pub(crate) struct IpTransport { metrics: Arc, } +fn bind_with_fallback(mut addr: SocketAddr) -> io::Result { + debug!(%addr, "binding"); + + // First try binding a preferred port, if specified + match netwatch::UdpSocket::bind_full(addr) { + Ok(socket) => { + let local_addr = socket.local_addr()?; + debug!(%addr, %local_addr, "successfully bound"); + return Ok(socket); + } + Err(err) => { + debug!(%addr, "failed to bind: {err:#}"); + // If that was already the fallback port, then error out + if addr.port() == 0 { + return Err(err); + } + } + } + + // Otherwise, try binding with port 0 + addr.set_port(0); + netwatch::UdpSocket::bind_full(addr) +} + impl IpTransport { + pub(crate) fn bind(bind_addr: SocketAddr, metrics: Arc) -> io::Result { + let socket = bind_with_fallback(bind_addr)?; + Ok(Self::new(bind_addr, Arc::new(socket), metrics.clone())) + } + pub(crate) fn new( bind_addr: SocketAddr, socket: Arc, diff --git a/iroh/src/magicsock/transports/relay/actor.rs b/iroh/src/magicsock/transports/relay/actor.rs index 140bfcefe77..684db1d0f6b 100644 --- a/iroh/src/magicsock/transports/relay/actor.rs +++ b/iroh/src/magicsock/transports/relay/actor.rs @@ -836,7 +836,7 @@ pub(super) struct RelayActor { cancel_token: CancellationToken, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Config { pub my_relay: Watchable>, pub secret_key: SecretKey, From 1a7a88b113cea094ee75690d06a5b588d08f303e Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Mon, 17 Nov 2025 13:54:07 +0100 Subject: [PATCH 146/164] refactor: remove Endpoint::path_selection (#3668) ## Description Remove the test-only `Endpoint::path_selection` API and instead use `Endpoint::clear_ip_transports` for `PathSelection::RelayOnly `, now that this public API was added in https://github.com/n0-computer/iroh/pull/3651. ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- iroh/bench/src/iroh.rs | 16 +++++------- iroh/examples/transfer.rs | 13 ++-------- iroh/src/endpoint.rs | 26 ------------------- iroh/src/magicsock.rs | 17 +----------- iroh/src/magicsock/endpoint_map.rs | 4 --- .../magicsock/endpoint_map/endpoint_state.rs | 4 --- 6 files changed, 9 insertions(+), 71 deletions(-) diff --git a/iroh/bench/src/iroh.rs b/iroh/bench/src/iroh.rs index 601a80179ac..8ab164e980e 100644 --- a/iroh/bench/src/iroh.rs +++ b/iroh/bench/src/iroh.rs @@ -34,11 +34,9 @@ pub fn server_endpoint( #[cfg(feature = "local-relay")] { builder = builder.insecure_skip_relay_cert_verify(relay_url.is_some()); - let path_selection = match opt.only_relay { - true => iroh::endpoint::PathSelection::RelayOnly, - false => iroh::endpoint::PathSelection::default(), - }; - builder = builder.path_selection(path_selection); + if opt.only_relay { + builder = builder.clear_ip_transports(); + } } let ep = builder .alpns(vec![ALPN.to_vec()]) @@ -95,11 +93,9 @@ pub async fn connect_client( #[cfg(feature = "local-relay")] { builder = builder.insecure_skip_relay_cert_verify(relay_url.is_some()); - let path_selection = match opt.only_relay { - true => iroh::endpoint::PathSelection::RelayOnly, - false => iroh::endpoint::PathSelection::default(), - }; - builder = builder.path_selection(path_selection); + if opt.only_relay { + builder = builder.clear_ip_transports(); + } } let endpoint = builder .alpns(vec![ALPN.to_vec()]) diff --git a/iroh/examples/transfer.rs b/iroh/examples/transfer.rs index 3f76e522bc3..3b6c54eb7bf 100644 --- a/iroh/examples/transfer.rs +++ b/iroh/examples/transfer.rs @@ -242,16 +242,7 @@ impl EndpointArgs { } if self.relay_only { - #[cfg(feature = "test-utils")] - { - builder = builder.path_selection(iroh::endpoint::PathSelection::RelayOnly) - } - #[cfg(not(feature = "test-utils"))] - { - n0_error::bail_any!( - "Must have the `discovery-local-network` enabled when using the `--mdns` flag" - ); - } + builder = builder.clear_ip_transports(); } if let Some(host) = self.dns_server { @@ -280,7 +271,7 @@ impl EndpointArgs { #[cfg(not(feature = "discovery-local-network"))] { n0_error::bail_any!( - "Must have the `test-utils` feature enabled when using the `--relay-only` flag" + "Must have the `discovery-local-network` enabled when using the `--mdns` flag" ); } } diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index cd2a1fc8e5d..51d95dc9904 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -81,18 +81,6 @@ pub use crate::magicsock::transports::TransportConfig; /// is still no connection the configured [`crate::discovery::Discovery`] will be used however. const DISCOVERY_WAIT_PERIOD: Duration = Duration::from_millis(500); -/// Defines the mode of path selection for all traffic flowing through -/// the endpoint. -#[cfg(any(test, feature = "test-utils"))] -#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] -pub enum PathSelection { - /// Uses all available paths - #[default] - All, - /// Forces all traffic to go exclusively through relays - RelayOnly, -} - /// Builder for [`Endpoint`]. /// /// By default the endpoint will generate a new random [`SecretKey`], which will result in a @@ -113,8 +101,6 @@ pub struct Builder { #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: bool, transports: Vec, - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection, max_tls_tickets: usize, } @@ -176,8 +162,6 @@ impl Builder { dns_resolver: None, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, - #[cfg(any(test, feature = "test-utils"))] - path_selection: PathSelection::default(), max_tls_tickets: DEFAULT_MAX_TLS_TICKETS, transports, } @@ -224,8 +208,6 @@ impl Builder { server_config, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: self.insecure_skip_relay_cert_verify, - // #[cfg(any(test, feature = "test-utils"))] - // path_selection: self.path_selection, metrics, }; @@ -471,14 +453,6 @@ impl Builder { self } - /// This implies we only use the relay to communicate - /// and do not attempt to do any hole punching. - #[cfg(any(test, feature = "test-utils"))] - pub fn path_selection(mut self, path_selection: PathSelection) -> Self { - self.path_selection = path_selection; - self - } - /// Set the maximum number of TLS tickets to cache. /// /// Set this to a larger value if you want to do 0rtt connections to a large diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 22ff0efd13d..58bebf7647a 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -53,8 +53,6 @@ use self::{ }; #[cfg(not(wasm_browser))] use crate::dns::DnsResolver; -// #[cfg(any(test, feature = "test-utils"))] -// use crate::endpoint::PathSelection; #[cfg(not(wasm_browser))] use crate::net_report::QuicConfig; use crate::{ @@ -136,10 +134,6 @@ pub(crate) struct Options { /// May only be used in tests. #[cfg(any(test, feature = "test-utils"))] pub(crate) insecure_skip_relay_cert_verify: bool, - - // /// Configuration for what path selection to use - // #[cfg(any(test, feature = "test-utils"))] - // pub(crate) path_selection: PathSelection, pub(crate) metrics: EndpointMetrics, } @@ -936,8 +930,6 @@ impl Handle { server_config, #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify, - // #[cfg(any(test, feature = "test-utils"))] - // path_selection, metrics, } = opts; @@ -1026,8 +1018,6 @@ impl Handle { let endpoint_map = { EndpointMap::new( secret_key.public(), - // #[cfg(any(test, feature = "test-utils"))] - // path_selection, metrics.magicsock.clone(), direct_addrs.addrs.watch(), disco.clone(), @@ -1856,12 +1846,9 @@ mod tests { use super::{EndpointIdMappedAddr, Options, endpoint_map::Source, mapped_addrs::MappedAddr}; use crate::{ - Endpoint, - RelayMode, - SecretKey, + Endpoint, RelayMode, SecretKey, discovery::static_provider::StaticProvider, dns::DnsResolver, - // endpoint::PathSelection, magicsock::{Handle, MagicSock, TransportConfig}, tls::{self, DEFAULT_MAX_TLS_TICKETS}, }; @@ -1883,7 +1870,6 @@ mod tests { #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: false, #[cfg(any(test, feature = "test-utils"))] - // path_selection: PathSelection::default(), discovery_user_data: None, metrics: Default::default(), } @@ -2319,7 +2305,6 @@ mod tests { proxy_url: None, server_config, insecure_skip_relay_cert_verify: false, - // path_selection: PathSelection::default(), metrics: Default::default(), }; let msock = MagicSock::spawn(opts).await?; diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/endpoint_map.rs index 71a4924d65d..b35768c8395 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/endpoint_map.rs @@ -12,8 +12,6 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::warn; -// #[cfg(any(test, feature = "test-utils"))] -// use crate::endpoint::PathSelection; pub(super) use self::endpoint_state::EndpointStateMessage; pub(crate) use self::endpoint_state::PathsWatcher; use self::endpoint_state::{EndpointStateActor, EndpointStateHandle}; @@ -69,8 +67,6 @@ impl EndpointMap { /// Creates a new [`EndpointMap`]. pub(super) fn new( local_endpoint_id: EndpointId, - // TODO: - // #[cfg(any(test, feature = "test-utils"))] path_selection: PathSelection, metrics: Arc, local_addrs: n0_watcher::Direct>, diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/endpoint_map/endpoint_state.rs index f51d18956c5..0d06a064c86 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/endpoint_map/endpoint_state.rs @@ -35,10 +35,6 @@ use crate::{ util::MaybeFuture, }; -// TODO: Use this -// #[cfg(any(test, feature = "test-utils"))] -// use crate::endpoint::PathSelection; - mod guarded_channel; // TODO: use this From 34f52c65f2c12f451d8be48cd3c5e0a0e2f4cc1e Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 18 Nov 2025 10:21:24 +0100 Subject: [PATCH 147/164] refactor(multipath): rename EndpointMap/EndpointState to RemoteMap/RemoteState (#3673) ## Description Renames: * renamed `endpoint_map` -> `remote_map`, `EndpointMap` -> `RemoteMap`, `endpoint_state` -> `remote_state`, `EndpointStateActor` -> `RemoteStateActor` Moved: * moved `path_state` module under `remote_state` (prev `endpoint_state`), its items are used only there and nowhere else ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --------- Co-authored-by: Floris Bruynooghe --- iroh/src/discovery.rs | 2 +- iroh/src/endpoint.rs | 2 +- iroh/src/endpoint/connection.rs | 16 ++-- iroh/src/magicsock.rs | 82 +++++++++---------- .../{endpoint_map.rs => remote_map.rs} | 72 ++++++++-------- .../remote_state.rs} | 74 +++++++++-------- .../remote_state}/guarded_channel.rs | 0 .../remote_state}/path_state.rs | 4 +- iroh/src/magicsock/transports.rs | 12 +-- iroh/src/magicsock/transports/relay/actor.rs | 7 -- 10 files changed, 132 insertions(+), 139 deletions(-) rename iroh/src/magicsock/{endpoint_map.rs => remote_map.rs} (79%) rename iroh/src/magicsock/{endpoint_map/endpoint_state.rs => remote_map/remote_state.rs} (96%) rename iroh/src/magicsock/{endpoint_map/endpoint_state => remote_map/remote_state}/guarded_channel.rs (100%) rename iroh/src/magicsock/{endpoint_map => remote_map/remote_state}/path_state.rs (86%) diff --git a/iroh/src/discovery.rs b/iroh/src/discovery.rs index 9ce4054a6b0..0f9aa46bddd 100644 --- a/iroh/src/discovery.rs +++ b/iroh/src/discovery.rs @@ -124,7 +124,7 @@ use tokio::sync::oneshot; use tracing::{Instrument, debug, error_span, warn}; pub use crate::endpoint_info::{EndpointData, EndpointInfo, ParseError, UserData}; -use crate::{Endpoint, magicsock::endpoint_map::Source}; +use crate::{Endpoint, magicsock::remote_map::Source}; #[cfg(not(wasm_browser))] pub mod dns; diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 51d95dc9904..54a8058f453 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -26,7 +26,7 @@ use url::Url; pub use super::magicsock::{ AddEndpointAddrError, DirectAddr, DirectAddrType, PathInfo, - endpoint_map::{PathInfoList, Source}, + remote_map::{PathInfoList, Source}, }; #[cfg(wasm_browser)] use crate::discovery::pkarr::PkarrResolver; diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index dfd4203f207..e1840dfa052 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -42,8 +42,8 @@ use crate::{ Endpoint, discovery::DiscoveryTask, magicsock::{ - EndpointStateActorStoppedError, - endpoint_map::{PathInfoList, PathsWatcher}, + RemoteStateActorStoppedError, + remote_map::{PathInfoList, PathsWatcher}, }, }; @@ -208,9 +208,9 @@ pub enum AuthenticationError { NoAlpn {}, } -impl From for ConnectingError { +impl From for ConnectingError { #[track_caller] - fn from(_value: EndpointStateActorStoppedError) -> Self { + fn from(_value: RemoteStateActorStoppedError) -> Self { e!(Self::InternalConsistencyError) } } @@ -221,7 +221,7 @@ impl From for ConnectingError { /// or if the remote did not set an ALPN. /// /// Otherwise returns a future that completes once the connection has been registered with the -/// magicsock. This future can return an [`EndpointStateActorStoppedError`], which will only be +/// magicsock. This future can return an [`RemoteStateActorStoppedError`], which will only be /// emitted if the endpoint is closing. /// /// The returned future is `'static`, so it can be stored without being lifetime-bound on `&ep`. @@ -229,7 +229,7 @@ fn conn_from_quinn_conn( conn: quinn::Connection, ep: &Endpoint, ) -> Result< - impl Future> + Send + 'static, + impl Future> + Send + 'static, ConnectingError, > { let (remote_id, alpn) = match static_info_from_conn(&conn) { @@ -328,7 +328,7 @@ pub struct Connecting { _discovery_drop_guard: Option, } -type RegisterWithMagicsockFut = BoxFuture>; +type RegisterWithMagicsockFut = BoxFuture>; /// In-progress connection attempt future #[derive(derive_more::Debug)] @@ -370,7 +370,7 @@ pub enum ConnectingError { }, #[error("Failure finalizing the handshake")] HandshakeFailure { source: AuthenticationError }, - #[error("internal consistency error: EndpointStateActor stopped")] + #[error("internal consistency error: RemoteStateActor stopped")] InternalConsistencyError, } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 58bebf7647a..cdc1111b1f5 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -47,8 +47,8 @@ use transports::{LocalAddrsWatch, MagicTransport, TransportConfig}; use url::Url; use self::{ - endpoint_map::{EndpointMap, EndpointStateMessage}, metrics::Metrics as MagicsockMetrics, + remote_map::{RemoteMap, RemoteStateMessage}, transports::{RelayActorConfig, Transports, TransportsSender}, }; #[cfg(not(wasm_browser))] @@ -60,22 +60,22 @@ use crate::{ disco::{self, SendAddr}, discovery::{ConcurrentDiscovery, Discovery, EndpointData, UserData}, key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, - magicsock::endpoint_map::PathsWatcher, + magicsock::remote_map::PathsWatcher, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, }; mod metrics; -pub(crate) mod endpoint_map; pub(crate) mod mapped_addrs; +pub(crate) mod remote_map; pub(crate) mod transports; -pub use self::{endpoint_map::PathInfo, metrics::Metrics}; use self::{ mapped_addrs::{EndpointIdMappedAddr, MappedAddr}, transports::Addr, }; +pub use self::{metrics::Metrics, remote_map::PathInfo}; // TODO: Use this // /// How long we consider a QAD-derived endpoint valid for. UDP NAT mappings typically @@ -102,7 +102,7 @@ pub(crate) const MAX_MULTIPATH_PATHS: u32 = 16; /// Error returned when the endpoint state actor stopped while waiting for a reply. #[stack_error(derive)] #[error("endpoint state actor stopped")] -pub(crate) struct EndpointStateActorStoppedError; +pub(crate) struct RemoteStateActorStoppedError; /// Contains options for `MagicSock::listen`. #[derive(derive_more::Debug)] @@ -183,7 +183,7 @@ pub(crate) struct MagicSock { /// If the last net_report report, reports IPv6 to be available. ipv6_reported: Arc, /// Tracks the networkmap endpoint entity for each endpoint discovery key. - pub(crate) endpoint_map: EndpointMap, + pub(crate) remote_map: RemoteMap, /// Local addresses local_addrs_watch: LocalAddrsWatch, @@ -252,7 +252,7 @@ impl MagicSock { self.local_addrs_watch.clone().get() } - /// Registers the connection in the `EndpointStateActor`. + /// Registers the connection in the `RemoteStateActor`. /// /// The actor is responsible for holepunching and opening additional paths to this /// connection. @@ -264,16 +264,16 @@ impl MagicSock { &self, remote: EndpointId, conn: WeakConnectionHandle, - ) -> impl Future> + Send + 'static + ) -> impl Future> + Send + 'static { let (tx, rx) = oneshot::channel(); - let sender = self.endpoint_map.endpoint_state_actor(remote); + let sender = self.remote_map.remote_state_actor(remote); async move { sender - .send(EndpointStateMessage::AddConnection(conn, tx)) + .send(RemoteStateMessage::AddConnection(conn, tx)) .await - .map_err(|_| EndpointStateActorStoppedError)?; - rx.await.map_err(|_| EndpointStateActorStoppedError) + .map_err(|_| RemoteStateActorStoppedError)?; + rx.await.map_err(|_| RemoteStateActorStoppedError) } } @@ -290,9 +290,9 @@ impl MagicSock { /// Returns `true` if we have at least one candidate address where we can send packets to. pub(crate) async fn has_send_address(&self, eid: EndpointId) -> bool { - let actor = self.endpoint_map.endpoint_state_actor(eid); + let actor = self.remote_map.remote_state_actor(eid); let (tx, rx) = oneshot::channel(); - if actor.send(EndpointStateMessage::CanSend(tx)).await.is_err() { + if actor.send(RemoteStateMessage::CanSend(tx)).await.is_err() { return false; } rx.await.unwrap_or(false) @@ -379,10 +379,10 @@ impl MagicSock { // TODO: Build better info to expose to the user about remote nodes. We probably want // to expose this as part of path information instead. pub(crate) async fn latency(&self, eid: EndpointId) -> Option { - let endpoint_state = self.endpoint_map.endpoint_state_actor(eid); + let remote_state = self.remote_map.remote_state_actor(eid); let (tx, rx) = oneshot::channel(); - endpoint_state - .send(EndpointStateMessage::Latency(tx)) + remote_state + .send(RemoteStateMessage::Latency(tx)) .await .ok(); rx.await.unwrap_or_default() @@ -390,10 +390,10 @@ impl MagicSock { /// Returns the socket address which can be used by the QUIC layer to dial this endpoint. pub(crate) fn get_endpoint_mapped_addr(&self, eid: EndpointId) -> EndpointIdMappedAddr { - self.endpoint_map.endpoint_mapped_addr(eid) + self.remote_map.endpoint_mapped_addr(eid) } - /// Add potential addresses for a endpoint to the `EndpointStateActor`. + /// Add potential addresses for a endpoint to the `RemoteStateActor`. /// /// This is used to add possible paths that the remote endpoint might be reachable on. They /// will be used when there is no active connection to the endpoint to attempt to establish @@ -402,7 +402,7 @@ impl MagicSock { pub(crate) async fn add_endpoint_addr( &self, mut addr: EndpointAddr, - source: endpoint_map::Source, + source: remote_map::Source, ) -> Result<(), AddEndpointAddrError> { let mut pruned: usize = 0; for my_addr in self.direct_addrs.sockaddrs() { @@ -412,8 +412,8 @@ impl MagicSock { } } if !addr.is_empty() { - // Add addr to the internal EndpointMap - self.endpoint_map + // Add addr to the internal RemoteMap + self.remote_map .add_endpoint_addr(addr.clone(), source) .await; Ok(()) @@ -522,8 +522,8 @@ impl MagicSock { // result in the wrong address family and Windows trips up on that. // // What should be done is that this dst_ip from the RecvMeta is stored in the - // EndpointState/PathState. Then on the send path it should be retrieved from the - // EndpointState/PathSate together with the send address and substituted at send time. + // RemoteState/PathState. Then on the send path it should be retrieved from the + // RemoteState/PathState together with the send address and substituted at send time. // This is relevant for IPv6 link-local addresses where the OS otherwise does not // know which interface to send from. #[cfg(not(windows))] @@ -605,7 +605,7 @@ impl MagicSock { } transports::Addr::Relay(src_url, src_endpoint) => { let mapped_addr = self - .endpoint_map + .remote_map .relay_mapped_addrs .get(&(src_url.clone(), *src_endpoint)); quinn_meta.addr = mapped_addr.private_socket_addr(); @@ -679,15 +679,15 @@ impl MagicSock { match dm { disco::Message::Ping(ping) => { self.metrics.magicsock.recv_disco_ping.inc(); - self.endpoint_map.handle_ping(ping, sender, src.clone()); + self.remote_map.handle_ping(ping, sender, src.clone()); } disco::Message::Pong(pong) => { self.metrics.magicsock.recv_disco_pong.inc(); - self.endpoint_map.handle_pong(pong, sender, src.clone()); + self.remote_map.handle_pong(pong, sender, src.clone()); } disco::Message::CallMeMaybe(cm) => { self.metrics.magicsock.recv_disco_call_me_maybe.inc(); - self.endpoint_map + self.remote_map .handle_call_me_maybe(cm, sender, src.clone()); } } @@ -1015,8 +1015,8 @@ impl Handle { let direct_addrs = DiscoveredDirectAddrs::default(); let (disco, disco_receiver) = DiscoState::new(&secret_key); - let endpoint_map = { - EndpointMap::new( + let remote_map = { + RemoteMap::new( secret_key.public(), metrics.magicsock.clone(), direct_addrs.addrs.watch(), @@ -1032,7 +1032,7 @@ impl Handle { disco, actor_sender: actor_sender.clone(), ipv6_reported, - endpoint_map, + remote_map, discovery, relay_map: relay_map.clone(), discovery_user_data: RwLock::new(discovery_user_data), @@ -1358,8 +1358,8 @@ impl Actor { // ensure we are doing an initial publish of our addresses self.msock.publish_my_addr(); - // Interval timer to remove closed `EndpointStateActor` handles from the endpoint map. - let mut endpoint_map_gc = time::interval(endpoint_map::ENDPOINT_MAP_GC_INTERVAL); + // Interval timer to remove closed `RemoteStateActor` handles from the endpoint map. + let mut remote_map_gc = time::interval(remote_map::REMOTE_MAP_GC_INTERVAL); loop { self.msock.metrics.magicsock.actor_tick_main.inc(); @@ -1472,8 +1472,8 @@ impl Actor { warn!(%dst, endpoint = %dst_key.fmt_short(), ?err, "failed to send disco message (UDP)"); } } - _ = endpoint_map_gc.tick() => { - self.msock.endpoint_map.remove_closed_endpoint_state_actors(); + _ = remote_map_gc.tick() => { + self.msock.remote_map.remove_closed_remote_state_actors(); } } } @@ -1844,7 +1844,7 @@ mod tests { use tracing::{Instrument, error, info, info_span, instrument}; use tracing_test::traced_test; - use super::{EndpointIdMappedAddr, Options, endpoint_map::Source, mapped_addrs::MappedAddr}; + use super::{EndpointIdMappedAddr, Options, mapped_addrs::MappedAddr, remote_map::Source}; use crate::{ Endpoint, RelayMode, SecretKey, discovery::static_provider::StaticProvider, @@ -2379,7 +2379,7 @@ mod tests { let msock_1 = magicsock_ep(secret_key_1.clone()).await.unwrap(); - // Generate an address not present in the EndpointMap. + // Generate an address not present in the RemoteMap. let bad_addr = EndpointIdMappedAddr::generate(); // 500ms is rather fast here. Running this locally it should always be the correct @@ -2505,9 +2505,9 @@ mod tests { }); let _accept_task = AbortOnDropHandle::new(accept_task); - // Add an empty entry in the EndpointMap of ep_1 + // Add an empty entry in the RemoteMap of ep_1 msock_1 - .endpoint_map + .remote_map .add_endpoint_addr( EndpointAddr { id: endpoint_id_2, @@ -2545,7 +2545,7 @@ mod tests { // Provide correct addressing information msock_1 - .endpoint_map + .remote_map .add_endpoint_addr( EndpointAddr { id: endpoint_id_2, @@ -2584,6 +2584,6 @@ mod tests { .expect("connection timed out"); // TODO: could remove the addresses again, send, add it back and see it recover. - // But we don't have that much private access to the EndpointMap. This will do for now. + // But we don't have that much private access to the RemoteMap. This will do for now. } } diff --git a/iroh/src/magicsock/endpoint_map.rs b/iroh/src/magicsock/remote_map.rs similarity index 79% rename from iroh/src/magicsock/endpoint_map.rs rename to iroh/src/magicsock/remote_map.rs index b35768c8395..ba3a7fc5180 100644 --- a/iroh/src/magicsock/endpoint_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -12,10 +12,10 @@ use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; use tracing::warn; -pub(super) use self::endpoint_state::EndpointStateMessage; -pub(crate) use self::endpoint_state::PathsWatcher; -use self::endpoint_state::{EndpointStateActor, EndpointStateHandle}; -pub use self::endpoint_state::{PathInfo, PathInfoList}; +pub(crate) use self::remote_state::PathsWatcher; +pub(super) use self::remote_state::RemoteStateMessage; +pub use self::remote_state::{PathInfo, PathInfoList}; +use self::remote_state::{RemoteStateActor, RemoteStateHandle}; use super::{ DirectAddr, DiscoState, MagicsockMetrics, mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, @@ -23,11 +23,10 @@ use super::{ }; use crate::disco; -mod endpoint_state; -mod path_state; +mod remote_state; -/// Interval in which handles to closed [`EndpointStateActor`]s should be removed. -pub(super) const ENDPOINT_MAP_GC_INTERVAL: Duration = Duration::from_secs(60); +/// Interval in which handles to closed [`RemoteStateActor`]s should be removed. +pub(super) const REMOTE_MAP_GC_INTERVAL: Duration = Duration::from_secs(60); // TODO: use this // /// Number of endpoints that are inactive for which we keep info about. This limit is enforced @@ -41,19 +40,19 @@ pub(super) const ENDPOINT_MAP_GC_INTERVAL: Duration = Duration::from_secs(60); /// - Has the mapped addresses we use to refer to non-IP transports destinations into IPv6 /// addressing space that is used by Quinn. #[derive(Debug)] -pub(crate) struct EndpointMap { +pub(crate) struct RemoteMap { // // State we keep about remote endpoints. // /// The actors tracking each remote endpoint. - actor_handles: Mutex>, + actor_handles: Mutex>, /// The mapping between [`EndpointId`]s and [`EndpointIdMappedAddr`]s. pub(super) endpoint_mapped_addrs: AddrMap, /// The mapping between endpoints via a relay and their [`RelayMappedAddr`]s. pub(super) relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, // - // State needed to start a new EndpointStateHandle. + // State needed to start a new RemoteStateHandle. // /// The endpoint ID of the local endpoint. local_endpoint_id: EndpointId, @@ -63,8 +62,8 @@ pub(crate) struct EndpointMap { sender: TransportsSender, } -impl EndpointMap { - /// Creates a new [`EndpointMap`]. +impl RemoteMap { + /// Creates a new [`RemoteMap`]. pub(super) fn new( local_endpoint_id: EndpointId, metrics: Arc, @@ -92,12 +91,12 @@ impl EndpointMap { self.relay_mapped_addrs .get(&(url.clone(), endpoint_addr.id)); } - let actor = self.endpoint_state_actor(endpoint_addr.id); + let actor = self.remote_state_actor(endpoint_addr.id); - // This only fails if the sender is closed. That means the EndpointStateActor has + // This only fails if the sender is closed. That means the RemoteStateActor has // stopped, which only happens during shutdown. actor - .send(EndpointStateMessage::AddEndpointAddr(endpoint_addr, source)) + .send(RemoteStateMessage::AddEndpointAddr(endpoint_addr, source)) .await .ok(); } @@ -106,24 +105,21 @@ impl EndpointMap { self.endpoint_mapped_addrs.get(&eid) } - /// Removes the handles for terminated [`EndpointStateActor`]s from the endpoint map. + /// Removes the handles for terminated [`RemoteStateActor`]s from the endpoint map. /// /// This should be called periodically to remove handles to endpoint state actors /// that have shutdown after their idle timeout expired. - pub(super) fn remove_closed_endpoint_state_actors(&self) { + pub(super) fn remove_closed_remote_state_actors(&self) { let mut handles = self.actor_handles.lock().expect("poisoned"); handles.retain(|_eid, handle| !handle.sender.is_closed()) } - /// Returns the sender for the [`EndpointStateActor`]. + /// Returns the sender for the [`RemoteStateActor`]. /// /// If needed a new actor is started on demand. /// - /// [`EndpointStateActor`]: endpoint_state::EndpointStateActor - pub(super) fn endpoint_state_actor( - &self, - eid: EndpointId, - ) -> mpsc::Sender { + /// [`RemoteStateActor`]: remote_state::RemoteStateActor + pub(super) fn remote_state_actor(&self, eid: EndpointId) -> mpsc::Sender { let mut handles = self.actor_handles.lock().expect("poisoned"); match handles.entry(eid) { hash_map::Entry::Occupied(mut entry) => { @@ -131,29 +127,29 @@ impl EndpointMap { sender } else { // The actor is dead: Start a new actor. - let (handle, sender) = self.start_endpoint_state_actor(eid); + let (handle, sender) = self.start_remote_state_actor(eid); entry.insert(handle); sender } } hash_map::Entry::Vacant(entry) => { - let (handle, sender) = self.start_endpoint_state_actor(eid); + let (handle, sender) = self.start_remote_state_actor(eid); entry.insert(handle); sender } } } - /// Starts a new endpoint state actor and returns a handle and a sender. + /// Starts a new remote state actor and returns a handle and a sender. /// /// The handle is not inserted into the endpoint map, this must be done by the caller of this function. - fn start_endpoint_state_actor( + fn start_remote_state_actor( &self, eid: EndpointId, - ) -> (EndpointStateHandle, mpsc::Sender) { - // Ensure there is a EndpointMappedAddr for this EndpointId. + ) -> (RemoteStateHandle, mpsc::Sender) { + // Ensure there is a RemoteMappedAddr for this EndpointId. self.endpoint_mapped_addrs.get(&eid); - let handle = EndpointStateActor::new( + let handle = RemoteStateActor::new( eid, self.local_endpoint_id, self.local_addrs.clone(), @@ -172,8 +168,8 @@ impl EndpointMap { warn!("DISCO Ping EndpointId mismatch, ignoring ping"); return; } - let endpoint_state = self.endpoint_state_actor(sender); - if let Err(err) = endpoint_state.try_send(EndpointStateMessage::PingReceived(msg, src)) { + let remote_state = self.remote_state_actor(sender); + if let Err(err) = remote_state.try_send(RemoteStateMessage::PingReceived(msg, src)) { // TODO: This is really, really bad and will drop pings under load. But // DISCO pings are going away with QUIC-NAT-TRAVERSAL so I don't care. warn!("DISCO Ping dropped: {err:#}"); @@ -181,8 +177,8 @@ impl EndpointMap { } pub(super) fn handle_pong(&self, msg: disco::Pong, sender: EndpointId, src: transports::Addr) { - let actor = self.endpoint_state_actor(sender); - if let Err(err) = actor.try_send(EndpointStateMessage::PongReceived(msg, src)) { + let actor = self.remote_state_actor(sender); + if let Err(err) = actor.try_send(RemoteStateMessage::PongReceived(msg, src)) { // TODO: This is really, really bad and will drop pongs under load. But // DISCO pongs are going away with QUIC-NAT-TRAVERSAL so I don't care. warn!("DISCO Pong dropped: {err:#}"); @@ -199,8 +195,8 @@ impl EndpointMap { warn!("DISCO CallMeMaybe packets should only come via relay"); return; } - let actor = self.endpoint_state_actor(sender); - if let Err(err) = actor.try_send(EndpointStateMessage::CallMeMaybeReceived(msg)) { + let actor = self.remote_state_actor(sender); + if let Err(err) = actor.try_send(RemoteStateMessage::CallMeMaybeReceived(msg)) { // TODO: This is bad and will drop call-me-maybe's under load. But // DISCO CallMeMaybe going away with QUIC-NAT-TRAVERSAL so I don't care. warn!("DISCO CallMeMaybe dropped: {err:#}"); @@ -254,7 +250,7 @@ pub enum Source { /// We established a connection on this address. /// /// Currently this means the path was in uses as [`PathId::ZERO`] when the a connection - /// was added to the `EndpointStateActor`. + /// was added to the `RemoteStateActor`. /// /// [`PathId::ZERO`]: quinn_proto::PathId::ZERO #[strum(serialize = "Connection")] diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs similarity index 96% rename from iroh/src/magicsock/endpoint_map/endpoint_state.rs rename to iroh/src/magicsock/remote_map/remote_state.rs index 0d06a064c86..56d507c6171 100644 --- a/iroh/src/magicsock/endpoint_map/endpoint_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -21,26 +21,30 @@ use tokio::sync::oneshot; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; -use self::guarded_channel::{GuardedReceiver, GuardedSender, guarded_channel}; -use super::{Source, path_state::PathState}; +use self::{ + guarded_channel::{GuardedReceiver, GuardedSender, guarded_channel}, + path_state::PathState, +}; +use super::Source; use crate::{ disco::{self}, endpoint::DirectAddr, magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, - endpoint_map::Private, mapped_addrs::{AddrMap, MappedAddr, RelayMappedAddr}, + remote_map::Private, transports::{self, OwnedTransmit, TransportsSender}, }, util::MaybeFuture, }; mod guarded_channel; +mod path_state; // TODO: use this // /// Number of addresses that are not active that we keep around per endpoint. // /// -// /// See [`EndpointState::prune_direct_addresses`]. +// /// See [`RemoteState::prune_direct_addresses`]. // pub(super) const MAX_INACTIVE_DIRECT_ADDRESSES: usize = 20; // TODO: use this @@ -67,7 +71,7 @@ mod guarded_channel; // TODO: Quinn should just do this. Also, I made this value up. const APPLICATION_ABANDON_PATH: u8 = 30; -/// The time after which an idle [`EndpointStateActor`] stops. +/// The time after which an idle [`RemoteStateActor`] stops. /// /// The actor only enters the idle state if no connections are active and no inbox senders exist /// apart from the one stored in the endpoint map. Stopping and restarting the actor in this state @@ -92,7 +96,7 @@ pub(crate) type PathAddrList = SmallVec<[(TransportAddr, PathId); 4]>; /// /// This actor manages all connections to the remote endpoint. It will trigger holepunching /// and select the best path etc. -pub(super) struct EndpointStateActor { +pub(super) struct RemoteStateActor { /// The endpoint ID of the remote endpoint. endpoint_id: EndpointId, /// The endpoint ID of the local endpoint. @@ -149,7 +153,7 @@ pub(super) struct EndpointStateActor { pending_open_paths: VecDeque, } -impl EndpointStateActor { +impl RemoteStateActor { pub(super) fn new( endpoint_id: EndpointId, local_endpoint_id: EndpointId, @@ -179,7 +183,7 @@ impl EndpointStateActor { } } - pub(super) fn start(mut self) -> EndpointStateHandle { + pub(super) fn start(mut self) -> RemoteStateHandle { let (tx, rx) = guarded_channel(16); let me = self.local_endpoint_id; let endpoint_id = self.endpoint_id; @@ -197,12 +201,12 @@ impl EndpointStateActor { } .instrument(info_span!( parent: None, - "EndpointStateActor", + "RemoteStateActor", me = %me.fmt_short(), remote = %endpoint_id.fmt_short(), )), ); - EndpointStateHandle { + RemoteStateHandle { sender: tx, _task: AbortOnDropHandle::new(task), } @@ -215,7 +219,7 @@ impl EndpointStateActor { /// discipline is needed to not turn pending for a long time. async fn run( &mut self, - mut inbox: GuardedReceiver, + mut inbox: GuardedReceiver, ) -> n0_error::Result<()> { trace!("actor started"); let idle_timeout = MaybeFuture::None; @@ -292,31 +296,31 @@ impl EndpointStateActor { /// /// Error returns are fatal and kill the actor. #[instrument(skip(self))] - async fn handle_message(&mut self, msg: EndpointStateMessage) -> n0_error::Result<()> { + async fn handle_message(&mut self, msg: RemoteStateMessage) -> n0_error::Result<()> { // trace!("handling message"); match msg { - EndpointStateMessage::SendDatagram(transmit) => { + RemoteStateMessage::SendDatagram(transmit) => { self.handle_msg_send_datagram(transmit).await?; } - EndpointStateMessage::AddConnection(handle, tx) => { + RemoteStateMessage::AddConnection(handle, tx) => { self.handle_msg_add_connection(handle, tx).await; } - EndpointStateMessage::AddEndpointAddr(addr, source) => { + RemoteStateMessage::AddEndpointAddr(addr, source) => { self.handle_msg_add_endpoint_addr(addr, source); } - EndpointStateMessage::CallMeMaybeReceived(msg) => { + RemoteStateMessage::CallMeMaybeReceived(msg) => { self.handle_msg_call_me_maybe_received(msg).await; } - EndpointStateMessage::PingReceived(ping, src) => { + RemoteStateMessage::PingReceived(ping, src) => { self.handle_msg_ping_received(ping, src).await; } - EndpointStateMessage::PongReceived(pong, src) => { + RemoteStateMessage::PongReceived(pong, src) => { self.handle_msg_pong_received(pong, src); } - EndpointStateMessage::CanSend(tx) => { + RemoteStateMessage::CanSend(tx) => { self.handle_msg_can_send(tx); } - EndpointStateMessage::Latency(tx) => { + RemoteStateMessage::Latency(tx) => { self.handle_msg_latency(tx); } } @@ -337,7 +341,7 @@ impl EndpointStateActor { Ok(()) } - /// Handles [`EndpointStateMessage::SendDatagram`]. + /// Handles [`RemoteStateMessage::SendDatagram`]. /// /// Error returns are fatal and kill the actor. async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) -> n0_error::Result<()> { @@ -359,7 +363,7 @@ impl EndpointStateActor { Ok(()) } - /// Handles [`EndpointStateMessage::AddConnection`]. + /// Handles [`RemoteStateMessage::AddConnection`]. /// /// Error returns are fatal and kill the actor. async fn handle_msg_add_connection( @@ -435,7 +439,7 @@ impl EndpointStateActor { .ok(); } - /// Handles [`EndpointStateMessage::AddEndpointAddr`]. + /// Handles [`RemoteStateMessage::AddEndpointAddr`]. fn handle_msg_add_endpoint_addr(&mut self, addr: EndpointAddr, source: Source) { for sockaddr in addr.ip_addrs() { let addr = transports::Addr::from(sockaddr); @@ -456,7 +460,7 @@ impl EndpointStateActor { trace!("added addressing information"); } - /// Handles [`EndpointStateMessage::CallMeMaybeReceived`]. + /// Handles [`RemoteStateMessage::CallMeMaybeReceived`]. async fn handle_msg_call_me_maybe_received(&mut self, msg: disco::CallMeMaybe) { event!( target: "iroh::_events::call_me_maybe::recv", @@ -485,7 +489,7 @@ impl EndpointStateActor { } } - /// Handles [`EndpointStateMessage::PingReceived`]. + /// Handles [`RemoteStateMessage::PingReceived`]. async fn handle_msg_ping_received(&mut self, ping: disco::Ping, src: transports::Addr) { let transports::Addr::Ip(addr) = src else { warn!("received ping via relay transport, ignored"); @@ -520,7 +524,7 @@ impl EndpointStateActor { self.trigger_holepunching().await; } - /// Handles [`EndpointStateMessage::PongReceived`]. + /// Handles [`RemoteStateMessage::PongReceived`]. fn handle_msg_pong_received(&mut self, pong: disco::Pong, src: transports::Addr) { let Some(state) = self.paths.get(&src) else { warn!(path = ?src, ?self.paths, "ignoring DISCO Pong for unknown path"); @@ -542,13 +546,13 @@ impl EndpointStateActor { self.open_path(&src); } - /// Handles [`EndpointStateMessage::CanSend`]. + /// Handles [`RemoteStateMessage::CanSend`]. fn handle_msg_can_send(&self, tx: oneshot::Sender) { let can_send = !self.paths.is_empty(); tx.send(can_send).ok(); } - /// Handles [`EndpointStateMessage::Latency`]. + /// Handles [`RemoteStateMessage::Latency`]. fn handle_msg_latency(&self, tx: oneshot::Sender>) { let rtt = self.selected_path.get().and_then(|addr| { for conn_state in self.connections.values() { @@ -816,7 +820,7 @@ impl EndpointStateActor { event: Result, ) { let Ok(event) = event else { - warn!("missed a PathEvent, EndpointStateActor lagging"); + warn!("missed a PathEvent, RemoteStateActor lagging"); // TODO: Is it possible to recover using the sync APIs to figure out what the // state of the connection and it's paths are? return; @@ -1014,9 +1018,9 @@ impl EndpointStateActor { } } -/// Messages to send to the [`EndpointStateActor`]. +/// Messages to send to the [`RemoteStateActor`]. #[derive(derive_more::Debug)] -pub(crate) enum EndpointStateMessage { +pub(crate) enum RemoteStateMessage { /// Sends a datagram to all known paths. /// /// Used to send QUIC Initial packets. If there is no working direct path this will @@ -1056,18 +1060,18 @@ pub(crate) enum EndpointStateMessage { Latency(oneshot::Sender>), } -/// A handle to a [`EndpointStateActor`]. +/// A handle to a [`RemoteStateActor`]. /// /// Dropping this will stop the actor. The actor will also stop after an idle timeout /// if it has no connections, an empty inbox, and no other senders than the one stored /// in the endpoint map exist. #[derive(Debug)] -pub(super) struct EndpointStateHandle { - /// Sender for the channel into the [`EndpointStateActor`]. +pub(super) struct RemoteStateHandle { + /// Sender for the channel into the [`RemoteStateActor`]. /// /// This is a [`GuardedSender`], from which we can get a sender but only if the receiver /// hasn't been closed. - pub(super) sender: GuardedSender, + pub(super) sender: GuardedSender, _task: AbortOnDropHandle<()>, } diff --git a/iroh/src/magicsock/endpoint_map/endpoint_state/guarded_channel.rs b/iroh/src/magicsock/remote_map/remote_state/guarded_channel.rs similarity index 100% rename from iroh/src/magicsock/endpoint_map/endpoint_state/guarded_channel.rs rename to iroh/src/magicsock/remote_map/remote_state/guarded_channel.rs diff --git a/iroh/src/magicsock/endpoint_map/path_state.rs b/iroh/src/magicsock/remote_map/remote_state/path_state.rs similarity index 86% rename from iroh/src/magicsock/endpoint_map/path_state.rs rename to iroh/src/magicsock/remote_map/remote_state/path_state.rs index 8a097a5edc7..83eac70a604 100644 --- a/iroh/src/magicsock/endpoint_map/path_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state/path_state.rs @@ -10,10 +10,10 @@ use crate::disco::TransactionId; /// The state of a single path to the remote endpoint. /// /// Each path is identified by the destination [`transports::Addr`] and they are stored in -/// the [`EndpointStateActor::paths`] map. +/// the [`RemoteStateActor::paths`] map. /// /// [`transports::Addr`]: super::transports::Addr -/// [`EndpointStateActor::paths`]: super::endpoint_state::EndpointStateActor +/// [`RemoteStateActor::paths`]: super::RemoteStateActor #[derive(Debug, Default)] pub(super) struct PathState { /// How we learned about this path, and when. diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 5ed652928a2..d6a31f72566 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -15,7 +15,7 @@ use relay::{RelayNetworkChangeSender, RelaySender}; use tokio_util::sync::CancellationToken; use tracing::{debug, error, instrument, trace, warn}; -use super::{MagicSock, endpoint_map::EndpointStateMessage, mapped_addrs::MultipathMappedAddr}; +use super::{MagicSock, mapped_addrs::MultipathMappedAddr, remote_map::RemoteStateMessage}; use crate::{metrics::EndpointMetrics, net_report::Report}; #[cfg(not(wasm_browser))] @@ -727,7 +727,7 @@ impl quinn::UdpSender for MagicSender { MultipathMappedAddr::Mixed(mapped_addr) => { let Some(node_id) = self .msock - .endpoint_map + .remote_map .endpoint_mapped_addrs .lookup(&mapped_addr) else { @@ -743,9 +743,9 @@ impl quinn::UdpSender for MagicSender { "oops, flub didn't think this would happen"); } - let sender = self.msock.endpoint_map.endpoint_state_actor(node_id); + let sender = self.msock.remote_map.remote_state_actor(node_id); let transmit = OwnedTransmit::from(quinn_transmit); - return match sender.try_send(EndpointStateMessage::SendDatagram(transmit)) { + return match sender.try_send(RemoteStateMessage::SendDatagram(transmit)) { Ok(()) => { trace!(dst = ?mapped_addr, dst_node = %node_id.fmt_short(), "sent transmit"); Poll::Ready(Ok(())) @@ -756,7 +756,7 @@ impl quinn::UdpSender for MagicSender { // a lost datagram. // TODO: Revisit this: we might want to do something better. debug!(dst = ?mapped_addr, dst_node = %node_id.fmt_short(), - "EndpointStateActor inbox {err:#}, dropped transmit"); + "RemoteStateActor inbox {err:#}, dropped transmit"); Poll::Ready(Ok(())) } }; @@ -764,7 +764,7 @@ impl quinn::UdpSender for MagicSender { MultipathMappedAddr::Relay(relay_mapped_addr) => { match self .msock - .endpoint_map + .remote_map .relay_mapped_addrs .lookup(&relay_mapped_addr) { diff --git a/iroh/src/magicsock/transports/relay/actor.rs b/iroh/src/magicsock/transports/relay/actor.rs index 684db1d0f6b..55b908cef84 100644 --- a/iroh/src/magicsock/transports/relay/actor.rs +++ b/iroh/src/magicsock/transports/relay/actor.rs @@ -314,10 +314,6 @@ impl ActiveRelayActor { /// /// Primarily switches between the dialing and connected states. async fn run(mut self) { - // TODO(frando): decide what this metric means, it's either wrong here or in endpoint_state.rs. - // From the existing description, it is wrong here. - // self.metrics.num_relay_conns_added.inc(); - let mut backoff = Self::build_backoff(); while let Err(err) = self.run_once().await { @@ -341,9 +337,6 @@ impl ActiveRelayActor { } } debug!("exiting"); - // TODO(frando): decide what this metric means, it's either wrong here or in endpoint_state.rs. - // From the existing description, it is wrong here. - // self.metrics.num_relay_conns_removed.inc(); } fn build_backoff() -> impl Backoff { From d538b11a4e437207d97100f21f12901d5a194122 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 18 Nov 2025 11:12:27 +0100 Subject: [PATCH 148/164] multipath: merge main (#3674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description Merges main and adapts for the changes from #3619 ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --------- Co-authored-by: Rüdiger Klaehn Co-authored-by: Friedel Ziegelmayer --- iroh/src/discovery.rs | 15 +- iroh/src/discovery/static_provider.rs | 20 +- iroh/src/endpoint.rs | 4 +- iroh/src/endpoint/connection.rs | 844 ++++--------------- iroh/src/magicsock.rs | 28 +- iroh/src/magicsock/transports/relay/actor.rs | 1 + 6 files changed, 212 insertions(+), 700 deletions(-) diff --git a/iroh/src/discovery.rs b/iroh/src/discovery.rs index 0f9aa46bddd..939ed7abfb0 100644 --- a/iroh/src/discovery.rs +++ b/iroh/src/discovery.rs @@ -893,12 +893,10 @@ mod tests { let (ep2, _guard2) = new_endpoint(&mut rng, |ep| disco_shared.create_discovery(ep.id())).await; - let ep1_wrong_addr = EndpointAddr { - id: ep1.id(), - addrs: [TransportAddr::Ip("240.0.0.1:1000".parse().unwrap())] - .into_iter() - .collect(), - }; + let ep1_wrong_addr = EndpointAddr::from_parts( + ep1.id(), + [TransportAddr::Ip("240.0.0.1:1000".parse().unwrap())], + ); let _conn = ep2.connect(ep1_wrong_addr, TEST_ALPN).await?; Ok(()) } @@ -1043,10 +1041,7 @@ mod test_dns_pkarr { .await?; println!("resolved {resolved:?}"); - let expected_addr = EndpointAddr { - id: endpoint_id, - addrs: relay_url.into_iter().collect(), - }; + let expected_addr = EndpointAddr::from_parts(endpoint_id, relay_url); assert_eq!(resolved.to_endpoint_addr(), expected_addr); assert_eq!(resolved.user_data(), Some(&user_data)); diff --git a/iroh/src/discovery/static_provider.rs b/iroh/src/discovery/static_provider.rs index 05e16b85d69..edb0c601c0a 100644 --- a/iroh/src/discovery/static_provider.rs +++ b/iroh/src/discovery/static_provider.rs @@ -248,12 +248,10 @@ mod tests { .await?; let key = SecretKey::from_bytes(&[0u8; 32]); - let addr = EndpointAddr { - id: key.public(), - addrs: [TransportAddr::Relay("https://example.com".parse()?)] - .into_iter() - .collect(), - }; + let addr = EndpointAddr::from_parts( + key.public(), + [TransportAddr::Relay("https://example.com".parse()?)], + ); let user_data = Some("foobar".parse().unwrap()); let endpoint_info = EndpointInfo::from(addr.clone()).with_user_data(user_data.clone()); discovery.add_endpoint_info(endpoint_info.clone()); @@ -280,12 +278,10 @@ mod tests { async fn test_provenance() -> Result { let discovery = StaticProvider::with_provenance("foo"); let key = SecretKey::from_bytes(&[0u8; 32]); - let addr = EndpointAddr { - id: key.public(), - addrs: [TransportAddr::Relay("https://example.com".parse()?)] - .into_iter() - .collect(), - }; + let addr = EndpointAddr::from_parts( + key.public(), + [TransportAddr::Relay("https://example.com".parse()?)], + ); discovery.add_endpoint_info(addr); let mut stream = discovery.resolve(key.public()).unwrap(); let item = stream.next().await.unwrap()?; diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 54a8058f453..e2fabf9e3f2 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -69,8 +69,8 @@ pub use quinn_proto::{ pub use self::connection::{ Accept, Accepting, AlpnError, AuthenticationError, Connecting, ConnectingError, Connection, - Incoming, IncomingZeroRttConnection, OutgoingZeroRttConnection, RemoteEndpointIdError, - ZeroRttStatus, + ConnectionState, HandshakeCompleted, Incoming, IncomingZeroRtt, IncomingZeroRttConnection, + OutgoingZeroRtt, OutgoingZeroRttConnection, RemoteEndpointIdError, ZeroRttStatus, }; pub use crate::magicsock::transports::TransportConfig; diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index e1840dfa052..456ff8e3f8d 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -232,7 +232,7 @@ fn conn_from_quinn_conn( impl Future> + Send + 'static, ConnectingError, > { - let (remote_id, alpn) = match static_info_from_conn(&conn) { + let info = match static_info_from_conn(&conn) { Ok(val) => val, Err(auth_err) => { // If the authentication error raced with a connection error, the connection @@ -246,24 +246,22 @@ fn conn_from_quinn_conn( }; // Register this connection with the magicsock. - let fut = ep.msock.register_connection(remote_id, conn.weak_handle()); + let fut = ep + .msock + .register_connection(info.endpoint_id, conn.weak_handle()); Ok(async move { let paths = fut.await?; Ok(Connection { - paths, - remote_id, - alpn, + data: HandshakeCompletedData { info, paths }, inner: conn, }) }) } -fn static_info_from_conn( - conn: &quinn::Connection, -) -> Result<(EndpointId, Vec), AuthenticationError> { - let remote_id = remote_id_from_quinn_conn(conn)?; +fn static_info_from_conn(conn: &quinn::Connection) -> Result { + let endpoint_id = remote_id_from_quinn_conn(conn)?; let alpn = alpn_from_quinn_conn(conn).ok_or_else(|| e!(AuthenticationError::NoAlpn))?; - Ok((remote_id, alpn)) + Ok(StaticInfo { endpoint_id, alpn }) } /// Returns the [`EndpointId`] from the peer's TLS certificate. @@ -429,7 +427,7 @@ impl Connecting { pub fn into_0rtt(self) -> Result { match self.inner.into_0rtt() { Ok((quinn_conn, zrtt_accepted)) => { - let handshake_completed_fut: BoxFuture<_> = Box::pin({ + let accepted: BoxFuture<_> = Box::pin({ let quinn_conn = quinn_conn.clone(); async move { let accepted = zrtt_accepted.await; @@ -441,9 +439,10 @@ impl Connecting { }) } }); - Ok(OutgoingZeroRttConnection { + let accepted = accepted.shared(); + Ok(Connection { inner: quinn_conn, - handshake_completed_fut: handshake_completed_fut.shared(), + data: OutgoingZeroRttData { accepted }, }) } Err(inner) => Err(Self { inner, ..self }), @@ -523,7 +522,7 @@ impl Accepting { .inner .into_0rtt() .expect("incoming connections can always be converted to 0-RTT"); - let handshake_completed_fut: BoxFuture<_> = Box::pin({ + let accepted: BoxFuture<_> = Box::pin({ let quinn_conn = quinn_conn.clone(); async move { zrtt_accepted.await; @@ -531,9 +530,10 @@ impl Accepting { Ok(conn) } }); + let accepted = accepted.shared(); IncomingZeroRttConnection { inner: quinn_conn, - handshake_completed_fut: handshake_completed_fut.shared(), + data: IncomingZeroRttData { accepted }, } } @@ -577,11 +577,7 @@ impl Future for Accepting { /// /// Look at the [`OutgoingZeroRttConnection::handshake_completed`] method for /// more details. -#[derive(Debug, Clone)] -pub struct OutgoingZeroRttConnection { - inner: quinn::Connection, - handshake_completed_fut: Shared>>, -} +pub type OutgoingZeroRttConnection = Connection; /// Returned from [`OutgoingZeroRttConnection::handshake_completed`]. #[derive(Debug, Clone)] @@ -595,30 +591,116 @@ pub enum ZeroRttStatus { Rejected(Connection), } -impl OutgoingZeroRttConnection { - /// Waits until the full handshake occurs and returns a [`ZeroRttStatus`]. - /// - /// If `ZeroRttStatus::Accepted` is returned, than any streams created before - /// the handshake has completed can still be used. - /// - /// If `ZeroRttStatus::Rejected` is returned, than any streams created before - /// the handshake will error and any data sent should be re-sent on a - /// new stream. - /// - /// This may fail with [`ConnectingError::ConnectionError`], if there was - /// some general failure with the connection, such as a network timeout since - /// we initiated the connection. - /// - /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side - /// doesn't use the right TLS authentication, which usually every iroh endpoint - /// uses and requires. - /// - /// Thus, those errors should only occur if someone connects to you with a - /// modified iroh endpoint or with a plain QUIC client. - pub async fn handshake_completed(self) -> Result { - self.handshake_completed_fut.await - } +/// A QUIC connection on the server-side that can possibly accept 0-RTT data. +/// +/// It is very similar to a `Connection`, but the `IncomingZeroRttConnection::remote_id` +/// and `IncomingZeroRttConnection::alpn` may not be set yet, since the handshake has +/// not necessarily occurred yet. +/// +/// If the `IncomingZeroRttConnection` has rejected 0-RTT or does not have enough information +/// to accept 0-RTT, any received 0-RTT packets will simply be dropped before +/// reaching any receive streams. +/// +/// Any streams that are created to send or receive data can continue to be used +/// even after the handshake has completed and we are no longer in a 0-RTT +/// situation. +/// +/// Use the [`IncomingZeroRttConnection::handshake_completed`] method to get a [`Connection`] from a +/// `IncomingZeroRttConnection`. This waits until 0-RTT connection has completed +/// the handshake and can now confidently derive the ALPN and the +/// [`EndpointId`] of the remote endpoint. +pub type IncomingZeroRttConnection = Connection; + +/// A QUIC connection. +/// +/// If all references to a connection (including every clone of the Connection handle, +/// streams of incoming streams, and the various stream types) have been dropped, then the +/// connection will be automatically closed with an error_code of 0 and an empty reason. You +/// can also close the connection explicitly by calling [`Connection::close`]. +/// +/// Closing the connection immediately abandons efforts to deliver data to the peer. Upon +/// receiving CONNECTION_CLOSE the peer may drop any stream data not yet delivered to the +/// application. [`Connection::close`] describes in more detail how to gracefully close a +/// connection without losing application data. +/// +/// May be cloned to obtain another handle to the same connection. +#[derive(Debug, Clone)] +pub struct Connection { + inner: quinn::Connection, + /// State-specific information + data: State::Data, +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct HandshakeCompletedData { + info: StaticInfo, + paths: PathsWatcher, +} + +/// Static info from a completed TLS handshake. +#[derive(Debug, Clone)] +struct StaticInfo { + endpoint_id: EndpointId, + alpn: Vec, +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct IncomingZeroRttData { + accepted: Shared>>, +} + +#[doc(hidden)] +#[derive(Debug, Clone)] +pub struct OutgoingZeroRttData { + accepted: Shared>>, +} + +mod sealed { + pub trait Sealed {} +} + +/// Trait to track the state of a [`Connection`] at compile time. +pub trait ConnectionState: sealed::Sealed { + /// State-specific data stored in the [`Connection`]. + type Data: std::fmt::Debug + Clone; +} + +/// Marker type for a connection that has completed the handshake. +#[derive(Debug, Clone)] +pub struct HandshakeCompleted; + +/// Marker type for a connection that is in the incoming 0-RTT state. +#[derive(Debug, Clone)] +pub struct IncomingZeroRtt; + +/// Marker type for a connection that is in the outgoing 0-RTT state. +#[derive(Debug, Clone)] +pub struct OutgoingZeroRtt; + +impl sealed::Sealed for HandshakeCompleted {} +impl ConnectionState for HandshakeCompleted { + type Data = HandshakeCompletedData; +} + +impl sealed::Sealed for IncomingZeroRtt {} +impl ConnectionState for IncomingZeroRtt { + type Data = IncomingZeroRttData; +} + +impl sealed::Sealed for OutgoingZeroRtt {} +impl ConnectionState for OutgoingZeroRtt { + type Data = OutgoingZeroRttData; +} + +#[allow(missing_docs)] +#[stack_error(add_meta, derive)] +#[error("Protocol error: no remote id available")] +#[derive(Clone)] +pub struct RemoteEndpointIdError; +impl Connection { /// Initiates a new outgoing unidirectional stream. /// /// Streams are cheap and instantaneous to open unless blocked by flow control. As a @@ -806,11 +888,6 @@ impl OutgoingZeroRttConnection { self.inner.handshake_data() } - /// Extracts the ALPN protocol from the peer's handshake data. - pub fn alpn(&self) -> Option> { - alpn_from_quinn_conn(&self.inner) - } - /// Cryptographic identity of the peer. /// /// The dynamic type returned is determined by the configured [`Session`]. For the @@ -824,18 +901,6 @@ impl OutgoingZeroRttConnection { self.inner.peer_identity() } - /// Returns the [`EndpointId`] from the peer's TLS certificate. - /// - /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is - /// included in the TLS certificate presented during the handshake when connecting. - /// This function allows you to get the [`EndpointId`] of the remote endpoint of this - /// connection. - /// - /// [`PublicKey`]: iroh_base::PublicKey - pub fn remote_id(&self) -> Result { - remote_id_from_quinn_conn(&self.inner) - } - /// A stable identifier for this connection. /// /// Peer addresses and connection IDs can change, but this value will remain fixed for @@ -888,627 +953,92 @@ impl OutgoingZeroRttConnection { } } -/// A QUIC connection on the server-side that can possibly accept 0-RTT data. -/// -/// It is very similar to a `Connection`, but the `IncomingZeroRttConnection::remote_id` -/// and `IncomingZeroRttConnection::alpn` may not be set yet, since the handshake has -/// not necessarily occurred yet. -/// -/// If the `IncomingZeroRttConnection` has rejected 0-RTT or does not have enough information -/// to accept 0-RTT, any received 0-RTT packets will simply be dropped before -/// reaching any receive streams. -/// -/// Any streams that are created to send or receive data can continue to be used -/// even after the handshake has completed and we are no longer in a 0-RTT -/// situation. -/// -/// Use the [`IncomingZeroRttConnection::handshake_completed`] method to get a [`Connection`] from a -/// `IncomingZeroRttConnection`. This waits until 0-RTT connection has completed -/// the handshake and can now confidently derive the ALPN and the -/// [`EndpointId`] of the remote endpoint. -#[derive(Debug)] -pub struct IncomingZeroRttConnection { - inner: quinn::Connection, - handshake_completed_fut: Shared>>, -} +impl Connection { + /// Extracts the ALPN protocol from the peer's handshake data. + pub fn alpn(&self) -> &[u8] { + &self.data.info.alpn + } -impl IncomingZeroRttConnection { - /// Waits until the full handshake occurs and then returns a [`Connection`]. - /// - /// This may fail with [`ConnectingError::ConnectionError`], if there was - /// some general failure with the connection, such as a network timeout since - /// we accepted the connection. + /// Returns the [`EndpointId`] from the peer's TLS certificate. /// - /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side - /// doesn't use the right TLS authentication, which usually every iroh endpoint - /// uses and requires. + /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is + /// included in the TLS certificate presented during the handshake when connecting. + /// This function allows you to get the [`EndpointId`] of the remote endpoint of this + /// connection. /// - /// Thus, those errors should only occur if someone connects to you with a - /// modified iroh endpoint or with a plain QUIC client. - pub async fn handshake_completed(self) -> Result { - self.handshake_completed_fut.await + /// [`PublicKey`]: iroh_base::PublicKey + pub fn remote_id(&self) -> EndpointId { + self.data.info.endpoint_id } - /// Initiates a new outgoing unidirectional stream. + /// Returns a [`Watcher`] for the network paths of this connection. /// - /// Streams are cheap and instantaneous to open unless blocked by flow control. As a - /// consequence, the peer won’t be notified that a stream has been opened until the - /// stream is actually used. - #[inline] - pub fn open_uni(&self) -> OpenUni<'_> { - self.inner.open_uni() - } - - /// Initiates a new outgoing bidirectional stream. + /// A connection can have several network paths to the remote endpoint, commonly there + /// will be a path via the relay server and a holepunched path. /// - /// Streams are cheap and instantaneous to open unless blocked by flow control. As a - /// consequence, the peer won't be notified that a stream has been opened until the - /// stream is actually used. Calling [`open_bi`] then waiting on the [`RecvStream`] - /// without writing anything to [`SendStream`] will never succeed. + /// The watcher is updated whenever a path is opened or closed, or when the path selected + /// for transmission changes (see [`PathInfo::is_selected`]). /// - /// [`open_bi`]: Connection::open_bi - /// [`SendStream`]: quinn::SendStream - /// [`RecvStream`]: quinn::RecvStream - #[inline] - pub fn open_bi(&self) -> OpenBi<'_> { - self.inner.open_bi() + /// The [`PathInfoList`] returned from the watcher contains a [`PathInfo`] for each + /// transmission path. + /// + /// [`PathInfo::is_selected`]: crate::magicsock::PathInfo::is_selected + /// [`PathInfo`]: crate::magicsock::PathInfo + pub fn paths(&self) -> impl Watcher + Unpin + Send + Sync + 'static { + self.data.paths.clone() } +} - /// Accepts the next incoming uni-directional stream. - #[inline] - pub fn accept_uni(&self) -> AcceptUni<'_> { - self.inner.accept_uni() +impl Connection { + /// Extracts the ALPN protocol from the peer's handshake data. + pub fn alpn(&self) -> Option> { + alpn_from_quinn_conn(&self.inner) } - /// Accept the next incoming bidirectional stream. + /// Waits until the full handshake occurs and then returns a [`Connection`]. /// - /// **Important Note**: The peer that calls [`open_bi`] must write to its [`SendStream`] - /// before the peer `Connection` is able to accept the stream using - /// `accept_bi()`. Calling [`open_bi`] then waiting on the [`RecvStream`] without - /// writing anything to the connected [`SendStream`] will never succeed. + /// This may fail with [`ConnectingError::ConnectionError`], if there was + /// some general failure with the connection, such as a network timeout since + /// we accepted the connection. /// - /// [`open_bi`]: Connection::open_bi - /// [`SendStream`]: quinn::SendStream - /// [`RecvStream`]: quinn::RecvStream - #[inline] - pub fn accept_bi(&self) -> AcceptBi<'_> { - self.inner.accept_bi() - } - - /// Receives an application datagram. - #[inline] - pub fn read_datagram(&self) -> ReadDatagram<'_> { - self.inner.read_datagram() - } - - /// Wait for the connection to be closed for any reason. + /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side + /// doesn't use the right TLS authentication, which usually every iroh endpoint + /// uses and requires. /// - /// Despite the return type's name, closed connections are often not an error condition - /// at the application layer. Cases that might be routine include - /// [`ConnectionError::LocallyClosed`] and [`ConnectionError::ApplicationClosed`]. - #[inline] - pub async fn closed(&self) -> ConnectionError { - self.inner.closed().await + /// Thus, those errors should only occur if someone connects to you with a + /// modified iroh endpoint or with a plain QUIC client. + pub async fn handshake_completed(&self) -> Result { + self.data.accepted.clone().await } +} - /// If the connection is closed, the reason why. - /// - /// Returns `None` if the connection is still open. - #[inline] - pub fn close_reason(&self) -> Option { - self.inner.close_reason() +impl Connection { + /// Extracts the ALPN protocol from the peer's handshake data. + pub fn alpn(&self) -> Option> { + alpn_from_quinn_conn(&self.inner) } - /// Closes the connection immediately. + /// Waits until the full handshake occurs and returns a [`ZeroRttStatus`]. /// - /// Pending operations will fail immediately with [`ConnectionError::LocallyClosed`]. No - /// more data is sent to the peer and the peer may drop buffered data upon receiving the - /// CONNECTION_CLOSE frame. + /// If `ZeroRttStatus::Accepted` is returned, than any streams created before + /// the handshake has completed can still be used. /// - /// `error_code` and `reason` are not interpreted, and are provided directly to the - /// peer. + /// If `ZeroRttStatus::Rejected` is returned, than any streams created before + /// the handshake will error and any data sent should be re-sent on a + /// new stream. /// - /// `reason` will be truncated to fit in a single packet with overhead; to improve odds - /// that it is preserved in full, it should be kept under 1KiB. + /// This may fail with [`ConnectingError::ConnectionError`], if there was + /// some general failure with the connection, such as a network timeout since + /// we initiated the connection. /// - /// # Gracefully closing a connection + /// This may fail with [`ConnectingError::HandshakeFailure`], if the other side + /// doesn't use the right TLS authentication, which usually every iroh endpoint + /// uses and requires. /// - /// Only the peer last receiving application data can be certain that all data is - /// delivered. The only reliable action it can then take is to close the connection, - /// potentially with a custom error code. The delivery of the final CONNECTION_CLOSE - /// frame is very likely if both endpoints stay online long enough, calling - /// [`Endpoint::close`] will wait to provide sufficient time. Otherwise, the remote peer - /// will time out the connection, provided that the idle timeout is not disabled. - /// - /// The sending side can not guarantee all stream data is delivered to the remote - /// application. It only knows the data is delivered to the QUIC stack of the remote - /// endpoint. Once the local side sends a CONNECTION_CLOSE frame in response to calling - /// [`close`] the remote endpoint may drop any data it received but is as yet - /// undelivered to the application, including data that was acknowledged as received to - /// the local endpoint. - /// - /// [`close`]: Connection::close - #[inline] - pub fn close(&self, error_code: VarInt, reason: &[u8]) { - self.inner.close(error_code, reason) - } - - /// Transmits `data` as an unreliable, unordered application datagram. - /// - /// Application datagrams are a low-level primitive. They may be lost or delivered out - /// of order, and `data` must both fit inside a single QUIC packet and be smaller than - /// the maximum dictated by the peer. - #[inline] - pub fn send_datagram(&self, data: bytes::Bytes) -> Result<(), SendDatagramError> { - self.inner.send_datagram(data) - } - - // TODO: It seems `SendDatagram` is not yet exposed by quinn. This has been fixed - // upstream and will be in the next release. - // /// Transmits `data` as an unreliable, unordered application datagram - // /// - // /// Unlike [`send_datagram()`], this method will wait for buffer space during congestion - // /// conditions, which effectively prioritizes old datagrams over new datagrams. - // /// - // /// See [`send_datagram()`] for details. - // /// - // /// [`send_datagram()`]: Connection::send_datagram - // #[inline] - // pub fn send_datagram_wait(&self, data: bytes::Bytes) -> SendDatagram<'_> { - // self.inner.send_datagram_wait(data) - // } - - /// Computes the maximum size of datagrams that may be passed to [`send_datagram`]. - /// - /// Returns `None` if datagrams are unsupported by the peer or disabled locally. - /// - /// This may change over the lifetime of a connection according to variation in the path - /// MTU estimate. The peer can also enforce an arbitrarily small fixed limit, but if the - /// peer's limit is large this is guaranteed to be a little over a kilobyte at minimum. - /// - /// Not necessarily the maximum size of received datagrams. - /// - /// [`send_datagram`]: Self::send_datagram - #[inline] - pub fn max_datagram_size(&self) -> Option { - self.inner.max_datagram_size() - } - - /// Bytes available in the outgoing datagram buffer. - /// - /// When greater than zero, calling [`send_datagram`] with a - /// datagram of at most this size is guaranteed not to cause older datagrams to be - /// dropped. - /// - /// [`send_datagram`]: Self::send_datagram - #[inline] - pub fn datagram_send_buffer_space(&self) -> usize { - self.inner.datagram_send_buffer_space() - } - - /// Current best estimate of this connection's latency (round-trip-time). - #[inline] - pub fn rtt(&self) -> Duration { - self.inner.rtt() - } - - /// Returns connection statistics. - #[inline] - pub fn stats(&self) -> ConnectionStats { - self.inner.stats() - } - - /// Current state of the congestion control algorithm, for debugging purposes. - #[inline] - pub fn congestion_state(&self) -> Box { - self.inner.congestion_state() - } - - /// Parameters negotiated during the handshake. - /// - /// Guaranteed to return `Some` on fully established connections or after - /// [`Connecting::handshake_data()`] succeeds. See that method's documentations for - /// details on the returned value. - /// - /// [`Connection::handshake_data()`]: crate::endpoint::Connecting::handshake_data - #[inline] - pub fn handshake_data(&self) -> Option> { - self.inner.handshake_data() - } - - /// Extracts the ALPN protocol from the peer's handshake data. - pub fn alpn(&self) -> Option> { - alpn_from_quinn_conn(&self.inner) - } - - /// Cryptographic identity of the peer. - /// - /// The dynamic type returned is determined by the configured [`Session`]. For the - /// default `rustls` session, the return value can be [`downcast`] to a - /// Vec<[rustls::pki_types::CertificateDer]> - /// - /// [`Session`]: quinn_proto::crypto::Session - /// [`downcast`]: Box::downcast - #[inline] - pub fn peer_identity(&self) -> Option> { - self.inner.peer_identity() - } - - /// Returns the [`EndpointId`] from the peer's TLS certificate. - /// - /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is - /// included in the TLS certificate presented during the handshake when connecting. - /// This function allows you to get the [`EndpointId`] of the remote endpoint of this - /// connection. - /// - /// [`PublicKey`]: iroh_base::PublicKey - pub fn remote_id(&self) -> Result { - remote_id_from_quinn_conn(&self.inner) - } - - /// A stable identifier for this connection. - /// - /// Peer addresses and connection IDs can change, but this value will remain fixed for - /// the lifetime of the connection. - #[inline] - pub fn stable_id(&self) -> usize { - self.inner.stable_id() - } - - /// Derives keying material from this connection's TLS session secrets. - /// - /// When both peers call this method with the same `label` and `context` - /// arguments and `output` buffers of equal length, they will get the - /// same sequence of bytes in `output`. These bytes are cryptographically - /// strong and pseudorandom, and are suitable for use as keying material. - /// - /// See [RFC5705](https://tools.ietf.org/html/rfc5705) for more information. - #[inline] - pub fn export_keying_material( - &self, - output: &mut [u8], - label: &[u8], - context: &[u8], - ) -> Result<(), quinn_proto::crypto::ExportKeyingMaterialError> { - self.inner.export_keying_material(output, label, context) - } - - /// Modifies the number of unidirectional streams that may be concurrently opened. - /// - /// No streams may be opened by the peer unless fewer than `count` are already - /// open. Large `count`s increase both minimum and worst-case memory consumption. - #[inline] - pub fn set_max_concurrent_uni_streams(&self, count: VarInt) { - self.inner.set_max_concurrent_uni_streams(count) - } - - /// See [`quinn_proto::TransportConfig::receive_window`]. - #[inline] - pub fn set_receive_window(&self, receive_window: VarInt) { - self.inner.set_receive_window(receive_window) - } - - /// Modifies the number of bidirectional streams that may be concurrently opened. - /// - /// No streams may be opened by the peer unless fewer than `count` are already - /// open. Large `count`s increase both minimum and worst-case memory consumption. - #[inline] - pub fn set_max_concurrent_bi_streams(&self, count: VarInt) { - self.inner.set_max_concurrent_bi_streams(count) - } -} - -/// A QUIC connection. -/// -/// If all references to a connection (including every clone of the Connection handle, -/// streams of incoming streams, and the various stream types) have been dropped, then the -/// connection will be automatically closed with an error_code of 0 and an empty reason. You -/// can also close the connection explicitly by calling [`Connection::close`]. -/// -/// Closing the connection immediately abandons efforts to deliver data to the peer. Upon -/// receiving CONNECTION_CLOSE the peer may drop any stream data not yet delivered to the -/// application. [`Connection::close`] describes in more detail how to gracefully close a -/// connection without losing application data. -/// -/// May be cloned to obtain another handle to the same connection. -#[derive(derive_more::Debug, Clone)] -pub struct Connection { - inner: quinn::Connection, - remote_id: EndpointId, - alpn: Vec, - paths: PathsWatcher, -} - -#[allow(missing_docs)] -#[stack_error(add_meta, derive)] -#[error("Protocol error: no remote id available")] -#[derive(Clone)] -pub struct RemoteEndpointIdError; - -impl Connection { - /// Initiates a new outgoing unidirectional stream. - /// - /// Streams are cheap and instantaneous to open unless blocked by flow control. As a - /// consequence, the peer won’t be notified that a stream has been opened until the - /// stream is actually used. - #[inline] - pub fn open_uni(&self) -> OpenUni<'_> { - self.inner.open_uni() - } - - /// Initiates a new outgoing bidirectional stream. - /// - /// Streams are cheap and instantaneous to open unless blocked by flow control. As a - /// consequence, the peer won't be notified that a stream has been opened until the - /// stream is actually used. Calling [`open_bi`] then waiting on the [`RecvStream`] - /// without writing anything to [`SendStream`] will never succeed. - /// - /// [`open_bi`]: Connection::open_bi - /// [`SendStream`]: quinn::SendStream - /// [`RecvStream`]: quinn::RecvStream - #[inline] - pub fn open_bi(&self) -> OpenBi<'_> { - self.inner.open_bi() - } - - /// Accepts the next incoming uni-directional stream. - #[inline] - pub fn accept_uni(&self) -> AcceptUni<'_> { - self.inner.accept_uni() - } - - /// Accept the next incoming bidirectional stream. - /// - /// **Important Note**: The peer that calls [`open_bi`] must write to its [`SendStream`] - /// before the peer `Connection` is able to accept the stream using - /// `accept_bi()`. Calling [`open_bi`] then waiting on the [`RecvStream`] without - /// writing anything to the connected [`SendStream`] will never succeed. - /// - /// [`open_bi`]: Connection::open_bi - /// [`SendStream`]: quinn::SendStream - /// [`RecvStream`]: quinn::RecvStream - #[inline] - pub fn accept_bi(&self) -> AcceptBi<'_> { - self.inner.accept_bi() - } - - /// Receives an application datagram. - #[inline] - pub fn read_datagram(&self) -> ReadDatagram<'_> { - self.inner.read_datagram() - } - - /// Wait for the connection to be closed for any reason. - /// - /// Despite the return type's name, closed connections are often not an error condition - /// at the application layer. Cases that might be routine include - /// [`ConnectionError::LocallyClosed`] and [`ConnectionError::ApplicationClosed`]. - #[inline] - pub async fn closed(&self) -> ConnectionError { - self.inner.closed().await - } - - /// If the connection is closed, the reason why. - /// - /// Returns `None` if the connection is still open. - #[inline] - pub fn close_reason(&self) -> Option { - self.inner.close_reason() - } - - /// Closes the connection immediately. - /// - /// Pending operations will fail immediately with [`ConnectionError::LocallyClosed`]. No - /// more data is sent to the peer and the peer may drop buffered data upon receiving the - /// CONNECTION_CLOSE frame. - /// - /// `error_code` and `reason` are not interpreted, and are provided directly to the - /// peer. - /// - /// `reason` will be truncated to fit in a single packet with overhead; to improve odds - /// that it is preserved in full, it should be kept under 1KiB. - /// - /// # Gracefully closing a connection - /// - /// Only the peer last receiving application data can be certain that all data is - /// delivered. The only reliable action it can then take is to close the connection, - /// potentially with a custom error code. The delivery of the final CONNECTION_CLOSE - /// frame is very likely if both endpoints stay online long enough, calling - /// [`Endpoint::close`] will wait to provide sufficient time. Otherwise, the remote peer - /// will time out the connection, provided that the idle timeout is not disabled. - /// - /// The sending side can not guarantee all stream data is delivered to the remote - /// application. It only knows the data is delivered to the QUIC stack of the remote - /// endpoint. Once the local side sends a CONNECTION_CLOSE frame in response to calling - /// [`close`] the remote endpoint may drop any data it received but is as yet - /// undelivered to the application, including data that was acknowledged as received to - /// the local endpoint. - /// - /// [`close`]: Connection::close - #[inline] - pub fn close(&self, error_code: VarInt, reason: &[u8]) { - self.inner.close(error_code, reason) - } - - /// Transmits `data` as an unreliable, unordered application datagram. - /// - /// Application datagrams are a low-level primitive. They may be lost or delivered out - /// of order, and `data` must both fit inside a single QUIC packet and be smaller than - /// the maximum dictated by the peer. - #[inline] - pub fn send_datagram(&self, data: bytes::Bytes) -> Result<(), SendDatagramError> { - self.inner.send_datagram(data) - } - - // TODO: It seems `SendDatagram` is not yet exposed by quinn. This has been fixed - // upstream and will be in the next release. - // /// Transmits `data` as an unreliable, unordered application datagram - // /// - // /// Unlike [`send_datagram()`], this method will wait for buffer space during congestion - // /// conditions, which effectively prioritizes old datagrams over new datagrams. - // /// - // /// See [`send_datagram()`] for details. - // /// - // /// [`send_datagram()`]: Connection::send_datagram - // #[inline] - // pub fn send_datagram_wait(&self, data: bytes::Bytes) -> SendDatagram<'_> { - // self.inner.send_datagram_wait(data) - // } - - /// Computes the maximum size of datagrams that may be passed to [`send_datagram`]. - /// - /// Returns `None` if datagrams are unsupported by the peer or disabled locally. - /// - /// This may change over the lifetime of a connection according to variation in the path - /// MTU estimate. The peer can also enforce an arbitrarily small fixed limit, but if the - /// peer's limit is large this is guaranteed to be a little over a kilobyte at minimum. - /// - /// Not necessarily the maximum size of received datagrams. - /// - /// [`send_datagram`]: Self::send_datagram - #[inline] - pub fn max_datagram_size(&self) -> Option { - self.inner.max_datagram_size() - } - - /// Bytes available in the outgoing datagram buffer. - /// - /// When greater than zero, calling [`send_datagram`] with a - /// datagram of at most this size is guaranteed not to cause older datagrams to be - /// dropped. - /// - /// [`send_datagram`]: Self::send_datagram - #[inline] - pub fn datagram_send_buffer_space(&self) -> usize { - self.inner.datagram_send_buffer_space() - } - - /// Current best estimate of this connection's latency (round-trip-time). - #[inline] - pub fn rtt(&self) -> Duration { - self.inner.rtt() - } - - /// Returns connection statistics. - #[inline] - pub fn stats(&self) -> ConnectionStats { - self.inner.stats() - } - - /// Current state of the congestion control algorithm, for debugging purposes. - #[inline] - pub fn congestion_state(&self) -> Box { - self.inner.congestion_state() - } - - /// Parameters negotiated during the handshake. - /// - /// Guaranteed to return `Some` on fully established connections or after - /// [`Connecting::handshake_data()`] succeeds. See that method's documentations for - /// details on the returned value. - /// - /// [`Connection::handshake_data()`]: crate::endpoint::Connecting::handshake_data - #[inline] - pub fn handshake_data(&self) -> Option> { - self.inner.handshake_data() - } - - /// Extracts the ALPN protocol from the peer's handshake data. - pub fn alpn(&self) -> &[u8] { - &self.alpn - } - - /// Cryptographic identity of the peer. - /// - /// The dynamic type returned is determined by the configured [`Session`]. For the - /// default `rustls` session, the return value can be [`downcast`] to a - /// Vec<[rustls::pki_types::CertificateDer]> - /// - /// [`Session`]: quinn_proto::crypto::Session - /// [`downcast`]: Box::downcast - #[inline] - pub fn peer_identity(&self) -> Option> { - self.inner.peer_identity() - } - - /// Returns the [`EndpointId`] from the peer's TLS certificate. - /// - /// The [`PublicKey`] of an endpoint is also known as an [`EndpointId`]. This [`PublicKey`] is - /// included in the TLS certificate presented during the handshake when connecting. - /// This function allows you to get the [`EndpointId`] of the remote endpoint of this - /// connection. - /// - /// [`PublicKey`]: iroh_base::PublicKey - pub fn remote_id(&self) -> EndpointId { - self.remote_id - } - - /// A stable identifier for this connection. - /// - /// Peer addresses and connection IDs can change, but this value will remain fixed for - /// the lifetime of the connection. - #[inline] - pub fn stable_id(&self) -> usize { - self.inner.stable_id() - } - - /// Returns a [`Watcher`] for the network paths of this connection. - /// - /// A connection can have several network paths to the remote endpoint, commonly there - /// will be a path via the relay server and a holepunched path. - /// - /// The watcher is updated whenever a path is opened or closed, or when the path selected - /// for transmission changes (see [`PathInfo::is_selected`]). - /// - /// The [`PathInfoList`] returned from the watcher contains a [`PathInfo`] for each - /// transmission path. - /// - /// [`PathInfo::is_selected`]: crate::magicsock::PathInfo::is_selected - /// [`PathInfo`]: crate::magicsock::PathInfo - pub fn paths(&self) -> impl Watcher + Unpin + Send + Sync + 'static { - self.paths.clone() - } - - /// Derives keying material from this connection's TLS session secrets. - /// - /// When both peers call this method with the same `label` and `context` - /// arguments and `output` buffers of equal length, they will get the - /// same sequence of bytes in `output`. These bytes are cryptographically - /// strong and pseudorandom, and are suitable for use as keying material. - /// - /// See [RFC5705](https://tools.ietf.org/html/rfc5705) for more information. - #[inline] - pub fn export_keying_material( - &self, - output: &mut [u8], - label: &[u8], - context: &[u8], - ) -> Result<(), quinn_proto::crypto::ExportKeyingMaterialError> { - self.inner.export_keying_material(output, label, context) - } - - /// Modifies the number of unidirectional streams that may be concurrently opened. - /// - /// No streams may be opened by the peer unless fewer than `count` are already - /// open. Large `count`s increase both minimum and worst-case memory consumption. - #[inline] - pub fn set_max_concurrent_uni_streams(&self, count: VarInt) { - self.inner.set_max_concurrent_uni_streams(count) - } - - /// See [`quinn_proto::TransportConfig::receive_window`]. - #[inline] - pub fn set_receive_window(&self, receive_window: VarInt) { - self.inner.set_receive_window(receive_window) - } - - /// Modifies the number of bidirectional streams that may be concurrently opened. - /// - /// No streams may be opened by the peer unless fewer than `count` are already - /// open. Large `count`s increase both minimum and worst-case memory consumption. - #[inline] - pub fn set_max_concurrent_bi_streams(&self, count: VarInt) { - self.inner.set_max_concurrent_bi_streams(count) + /// Thus, those errors should only occur if someone connects to you with a + /// modified iroh endpoint or with a plain QUIC client. + pub async fn handshake_completed(&self) -> Result { + self.data.accepted.clone().await } } diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index cdc1111b1f5..33ca0ccf557 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -2430,12 +2430,8 @@ mod tests { .ip_addrs() .get() .into_iter() - .map(|x| TransportAddr::Ip(x.addr)) - .collect(); - let endpoint_addr_2 = EndpointAddr { - id: endpoint_id_2, - addrs, - }; + .map(|x| TransportAddr::Ip(x.addr)); + let endpoint_addr_2 = EndpointAddr::from_parts(endpoint_id_2, addrs); msock_1 .add_endpoint_addr( endpoint_addr_2, @@ -2509,10 +2505,7 @@ mod tests { msock_1 .remote_map .add_endpoint_addr( - EndpointAddr { - id: endpoint_id_2, - addrs: Default::default(), - }, + EndpointAddr::from_parts(endpoint_id_2, []), Source::NamedApp { name: "test".into(), }, @@ -2544,18 +2537,15 @@ mod tests { info!("first connect timed out as expected"); // Provide correct addressing information + let addrs = msock_2 + .ip_addrs() + .get() + .into_iter() + .map(|x| TransportAddr::Ip(x.addr)); msock_1 .remote_map .add_endpoint_addr( - EndpointAddr { - id: endpoint_id_2, - addrs: msock_2 - .ip_addrs() - .get() - .into_iter() - .map(|x| TransportAddr::Ip(x.addr)) - .collect(), - }, + EndpointAddr::from_parts(endpoint_id_2, addrs), Source::NamedApp { name: "test".into(), }, diff --git a/iroh/src/magicsock/transports/relay/actor.rs b/iroh/src/magicsock/transports/relay/actor.rs index 55b908cef84..f26b3d73338 100644 --- a/iroh/src/magicsock/transports/relay/actor.rs +++ b/iroh/src/magicsock/transports/relay/actor.rs @@ -1459,6 +1459,7 @@ mod tests { #[tokio::test] #[traced_test] + #[ignore = "flaky"] async fn test_active_relay_inactive() -> Result { let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?; From 59a7e851fdb5c40b5000beeeb4aa8470610a9f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Tue, 18 Nov 2025 12:47:32 +0100 Subject: [PATCH 149/164] test(iroh): Fix `test_two_devices_setup_teardown` hanging (#3675) ## Description Avoid potentially busy looping in a tokio task. I think this blocking leads to tokio not being able to close the runtime properly. --- iroh/src/magicsock.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 33ca0ccf557..772ee2e88e4 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -2058,10 +2058,8 @@ mod tests { let ep2_addr_stream = ep2.watch_addr().stream(); let mut addr_stream = MergeBounded::from_iter([ep1_addr_stream, ep2_addr_stream]); let task = tokio::spawn(async move { - loop { - while let Some(addr) = addr_stream.next().await { - discovery.add_endpoint_info(addr); - } + while let Some(addr) = addr_stream.next().await { + discovery.add_endpoint_info(addr); } }); From 01545ee6b0ec5355f95a478220182699c0ad2d84 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 18 Nov 2025 16:17:47 +0100 Subject: [PATCH 150/164] refactor(multipath): move discovery into `EndpointStateActor` (#3645) ## Description Fixes #3642 This moves discovery handling fully into the `EndpointStateActor`. The pub(crate) interface to trigger discovery and get a EndpointMappedAddr is now `Magicsock::resolve_remote`, which sends the provided addresses to the EndpointStateActor. The actor starts discovery if it does not have a selected path and if discovery is not running. It returns either immediately if there are any known paths, or waits for discovery to produce at least one result or an error. Once this returns, `resolve_remote` returns either with a EndpointMappedAddr or with the discovery error. This means the current behavior is kept: We only start `quinn::Endpoint::connect` once we have at least one transport address for the remote. If not, we return the discovery error immediately from `iroh::Endpoint::connect`. This opens the door for us to easily tune when to run discovery in other siutations, e.g. when all available paths to a remote are closed. However, for now this PR still only starts discovery when `Endpoint::connect` is called and no path is selected at the moment. ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- Cargo.lock | 3 +- iroh/Cargo.toml | 1 + iroh/src/discovery.rs | 166 +----------- iroh/src/endpoint.rs | 172 ++----------- iroh/src/endpoint/connection.rs | 22 +- iroh/src/magicsock.rs | 169 ++++++------- iroh/src/magicsock/remote_map.rs | 25 +- iroh/src/magicsock/remote_map/remote_state.rs | 239 +++++++++++------- .../remote_map/remote_state/path_state.rs | 138 +++++++++- 9 files changed, 398 insertions(+), 537 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69d24526694..197ad1f5915 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2184,6 +2184,7 @@ dependencies = [ "smallvec", "strum", "swarm-discovery", + "sync_wrapper", "time", "tokio", "tokio-stream", @@ -5128,7 +5129,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index 882cf61e6bb..f06fff30bc7 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -76,6 +76,7 @@ futures-util = "0.3" # test_utils axum = { version = "0.8", optional = true } +sync_wrapper = { version = "1.0.2", features = ["futures"] } # non-wasm-in-browser dependencies [target.'cfg(not(all(target_family = "wasm", target_os = "unknown")))'.dependencies] diff --git a/iroh/src/discovery.rs b/iroh/src/discovery.rs index 939ed7abfb0..aa6902e70ac 100644 --- a/iroh/src/discovery.rs +++ b/iroh/src/discovery.rs @@ -113,18 +113,11 @@ use std::sync::{Arc, RwLock}; use iroh_base::{EndpointAddr, EndpointId}; -use n0_error::{AnyError, e, ensure, stack_error}; -use n0_future::{ - boxed::BoxStream, - stream::StreamExt, - task::{self, AbortOnDropHandle}, - time::{self, Duration}, -}; -use tokio::sync::oneshot; -use tracing::{Instrument, debug, error_span, warn}; +use n0_error::{AnyError, e, stack_error}; +use n0_future::boxed::BoxStream; +use crate::Endpoint; pub use crate::endpoint_info::{EndpointData, EndpointInfo, ParseError, UserData}; -use crate::{Endpoint, magicsock::remote_map::Source}; #[cfg(not(wasm_browser))] pub mod dns; @@ -218,15 +211,16 @@ impl IntoDiscoveryError { #[allow(missing_docs)] #[stack_error(derive, add_meta)] #[non_exhaustive] +#[derive(Clone)] pub enum DiscoveryError { #[error("No discovery service configured")] NoServiceConfigured, - #[error("Discovery produced no results for {}", endpoint_id.fmt_short())] - NoResults { endpoint_id: EndpointId }, + #[error("Discovery produced no results")] + NoResults, #[error("Service '{provenance}' error")] User { provenance: &'static str, - source: AnyError, + source: Arc, }, } @@ -237,10 +231,7 @@ impl DiscoveryError { provenance: &'static str, source: T, ) -> Self { - e!(DiscoveryError::User { - provenance, - source: AnyError::from_std(source) - }) + Self::from_err_any(provenance, AnyError::from_std(source)) } /// Creates a new user error from an arbitrary boxed error type. @@ -249,10 +240,7 @@ impl DiscoveryError { provenance: &'static str, source: Box, ) -> Self { - e!(DiscoveryError::User { - provenance, - source: AnyError::from_std_box(source) - }) + Self::from_err_any(provenance, AnyError::from_std_box(source)) } /// Creates a new user error from an arbitrary error type that can be converted into [`AnyError`]. @@ -260,7 +248,7 @@ impl DiscoveryError { pub fn from_err_any(provenance: &'static str, source: impl Into) -> Self { e!(DiscoveryError::User { provenance, - source: source.into() + source: Arc::new(source.into()) }) } } @@ -502,148 +490,18 @@ impl Discovery for ConcurrentDiscovery { } } -/// A wrapper around a tokio task which runs a node discovery. -pub(super) struct DiscoveryTask { - on_first_rx: oneshot::Receiver>, - _task: AbortOnDropHandle<()>, -} - -impl DiscoveryTask { - /// Starts a discovery task. - pub(super) fn start(ep: Endpoint, endpoint_id: EndpointId) -> Result { - ensure!( - !ep.discovery().is_empty(), - DiscoveryError::NoServiceConfigured - ); - let (on_first_tx, on_first_rx) = oneshot::channel(); - let me = ep.id(); - let task = task::spawn( - async move { Self::run(ep, endpoint_id, on_first_tx).await }.instrument( - error_span!("discovery", me = %me.fmt_short(), endpoint = %endpoint_id.fmt_short()), - ), - ); - Ok(Self { - _task: AbortOnDropHandle::new(task), - on_first_rx, - }) - } - - /// Starts a discovery task after a delay and only if no path to the endpoint was recently active. - /// - /// This returns `None` if we received data or control messages from the remote endpoint - /// recently enough. If not it returns a [`DiscoveryTask`]. - /// - /// If `delay` is set, the [`DiscoveryTask`] will first wait for `delay` and then check again - /// if we recently received messages from remote endpoint. If true, the task will abort. - /// Otherwise, or if no `delay` is set, the discovery will be started. - pub(super) fn start_after_delay( - ep: &Endpoint, - endpoint_id: EndpointId, - delay: Duration, - ) -> Result, DiscoveryError> { - // If discovery is not needed, don't even spawn a task. - ensure!( - !ep.discovery().is_empty(), - DiscoveryError::NoServiceConfigured - ); - let (on_first_tx, on_first_rx) = oneshot::channel(); - let ep = ep.clone(); - let me = ep.id(); - let task = task::spawn( - async move { - time::sleep(delay).await; - Self::run(ep, endpoint_id, on_first_tx).await - } - .instrument( - error_span!("discovery", me = %me.fmt_short(), endpoint = %endpoint_id.fmt_short()), - ), - ); - Ok(Some(Self { - _task: AbortOnDropHandle::new(task), - on_first_rx, - })) - } - - /// Waits until the discovery task produced at least one result. - pub(super) async fn first_arrived(&mut self) -> Result<(), DiscoveryError> { - let fut = &mut self.on_first_rx; - fut.await.expect("sender dropped")?; - Ok(()) - } - - fn create_stream( - ep: &Endpoint, - endpoint_id: EndpointId, - ) -> Result>, DiscoveryError> { - ensure!( - !ep.discovery().is_empty(), - DiscoveryError::NoServiceConfigured - ); - let stream = ep - .discovery() - .resolve(endpoint_id) - .ok_or_else(|| e!(DiscoveryError::NoResults { endpoint_id }))?; - Ok(stream) - } - - async fn run( - ep: Endpoint, - endpoint_id: EndpointId, - on_first_tx: oneshot::Sender>, - ) { - let mut stream = match Self::create_stream(&ep, endpoint_id) { - Ok(stream) => stream, - Err(err) => { - on_first_tx.send(Err(err)).ok(); - return; - } - }; - let mut on_first_tx = Some(on_first_tx); - debug!("starting"); - loop { - match stream.next().await { - Some(Ok(r)) => { - let provenance = r.provenance; - let endpoint_addr = r.to_endpoint_addr(); - if endpoint_addr.is_empty() { - debug!(%provenance, "empty address found"); - continue; - } - debug!(%provenance, addr = ?endpoint_addr, "new address found"); - let source = Source::Discovery { - name: provenance.to_string(), - }; - ep.add_endpoint_addr(endpoint_addr, source).await.ok(); - - if let Some(tx) = on_first_tx.take() { - tx.send(Ok(())).ok(); - } - } - Some(Err(err)) => { - warn!(?err, "discovery service produced error"); - break; - } - None => break, - } - } - if let Some(tx) = on_first_tx.take() { - tx.send(Err(e!(DiscoveryError::NoResults { endpoint_id }))) - .ok(); - } - } -} - #[cfg(test)] mod tests { use std::{ collections::HashMap, net::SocketAddr, sync::{Arc, Mutex}, - time::SystemTime, + time::{Duration, SystemTime}, }; use iroh_base::{EndpointAddr, SecretKey, TransportAddr}; use n0_error::{AnyError as Error, Result, StackResultExt}; + use n0_future::{StreamExt, time}; use quinn::{IdleTimeout, TransportConfig}; use rand::{CryptoRng, Rng, SeedableRng}; use tokio_util::task::AbortOnDropHandle; diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index e2fabf9e3f2..b608091411f 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -18,14 +18,14 @@ use std::{ use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr}; use iroh_relay::{RelayConfig, RelayMap}; -use n0_error::{e, ensure, stack_error}; +use n0_error::{ensure, stack_error}; use n0_future::time::Duration; use n0_watcher::Watcher; use tracing::{debug, instrument, trace, warn}; use url::Url; pub use super::magicsock::{ - AddEndpointAddrError, DirectAddr, DirectAddrType, PathInfo, + DirectAddr, DirectAddrType, PathInfo, remote_map::{PathInfoList, Source}, }; #[cfg(wasm_browser)] @@ -33,14 +33,11 @@ use crate::discovery::pkarr::PkarrResolver; #[cfg(not(wasm_browser))] use crate::dns::DnsResolver; use crate::{ - discovery::{ - ConcurrentDiscovery, DiscoveryError, DiscoveryTask, DynIntoDiscovery, IntoDiscovery, - UserData, - }, + discovery::{ConcurrentDiscovery, DiscoveryError, DynIntoDiscovery, IntoDiscovery, UserData}, endpoint::presets::Preset, magicsock::{ self, HEARTBEAT_INTERVAL, Handle, MAX_MULTIPATH_PATHS, PATH_MAX_IDLE_TIMEOUT, - mapped_addrs::{EndpointIdMappedAddr, MappedAddr}, + RemoteStateActorStoppedError, mapped_addrs::MappedAddr, }, metrics::EndpointMetrics, net_report::Report, @@ -74,13 +71,6 @@ pub use self::connection::{ }; pub use crate::magicsock::transports::TransportConfig; -/// The delay to fall back to discovery when direct addresses fail. -/// -/// When a connection is attempted with an [`EndpointAddr`] containing direct addresses the -/// [`Endpoint`] assumes one of those addresses probably works. If after this delay there -/// is still no connection the configured [`crate::discovery::Discovery`] will be used however. -const DISCOVERY_WAIT_PERIOD: Duration = Duration::from_millis(500); - /// Builder for [`Endpoint`]. /// /// By default the endpoint will generate a new random [`SecretKey`], which will result in a @@ -522,18 +512,22 @@ pub struct Endpoint { #[allow(missing_docs)] #[stack_error(derive, add_meta, from_sources)] #[non_exhaustive] +#[allow(private_interfaces)] pub enum ConnectWithOptsError { - #[error(transparent)] - AddEndpointAddr { source: AddEndpointAddrError }, #[error("Connecting to ourself is not supported")] SelfConnect, #[error("No addressing information available")] - NoAddress { source: GetMappingAddressError }, + NoAddress { source: DiscoveryError }, #[error("Unable to connect to remote")] Quinn { #[error(std_err)] source: quinn_proto::ConnectError, }, + #[error("Internal consistency error")] + InternalConsistencyError { + /// Private source type, cannot be created publicly. + source: RemoteStateActorStoppedError, + }, } #[allow(missing_docs)] @@ -565,18 +559,6 @@ pub enum BindError { }, } -#[allow(missing_docs)] -#[stack_error(derive, add_meta)] -#[non_exhaustive] -pub enum GetMappingAddressError { - #[error("Discovery service required due to missing addressing information")] - DiscoveryStart { source: DiscoveryError }, - #[error("Discovery service failed")] - Discover { source: DiscoveryError }, - #[error("No addressing information found")] - NoAddress, -} - impl Endpoint { // The ordering of public methods is reflected directly in the documentation. This is // roughly ordered by what is most commonly needed by users, but grouped in similar @@ -704,39 +686,21 @@ impl Endpoint { options: ConnectOptions, ) -> Result { let endpoint_addr: EndpointAddr = endpoint_addr.into(); - tracing::Span::current().record( - "remote", - tracing::field::display(endpoint_addr.id.fmt_short()), - ); + let endpoint_id = endpoint_addr.id; + + tracing::Span::current().record("remote", tracing::field::display(endpoint_id.fmt_short())); // Connecting to ourselves is not supported. - ensure!( - endpoint_addr.id != self.id(), - ConnectWithOptsError::SelfConnect - ); + ensure!(endpoint_id != self.id(), ConnectWithOptsError::SelfConnect); - if !endpoint_addr.is_empty() { - self.add_endpoint_addr(endpoint_addr.clone(), Source::App) - .await?; - } - let endpoint_id = endpoint_addr.id; - let ip_addresses: Vec<_> = endpoint_addr.ip_addrs().cloned().collect(); - let relay_url = endpoint_addr.relay_urls().next().cloned(); trace!( dst_endpoint_id = %endpoint_id.fmt_short(), - ?relay_url, - ?ip_addresses, + relay_url = ?endpoint_addr.relay_urls().next().cloned(), + ip_addresses = ?endpoint_addr.ip_addrs().cloned().collect::>(), "connecting", ); - // When we start a connection we want to send the QUIC Initial packets on all the - // known paths for the remote endpoint. For this we use an EndpointIdMappedAddr as - // destination for Quinn. Start discovery for this endpoint if it's enabled and we have - // no valid or verified address information for this endpoint. Dropping the discovery - // cancels any still running task. - let (mapped_addr, _discovery_drop_guard) = self - .get_mapping_addr_and_maybe_start_discovery(endpoint_addr) - .await?; + let mapped_addr = self.msock.resolve_remote(endpoint_addr).await??; let transport_config = options .transport_config @@ -764,12 +728,7 @@ impl Endpoint { .endpoint() .connect_with(client_config, dest_addr, server_name)?; - Ok(Connecting::new( - connect, - self.clone(), - endpoint_id, - _discovery_drop_guard, - )) + Ok(Connecting::new(connect, self.clone(), endpoint_id)) } /// Accepts an incoming connection on the endpoint. @@ -787,43 +746,6 @@ impl Endpoint { } } - // # Methods for manipulating the internal state about other endpoints. - - /// Informs this [`Endpoint`] about addresses of the iroh endpoint. - /// - /// This updates the local state for the remote endpoint. If the provided [`EndpointAddr`] - /// contains a [`RelayUrl`] this will be used as the new relay server for this endpoint. If - /// it contains any new IP endpoints they will also be stored and tried when next - /// connecting to this endpoint. Any address that matches this endpoint's direct addresses will be - /// silently ignored. - /// - /// The *source* is used for logging exclusively and will not be stored. - /// - /// # Using endpoint discovery instead - /// - /// It is strongly advised to use endpoint discovery using the [`StaticProvider`] instead. - /// This provides more flexibility and future proofing. - /// - /// # Errors - /// - /// Will return an error if we attempt to add our own [`EndpointId`] to the endpoint map or - /// if the direct addresses are a subset of ours. - /// - /// [`StaticProvider`]: crate::discovery::static_provider::StaticProvider - /// [`RelayUrl`]: crate::RelayUrl - pub(crate) async fn add_endpoint_addr( - &self, - endpoint_addr: EndpointAddr, - source: Source, - ) -> Result<(), AddEndpointAddrError> { - // Connecting to ourselves is not supported. - ensure!( - endpoint_addr.id != self.id(), - AddEndpointAddrError::OwnAddress - ); - self.msock.add_endpoint_addr(endpoint_addr, source).await - } - // # Getter methods for properties of this Endpoint itself. /// Returns the secret_key of this endpoint. @@ -1212,62 +1134,6 @@ impl Endpoint { // # Remaining private methods - /// Return the quic mapped address for this `endpoint_id` and possibly start discovery - /// services if discovery is enabled on this magic endpoint. - /// - /// This will launch discovery in all cases except if: - /// 1) we do not have discovery enabled - /// 2) we have discovery enabled, but already have at least one verified, unexpired - /// addresses for this `endpoint_id` - /// - /// # Errors - /// - /// This method may fail if we have no way of dialing the endpoint. This can occur if - /// we were given no dialing information in the [`EndpointAddr`] and no discovery - /// services were configured or if discovery failed to fetch any dialing information. - async fn get_mapping_addr_and_maybe_start_discovery( - &self, - endpoint_addr: EndpointAddr, - ) -> Result<(EndpointIdMappedAddr, Option), GetMappingAddressError> { - let endpoint_id = endpoint_addr.id; - - // Only return a mapped addr if we have some way of dialing this endpoint, in other - // words, we have either a relay URL or at least one direct address. - let addr = if self.msock.has_send_address(endpoint_id).await { - Some(self.msock.get_endpoint_mapped_addr(endpoint_id)) - } else { - None - }; - match addr { - Some(maddr) => { - // We have some way of dialing this endpoint, but that doesn't mean we can - // connect to any of these addresses. Start discovery after a small delay. - let discovery = - DiscoveryTask::start_after_delay(self, endpoint_id, DISCOVERY_WAIT_PERIOD) - .ok() - .flatten(); - Ok((maddr, discovery)) - } - - None => { - // We have no known addresses or relay URLs for this endpoint. - // So, we start a discovery task and wait for the first result to arrive, and - // only then continue, because otherwise we wouldn't have any - // path to the remote endpoint. - let res = DiscoveryTask::start(self.clone(), endpoint_id); - let mut discovery = - res.map_err(|err| e!(GetMappingAddressError::DiscoveryStart, err))?; - discovery - .first_arrived() - .await - .map_err(|err| e!(GetMappingAddressError::Discover, err))?; - - let addr = self.msock.get_endpoint_mapped_addr(endpoint_id); - Ok((addr, Some(discovery))) - } - } - } - #[cfg(test)] pub(crate) fn magic_sock(&self) -> Handle { self.msock.clone() diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index 456ff8e3f8d..8e099017da0 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -40,7 +40,6 @@ use tracing::warn; use crate::{ Endpoint, - discovery::DiscoveryTask, magicsock::{ RemoteStateActorStoppedError, remote_map::{PathInfoList, PathsWatcher}, @@ -208,13 +207,6 @@ pub enum AuthenticationError { NoAlpn {}, } -impl From for ConnectingError { - #[track_caller] - fn from(_value: RemoteStateActorStoppedError) -> Self { - e!(Self::InternalConsistencyError) - } -} - /// Converts a `quinn::Connection` to a `Connection`. /// /// Returns an error if there was a connection error, the handshake data has not completed @@ -321,9 +313,6 @@ pub struct Connecting { ep: Endpoint, /// `Some(remote_id)` if this is an outgoing connection, `None` if this is an incoming conn remote_endpoint_id: EndpointId, - /// We run discovery as long as we haven't established a connection yet. - #[debug("Option")] - _discovery_drop_guard: Option, } type RegisterWithMagicsockFut = BoxFuture>; @@ -360,6 +349,7 @@ pub enum AlpnError { #[allow(missing_docs)] #[non_exhaustive] #[derive(Clone)] +#[allow(private_interfaces)] pub enum ConnectingError { #[error(transparent)] ConnectionError { @@ -368,8 +358,11 @@ pub enum ConnectingError { }, #[error("Failure finalizing the handshake")] HandshakeFailure { source: AuthenticationError }, - #[error("internal consistency error: RemoteStateActor stopped")] - InternalConsistencyError, + #[error("internal consistency error")] + InternalConsistencyError { + /// Private source type, cannot be created publicly. + source: RemoteStateActorStoppedError, + }, } impl Connecting { @@ -377,14 +370,12 @@ impl Connecting { inner: quinn::Connecting, ep: Endpoint, remote_endpoint_id: EndpointId, - _discovery_drop_guard: Option, ) -> Self { Self { inner, ep, remote_endpoint_id, register_with_magicsock: None, - _discovery_drop_guard, } } @@ -432,7 +423,6 @@ impl Connecting { async move { let accepted = zrtt_accepted.await; let conn = conn_from_quinn_conn(quinn_conn, &self.ep)?.await?; - drop(self._discovery_drop_guard); Ok(match accepted { true => ZeroRttStatus::Accepted(conn), false => ZeroRttStatus::Rejected(conn), diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 772ee2e88e4..6f97fa3e817 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -58,7 +58,7 @@ use crate::net_report::QuicConfig; use crate::{ defaults::timeouts::NET_REPORT_TIMEOUT, disco::{self, SendAddr}, - discovery::{ConcurrentDiscovery, Discovery, EndpointData, UserData}, + discovery::{ConcurrentDiscovery, Discovery, DiscoveryError, EndpointData, UserData}, key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, magicsock::remote_map::PathsWatcher, metrics::EndpointMetrics, @@ -100,10 +100,18 @@ pub(crate) const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(6500); pub(crate) const MAX_MULTIPATH_PATHS: u32 = 16; /// Error returned when the endpoint state actor stopped while waiting for a reply. -#[stack_error(derive)] +#[stack_error(add_meta, derive)] #[error("endpoint state actor stopped")] +#[derive(Clone)] pub(crate) struct RemoteStateActorStoppedError; +impl From> for RemoteStateActorStoppedError { + #[track_caller] + fn from(_value: mpsc::error::SendError) -> Self { + Self::new() + } +} + /// Contains options for `MagicSock::listen`. #[derive(derive_more::Debug)] pub(crate) struct Options { @@ -208,18 +216,6 @@ pub(crate) struct MagicSock { pub(crate) metrics: EndpointMetrics, } -#[allow(missing_docs)] -#[stack_error(derive, add_meta)] -#[non_exhaustive] -pub enum AddEndpointAddrError { - #[error("Empty addressing info")] - Empty, - #[error("Empty addressing info, {pruned} direct address have been pruned")] - EmptyPruned { pruned: usize }, - #[error("Adding our own address is not supported")] - OwnAddress, -} - impl MagicSock { /// Creates a magic [`MagicSock`] listening. pub(crate) async fn spawn(opts: Options) -> Result { @@ -271,9 +267,8 @@ impl MagicSock { async move { sender .send(RemoteStateMessage::AddConnection(conn, tx)) - .await - .map_err(|_| RemoteStateActorStoppedError)?; - rx.await.map_err(|_| RemoteStateActorStoppedError) + .await?; + rx.await.map_err(|_| RemoteStateActorStoppedError::new()) } } @@ -288,14 +283,37 @@ impl MagicSock { .filter_map(|addr| addr.into_socket_addr()) } - /// Returns `true` if we have at least one candidate address where we can send packets to. - pub(crate) async fn has_send_address(&self, eid: EndpointId) -> bool { - let actor = self.remote_map.remote_state_actor(eid); + /// Resolves an [`EndpointAddr`] to an [`EndpointIdMappedAddr`] to connect to via [`Handle::endpoint`]. + /// + /// This starts a `RemoteStateActor` for the remote if not running already, and then checks + /// if the actor has any known paths to the remote. If not, it starts discovery and waits for + /// at least one result to arrive. + /// + /// Returns `Ok(Ok(EndpointIdMappedAddr))` if there is a known path or discovery produced + /// at least one result. This does not mean there is a working path, only that we have at least + /// one transport address we can try to connect to. + /// + /// Returns `Ok(Err(discovery_error))` if there are no known paths to the remote and discovery + /// failed or produced no results. This means that we don't have any transport address for + /// the remote, thus there is no point in trying to connect over the quinn endpoint. + /// + /// Returns `Err(RemoteStateActorStoppedError)` if the `RemoteStateActor` for the remote has stopped, + /// which may never happen and thus is a bug if it does. + pub(crate) async fn resolve_remote( + &self, + addr: EndpointAddr, + ) -> Result, RemoteStateActorStoppedError> { + let EndpointAddr { id, addrs } = addr; + let actor = self.remote_map.remote_state_actor(id); let (tx, rx) = oneshot::channel(); - if actor.send(RemoteStateMessage::CanSend(tx)).await.is_err() { - return false; + actor + .send(RemoteStateMessage::ResolveRemote(addrs, tx)) + .await?; + match rx.await { + Ok(Ok(())) => Ok(Ok(self.remote_map.endpoint_mapped_addr(id))), + Ok(Err(err)) => Ok(Err(err)), + Err(_) => Err(RemoteStateActorStoppedError::new()), } - rx.await.unwrap_or(false) } pub(crate) async fn insert_relay( @@ -388,42 +406,6 @@ impl MagicSock { rx.await.unwrap_or_default() } - /// Returns the socket address which can be used by the QUIC layer to dial this endpoint. - pub(crate) fn get_endpoint_mapped_addr(&self, eid: EndpointId) -> EndpointIdMappedAddr { - self.remote_map.endpoint_mapped_addr(eid) - } - - /// Add potential addresses for a endpoint to the `RemoteStateActor`. - /// - /// This is used to add possible paths that the remote endpoint might be reachable on. They - /// will be used when there is no active connection to the endpoint to attempt to establish - /// a connection. - #[instrument(skip_all)] - pub(crate) async fn add_endpoint_addr( - &self, - mut addr: EndpointAddr, - source: remote_map::Source, - ) -> Result<(), AddEndpointAddrError> { - let mut pruned: usize = 0; - for my_addr in self.direct_addrs.sockaddrs() { - if addr.addrs.remove(&TransportAddr::Ip(my_addr)) { - warn!( endpoint_id=%addr.id.fmt_short(), %my_addr, %source, "not adding our addr for endpoint"); - pruned += 1; - } - } - if !addr.is_empty() { - // Add addr to the internal RemoteMap - self.remote_map - .add_endpoint_addr(addr.clone(), source) - .await; - Ok(()) - } else if pruned != 0 { - Err(e!(AddEndpointAddrError::EmptyPruned { pruned })) - } else { - Err(e!(AddEndpointAddrError::Empty)) - } - } - /// Stores a new set of direct addresses. /// /// If the direct addresses have changed from the previous set, they are published to @@ -1022,6 +1004,7 @@ impl Handle { direct_addrs.addrs.watch(), disco.clone(), transports.create_sender(), + discovery.clone(), ) }; @@ -1831,7 +1814,7 @@ impl Display for DirectAddrType { #[cfg(test)] mod tests { - use std::{sync::Arc, time::Duration}; + use std::{net::SocketAddrV4, sync::Arc, time::Duration}; use data_encoding::HEXLOWER; use iroh_base::{EndpointAddr, EndpointId, TransportAddr}; @@ -1844,12 +1827,15 @@ mod tests { use tracing::{Instrument, error, info, info_span, instrument}; use tracing_test::traced_test; - use super::{EndpointIdMappedAddr, Options, mapped_addrs::MappedAddr, remote_map::Source}; + use super::Options; use crate::{ Endpoint, RelayMode, SecretKey, discovery::static_provider::StaticProvider, dns::DnsResolver, - magicsock::{Handle, MagicSock, TransportConfig}, + magicsock::{ + Handle, MagicSock, TransportConfig, + mapped_addrs::{EndpointIdMappedAddr, MappedAddr}, + }, tls::{self, DEFAULT_MAX_TLS_TICKETS}, }; @@ -2430,16 +2416,11 @@ mod tests { .into_iter() .map(|x| TransportAddr::Ip(x.addr)); let endpoint_addr_2 = EndpointAddr::from_parts(endpoint_id_2, addrs); - msock_1 - .add_endpoint_addr( - endpoint_addr_2, - Source::NamedApp { - name: "test".into(), - }, - ) + let addr = msock_1 + .resolve_remote(endpoint_addr_2) .await + .unwrap() .unwrap(); - let addr = msock_1.get_endpoint_mapped_addr(endpoint_id_2); let res = tokio::time::timeout( Duration::from_secs(10), magicsock_connect( @@ -2499,17 +2480,15 @@ mod tests { }); let _accept_task = AbortOnDropHandle::new(accept_task); - // Add an empty entry in the RemoteMap of ep_1 - msock_1 - .remote_map - .add_endpoint_addr( - EndpointAddr::from_parts(endpoint_id_2, []), - Source::NamedApp { - name: "test".into(), - }, - ) - .await; - let addr_2 = msock_1.get_endpoint_mapped_addr(endpoint_id_2); + // Add an entry in the RemoteMap of ep_1 with an invalid socket address + let empty_addr_2 = EndpointAddr::from_parts( + endpoint_id_2, + [TransportAddr::Ip( + // Reserved IP range for documentation (unreachable) + SocketAddrV4::new([192, 0, 2, 1].into(), 12345).into(), + )], + ); + let addr_2 = msock_1.resolve_remote(empty_addr_2).await.unwrap().unwrap(); // Set a low max_idle_timeout so quinn gives up on this quickly and our test does // not take forever. You need to check the log output to verify this is really @@ -2535,20 +2514,20 @@ mod tests { info!("first connect timed out as expected"); // Provide correct addressing information - let addrs = msock_2 - .ip_addrs() - .get() - .into_iter() - .map(|x| TransportAddr::Ip(x.addr)); - msock_1 - .remote_map - .add_endpoint_addr( - EndpointAddr::from_parts(endpoint_id_2, addrs), - Source::NamedApp { - name: "test".into(), - }, - ) - .await; + let correct_addr_2 = EndpointAddr::from_parts( + endpoint_id_2, + msock_2 + .ip_addrs() + .get() + .into_iter() + .map(|x| TransportAddr::Ip(x.addr)), + ); + let addr_2a = msock_1 + .resolve_remote(correct_addr_2) + .await + .unwrap() + .unwrap(); + assert_eq!(addr_2, addr_2a); // We can now connect tokio::time::timeout(Duration::from_secs(10), async move { diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index ba3a7fc5180..0861a039b09 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -6,7 +6,7 @@ use std::{ time::Duration, }; -use iroh_base::{EndpointAddr, EndpointId, RelayUrl}; +use iroh_base::{EndpointId, RelayUrl}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; @@ -21,7 +21,7 @@ use super::{ mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, transports::{self, TransportsSender}, }; -use crate::disco; +use crate::{disco, discovery::ConcurrentDiscovery}; mod remote_state; @@ -60,6 +60,7 @@ pub(crate) struct RemoteMap { local_addrs: n0_watcher::Direct>, disco: DiscoState, sender: TransportsSender, + discovery: ConcurrentDiscovery, } impl RemoteMap { @@ -71,6 +72,7 @@ impl RemoteMap { local_addrs: n0_watcher::Direct>, disco: DiscoState, sender: TransportsSender, + discovery: ConcurrentDiscovery, ) -> Self { Self { actor_handles: Mutex::new(FxHashMap::default()), @@ -81,26 +83,10 @@ impl RemoteMap { local_addrs, disco, sender, + discovery, } } - /// Adds addresses where an endpoint might be contactable. - pub(super) async fn add_endpoint_addr(&self, endpoint_addr: EndpointAddr, source: Source) { - for url in endpoint_addr.relay_urls() { - // Ensure we have a RelayMappedAddress. - self.relay_mapped_addrs - .get(&(url.clone(), endpoint_addr.id)); - } - let actor = self.remote_state_actor(endpoint_addr.id); - - // This only fails if the sender is closed. That means the RemoteStateActor has - // stopped, which only happens during shutdown. - actor - .send(RemoteStateMessage::AddEndpointAddr(endpoint_addr, source)) - .await - .ok(); - } - pub(super) fn endpoint_mapped_addr(&self, eid: EndpointId) -> EndpointIdMappedAddr { self.endpoint_mapped_addrs.get(&eid) } @@ -157,6 +143,7 @@ impl RemoteMap { self.relay_mapped_addrs.clone(), self.metrics.clone(), self.sender.clone(), + self.discovery.clone(), ) .start(); let sender = handle.sender.get().expect("just created"); diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index 56d507c6171..f7f13f95573 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -6,9 +6,10 @@ use std::{ task::Poll, }; -use iroh_base::{EndpointAddr, EndpointId, RelayUrl, TransportAddr}; +use iroh_base::{EndpointId, RelayUrl, TransportAddr}; use n0_future::{ - FuturesUnordered, MergeUnbounded, Stream, StreamExt, + Either, FuturesUnordered, MergeUnbounded, Stream, StreamExt, + boxed::BoxStream, task::{self, AbortOnDropHandle}, time::{self, Duration, Instant}, }; @@ -17,17 +18,19 @@ use quinn::{PathStats, WeakConnectionHandle}; use quinn_proto::{PathError, PathEvent, PathId, PathStatus}; use rustc_hash::FxHashMap; use smallvec::SmallVec; +use sync_wrapper::SyncStream; use tokio::sync::oneshot; use tokio_stream::wrappers::{BroadcastStream, errors::BroadcastStreamRecvError}; use tracing::{Instrument, Level, debug, error, event, info_span, instrument, trace, warn}; use self::{ guarded_channel::{GuardedReceiver, GuardedSender, guarded_channel}, - path_state::PathState, + path_state::RemotePathState, }; use super::Source; use crate::{ disco::{self}, + discovery::{ConcurrentDiscovery, Discovery, DiscoveryError, DiscoveryItem}, endpoint::DirectAddr, magicsock::{ DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, @@ -89,6 +92,18 @@ type PathEvents = MergeUnbounded< >, >; +/// Either a stream of incoming results from [`ConcurrentDiscovery::resolve`] or infinitely pending. +/// +/// Set to [`Either::Left`] with an always-pending stream while discovery is not running, and to +/// [`Either::Right`] while discovery is running. +/// +/// The stream returned from [`ConcurrentDiscovery::resolve`] is `!Sync`. We use the (safe) [`SyncStream`] +/// wrapper to make it `Sync` so that the [`RemoteStateActor::run`] future stays `Send`. +type DiscoveryStream = Either< + n0_future::stream::Pending>, + SyncStream>>, +>; + /// List of addrs and path ids for open paths in a connection. pub(crate) type PathAddrList = SmallVec<[(TransportAddr, PathId); 4]>; @@ -115,6 +130,8 @@ pub(super) struct RemoteStateActor { disco: DiscoState, /// The mapping between endpoints via a relay and their [`RelayMappedAddr`]s. relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, + /// Discovery service, cloned from the magicsock. + discovery: ConcurrentDiscovery, // Internal state - Quinn Connections we are managing. // @@ -131,7 +148,7 @@ pub(super) struct RemoteStateActor { /// /// These paths might be entirely impossible to use, since they are added by discovery /// mechanisms. The are only potentially usable. - paths: FxHashMap, + paths: RemotePathState, /// Information about the last holepunching attempt. last_holepunch: Option, /// The path we currently consider the preferred path to the remote endpoint. @@ -151,9 +168,15 @@ pub(super) struct RemoteStateActor { /// /// They failed to open because we did not have enough CIDs issued by the remote. pending_open_paths: VecDeque, + + // Internal state - Discovery + // + /// Stream of discovery results, or always pending if discovery is not running. + discovery_stream: DiscoveryStream, } impl RemoteStateActor { + #[allow(clippy::too_many_arguments)] pub(super) fn new( endpoint_id: EndpointId, local_endpoint_id: EndpointId, @@ -162,6 +185,7 @@ impl RemoteStateActor { relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, metrics: Arc, sender: TransportsSender, + discovery: ConcurrentDiscovery, ) -> Self { Self { endpoint_id, @@ -169,21 +193,23 @@ impl RemoteStateActor { metrics, local_addrs, relay_mapped_addrs, + discovery, disco, connections: FxHashMap::default(), connections_close: Default::default(), path_events: Default::default(), - paths: FxHashMap::default(), + paths: Default::default(), last_holepunch: None, selected_path: Default::default(), scheduled_holepunch: None, scheduled_open_path: None, pending_open_paths: VecDeque::new(), sender, + discovery_stream: Either::Left(n0_future::stream::pending()), } } - pub(super) fn start(mut self) -> RemoteStateHandle { + pub(super) fn start(self) -> RemoteStateHandle { let (tx, rx) = guarded_channel(16); let me = self.local_endpoint_id; let endpoint_id = self.endpoint_id; @@ -217,10 +243,7 @@ impl RemoteStateActor { /// Note that the actor uses async handlers for tasks from the main loop. The actor is /// not processing items from the inbox while waiting on any async calls. So some /// discipline is needed to not turn pending for a long time. - async fn run( - &mut self, - mut inbox: GuardedReceiver, - ) -> n0_error::Result<()> { + async fn run(mut self, mut inbox: GuardedReceiver) -> n0_error::Result<()> { trace!("actor started"); let idle_timeout = MaybeFuture::None; tokio::pin!(idle_timeout); @@ -270,6 +293,9 @@ impl RemoteStateActor { self.scheduled_holepunch = None; self.trigger_holepunching().await; } + item = self.discovery_stream.next() => { + self.handle_discovery_item(item); + } _ = &mut idle_timeout => { if self.connections.is_empty() && inbox.close_if_idle() { trace!("idle timeout expired and still idle: terminate actor"); @@ -305,9 +331,6 @@ impl RemoteStateActor { RemoteStateMessage::AddConnection(handle, tx) => { self.handle_msg_add_connection(handle, tx).await; } - RemoteStateMessage::AddEndpointAddr(addr, source) => { - self.handle_msg_add_endpoint_addr(addr, source); - } RemoteStateMessage::CallMeMaybeReceived(msg) => { self.handle_msg_call_me_maybe_received(msg).await; } @@ -317,8 +340,8 @@ impl RemoteStateActor { RemoteStateMessage::PongReceived(pong, src) => { self.handle_msg_pong_received(pong, src); } - RemoteStateMessage::CanSend(tx) => { - self.handle_msg_can_send(tx); + RemoteStateMessage::ResolveRemote(addrs, tx) => { + self.handle_msg_resolve_remote(addrs, tx); } RemoteStateMessage::Latency(tx) => { self.handle_msg_latency(tx); @@ -350,11 +373,19 @@ impl RemoteStateActor { self.send_datagram(addr, transmit).await?; } else { trace!( - paths = ?self.paths.keys().collect::>(), + paths = ?self.paths.addrs().collect::>(), "sending datagram to all known paths", ); - for addr in self.paths.keys() { - self.send_datagram(addr.clone(), transmit.clone()).await?; + for addr in self.paths.addrs() { + // We never want to send to our local addresses. + // The local address set is updated in the main loop so we can use `peek` here. + if let transports::Addr::Ip(sockaddr) = addr + && self.local_addrs.peek().iter().any(|a| a.addr == *sockaddr) + { + trace!(%sockaddr, "not sending datagram to our own address"); + } else { + self.send_datagram(addr.clone(), transmit.clone()).await?; + } } // This message is received *before* a connection is added. So we do // not yet have a connection to holepunch. Instead we trigger @@ -409,10 +440,7 @@ impl RemoteStateActor { path.set_status(status).ok(); conn_state.add_open_path(path_remote.clone(), PathId::ZERO); self.paths - .entry(path_remote) - .or_default() - .sources - .insert(Source::Connection { _0: Private }, Instant::now()); + .insert(path_remote, Source::Connection { _0: Private }); self.select_path(); if path_remote_is_ip { @@ -420,7 +448,7 @@ impl RemoteStateActor { // relay addresses we have back. let relays = self .paths - .keys() + .addrs() .filter(|a| a.is_relay()) .cloned() .collect::>(); @@ -439,27 +467,6 @@ impl RemoteStateActor { .ok(); } - /// Handles [`RemoteStateMessage::AddEndpointAddr`]. - fn handle_msg_add_endpoint_addr(&mut self, addr: EndpointAddr, source: Source) { - for sockaddr in addr.ip_addrs() { - let addr = transports::Addr::from(sockaddr); - self.paths - .entry(addr) - .or_default() - .sources - .insert(source.clone(), Instant::now()); - } - for relay_url in addr.relay_urls() { - let addr = transports::Addr::from((relay_url.clone(), self.endpoint_id)); - self.paths - .entry(addr) - .or_default() - .sources - .insert(source.clone(), Instant::now()); - } - trace!("added addressing information"); - } - /// Handles [`RemoteStateMessage::CallMeMaybeReceived`]. async fn handle_msg_call_me_maybe_received(&mut self, msg: disco::CallMeMaybe) { event!( @@ -468,15 +475,13 @@ impl RemoteStateActor { remote = %self.endpoint_id.fmt_short(), addrs = ?msg.my_numbers, ); - let now = Instant::now(); for addr in msg.my_numbers { let dst = transports::Addr::Ip(addr); let ping = disco::Ping::new(self.local_endpoint_id); - let path = self.paths.entry(dst.clone()).or_default(); - path.sources - .insert(Source::CallMeMaybe { _0: Private }, now); - path.ping_sent = Some(ping.tx_id); + self.paths + .insert(dst.clone(), Source::CallMeMaybe { _0: Private }); + self.paths.disco_ping_sent(dst.clone(), ping.tx_id); event!( target: "iroh::_events::ping::sent", @@ -516,9 +521,7 @@ impl RemoteStateActor { self.send_disco_message(src.clone(), disco::Message::Pong(pong)) .await; - let path = self.paths.entry(src).or_default(); - path.sources - .insert(Source::Ping { _0: Private }, Instant::now()); + self.paths.insert(src, Source::Ping { _0: Private }); trace!("ping received, triggering holepunching"); self.trigger_holepunching().await; @@ -526,30 +529,30 @@ impl RemoteStateActor { /// Handles [`RemoteStateMessage::PongReceived`]. fn handle_msg_pong_received(&mut self, pong: disco::Pong, src: transports::Addr) { - let Some(state) = self.paths.get(&src) else { - warn!(path = ?src, ?self.paths, "ignoring DISCO Pong for unknown path"); - return; - }; - if state.ping_sent != Some(pong.tx_id) { - debug!(path = ?src, ?state.ping_sent, pong_tx = ?pong.tx_id, - "ignoring unknown DISCO Pong for path"); - return; - } - event!( - target: "iroh::_events::pong::recv", - Level::DEBUG, - remote_endpoint = %self.endpoint_id.fmt_short(), - ?src, - txn = ?pong.tx_id, - ); + if self.paths.disco_pong_received(&src, pong.tx_id) { + event!( + target: "iroh::_events::pong::recv", + Level::DEBUG, + remote_endpoint = %self.endpoint_id.fmt_short(), + ?src, + txn = ?pong.tx_id, + ); - self.open_path(&src); + self.open_path(&src); + } } - /// Handles [`RemoteStateMessage::CanSend`]. - fn handle_msg_can_send(&self, tx: oneshot::Sender) { - let can_send = !self.paths.is_empty(); - tx.send(can_send).ok(); + /// Handles [`RemoteStateMessage::ResolveRemote`]. + fn handle_msg_resolve_remote( + &mut self, + addrs: BTreeSet, + tx: oneshot::Sender>, + ) { + let addrs = to_transports_addr(self.endpoint_id, addrs); + self.paths.insert_multiple(addrs, Source::App); + self.paths.resolve_remote(tx); + // Start discovery if we have no selected path. + self.trigger_discovery(); } /// Handles [`RemoteStateMessage::Latency`]. @@ -575,6 +578,45 @@ impl RemoteStateActor { tx.send(rtt).ok(); } + fn handle_discovery_item(&mut self, item: Option>) { + match item { + None => { + self.discovery_stream = Either::Left(n0_future::stream::pending()); + self.paths.discovery_finished(Ok(())); + } + Some(Err(err)) => { + warn!("Discovery failed: {err:#}"); + self.discovery_stream = Either::Left(n0_future::stream::pending()); + self.paths.discovery_finished(Err(err)); + } + Some(Ok(item)) => { + if item.endpoint_id() != self.endpoint_id { + warn!(?item, "Discovery emitted item for wrong remote endpoint"); + } else { + let source = Source::Discovery { + name: item.provenance().to_string(), + }; + let addrs = + to_transports_addr(self.endpoint_id, item.into_endpoint_addr().addrs); + self.paths.insert_multiple(addrs, source); + } + } + } + } + + /// Triggers discovery for the remote endpoint, if needed. + /// + /// Does not start discovery if we have a selected path or if discovery is currently running. + fn trigger_discovery(&mut self) { + if self.selected_path.get().is_some() || matches!(self.discovery_stream, Either::Right(_)) { + return; + } + match self.discovery.resolve(self.endpoint_id) { + Some(stream) => self.discovery_stream = Either::Right(SyncStream::new(stream)), + None => self.paths.discovery_finished(Ok(())), + } + } + /// Triggers holepunching to the remote endpoint. /// /// This will manage the entire process of holepunching with the remote endpoint. @@ -682,16 +724,7 @@ impl RemoteStateActor { /// - A DISCO call-me-maybe message advertising our own addresses will be sent. #[instrument(skip_all)] async fn do_holepunching(&mut self) { - let Some(relay_addr) = self - .paths - .iter() - .filter_map(|(addr, _)| match addr { - transports::Addr::Ip(_) => None, - transports::Addr::Relay(_, _) => Some(addr), - }) - .next() - .cloned() - else { + let Some(relay_addr) = self.paths.addrs().find(|addr| addr.is_relay()).cloned() else { warn!("holepunching requested but have no relay address"); return; }; @@ -708,7 +741,7 @@ impl RemoteStateActor { txn = ?msg.tx_id, ); let addr = transports::Addr::Ip(*dst); - self.paths.entry(addr.clone()).or_default().ping_sent = Some(msg.tx_id); + self.paths.disco_ping_sent(addr.clone(), msg.tx_id); self.send_disco_message(addr, disco::Message::Ping(msg)) .await; } @@ -858,10 +891,7 @@ impl RemoteStateActor { ); conn_state.add_open_path(path_remote.clone(), path_id); self.paths - .entry(path_remote.clone()) - .or_default() - .sources - .insert(Source::Connection { _0: Private }, Instant::now()); + .insert(path_remote, Source::Connection { _0: Private }); } self.select_path(); @@ -1038,8 +1068,6 @@ pub(crate) enum RemoteStateMessage { /// will be removed etc. #[debug("AddConnection(..)")] AddConnection(WeakConnectionHandle, oneshot::Sender), - /// Adds a [`EndpointAddr`] with locations where the endpoint might be reachable. - AddEndpointAddr(EndpointAddr, Source), /// Process a received DISCO CallMeMaybe message. CallMeMaybeReceived(disco::CallMeMaybe), /// Process a received DISCO Ping message. @@ -1048,11 +1076,19 @@ pub(crate) enum RemoteStateMessage { /// Process a received DISCO Pong message. #[debug("PongReceived({:?}, src: {_1:?})", _0.tx_id)] PongReceived(disco::Pong, transports::Addr), - /// Asks if there is any possible path that could be used. + /// Ensure we have at least one transport address for a remote. + /// + /// This adds the provided transport addresses to the list of potential paths for this remote + /// and starts discovery if needed. /// - /// This does not mean there is any guarantee that the remote endpoint is reachable. - #[debug("CanSend(..)")] - CanSend(oneshot::Sender), + /// Returns `Ok` immediately if the provided address list is non-empy or we have are other known paths. + /// Otherwise returns `Ok` once discovery produces a result, or the discovery error if discovery fails + /// or produces no results, + #[debug("ResolveRemote(..)")] + ResolveRemote( + BTreeSet, + oneshot::Sender>, + ), /// Returns the current latency to the remote endpoint. /// /// TODO: This is more of a placeholder message currently. Check MagicSock::latency. @@ -1376,3 +1412,18 @@ impl Future for OnClosed { Poll::Ready(self.conn_id) } } + +/// Converts an iterator of [`TransportAddr'] into an iterator of [`transports::Addr`]. +fn to_transports_addr( + endpoint_id: EndpointId, + addrs: impl IntoIterator, +) -> impl Iterator { + addrs.into_iter().filter_map(move |addr| match addr { + TransportAddr::Relay(relay_url) => Some(transports::Addr::from((relay_url, endpoint_id))), + TransportAddr::Ip(sockaddr) => Some(transports::Addr::from(sockaddr)), + _ => { + warn!(?addr, "Unsupported TransportAddr"); + None + } + }) +} diff --git a/iroh/src/magicsock/remote_map/remote_state/path_state.rs b/iroh/src/magicsock/remote_map/remote_state/path_state.rs index 83eac70a604..aeea1b2a5f7 100644 --- a/iroh/src/magicsock/remote_map/remote_state/path_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state/path_state.rs @@ -1,19 +1,147 @@ //! The state kept for each network path to a remote endpoint. -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; +use n0_error::e; use n0_future::time::Instant; +use rustc_hash::FxHashMap; +use tokio::sync::oneshot; +use tracing::{debug, trace, warn}; use super::Source; -use crate::disco::TransactionId; +use crate::{disco::TransactionId, discovery::DiscoveryError, magicsock::transports}; + +/// Map of all paths that we are aware of for a remote endpoint. +/// +/// Also stores a list of resolve requests which are triggered once at least one path is known, +/// or once this struct is notified of a failed discovery run. +#[derive(Debug, Default)] +pub(super) struct RemotePathState { + /// All possible paths we are aware of. + /// + /// These paths might be entirely impossible to use, since they are added by discovery + /// mechanisms. The are only potentially usable. + paths: FxHashMap, + /// Pending resolve requests from [`Self::resolve_remote`]. + pending_resolve_requests: VecDeque>>, +} + +impl RemotePathState { + /// Insert a new address into our list of potential paths. + /// + /// This will emit pending resolve requests. + pub(super) fn insert(&mut self, addr: transports::Addr, source: Source) { + self.paths + .entry(addr) + .or_default() + .sources + .insert(source.clone(), Instant::now()); + self.emit_pending_resolve_requests(None); + } + + /// Inserts multiple addresses into our list of potential paths. + /// + /// This will emit pending resolve requests. + pub(super) fn insert_multiple( + &mut self, + addrs: impl Iterator, + source: Source, + ) { + let now = Instant::now(); + for addr in addrs { + self.paths + .entry(addr) + .or_default() + .sources + .insert(source.clone(), now); + } + trace!("added addressing information"); + self.emit_pending_resolve_requests(None); + } + + /// Triggers `tx` immediately if there are any known paths, or store in the list of pending requests. + /// + /// The pending requests will be resolved once a path becomes known, or once discovery + /// concludes without results, whichever comes first. + /// + /// Sends `Ok(())` over `tx` if there are any known paths, and a [`DiscoveryError`] if there are + /// no known paths by the time a discovery run finished with an error or without results. + pub(super) fn resolve_remote(&mut self, tx: oneshot::Sender>) { + if !self.paths.is_empty() { + tx.send(Ok(())).ok(); + } else { + self.pending_resolve_requests.push_back(tx); + } + } + + /// Records a sent disco ping for a path. + pub(super) fn disco_ping_sent(&mut self, addr: transports::Addr, tx_id: TransactionId) { + let path = self.paths.entry(addr.clone()).or_default(); + path.ping_sent = Some(tx_id); + } + + /// Records a received disco pong for a path. + /// + /// Returns `true` if we have sent a ping with `tx_id` on the same path. + pub(super) fn disco_pong_received( + &mut self, + src: &transports::Addr, + tx_id: TransactionId, + ) -> bool { + let Some(state) = self.paths.get(src) else { + warn!(path = ?src, ?self.paths, "ignoring DISCO Pong for unknown path"); + return false; + }; + if state.ping_sent != Some(tx_id) { + debug!(path = ?src, ?state.ping_sent, pong_tx = ?tx_id, "ignoring unknown DISCO Pong for path"); + false + } else { + true + } + } + + /// Notifies that a discovery run has finished. + /// + /// This will emit pending resolve requests. + pub(super) fn discovery_finished(&mut self, result: Result<(), DiscoveryError>) { + self.emit_pending_resolve_requests(result.err()); + } + + /// Returns an iterator over all paths and their state. + pub(super) fn iter(&self) -> impl Iterator { + self.paths.iter() + } + + /// Returns an iterator over the addresses of all paths. + pub(super) fn addrs(&self) -> impl Iterator { + self.paths.keys() + } + + /// Replies to all pending resolve requests. + /// + /// This is a no-op if no requests are queued. Replies `Ok` if we have any known paths, + /// otherwise with the provided `discovery_error` or with [`DiscoveryError::NoResults`]. + fn emit_pending_resolve_requests(&mut self, discovery_error: Option) { + if self.pending_resolve_requests.is_empty() { + return; + } + let result = match (self.paths.is_empty(), discovery_error) { + (false, _) => Ok(()), + (true, Some(err)) => Err(err), + (true, None) => Err(e!(DiscoveryError::NoResults)), + }; + for tx in self.pending_resolve_requests.drain(..) { + tx.send(result.clone()).ok(); + } + } +} /// The state of a single path to the remote endpoint. /// /// Each path is identified by the destination [`transports::Addr`] and they are stored in -/// the [`RemoteStateActor::paths`] map. +/// the [`RemotePathState`] map at [`RemoteStateActor::paths`]. /// -/// [`transports::Addr`]: super::transports::Addr -/// [`RemoteStateActor::paths`]: super::RemoteStateActor +/// [`RemoteStateActor::paths`]: super::RemoteStateActor::paths #[derive(Debug, Default)] pub(super) struct PathState { /// How we learned about this path, and when. From d328bf231307e98d3d9d8fb7b5d406f79418e645 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Thu, 20 Nov 2025 14:53:00 +0100 Subject: [PATCH 151/164] fix(multipath): fix remote state actor termination (#3676) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description * fix idle timeout clear condition (previously it would hot loop) * fix hot loop when local_addrs watchable becomes disconnected during shutdown * when sending a datagram fails in the transports sender, include the dst address in the error message * do not break the RemoteStateActor when sending a datagram fails ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --------- Co-authored-by: Philipp Krüger --- iroh/src/magicsock/remote_map.rs | 9 +- iroh/src/magicsock/remote_map/remote_state.rs | 103 ++++++++++-------- .../remote_map/remote_state/path_state.rs | 5 + iroh/src/magicsock/transports.rs | 4 +- 4 files changed, 70 insertions(+), 51 deletions(-) diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index 0861a039b09..76d8fdf0a41 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -57,7 +57,8 @@ pub(crate) struct RemoteMap { /// The endpoint ID of the local endpoint. local_endpoint_id: EndpointId, metrics: Arc, - local_addrs: n0_watcher::Direct>, + /// The "direct" addresses known for our local endpoint + local_direct_addrs: n0_watcher::Direct>, disco: DiscoState, sender: TransportsSender, discovery: ConcurrentDiscovery, @@ -69,7 +70,7 @@ impl RemoteMap { local_endpoint_id: EndpointId, metrics: Arc, - local_addrs: n0_watcher::Direct>, + local_direct_addrs: n0_watcher::Direct>, disco: DiscoState, sender: TransportsSender, discovery: ConcurrentDiscovery, @@ -80,7 +81,7 @@ impl RemoteMap { relay_mapped_addrs: Default::default(), local_endpoint_id, metrics, - local_addrs, + local_direct_addrs, disco, sender, discovery, @@ -138,7 +139,7 @@ impl RemoteMap { let handle = RemoteStateActor::new( eid, self.local_endpoint_id, - self.local_addrs.clone(), + self.local_direct_addrs.clone(), self.disco.clone(), self.relay_mapped_addrs.clone(), self.metrics.clone(), diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index f7f13f95573..6fda36e3c99 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -7,6 +7,7 @@ use std::{ }; use iroh_base::{EndpointId, RelayUrl, TransportAddr}; +use n0_error::StackResultExt; use n0_future::{ Either, FuturesUnordered, MergeUnbounded, Stream, StreamExt, boxed::BoxStream, @@ -125,7 +126,8 @@ pub(super) struct RemoteStateActor { /// Our local addresses. /// /// These are our local addresses and any reflexive transport addresses. - local_addrs: n0_watcher::Direct>, + /// They are called "direct addresses" in the magic socket actor. + local_direct_addrs: n0_watcher::Direct>, /// Shared state to allow to encrypt DISCO messages to peers. disco: DiscoState, /// The mapping between endpoints via a relay and their [`RelayMappedAddr`]s. @@ -180,7 +182,7 @@ impl RemoteStateActor { pub(super) fn new( endpoint_id: EndpointId, local_endpoint_id: EndpointId, - local_addrs: n0_watcher::Direct>, + local_direct_addrs: n0_watcher::Direct>, disco: DiscoState, relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, metrics: Arc, @@ -191,7 +193,7 @@ impl RemoteStateActor { endpoint_id, local_endpoint_id, metrics, - local_addrs, + local_direct_addrs, relay_mapped_addrs, discovery, disco, @@ -219,19 +221,12 @@ impl RemoteStateActor { // we don't explicitly set a span we get the spans from whatever call happens to // first create the actor, which is often very confusing as it then keeps those // spans for all logging of the actor. - let task = task::spawn( - async move { - if let Err(err) = self.run(rx).await { - error!("actor failed: {err:#}"); - } - } - .instrument(info_span!( - parent: None, - "RemoteStateActor", - me = %me.fmt_short(), - remote = %endpoint_id.fmt_short(), - )), - ); + let task = task::spawn(self.run(rx).instrument(info_span!( + parent: None, + "RemoteStateActor", + me = %me.fmt_short(), + remote = %endpoint_id.fmt_short(), + ))); RemoteStateHandle { sender: tx, _task: AbortOnDropHandle::new(task), @@ -243,10 +238,10 @@ impl RemoteStateActor { /// Note that the actor uses async handlers for tasks from the main loop. The actor is /// not processing items from the inbox while waiting on any async calls. So some /// discipline is needed to not turn pending for a long time. - async fn run(mut self, mut inbox: GuardedReceiver) -> n0_error::Result<()> { + async fn run(mut self, mut inbox: GuardedReceiver) { trace!("actor started"); - let idle_timeout = MaybeFuture::None; - tokio::pin!(idle_timeout); + let idle_timeout = time::sleep(ACTOR_MAX_IDLE_TIMEOUT); + n0_future::pin!(idle_timeout); loop { let scheduled_path_open = match self.scheduled_open_path { Some(when) => MaybeFuture::Some(time::sleep_until(when)), @@ -258,11 +253,16 @@ impl RemoteStateActor { None => MaybeFuture::None, }; n0_future::pin!(scheduled_hp); + if !inbox.is_idle() || !self.connections.is_empty() { + idle_timeout + .as_mut() + .reset(Instant::now() + ACTOR_MAX_IDLE_TIMEOUT); + } tokio::select! { biased; msg = inbox.recv() => { match msg { - Some(msg) => self.handle_message(msg).await?, + Some(msg) => self.handle_message(msg).await, None => break, } } @@ -276,7 +276,11 @@ impl RemoteStateActor { self.selected_path.set(None).ok(); } } - _ = self.local_addrs.updated() => { + res = self.local_direct_addrs.updated() => { + if let Err(n0_watcher::Disconnected) = res { + trace!("direct address watcher disconnected, shutting down"); + break; + } trace!("local addrs updated, triggering holepunching"); self.trigger_holepunching().await; } @@ -300,33 +304,25 @@ impl RemoteStateActor { if self.connections.is_empty() && inbox.close_if_idle() { trace!("idle timeout expired and still idle: terminate actor"); break; + } else { + // Seems like we weren't really idle, so we reset + idle_timeout.as_mut().reset(Instant::now() + ACTOR_MAX_IDLE_TIMEOUT); } } } - - if self.connections.is_empty() && inbox.is_idle() && idle_timeout.is_none() { - trace!("start idle timeout"); - idle_timeout - .as_mut() - .set_future(time::sleep(ACTOR_MAX_IDLE_TIMEOUT)); - } else if idle_timeout.is_some() { - trace!("abort idle timeout"); - idle_timeout.as_mut().set_none() - } } trace!("actor terminating"); - Ok(()) } /// Handles an actor message. /// /// Error returns are fatal and kill the actor. #[instrument(skip(self))] - async fn handle_message(&mut self, msg: RemoteStateMessage) -> n0_error::Result<()> { + async fn handle_message(&mut self, msg: RemoteStateMessage) { // trace!("handling message"); match msg { RemoteStateMessage::SendDatagram(transmit) => { - self.handle_msg_send_datagram(transmit).await?; + self.handle_msg_send_datagram(transmit).await; } RemoteStateMessage::AddConnection(handle, tx) => { self.handle_msg_add_connection(handle, tx).await; @@ -347,7 +343,6 @@ impl RemoteStateActor { self.handle_msg_latency(tx); } } - Ok(()) } async fn send_datagram( @@ -360,38 +355,54 @@ impl RemoteStateActor { contents: owned_transmit.contents.as_ref(), segment_size: owned_transmit.segment_size, }; - self.sender.send(&dst, None, &transmit).await?; + self.sender + .send(&dst, None, &transmit) + .await + .with_context(|_| format!("failed to send datagram to {dst:?}"))?; Ok(()) } /// Handles [`RemoteStateMessage::SendDatagram`]. - /// - /// Error returns are fatal and kill the actor. - async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) -> n0_error::Result<()> { + async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) { + // Sending datagrams might fail, e.g. because we don't have the right transports set + // up to handle sending this owned transmit to. + // After all, we try every single path that we know (relay URL, IP address), even + // though we might not have a relay transport or ip-capable transport set up. + // So these errors must not be fatal for this actor (or even this operation). + if let Some(addr) = self.selected_path.get() { trace!(?addr, "sending datagram to selected path"); - self.send_datagram(addr, transmit).await?; + + if let Err(err) = self.send_datagram(addr.clone(), transmit).await { + debug!(?addr, "failed to send datagram on selected_path: {err:#}"); + } } else { trace!( paths = ?self.paths.addrs().collect::>(), "sending datagram to all known paths", ); + if self.paths.is_empty() { + warn!("Cannot send datagrams: No paths to remote endpoint known"); + } for addr in self.paths.addrs() { // We never want to send to our local addresses. // The local address set is updated in the main loop so we can use `peek` here. if let transports::Addr::Ip(sockaddr) = addr - && self.local_addrs.peek().iter().any(|a| a.addr == *sockaddr) + && self + .local_direct_addrs + .peek() + .iter() + .any(|a| a.addr == *sockaddr) { trace!(%sockaddr, "not sending datagram to our own address"); - } else { - self.send_datagram(addr.clone(), transmit.clone()).await?; + } else if let Err(err) = self.send_datagram(addr.clone(), transmit.clone()).await { + debug!(?addr, "failed to send datagram: {err:#}"); } } // This message is received *before* a connection is added. So we do // not yet have a connection to holepunch. Instead we trigger // holepunching when AddConnection is received. } - Ok(()) } /// Handles [`RemoteStateMessage::AddConnection`]. @@ -656,7 +667,7 @@ impl RemoteStateActor { let remote_addrs: BTreeSet = self.remote_hp_addrs(); let local_addrs: BTreeSet = self - .local_addrs + .local_direct_addrs .get() .iter() .map(|daddr| daddr.addr) @@ -748,7 +759,7 @@ impl RemoteStateActor { // Send the DISCO CallMeMaybe message over the relay. let my_numbers: Vec = self - .local_addrs + .local_direct_addrs .get() .iter() .map(|daddr| daddr.addr) diff --git a/iroh/src/magicsock/remote_map/remote_state/path_state.rs b/iroh/src/magicsock/remote_map/remote_state/path_state.rs index aeea1b2a5f7..a35c9476501 100644 --- a/iroh/src/magicsock/remote_map/remote_state/path_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state/path_state.rs @@ -117,6 +117,11 @@ impl RemotePathState { self.paths.keys() } + /// Returns whether this stores any addresses. + pub(super) fn is_empty(&self) -> bool { + self.paths.is_empty() + } + /// Replies to all pending resolve requests. /// /// This is a no-op if no requests are queued. Replies `Ok` if we have any known paths, diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index d6a31f72566..85efb5cf2ec 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -541,7 +541,9 @@ impl TransportsSender { if any_match { Err(io::Error::other("all available transports failed")) } else { - Err(io::Error::other("no transport available")) + Err(io::Error::other( + "no transport available for this destination", + )) } } From 160d535b185716323c6a507759f829412c50a689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20Kr=C3=BCger?= Date: Thu, 20 Nov 2025 15:32:20 +0100 Subject: [PATCH 152/164] test(iroh): Fix `test_active_relay_inactive` test being flaky (#3680) ## Description This reverts a change from this PR: https://github.com/n0-computer/iroh/pull/3384 I originally thought I could make this test more reliable by pausing the tokio time across the `tokio::time::timeout` calls, but it turns out that actually makes the test *more* flaky: - When time is paused, the timeout will immediately fire once the tokio runtime has no more CPU work to do. - It's possible that there's no CPU work to do anymore, while there's something else that is actually still doing work, e.g. networking. - Before the `ActiveRelayActor` finishes its `run_connected` loop, it will call `client_sink.close().await`, which will do actual I/O. When the tokio runtime is paused at that moment, it'll immediately trigger the test's timeout. ## Notes & open questions I couldn't reproduce this problem even across a couple thousand runs of the test locally. I'm not super confident that this fixes things, but I've analyzed the logs and this seems to be the most likely thing that's happening to me. Closes #3613 ## Change checklist - [x] Self-review. --- iroh/src/magicsock/transports/relay/actor.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/iroh/src/magicsock/transports/relay/actor.rs b/iroh/src/magicsock/transports/relay/actor.rs index f26b3d73338..ea463da3769 100644 --- a/iroh/src/magicsock/transports/relay/actor.rs +++ b/iroh/src/magicsock/transports/relay/actor.rs @@ -1459,7 +1459,6 @@ mod tests { #[tokio::test] #[traced_test] - #[ignore = "flaky"] async fn test_active_relay_inactive() -> Result { let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?; @@ -1481,11 +1480,11 @@ mod tests { ); // Wait until the actor is connected to the relay server. - tokio::time::timeout(Duration::from_millis(200), async { + tokio::time::timeout(Duration::from_secs(5), async { loop { let (tx, rx) = oneshot::channel(); inbox_tx.send(ActiveRelayMessage::PingServer(tx)).await.ok(); - if tokio::time::timeout(Duration::from_millis(100), rx) + if tokio::time::timeout(Duration::from_millis(200), rx) .await .map(|resp| resp.is_ok()) .unwrap_or_default() @@ -1497,12 +1496,12 @@ mod tests { .await .std_context("timeout")?; - // From now on, we pause time - tokio::time::pause(); // We now have an idling ActiveRelayActor. If we advance time just a little it // should stay alive. info!("Stepping time forwards by RELAY_INACTIVE_CLEANUP_TIME / 2"); + tokio::time::pause(); tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME / 2).await; + tokio::time::resume(); assert!( tokio::time::timeout(Duration::from_millis(100), &mut task) @@ -1513,15 +1512,20 @@ mod tests { // If we advance time a lot it should finish. info!("Stepping time forwards by RELAY_INACTIVE_CLEANUP_TIME"); + tokio::time::pause(); tokio::time::advance(RELAY_INACTIVE_CLEANUP_TIME).await; + tokio::time::resume(); + + // We resume time for these timeouts, as there's actual I/O happening, + // for example closing the TCP stream, so we actually need the tokio + // runtime to idle a bit while the kernel is doing its thing. assert!( - tokio::time::timeout(Duration::from_millis(1000), task) + tokio::time::timeout(Duration::from_secs(1), task) .await .is_ok(), "actor task still running" ); - tokio::time::resume(); cancel_token.cancel(); Ok(()) } From e620b5effcd7205d11a7d5e4ed89b4fa72bcf2ff Mon Sep 17 00:00:00 2001 From: dignifiedquire Date: Sun, 23 Nov 2025 11:38:09 +0100 Subject: [PATCH 153/164] test: mark test_active_relay_inactive as non flaky --- iroh/src/magicsock/transports/relay/actor.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/iroh/src/magicsock/transports/relay/actor.rs b/iroh/src/magicsock/transports/relay/actor.rs index 8b74422873a..ea463da3769 100644 --- a/iroh/src/magicsock/transports/relay/actor.rs +++ b/iroh/src/magicsock/transports/relay/actor.rs @@ -1459,7 +1459,6 @@ mod tests { #[tokio::test] #[traced_test] - #[ignore = "flaky"] async fn test_active_relay_inactive() -> Result { let (_relay_map, relay_url, _server) = test_utils::run_relay_server().await?; From 94c150a6f4cacc4ccea03c9eb87e375164113730 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sun, 23 Nov 2025 17:35:52 +0100 Subject: [PATCH 154/164] Switch to QUIC-NAT-Traversal instead of DISCO (#3595) ## Description This switches from the old DISCO to the so-new-it-doesnt-exit-yet QUIC NAT Traversal. ## Breaking Changes Nothing visible? Maybe? ## Notes & open questions The QUIC NAT Traversal API doesn't exist yet, so this won't even build on any machine that's not mine. I've locally patched in the dummy methods that I use. --------- Co-authored-by: dignifiedquire Co-authored-by: Frando --- .config/nextest.toml | 2 +- Cargo.lock | 331 +++------- Cargo.toml | 22 +- iroh-relay/Cargo.toml | 8 +- iroh/Cargo.toml | 11 +- iroh/bench/Cargo.toml | 2 +- iroh/src/disco.rs | 619 ------------------ iroh/src/endpoint.rs | 4 +- iroh/src/key.rs | 158 ----- iroh/src/lib.rs | 2 - iroh/src/magicsock.rs | 368 ++--------- iroh/src/magicsock/metrics.rs | 24 +- iroh/src/magicsock/remote_map.rs | 52 +- iroh/src/magicsock/remote_map/remote_state.rs | 403 +++++------- .../remote_map/remote_state/path_state.rs | 37 +- 15 files changed, 329 insertions(+), 1714 deletions(-) delete mode 100644 iroh/src/disco.rs delete mode 100644 iroh/src/key.rs diff --git a/.config/nextest.toml b/.config/nextest.toml index d2ae495e9a4..a549d4068f2 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -15,4 +15,4 @@ test-group = 'run-in-isolation' threads-required = 32 [profile.default] -slow-timeout = { period = "20s", terminate-after = 3 } +slow-timeout = { period = "10s", terminate-after = 3 } diff --git a/Cargo.lock b/Cargo.lock index 197ad1f5915..cb174c61921 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,17 +16,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "aead" -version = "0.6.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac8202ab55fcbf46ca829833f347a82a2a4ce0596f0304ac322c2d100030cd56" -dependencies = [ - "bytes", - "crypto-common", - "inout", -] - [[package]] name = "ahash" version = "0.8.12" @@ -102,22 +91,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -253,9 +242,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "axum-macros", @@ -317,9 +306,9 @@ dependencies = [ [[package]] name = "axum-server" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab" +checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9" dependencies = [ "arc-swap", "bytes", @@ -348,12 +337,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "base16ct" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b59d472eab27ade8d770dcb11da7201c11234bef9f82ce7aa517be028d462b" - [[package]] name = "base32" version = "0.5.1" @@ -426,12 +409,11 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.11.0-rc.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ef36a6fcdb072aa548f3da057640ec10859eb4e91ddf526ee648d50c76a949" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" dependencies = [ "hybrid-array", - "zeroize", ] [[package]] @@ -448,9 +430,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cast" @@ -460,9 +442,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.43" +version = "1.2.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "739eb0f94557554b3ca9a86d2d37bebd49c5e6d0c1d2bda35ba5bdac830befc2" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" dependencies = [ "find-msvc-tools", "shlex", @@ -486,18 +468,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.10.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd162f2b8af3e0639d83f28a637e4e55657b7a74508dba5a9bf4da523d5c9e9" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", - "zeroize", -] - [[package]] name = "chrono" version = "0.4.42" @@ -537,23 +507,11 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.5.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e12a13eb01ded5d32ee9658d94f553a19e804204f2dc811df69ab4d9e0cb8c7" -dependencies = [ - "block-buffer", - "crypto-common", - "inout", - "zeroize", -] - [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -561,9 +519,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -795,39 +753,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8235645834fbc6832939736ce2f2d08192652269e11010a6240f61b908a1c6" dependencies = [ "hybrid-array", - "rand_core", -] - -[[package]] -name = "crypto_box" -version = "0.10.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bda4de3e070830cf3a27a394de135b6709aefcc54d1e16f2f029271254a6ed9" -dependencies = [ - "aead", - "chacha20", - "crypto_secretbox", - "curve25519-dalek", - "salsa20", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto_secretbox" -version = "0.2.0-pre.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54532aae6546084a52cef855593daf9555945719eeeda9974150e0def854873e" -dependencies = [ - "aead", - "chacha20", - "cipher", - "hybrid-array", - "poly1305", - "salsa20", - "subtle", - "zeroize", ] [[package]] @@ -881,9 +806,9 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.8.0-rc.9" +version = "0.8.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d8dd2f26c86b27a2a8ea2767ec7f9df7a89516e4794e54ac01ee618dda3aa4" +checksum = "02c1d73e9668ea6b6a28172aa55f3ebec38507131ce179051c8033b5c6037653" dependencies = [ "const-oid", "pem-rfc7468", @@ -1038,9 +963,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ed25519" -version = "3.0.0-rc.1" +version = "3.0.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef49c0b20c0ad088893ad2a790a29c06a012b3f05bcfc66661fd22a94b32129" +checksum = "594435fe09e345ee388e4e8422072ff7dfeca8729389fbd997b3f5504c44cd47" dependencies = [ "pkcs8", "serde", @@ -1121,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1150,9 +1075,9 @@ checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flume" @@ -1171,12 +1096,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1204,9 +1123,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.3" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" +checksum = "62d91fd049c123429b018c47887d3f75a265540dd3c30ba9cb7bae9197edb03a" dependencies = [ "autocfg", "tokio", @@ -1554,9 +1473,9 @@ dependencies = [ [[package]] name = "governor" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" +checksum = "6e23d5986fd4364c2fb7498523540618b4b8d92eec6c36a02e565f66748e2f79" dependencies = [ "cfg-if", "dashmap", @@ -1564,7 +1483,7 @@ dependencies = [ "futures-timer", "futures-util", "getrandom 0.3.4", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "nonzero_ext", "parking_lot", "portable-atomic", @@ -1622,24 +1541,13 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1835,14 +1743,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" dependencies = [ "typenum", - "zeroize", ] [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -1880,9 +1787,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64", "bytes", @@ -1896,7 +1803,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2057,19 +1964,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] name = "indicatif" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" dependencies = [ "console", "portable-atomic", @@ -2079,15 +1986,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "inout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7357b6e7aa75618c7864ebd0634b115a7218b0615f4cb1df33ac3eca23943d4" -dependencies = [ - "hybrid-array", -] - [[package]] name = "instant" version = "0.1.13" @@ -2123,9 +2021,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -2135,14 +2033,12 @@ dependencies = [ name = "iroh" version = "0.95.1" dependencies = [ - "aead", "axum", "backon", "bytes", "cfg_aliases", "clap", "console_error_panic_hook", - "crypto_box", "data-encoding", "derive_more 2.0.1", "ed25519-dalek", @@ -2329,7 +2225,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#246779c6f8a704db8a7e291257bb099cbf6a8eb6" +source = "git+https://github.com/n0-computer/quinn?branch=protocol-simplification#02fd7eefcdf4263079216cecf078ff7eb49aab0f" dependencies = [ "bytes", "cfg_aliases", @@ -2338,7 +2234,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2349,7 +2245,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#246779c6f8a704db8a7e291257bb099cbf6a8eb6" +source = "git+https://github.com/n0-computer/quinn?branch=protocol-simplification#02fd7eefcdf4263079216cecf078ff7eb49aab0f" dependencies = [ "bytes", "fastbloom", @@ -2372,14 +2268,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#246779c6f8a704db8a7e291257bb099cbf6a8eb6" +source = "git+https://github.com/n0-computer/quinn?branch=protocol-simplification#02fd7eefcdf4263079216cecf078ff7eb49aab0f" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -2391,7 +2287,6 @@ dependencies = [ "bytes", "cfg_aliases", "clap", - "crypto_box", "dashmap", "data-encoding", "derive_more 2.0.1", @@ -2590,7 +2485,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" dependencies = [ - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] @@ -2607,9 +2502,9 @@ checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" [[package]] name = "mainline" -version = "6.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be6c12ff79bfbf65bcbec84882a4bf700177df6d83a7b866c6a01cda7db4777" +checksum = "6ff27d378ca495eaf3be8616d5d7319c1c18e93fd60e13698fcdc7e19448f1a4" dependencies = [ "crc", "document-features", @@ -3052,9 +2947,9 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "1.0.0-rc.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8e58fab693c712c0d4e88f8eb3087b6521d060bcaf76aeb20cb192d809115ba" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] @@ -3141,9 +3036,9 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.11.0-rc.7" +version = "0.11.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93eac55f10aceed84769df670ea4a32d2ffad7399400d41ee1c13b1cd8e1b478" +checksum = "77089aec8290d0b7bb01b671b091095cf1937670725af4fd73d47249f03b12c0" dependencies = [ "der", "spki", @@ -3177,16 +3072,6 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "poly1305" -version = "0.9.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb78a635f75d76d856374961deecf61031c0b6f928c83dc9c0924ab6c019c298" -dependencies = [ - "cpufeatures", - "universal-hash", -] - [[package]] name = "portable-atomic" version = "1.11.1" @@ -3363,7 +3248,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3400,16 +3285,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -3619,9 +3504,9 @@ dependencies = [ [[package]] name = "resolv-conf" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3789b30bd25ba102de4beabd95d21ac45b69b1be7d14522bab988c526d6799" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" [[package]] name = "ring" @@ -3671,14 +3556,14 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.34" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", @@ -3773,7 +3658,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 0.26.11", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3793,8 +3678,8 @@ dependencies = [ "rustls-webpki", "security-framework", "security-framework-sys", - "webpki-root-certs 1.0.3", - "windows-sys 0.52.0", + "webpki-root-certs 1.0.4", + "windows-sys 0.61.2", ] [[package]] @@ -3838,16 +3723,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "salsa20" -version = "0.11.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ff3b81c8a6e381bc1673768141383f9328048a60edddcfc752a8291a138443" -dependencies = [ - "cfg-if", - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -4034,16 +3909,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serdect" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3ef0e35b322ddfaecbc60f34ab448e157e48531288ee49fafbb053696b8ffe2" -dependencies = [ - "base16ct", - "serde", -] - [[package]] name = "sha1" version = "0.11.0-rc.2" @@ -4089,18 +3954,18 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] [[package]] name = "signature" -version = "3.0.0-rc.4" +version = "3.0.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc280a6ff65c79fbd6622f64d7127f32b85563bca8c53cd2e9141d6744a9056d" +checksum = "2a0251c9d6468f4ba853b6352b190fb7c1e405087779917c238445eb03993826" [[package]] name = "simdutf8" @@ -4291,9 +4156,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -4357,7 +4222,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4559,9 +4424,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -4849,19 +4714,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "unit-prefix" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323402cff2dd658f39ca17c789b502021b3f18707c91cdf22e3838e1b4023817" - -[[package]] -name = "universal-hash" -version = "0.6.0-rc.2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55be643b40a21558f44806b53ee9319595bc7ca6896372e4e08e5d7d83c9cd6" -dependencies = [ - "crypto-common", - "subtle", -] +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" [[package]] name = "untrusted" @@ -5080,23 +4935,23 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" dependencies = [ - "webpki-root-certs 1.0.3", + "webpki-root-certs 1.0.4", ] [[package]] name = "webpki-root-certs" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d651ec480de84b762e7be71e6efa7461699c19d9e2c272c8d93455f567786e" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" dependencies = [ "rustls-pki-types", ] [[package]] name = "webpki-roots" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b130c0d2d49f8b6889abc456e795e82525204f27c42cf767cf0d7734e089b8" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -5778,18 +5633,18 @@ checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "43fa6694ed34d6e57407afbccdeecfa268c470a7d2a5b0cf49ce9fcc345afb90" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 89721a6363d..e7859a75768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,19 +43,17 @@ unused-async = "warn" [patch.crates-io] -iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } -iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } -iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } +iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } +iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "main" } -# iroh-quinn = { path = "../iroh-quinn/quinn" } -# iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } -# iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } +# iroh-quinn = { path = "../quinn/quinn" } +# iroh-quinn-proto = { path = "../quinn/quinn-proto" } +# iroh-quinn-udp = { path = "../quinn/quinn-udp" } - -[patch."https://github.com/n0-computer/quinn"] - -# iroh-quinn = { path = "../iroh-quinn/quinn" } -# iroh-quinn-proto = { path = "../iroh-quinn/quinn-proto" } -# iroh-quinn-udp = { path = "../iroh-quinn/quinn-udp" } +# [patch."https://github.com/n0-computer/quinn"] +# iroh-quinn = { path = "../quinn/quinn" } +# iroh-quinn-proto = { path = "../quinn/quinn-proto" } +# iroh-quinn-udp = { path = "../quinn/quinn-udp" } diff --git a/iroh-relay/Cargo.toml b/iroh-relay/Cargo.toml index 7a69c4a112b..5e18ed900d9 100644 --- a/iroh-relay/Cargo.toml +++ b/iroh-relay/Cargo.toml @@ -42,8 +42,8 @@ postcard = { version = "1", default-features = false, features = [ "use-std", "experimental-derive", ] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", @@ -61,7 +61,6 @@ tokio-rustls = { version = "0.26", default-features = false, features = [ "logging", "ring", ] } -sha1 = "0.11.0-rc.2" tokio-util = { version = "0.7", features = ["io-util", "io", "codec", "rt"] } tracing = "0.1" url = { version = "2.5.3", features = ["serde"] } @@ -85,6 +84,7 @@ time = { version = "0.3.37", optional = true } tokio-rustls-acme = { version = "0.8", optional = true } tokio-websockets = { version = "0.12", features = ["rustls-bring-your-own-connector", "ring", "getrandom", "rand", "server"], optional = true } # server-side websocket implementation simdutf8 = { version = "0.1.5", optional = true } # minimal version fix +sha1 = { version = "0.11.0-rc.2", optional = true } toml = { version = "0.9", optional = true } tracing-subscriber = { version = "0.3", features = [ "env-filter", @@ -115,7 +115,6 @@ getrandom = { version = "0.3.2", features = ["wasm_js"] } [dev-dependencies] clap = { version = "4", features = ["derive"] } -crypto_box = { version = "0.10.0-pre.0", features = ["serde", "chacha20"] } proptest = "1.2.0" rand_chacha = "0.9" tokio = { version = "1", features = [ @@ -151,6 +150,7 @@ server = [ "dep:tokio-rustls-acme", "dep:tokio-websockets", "dep:simdutf8", + "dep:sha1", "dep:toml", "dep:tracing-subscriber", "quinn/log", diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index f06fff30bc7..aad59be6b98 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -21,10 +21,8 @@ crate-type = ["lib", "cdylib"] workspace = true [dependencies] -aead = { version = "=0.6.0-rc.2", features = ["bytes"] } backon = { version = "1.4" } bytes = "1.7" -crypto_box = { version = "0.10.0-pre.0", features = ["serde", "chacha20"] } data-encoding = "2.2" derive_more = { version = "2.0.1", features = ["debug", "display", "from", "try_into", "deref", "from_str", "into_iterator"] } ed25519-dalek = { version = "3.0.0-pre.1", features = ["serde", "rand_core", "zeroize", "pkcs8", "pem"] } @@ -37,9 +35,9 @@ n0-watcher = "0.6" netwatch = { version = "0.12" } pin-project = "1" pkarr = { version = "5", default-features = false, features = ["relays"] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } -quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } +quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", @@ -84,7 +82,7 @@ hickory-resolver = "0.25.1" igd-next = { version = "0.16", features = ["aio_tokio"] } netdev = { version = "0.39.0" } portmapper = { version = "0.12", default-features = false } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["runtime-tokio", "rustls-ring"] } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification", default-features = false, features = ["runtime-tokio", "rustls-ring"] } tokio = { version = "1", features = [ "io-util", "macros", @@ -130,6 +128,7 @@ tokio = { version = "1", features = [ serde_json = "1" iroh-relay = { path = "../iroh-relay", default-features = false, features = ["test-utils", "server"] } tracing-test = "0.2.5" +# tracing-test = { git = "https://github.com/Frando/tracing-test", branch = "feat/color-and-filter-on-cli", features = ["pretty-log-printing"] } clap = { version = "4", features = ["derive"] } tracing-subscriber = { version = "0.3", features = [ "env-filter", diff --git a/iroh/bench/Cargo.toml b/iroh/bench/Cargo.toml index 086d1ebc31a..8108a22c5e1 100644 --- a/iroh/bench/Cargo.toml +++ b/iroh/bench/Cargo.toml @@ -12,7 +12,7 @@ iroh = { path = "..", default-features = false } iroh-metrics = { version = "0.37", optional = true } n0-future = "0.3.0" n0-error = "0.1.0" -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } rand = "0.9.2" rcgen = "0.14" rustls = { version = "0.23.33", default-features = false, features = ["ring"] } diff --git a/iroh/src/disco.rs b/iroh/src/disco.rs deleted file mode 100644 index 3da109bd00f..00000000000 --- a/iroh/src/disco.rs +++ /dev/null @@ -1,619 +0,0 @@ -//! Contains the discovery message types. -//! -//! A discovery message is: -//! -//! Header: -//! -//! ```ignore -//! magic: [u8; 6] // “TS💬” (0x54 53 f0 9f 92 ac) -//! sender_disco_pub: [u8; 32] // nacl public key -//! nonce: [u8; 24] -//! ```` -//! The recipient then decrypts the bytes following (the nacl secretbox) -//! and then the inner payload structure is: -//! -//! ```ignore -//! message_type: u8 // (the MessageType constants below) -//! message_version: u8 // (0 for now; but always ignore bytes at the end) -//! message_payload: &[u8] -//! ``` - -use std::{ - fmt::{self, Display}, - net::{IpAddr, SocketAddr}, -}; - -use data_encoding::HEXLOWER; -use iroh_base::{EndpointId, PublicKey, RelayUrl}; -use n0_error::{e, ensure, stack_error}; -use rand::Rng; -use serde::{Deserialize, Serialize}; -use url::Url; - -use crate::magicsock::transports; - -// TODO: custom magicn -/// The 6 byte header of all discovery messages. -pub const MAGIC: &str = "TS💬"; // 6 bytes: 0x54 53 f0 9f 92 ac -pub const MAGIC_LEN: usize = MAGIC.len(); - -/// Current Version. -const V0: u8 = 0; - -pub(crate) const KEY_LEN: usize = 32; -const TX_LEN: usize = 12; - -// Sizes for the inner message structure. - -/// Header: Type | Version -const HEADER_LEN: usize = 2; - -const PING_LEN: usize = TX_LEN + iroh_base::PublicKey::LENGTH; -const EP_LENGTH: usize = 16 + 2; // 16 byte IP address + 2 byte port - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -#[repr(u8)] -pub enum MessageType { - Ping = 0x01, - Pong = 0x02, - CallMeMaybe = 0x03, -} - -impl TryFrom for MessageType { - type Error = u8; - - fn try_from(value: u8) -> std::result::Result { - match value { - 0x01 => Ok(MessageType::Ping), - 0x02 => Ok(MessageType::Pong), - 0x03 => Ok(MessageType::CallMeMaybe), - _ => Err(value), - } - } -} - -const MESSAGE_HEADER_LEN: usize = MAGIC_LEN + KEY_LEN; - -pub fn encode_message(sender: &PublicKey, seal: Vec) -> Vec { - let mut out = Vec::with_capacity(MESSAGE_HEADER_LEN); - out.extend_from_slice(MAGIC.as_bytes()); - out.extend_from_slice(sender.as_bytes()); - out.extend(seal); - - out -} - -/// Reports whether p looks like it's a packet containing an encrypted disco message. -pub fn looks_like_disco_wrapper(p: &[u8]) -> bool { - if p.len() < MESSAGE_HEADER_LEN { - return false; - } - - &p[..MAGIC_LEN] == MAGIC.as_bytes() -} - -/// If `p` looks like a disco message it returns the slice of `p` that represents the disco public key source, -/// and the part that is the box. -pub fn source_and_box(p: &[u8]) -> Option<(PublicKey, &[u8])> { - if !looks_like_disco_wrapper(p) { - return None; - } - - let source = &p[MAGIC_LEN..MAGIC_LEN + KEY_LEN]; - let sender = PublicKey::try_from(source).ok()?; - let sealed_box = &p[MAGIC_LEN + KEY_LEN..]; - Some((sender, sealed_box)) -} - -/// A discovery message. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Message { - Ping(Ping), - Pong(Pong), - CallMeMaybe(CallMeMaybe), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Ping { - /// Random client-generated per-ping transaction ID. - pub tx_id: TransactionId, - - /// Allegedly the ping sender's public key. - /// - /// It shouldn't be trusted by itself. - pub endpoint_key: PublicKey, -} - -impl Ping { - /// Creates a ping message to ping `node_id`. - /// - /// Uses a randomly generated STUN transaction ID. - pub(crate) fn new(endpoint_id: EndpointId) -> Self { - Self { - tx_id: TransactionId::default(), - endpoint_key: endpoint_id, - } - } - - fn from_bytes(p: &[u8]) -> Result { - // Deliberately lax on longer-than-expected messages, for future compatibility. - ensure!(p.len() >= PING_LEN, ParseError::TooShort); - let tx_id: [u8; TX_LEN] = p[..TX_LEN].try_into().expect("length checked"); - let raw_key = &p[TX_LEN..TX_LEN + iroh_base::PublicKey::LENGTH]; - let endpoint_key = - PublicKey::try_from(raw_key).map_err(|_| e!(ParseError::InvalidEncoding))?; - let tx_id = TransactionId::from(tx_id); - - Ok(Ping { - tx_id, - endpoint_key, - }) - } - - fn as_bytes(&self) -> Vec { - let header = msg_header(MessageType::Ping, V0); - let mut out = vec![0u8; PING_LEN + HEADER_LEN]; - - out[..HEADER_LEN].copy_from_slice(&header); - out[HEADER_LEN..HEADER_LEN + TX_LEN].copy_from_slice(&self.tx_id); - out[HEADER_LEN + TX_LEN..].copy_from_slice(self.endpoint_key.as_ref()); - - out - } -} - -/// A response a Ping. -/// -/// It includes the sender's source IP + port, so it's effectively a STUN response. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Pong { - pub tx_id: TransactionId, - /// The observed address off the ping sender. - /// - /// 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4. - pub ping_observed_addr: SendAddr, -} - -/// Addresses to which we can send. This is either a UDP or a relay address. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SendAddr { - /// UDP, the ip addr. - Udp(SocketAddr), - /// Relay Url. - Relay(RelayUrl), -} - -impl From for SendAddr { - fn from(addr: transports::Addr) -> Self { - match addr { - transports::Addr::Ip(addr) => SendAddr::Udp(addr), - transports::Addr::Relay(url, _) => SendAddr::Relay(url), - } - } -} - -impl From for SendAddr { - fn from(source: SocketAddr) -> Self { - SendAddr::Udp(source) - } -} - -impl From for SendAddr { - fn from(source: RelayUrl) -> Self { - SendAddr::Relay(source) - } -} - -impl PartialEq for SendAddr { - fn eq(&self, other: &SocketAddr) -> bool { - match self { - Self::Relay(_) => false, - Self::Udp(addr) => addr.eq(other), - } - } -} - -impl Display for SendAddr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SendAddr::Relay(id) => write!(f, "Relay({id})"), - SendAddr::Udp(addr) => write!(f, "UDP({addr})"), - } - } -} - -/// Message sent only over the relay to request that the recipient try -/// to open up a magicsock path back to the sender. -/// -/// The sender should've already sent UDP packets to the peer to open -/// up the stateful firewall mappings inbound. -/// -/// The recipient may choose to not open a path back, if it's already happy with its path. -/// But usually it will. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CallMeMaybe { - /// What the peer believes its endpoints are. - pub my_numbers: Vec, -} - -#[allow(missing_docs)] -#[stack_error(derive, add_meta)] -#[non_exhaustive] -pub enum ParseError { - #[error("message is too short")] - TooShort, - #[error("invalid encoding")] - InvalidEncoding, - #[error("unknown format")] - UnknownFormat, -} - -fn send_addr_from_bytes(p: &[u8]) -> Result { - ensure!(p.len() > 2, ParseError::TooShort); - match p[0] { - 0u8 => { - let bytes: [u8; EP_LENGTH] = p[1..].try_into().map_err(|_| e!(ParseError::TooShort))?; - let addr = socket_addr_from_bytes(bytes); - Ok(SendAddr::Udp(addr)) - } - 1u8 => { - let s = std::str::from_utf8(&p[1..]).map_err(|_| e!(ParseError::InvalidEncoding))?; - let u: Url = s.parse().map_err(|_| e!(ParseError::InvalidEncoding))?; - Ok(SendAddr::Relay(u.into())) - } - _ => Err(e!(ParseError::UnknownFormat)), - } -} - -fn send_addr_to_vec(addr: &SendAddr) -> Vec { - match addr { - SendAddr::Relay(url) => { - let mut out = vec![1u8]; - out.extend_from_slice(url.to_string().as_bytes()); - out - } - SendAddr::Udp(ip) => { - let mut out = vec![0u8]; - out.extend_from_slice(&socket_addr_as_bytes(ip)); - out - } - } -} - -// Assumes p.len() == EP_LENGTH -fn socket_addr_from_bytes(p: [u8; EP_LENGTH]) -> SocketAddr { - debug_assert_eq!(EP_LENGTH, 16 + 2); - - let raw_src_ip: [u8; 16] = p[..16].try_into().expect("array long enough"); - let raw_port: [u8; 2] = p[16..].try_into().expect("array long enough"); - - let src_ip = IpAddr::from(raw_src_ip).to_canonical(); - let src_port = u16::from_le_bytes(raw_port); - - SocketAddr::new(src_ip, src_port) -} - -fn socket_addr_as_bytes(addr: &SocketAddr) -> [u8; EP_LENGTH] { - let mut out = [0u8; EP_LENGTH]; - let ipv6 = match addr.ip() { - IpAddr::V4(v4) => v4.to_ipv6_mapped(), - IpAddr::V6(v6) => v6, - }; - out[..16].copy_from_slice(&ipv6.octets()); - out[16..].copy_from_slice(&addr.port().to_le_bytes()); - - out -} - -impl Pong { - fn from_bytes(p: &[u8]) -> Result { - let tx_id: [u8; TX_LEN] = p[..TX_LEN] - .try_into() - .map_err(|_| e!(ParseError::TooShort))?; - - let tx_id = TransactionId::from(tx_id); - let src = send_addr_from_bytes(&p[TX_LEN..])?; - - Ok(Pong { - tx_id, - ping_observed_addr: src, - }) - } - - fn as_bytes(&self) -> Vec { - let header = msg_header(MessageType::Pong, V0); - let mut out = header.to_vec(); - out.extend_from_slice(&self.tx_id); - - let src_bytes = send_addr_to_vec(&self.ping_observed_addr); - out.extend(src_bytes); - out - } -} - -impl CallMeMaybe { - fn from_bytes(p: &[u8]) -> Result { - ensure!(p.len() % EP_LENGTH == 0, ParseError::InvalidEncoding); - - let num_entries = p.len() / EP_LENGTH; - let mut m = CallMeMaybe { - my_numbers: Vec::with_capacity(num_entries), - }; - - for chunk in p.chunks_exact(EP_LENGTH) { - let bytes: [u8; EP_LENGTH] = chunk - .try_into() - .map_err(|_| e!(ParseError::InvalidEncoding))?; - let src = socket_addr_from_bytes(bytes); - m.my_numbers.push(src); - } - - Ok(m) - } - - fn as_bytes(&self) -> Vec { - let header = msg_header(MessageType::CallMeMaybe, V0); - let mut out = vec![0u8; HEADER_LEN + self.my_numbers.len() * EP_LENGTH]; - out[..HEADER_LEN].copy_from_slice(&header); - - for (m, chunk) in self - .my_numbers - .iter() - .zip(out[HEADER_LEN..].chunks_exact_mut(EP_LENGTH)) - { - let raw = socket_addr_as_bytes(m); - chunk.copy_from_slice(&raw); - } - - out - } -} - -impl Message { - /// Parses the encrypted part of the message from inside the nacl secretbox. - pub fn from_bytes(p: &[u8]) -> Result { - ensure!(p.len() >= 2, ParseError::TooShort); - - let t = MessageType::try_from(p[0]).map_err(|_| e!(ParseError::UnknownFormat))?; - let version = p[1]; - ensure!(version == V0, ParseError::UnknownFormat); - - let p = &p[2..]; - match t { - MessageType::Ping => { - let ping = Ping::from_bytes(p)?; - Ok(Message::Ping(ping)) - } - MessageType::Pong => { - let pong = Pong::from_bytes(p)?; - Ok(Message::Pong(pong)) - } - MessageType::CallMeMaybe => { - let cm = CallMeMaybe::from_bytes(p)?; - Ok(Message::CallMeMaybe(cm)) - } - } - } - - /// Serialize this message to bytes. - pub fn as_bytes(&self) -> Vec { - match self { - Message::Ping(ping) => ping.as_bytes(), - Message::Pong(pong) => pong.as_bytes(), - Message::CallMeMaybe(cm) => cm.as_bytes(), - } - } -} - -impl Display for Message { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Message::Ping(ping) => { - write!(f, "Ping(tx={})", HEXLOWER.encode(&ping.tx_id)) - } - Message::Pong(pong) => { - write!(f, "Pong(tx={})", HEXLOWER.encode(&pong.tx_id)) - } - Message::CallMeMaybe(_) => { - write!(f, "CallMeMaybe") - } - } - } -} - -const fn msg_header(t: MessageType, ver: u8) -> [u8; HEADER_LEN] { - [t as u8, ver] -} - -const TRANSACTION_ID_SIZE: usize = 12; - -/// The transaction ID is a 96-bit identifier -/// -/// It is used to uniquely identify STUN transactions. -/// It primarily serves to correlate requests with responses, -/// though it also plays a small role in helping to prevent -/// certain types of attacks. The server also uses the transaction ID as -/// a key to identify each transaction uniquely across all clients. -#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub(crate) struct TransactionId([u8; TRANSACTION_ID_SIZE]); - -impl fmt::Debug for TransactionId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "TransactionId(0x")?; - fmt_transcation_id(self.as_ref(), f) - } -} - -impl fmt::Display for TransactionId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "transaction id (0x")?; - fmt_transcation_id(self.as_ref(), f) - } -} - -fn fmt_transcation_id(bytes: &[u8], f: &mut fmt::Formatter) -> fmt::Result { - for byte in bytes { - write!(f, "{:02X}", byte)?; - } - write!(f, ")") -} - -impl std::ops::Deref for TransactionId { - type Target = [u8]; - - fn deref(&self) -> &[u8] { - &self.0 - } -} - -impl AsRef<[u8]> for TransactionId { - fn as_ref(&self) -> &[u8] { - &self.0[..] - } -} - -impl From<&[u8; TRANSACTION_ID_SIZE]> for TransactionId { - fn from(buff: &[u8; TRANSACTION_ID_SIZE]) -> Self { - Self(*buff) - } -} - -impl From<[u8; TRANSACTION_ID_SIZE]> for TransactionId { - fn from(buff: [u8; TRANSACTION_ID_SIZE]) -> Self { - Self(buff) - } -} - -impl rand::distr::Distribution for rand::distr::StandardUniform { - fn sample(&self, rng: &mut R) -> TransactionId { - let mut buffer = [0u8; TRANSACTION_ID_SIZE]; - rng.fill_bytes(&mut buffer); - TransactionId::from(buffer) - } -} - -impl Default for TransactionId { - /// Creates a cryptographically random transaction ID chosen from the interval 0 .. 2**96-1. - fn default() -> Self { - let mut rng = rand::rng(); - rng.random() - } -} - -#[cfg(test)] -mod tests { - use iroh_base::SecretKey; - use rand::SeedableRng; - - use super::*; - use crate::key::{SharedSecret, public_ed_box, secret_ed_box}; - - #[test] - fn test_to_from_bytes() { - struct Test { - name: &'static str, - m: Message, - want: &'static str, - } - let tests = [ - Test { - name: "ping_with_endpointkey_src", - m: Message::Ping(Ping { - tx_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].into(), - endpoint_key: PublicKey::try_from( - &[ - 190, 243, 65, 104, 37, 102, 175, 75, 243, 22, 69, 200, 167, 107, 24, - 63, 216, 140, 120, 43, 4, 112, 16, 62, 117, 155, 45, 215, 72, 175, 40, - 189, - ][..], - ) - .unwrap(), - }), - want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c be f3 41 68 25 66 af 4b f3 16 45 c8 a7 6b 18 3f d8 8c 78 2b 04 70 10 3e 75 9b 2d d7 48 af 28 bd", - }, - Test { - name: "pong", - m: Message::Pong(Pong { - tx_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].into(), - ping_observed_addr: SendAddr::Udp("2.3.4.5:1234".parse().unwrap()), - }), - want: "02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 00 00 00 00 00 00 00 00 00 00 00 ff ff 02 03 04 05 d2 04", - }, - Test { - name: "pongv6", - m: Message::Pong(Pong { - tx_id: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].into(), - ping_observed_addr: SendAddr::Udp("[fed0::12]:6666".parse().unwrap()), - }), - want: "02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 00 fe d0 00 00 00 00 00 00 00 00 00 00 00 00 00 12 0a 1a", - }, - Test { - name: "call_me_maybe", - m: Message::CallMeMaybe(CallMeMaybe { - my_numbers: Vec::new(), - }), - want: "03 00", - }, - Test { - name: "call_me_maybe_endpoints", - m: Message::CallMeMaybe(CallMeMaybe { - my_numbers: vec![ - "1.2.3.4:567".parse().unwrap(), - "[2001::3456]:789".parse().unwrap(), - ], - }), - want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 37 02 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 15 03", - }, - ]; - for test in tests { - println!("{}", test.name); - - let got = test.m.as_bytes(); - assert_eq!( - got, - data_encoding::HEXLOWER - .decode(test.want.replace(' ', "").as_bytes()) - .unwrap(), - "wrong as_bytes" - ); - - let back = Message::from_bytes(&got).expect("failed to parse"); - assert_eq!(test.m, back, "wrong from_bytes"); - } - } - - #[test] - fn test_extraction() { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let sender_key = SecretKey::generate(&mut rng); - let recv_key = SecretKey::generate(&mut rng); - - let msg = Message::Ping(Ping { - tx_id: TransactionId::default(), - endpoint_key: sender_key.public(), - }); - - let sender_secret = secret_ed_box(&sender_key); - let shared = SharedSecret::new(&sender_secret, &public_ed_box(&recv_key.public())); - let mut seal = msg.as_bytes(); - shared.seal(&mut seal); - - let bytes = encode_message(&sender_key.public(), seal.clone()); - - assert!(looks_like_disco_wrapper(&bytes)); - assert_eq!(source_and_box(&bytes).unwrap().0, sender_key.public()); - - let (raw_key, seal_back) = source_and_box(&bytes).unwrap(); - assert_eq!(raw_key, sender_key.public()); - assert_eq!(seal_back, seal); - - let recv_secret = secret_ed_box(&recv_key); - let shared_recv = SharedSecret::new(&recv_secret, &public_ed_box(&sender_key.public())); - let mut open_seal = seal_back.to_vec(); - shared_recv - .open(&mut open_seal) - .expect("failed to open seal_back"); - let msg_back = Message::from_bytes(&open_seal).unwrap(); - assert_eq!(msg_back, msg); - } -} diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index b608091411f..0bff432fc14 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -174,7 +174,9 @@ impl Builder { self.transport_config .default_path_max_idle_timeout(Some(PATH_MAX_IDLE_TIMEOUT)); self.transport_config - .max_concurrent_multipath_paths(MAX_MULTIPATH_PATHS); + .max_concurrent_multipath_paths(MAX_MULTIPATH_PATHS + 1); + self.transport_config + .set_max_remote_nat_traversal_addresses(MAX_MULTIPATH_PATHS as u8); let static_config = StaticConfig { transport_config: Arc::new(self.transport_config), diff --git a/iroh/src/key.rs b/iroh/src/key.rs deleted file mode 100644 index c6b9f6ee483..00000000000 --- a/iroh/src/key.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! The private and public keys of an endpoint. - -use std::fmt::Debug; - -use aead::{AeadCore, AeadInOut, Buffer}; -use iroh_base::{PublicKey, SecretKey}; -use n0_error::{e, ensure, stack_error}; - -pub(crate) const NONCE_LEN: usize = 24; - -const AEAD_DATA: &[u8] = &[]; - -pub(super) fn public_ed_box(key: &PublicKey) -> crypto_box::PublicKey { - let key = key.as_verifying_key(); - crypto_box::PublicKey::from(key.to_montgomery()) -} - -pub(super) fn secret_ed_box(key: &SecretKey) -> crypto_box::SecretKey { - let key = key.as_signing_key(); - crypto_box::SecretKey::from(key.to_scalar()) -} - -/// Shared Secret. -pub struct SharedSecret(crypto_box::ChaChaBox); - -/// Errors that can occur during [`SharedSecret::open`]. -#[stack_error(derive, add_meta, from_sources, std_sources)] -#[non_exhaustive] -pub enum DecryptionError { - /// The nonce had the wrong size. - #[error("Invalid nonce")] - InvalidNonce, - /// AEAD decryption failed. - #[error("Aead error")] - Aead { - #[error(std_err)] - source: aead::Error, - }, -} - -impl Debug for SharedSecret { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SharedSecret(crypto_box::ChaChaBox)") - } -} - -impl SharedSecret { - pub fn new(this: &crypto_box::SecretKey, other: &crypto_box::PublicKey) -> Self { - SharedSecret(crypto_box::ChaChaBox::new_from_clamped(other, this)) - } - - /// Seals the provided cleartext. - pub fn seal(&self, buffer: &mut dyn Buffer) { - let nonce = crypto_box::ChaChaBox::try_generate_nonce_with_rng(&mut rand::rng()) - .expect("not enough randomness"); - - self.0 - .encrypt_in_place(&nonce, AEAD_DATA, buffer) - .expect("encryption failed"); - - buffer.extend_from_slice(&nonce).expect("buffer too small"); - } - - /// Opens the ciphertext, which must have been created using `Self::seal`, and places the clear text into the provided buffer. - pub fn open(&self, buffer: &mut dyn Buffer) -> Result<(), DecryptionError> { - ensure!(buffer.len() >= NONCE_LEN, DecryptionError::InvalidNonce); - - let offset = buffer.len() - NONCE_LEN; - let nonce: [u8; NONCE_LEN] = buffer.as_ref()[offset..] - .try_into() - .map_err(|_| e!(DecryptionError::InvalidNonce))?; - - buffer.truncate(offset); - self.0.decrypt_in_place(&nonce.into(), AEAD_DATA, buffer)?; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use rand::SeedableRng; - - use super::*; - - fn shared(this: &iroh_base::SecretKey, other: &iroh_base::PublicKey) -> SharedSecret { - let secret_key = secret_ed_box(this); - let public_key = public_ed_box(other); - - SharedSecret::new(&secret_key, &public_key) - } - - #[test] - fn test_seal_open_roundtrip() { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let key_a = iroh_base::SecretKey::generate(&mut rng); - let key_b = iroh_base::SecretKey::generate(&mut rng); - - println!("a -> a"); - seal_open_roundtrip(&key_a, &key_a); - println!("b -> b"); - seal_open_roundtrip(&key_b, &key_b); - - println!("a -> b"); - seal_open_roundtrip(&key_a, &key_b); - println!("b -> a"); - seal_open_roundtrip(&key_b, &key_a); - } - - fn seal_open_roundtrip(key_a: &iroh_base::SecretKey, key_b: &iroh_base::SecretKey) { - let msg = b"super secret message!!!!".to_vec(); - let shared_a = shared(key_a, &key_b.public()); - let mut sealed_message = msg.clone(); - shared_a.seal(&mut sealed_message); - - let shared_b = shared(key_b, &key_a.public()); - let mut decrypted_message = sealed_message.clone(); - shared_b.open(&mut decrypted_message).unwrap(); - assert_eq!(&msg[..], &decrypted_message); - } - - #[test] - fn test_roundtrip_public_key() { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let key = crypto_box::SecretKey::generate(&mut rng); - let public_bytes = *key.public_key().as_bytes(); - let public_key_back = crypto_box::PublicKey::from(public_bytes); - assert_eq!(key.public_key(), public_key_back); - } - - #[test] - fn test_same_public_key_api() { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let key = iroh_base::SecretKey::generate(&mut rng); - let public_key1: crypto_box::PublicKey = public_ed_box(&key.public()); - let public_key2: crypto_box::PublicKey = secret_ed_box(&key).public_key(); - - assert_eq!(public_key1, public_key2); - } - - #[test] - fn test_same_public_key_low_level() { - let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); - let key = ed25519_dalek::SigningKey::generate(&mut rng); - let public_key1 = { - let m = key.verifying_key().to_montgomery(); - crypto_box::PublicKey::from(m) - }; - - let public_key2 = { - let s = key.to_scalar(); - let cs = crypto_box::SecretKey::from(s); - cs.public_key() - }; - - assert_eq!(public_key1, public_key2); - } -} diff --git a/iroh/src/lib.rs b/iroh/src/lib.rs index b6504381d6b..a2be352d20b 100644 --- a/iroh/src/lib.rs +++ b/iroh/src/lib.rs @@ -253,8 +253,6 @@ #![cfg_attr(not(test), deny(clippy::unwrap_used))] #![cfg_attr(iroh_docsrs, feature(doc_cfg))] -mod disco; -mod key; mod magicsock; mod tls; diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 6f97fa3e817..ad5c5c0c8a8 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -16,7 +16,7 @@ //! however, read any packets that come off the UDP sockets. use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, + collections::{BTreeMap, BTreeSet}, fmt::Display, io, net::{IpAddr, SocketAddr}, @@ -26,7 +26,6 @@ use std::{ }, }; -use bytes::Bytes; use iroh_base::{EndpointAddr, EndpointId, PublicKey, RelayUrl, SecretKey, TransportAddr}; use iroh_relay::{RelayConfig, RelayMap}; use n0_error::{bail, e, stack_error}; @@ -49,7 +48,7 @@ use url::Url; use self::{ metrics::Metrics as MagicsockMetrics, remote_map::{RemoteMap, RemoteStateMessage}, - transports::{RelayActorConfig, Transports, TransportsSender}, + transports::{RelayActorConfig, Transports}, }; #[cfg(not(wasm_browser))] use crate::dns::DnsResolver; @@ -57,9 +56,7 @@ use crate::dns::DnsResolver; use crate::net_report::QuicConfig; use crate::{ defaults::timeouts::NET_REPORT_TIMEOUT, - disco::{self, SendAddr}, discovery::{ConcurrentDiscovery, Discovery, DiscoveryError, EndpointData, UserData}, - key::{DecryptionError, SharedSecret, public_ed_box, secret_ed_box}, magicsock::remote_map::PathsWatcher, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, @@ -97,7 +94,7 @@ pub(crate) const PATH_MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(6500); /// Maximum number of concurrent QUIC multipath paths per connection. /// /// Pretty arbitrary and high right now. -pub(crate) const MAX_MULTIPATH_PATHS: u32 = 16; +pub(crate) const MAX_MULTIPATH_PATHS: u32 = 12; /// Error returned when the endpoint state actor stopped while waiting for a reply. #[stack_error(add_meta, derive)] @@ -203,9 +200,6 @@ pub(crate) struct MagicSock { dns_resolver: DnsResolver, relay_map: RelayMap, - /// Disco - disco: DiscoState, - // - Discovery /// Optional discovery service discovery: ConcurrentDiscovery, @@ -515,208 +509,59 @@ impl MagicSock { #[cfg(windows)] let dst_ip = None; - let mut quic_packets_total = 0; - // zip is slow :( for i in 0..metas.len() { let quinn_meta = &mut metas[i]; - let buf = &mut bufs[i]; let source_addr = &source_addrs[i]; - let mut buf_contains_quic_datagrams = false; - let mut quic_datagram_count = 0; + let datagram_count = quinn_meta.len.div_ceil(quinn_meta.stride); + self.metrics + .magicsock + .recv_datagrams + .inc_by(datagram_count as _); if quinn_meta.len > quinn_meta.stride { - trace!(%quinn_meta.len, %quinn_meta.stride, "GRO datagram received"); + trace!( + src = ?source_addr, + len = quinn_meta.len, + stride = %quinn_meta.stride, + datagram_count = quinn_meta.len.div_ceil(quinn_meta.stride), + "GRO datagram received", + ); self.metrics.magicsock.recv_gro_datagrams.inc(); + } else { + trace!(src = ?source_addr, len = quinn_meta.len, "datagram received"); } - - // Chunk through the datagrams in this GRO payload to find disco - // packets and forward them to the actor - for datagram in buf[..quinn_meta.len].chunks_mut(quinn_meta.stride) { - if datagram.len() < quinn_meta.stride { - trace!( - len = %datagram.len(), - %quinn_meta.stride, - "Last GRO datagram smaller than stride", - ); + match source_addr { + transports::Addr::Ip(SocketAddr::V4(..)) => { + self.metrics + .magicsock + .recv_data_ipv4 + .inc_by(quinn_meta.len as _); } - - // Detect DISCO datagrams and process them. Overwrite the first - // byte of those packets with zero to make Quinn ignore the packet. This - // relies on quinn::EndpointConfig::grease_quic_bit being set to `false`, - // which we do in Endpoint::bind. - if let Some((sender, sealed_box)) = disco::source_and_box(datagram) { - self.handle_disco_message(sender, sealed_box, source_addr); - datagram[0] = 0u8; - } else { - match source_addr { - transports::Addr::Ip(SocketAddr::V4(..)) => { - self.metrics - .magicsock - .recv_data_ipv4 - .inc_by(datagram.len() as _); - } - transports::Addr::Ip(SocketAddr::V6(..)) => { - self.metrics - .magicsock - .recv_data_ipv6 - .inc_by(datagram.len() as _); - } - transports::Addr::Relay(..) => { - self.metrics - .magicsock - .recv_data_relay - .inc_by(datagram.len() as _); - } - } - - quic_datagram_count += 1; - buf_contains_quic_datagrams = true; + transports::Addr::Ip(SocketAddr::V6(..)) => { + self.metrics + .magicsock + .recv_data_ipv6 + .inc_by(quinn_meta.len as _); } - } - - if buf_contains_quic_datagrams { - match source_addr { - #[cfg(wasm_browser)] - transports::Addr::Ip(_addr) => { - panic!("cannot use IP based addressing in the browser"); - } - #[cfg(not(wasm_browser))] - transports::Addr::Ip(_addr) => { - quic_packets_total += quic_datagram_count; - } - transports::Addr::Relay(src_url, src_endpoint) => { - let mapped_addr = self - .remote_map - .relay_mapped_addrs - .get(&(src_url.clone(), *src_endpoint)); - quinn_meta.addr = mapped_addr.private_socket_addr(); - } + transports::Addr::Relay(src_url, src_node) => { + self.metrics + .magicsock + .recv_data_relay + .inc_by(quinn_meta.len as _); + + // Fill in the correct mapped address + let mapped_addr = self + .remote_map + .relay_mapped_addrs + .get(&(src_url.clone(), *src_node)); + quinn_meta.addr = mapped_addr.private_socket_addr(); } - } else { - // If all datagrams in this buf are DISCO, set len to zero to make - // Quinn skip the buf completely. - quinn_meta.len = 0; } + // Normalize local_ip quinn_meta.dst_ip = dst_ip; } - - if quic_packets_total > 0 { - self.metrics - .magicsock - .recv_datagrams - .inc_by(quic_packets_total as _); - trace!("UDP recv: {} packets", quic_packets_total); - } - } - - /// Handles a discovery message. - #[instrument("disco_in", skip_all, fields(endpoint = %sender.fmt_short(), ?src))] - fn handle_disco_message(&self, sender: PublicKey, sealed_box: &[u8], src: &transports::Addr) { - if self.is_closed() { - return; - } - - if let transports::Addr::Relay(_, endpoint_id) = src { - if endpoint_id != &sender { - // TODO: return here? - warn!( - "Received relay disco message from connection for {}, but with message from {}", - endpoint_id.fmt_short(), - sender.fmt_short() - ); - } - } - - // We're now reasonably sure we're expecting communication from - // this endpoint, do the heavy crypto lifting to see what they want. - let dm = match self.disco.unseal_and_decode(sender, sealed_box) { - Ok(dm) => dm, - Err(DiscoBoxError::Open { source, .. }) => { - warn!(?source, "failed to open disco box"); - self.metrics.magicsock.recv_disco_bad_key.inc(); - return; - } - Err(DiscoBoxError::Parse { source, .. }) => { - // Couldn't parse it, but it was inside a correctly - // signed box, so just ignore it, assuming it's from a - // newer version of Tailscale that we don't - // understand. Not even worth logging about, lest it - // be too spammy for old clients. - - self.metrics.magicsock.recv_disco_bad_parse.inc(); - debug!(?source, "failed to parse disco message"); - return; - } - }; - - if src.is_relay() { - self.metrics.magicsock.recv_disco_relay.inc(); - } else { - self.metrics.magicsock.recv_disco_udp.inc(); - } - - trace!(?dm, "receive disco message"); - match dm { - disco::Message::Ping(ping) => { - self.metrics.magicsock.recv_disco_ping.inc(); - self.remote_map.handle_ping(ping, sender, src.clone()); - } - disco::Message::Pong(pong) => { - self.metrics.magicsock.recv_disco_pong.inc(); - self.remote_map.handle_pong(pong, sender, src.clone()); - } - disco::Message::CallMeMaybe(cm) => { - self.metrics.magicsock.recv_disco_call_me_maybe.inc(); - self.remote_map - .handle_call_me_maybe(cm, sender, src.clone()); - } - } - } - - /// Sends out a disco message. - async fn send_disco_message( - &self, - sender: &TransportsSender, - dst: SendAddr, - dst_key: PublicKey, - msg: disco::Message, - ) -> io::Result<()> { - let dst = match dst { - SendAddr::Udp(addr) => transports::Addr::Ip(addr), - SendAddr::Relay(url) => transports::Addr::Relay(url, dst_key), - }; - - trace!(?dst, %msg, "send disco message (UDP)"); - if self.is_closed() { - return Err(io::Error::new( - io::ErrorKind::NotConnected, - "connection closed", - )); - } - - let pkt = self.disco.encode_and_seal(dst_key, &msg); - - let transmit = transports::Transmit { - contents: &pkt, - ecn: None, - segment_size: None, - }; - - let dst2 = dst.clone(); - match sender.send(&dst2, None, &transmit).await { - Ok(()) => { - trace!(?dst, %msg, "sent disco message"); - self.metrics.magicsock.sent_disco_udp.inc(); - disco_message_sent(&msg, &self.metrics.magicsock); - Ok(()) - } - Err(err) => { - warn!(?dst, ?msg, ?err, "failed to send disco message"); - Err(err) - } - } } /// Publishes our address to a discovery service, if configured. @@ -995,14 +840,12 @@ impl Handle { .any(|addr| addr.is_ipv6()); let direct_addrs = DiscoveredDirectAddrs::default(); - let (disco, disco_receiver) = DiscoState::new(&secret_key); let remote_map = { RemoteMap::new( secret_key.public(), metrics.magicsock.clone(), direct_addrs.addrs.watch(), - disco.clone(), transports.create_sender(), discovery.clone(), ) @@ -1012,7 +855,6 @@ impl Handle { public_key: secret_key.public(), closing: AtomicBool::new(false), closed: AtomicBool::new(false), - disco, actor_sender: actor_sender.clone(), ipv6_reported, remote_map, @@ -1037,7 +879,6 @@ impl Handle { // the packet if grease_quic_bit is set to false. endpoint_config.grease_quic_bit(false); - let sender = transports.create_sender(); let local_addrs_watch = transports.local_addrs_watch(); let network_change_sender = transports.create_network_change_sender(); @@ -1110,8 +951,6 @@ impl Handle { direct_addr_update_state, network_change_sender, direct_addr_done_rx, - pending_call_me_maybes: Default::default(), - disco_receiver, }; // Initialize addresses #[cfg(not(wasm_browser))] @@ -1119,7 +958,7 @@ impl Handle { let actor_task = task::spawn( actor - .run(shutdown_token.child_token(), local_addrs_watch, sender) + .run(shutdown_token.child_token(), local_addrs_watch) .instrument(info_span!("actor")), ); @@ -1212,82 +1051,6 @@ fn default_quic_client_config() -> rustls::ClientConfig { .with_no_client_auth() } -#[derive(Debug, Clone)] -struct DiscoState { - /// The EndpointId/PublikeKey of this endpoint. - this_id: EndpointId, - /// Encryption key for this endpoint. - secret_encryption_key: Arc, - /// The state for an active DiscoKey. - secrets: Arc>>, - /// Disco (ping) queue - sender: mpsc::Sender<(SendAddr, PublicKey, disco::Message)>, -} - -impl DiscoState { - fn new( - secret_key: &SecretKey, - ) -> (Self, mpsc::Receiver<(SendAddr, PublicKey, disco::Message)>) { - let this_id = secret_key.public(); - let secret_encryption_key = secret_ed_box(secret_key); - let (disco_sender, disco_receiver) = mpsc::channel(256); - - ( - Self { - this_id, - secret_encryption_key: Arc::new(secret_encryption_key), - secrets: Default::default(), - sender: disco_sender, - }, - disco_receiver, - ) - } - - fn try_send(&self, dst: SendAddr, dst_key: PublicKey, msg: disco::Message) -> bool { - self.sender.try_send((dst, dst_key, msg)).is_ok() - } - - fn encode_and_seal(&self, other_key: PublicKey, msg: &disco::Message) -> Bytes { - let mut seal = msg.as_bytes(); - self.get_secret(other_key, |secret| secret.seal(&mut seal)); - disco::encode_message(&self.this_id, seal).into() - } - - fn unseal_and_decode( - &self, - endpoint_key: PublicKey, - sealed_box: &[u8], - ) -> Result { - let mut sealed_box = sealed_box.to_vec(); - self.get_secret(endpoint_key, |secret| secret.open(&mut sealed_box)) - .map_err(|source| e!(DiscoBoxError::Open { source }))?; - disco::Message::from_bytes(&sealed_box) - .map_err(|source| e!(DiscoBoxError::Parse { source })) - } - - fn get_secret(&self, endpoint_id: PublicKey, cb: F) -> T - where - F: FnOnce(&mut SharedSecret) -> T, - { - let mut inner = self.secrets.lock().expect("poisoned"); - let x = inner.entry(endpoint_id).or_insert_with(|| { - let public_key = public_ed_box(&endpoint_id); - SharedSecret::new(&self.secret_encryption_key, &public_key) - }); - cb(x) - } -} - -#[allow(missing_docs)] -#[stack_error(derive, add_meta)] -#[non_exhaustive] -enum DiscoBoxError { - #[error("Failed to open crypto box")] - Open { source: DecryptionError }, - #[error("Failed to parse disco message")] - Parse { source: disco::ParseError }, -} - #[derive(Debug)] #[allow(clippy::enum_variant_names)] enum ActorMessage { @@ -1309,11 +1072,6 @@ struct Actor { /// Indicates the direct addr update state. direct_addr_update_state: DirectAddrUpdateState, direct_addr_done_rx: mpsc::Receiver<()>, - - /// List of CallMeMaybe disco messages that should be sent out after - /// the next endpoint update completes - pending_call_me_maybes: HashMap, - disco_receiver: mpsc::Receiver<(SendAddr, PublicKey, disco::Message)>, } impl Actor { @@ -1321,7 +1079,6 @@ impl Actor { mut self, shutdown_token: CancellationToken, mut watcher: impl Watcher> + Send + Sync, - sender: TransportsSender, ) { // Setup network monitoring let mut current_netmon_state = self.netmon_watcher.get(); @@ -1450,11 +1207,6 @@ impl Actor { self.msock.metrics.magicsock.actor_link_change.inc(); self.handle_network_change(is_major).await; } - Some((dst, dst_key, msg)) = self.disco_receiver.recv() => { - if let Err(err) = self.msock.send_disco_message(&sender, dst.clone(), dst_key, msg).await { - warn!(%dst, endpoint = %dst_key.fmt_short(), ?err, "failed to send disco message (UDP)"); - } - } _ = remote_map_gc.tick() => { self.msock.remote_map.remove_closed_remote_state_actors(); } @@ -1582,7 +1334,6 @@ impl Actor { }) .collect(), ); - self.send_queued_call_me_maybes(); } #[cfg(not(wasm_browser))] @@ -1649,22 +1400,6 @@ impl Actor { } } - fn send_queued_call_me_maybes(&mut self) { - let msg = self.msock.direct_addrs.to_call_me_maybe_message(); - let msg = disco::Message::CallMeMaybe(msg); - // allocate, to minimize locking duration - - for (public_key, url) in self.pending_call_me_maybes.drain() { - if !self - .msock - .disco - .try_send(SendAddr::Relay(url), public_key, msg.clone()) - { - warn!(endpoint = %public_key.fmt_short(), "relay channel full, dropping call-me-maybe"); - } - } - } - fn handle_net_report_report(&mut self, mut report: Option) { if let Some(ref mut r) = report { self.msock.ipv6_reported.store(r.udp_v6, Ordering::Relaxed); @@ -1734,25 +1469,6 @@ impl DiscoveredDirectAddrs { fn sockaddrs(&self) -> impl Iterator { self.addrs.get().into_iter().map(|da| da.addr) } - - fn to_call_me_maybe_message(&self) -> disco::CallMeMaybe { - let my_numbers = self.addrs.get().into_iter().map(|da| da.addr).collect(); - disco::CallMeMaybe { my_numbers } - } -} - -fn disco_message_sent(msg: &disco::Message, metrics: &MagicsockMetrics) { - match msg { - disco::Message::Ping(_) => { - metrics.sent_disco_ping.inc(); - } - disco::Message::Pong(_) => { - metrics.sent_disco_pong.inc(); - } - disco::Message::CallMeMaybe(_) => { - metrics.sent_disco_call_me_maybe.inc(); - } - } } /// A *direct address* on which an iroh-endpoint might be contactable. diff --git a/iroh/src/magicsock/metrics.rs b/iroh/src/magicsock/metrics.rs index e6f53d95c7c..1bc9b9eb0b3 100644 --- a/iroh/src/magicsock/metrics.rs +++ b/iroh/src/magicsock/metrics.rs @@ -27,27 +27,15 @@ pub struct Metrics { /// Number of datagrams received using GRO pub recv_gro_datagrams: Counter, - // Disco packets - pub send_disco_udp: Counter, - pub send_disco_relay: Counter, - pub sent_disco_udp: Counter, - pub sent_disco_relay: Counter, - pub sent_disco_ping: Counter, - pub sent_disco_pong: Counter, - pub sent_disco_call_me_maybe: Counter, - pub recv_disco_bad_key: Counter, - pub recv_disco_bad_parse: Counter, - - pub recv_disco_udp: Counter, - pub recv_disco_relay: Counter, - pub recv_disco_ping: Counter, - pub recv_disco_pong: Counter, - pub recv_disco_call_me_maybe: Counter, - pub recv_disco_call_me_maybe_bad_disco: Counter, - // How many times our relay home endpoint DI has changed from non-zero to a different non-zero. pub relay_home_change: Counter, + /* + * Holepunching metrics + */ + /// The number of NAT traversal attempts initiated. + pub nat_traversal: Counter, + /* * Connection Metrics */ diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index 76d8fdf0a41..583b4916813 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -10,18 +10,17 @@ use iroh_base::{EndpointId, RelayUrl}; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; -use tracing::warn; pub(crate) use self::remote_state::PathsWatcher; pub(super) use self::remote_state::RemoteStateMessage; pub use self::remote_state::{PathInfo, PathInfoList}; use self::remote_state::{RemoteStateActor, RemoteStateHandle}; use super::{ - DirectAddr, DiscoState, MagicsockMetrics, + DirectAddr, MagicsockMetrics, mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, - transports::{self, TransportsSender}, + transports::TransportsSender, }; -use crate::{disco, discovery::ConcurrentDiscovery}; +use crate::discovery::ConcurrentDiscovery; mod remote_state; @@ -59,7 +58,6 @@ pub(crate) struct RemoteMap { metrics: Arc, /// The "direct" addresses known for our local endpoint local_direct_addrs: n0_watcher::Direct>, - disco: DiscoState, sender: TransportsSender, discovery: ConcurrentDiscovery, } @@ -69,9 +67,7 @@ impl RemoteMap { pub(super) fn new( local_endpoint_id: EndpointId, metrics: Arc, - local_direct_addrs: n0_watcher::Direct>, - disco: DiscoState, sender: TransportsSender, discovery: ConcurrentDiscovery, ) -> Self { @@ -82,7 +78,6 @@ impl RemoteMap { local_endpoint_id, metrics, local_direct_addrs, - disco, sender, discovery, } @@ -140,7 +135,6 @@ impl RemoteMap { eid, self.local_endpoint_id, self.local_direct_addrs.clone(), - self.disco.clone(), self.relay_mapped_addrs.clone(), self.metrics.clone(), self.sender.clone(), @@ -150,46 +144,6 @@ impl RemoteMap { let sender = handle.sender.get().expect("just created"); (handle, sender) } - - pub(super) fn handle_ping(&self, msg: disco::Ping, sender: EndpointId, src: transports::Addr) { - if msg.endpoint_key != sender { - warn!("DISCO Ping EndpointId mismatch, ignoring ping"); - return; - } - let remote_state = self.remote_state_actor(sender); - if let Err(err) = remote_state.try_send(RemoteStateMessage::PingReceived(msg, src)) { - // TODO: This is really, really bad and will drop pings under load. But - // DISCO pings are going away with QUIC-NAT-TRAVERSAL so I don't care. - warn!("DISCO Ping dropped: {err:#}"); - } - } - - pub(super) fn handle_pong(&self, msg: disco::Pong, sender: EndpointId, src: transports::Addr) { - let actor = self.remote_state_actor(sender); - if let Err(err) = actor.try_send(RemoteStateMessage::PongReceived(msg, src)) { - // TODO: This is really, really bad and will drop pongs under load. But - // DISCO pongs are going away with QUIC-NAT-TRAVERSAL so I don't care. - warn!("DISCO Pong dropped: {err:#}"); - } - } - - pub(super) fn handle_call_me_maybe( - &self, - msg: disco::CallMeMaybe, - sender: EndpointId, - src: transports::Addr, - ) { - if !src.is_relay() { - warn!("DISCO CallMeMaybe packets should only come via relay"); - return; - } - let actor = self.remote_state_actor(sender); - if let Err(err) = actor.try_send(RemoteStateMessage::CallMeMaybeReceived(msg)) { - // TODO: This is bad and will drop call-me-maybe's under load. But - // DISCO CallMeMaybe going away with QUIC-NAT-TRAVERSAL so I don't care. - warn!("DISCO CallMeMaybe dropped: {err:#}"); - } - } } /// The origin or *source* through which an address associated with a remote endpoint diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index 6fda36e3c99..bb451290488 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -16,7 +16,7 @@ use n0_future::{ }; use n0_watcher::{Watchable, Watcher}; use quinn::{PathStats, WeakConnectionHandle}; -use quinn_proto::{PathError, PathEvent, PathId, PathStatus}; +use quinn_proto::{PathError, PathEvent, PathId, PathStatus, iroh_hp}; use rustc_hash::FxHashMap; use smallvec::SmallVec; use sync_wrapper::SyncStream; @@ -30,11 +30,10 @@ use self::{ }; use super::Source; use crate::{ - disco::{self}, discovery::{ConcurrentDiscovery, Discovery, DiscoveryError, DiscoveryItem}, endpoint::DirectAddr, magicsock::{ - DiscoState, HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, + HEARTBEAT_INTERVAL, MagicsockMetrics, PATH_MAX_IDLE_TIMEOUT, mapped_addrs::{AddrMap, MappedAddr, RelayMappedAddr}, remote_map::Private, transports::{self, OwnedTransmit, TransportsSender}, @@ -42,6 +41,12 @@ use crate::{ util::MaybeFuture, }; +/// How often to attempt holepunching. +/// +/// If there have been no changes to the NAT address candidates, holepunching will not be +/// attempted more frequently than at this interval. +const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); + mod guarded_channel; mod path_state; @@ -93,6 +98,19 @@ type PathEvents = MergeUnbounded< >, >; +/// A stream of events of announced NAT traversal candidate addresses for all connections. +/// +/// The connection is identified using [`ConnId`]. +type AddrEvents = MergeUnbounded< + Pin< + Box< + dyn Stream)> + + Send + + Sync, + >, + >, +>; + /// Either a stream of incoming results from [`ConcurrentDiscovery::resolve`] or infinitely pending. /// /// Set to [`Either::Left`] with an always-pending stream while discovery is not running, and to @@ -126,10 +144,7 @@ pub(super) struct RemoteStateActor { /// Our local addresses. /// /// These are our local addresses and any reflexive transport addresses. - /// They are called "direct addresses" in the magic socket actor. local_direct_addrs: n0_watcher::Direct>, - /// Shared state to allow to encrypt DISCO messages to peers. - disco: DiscoState, /// The mapping between endpoints via a relay and their [`RelayMappedAddr`]s. relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, /// Discovery service, cloned from the magicsock. @@ -143,6 +158,8 @@ pub(super) struct RemoteStateActor { connections_close: FuturesUnordered, /// Events emitted by Quinn about path changes, for all paths, all connections. path_events: PathEvents, + /// A stream of events of announced NAT traversal candidate addresses for all connections. + addr_events: AddrEvents, // Internal state - Holepunching and path state. // @@ -183,7 +200,6 @@ impl RemoteStateActor { endpoint_id: EndpointId, local_endpoint_id: EndpointId, local_direct_addrs: n0_watcher::Direct>, - disco: DiscoState, relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, metrics: Arc, sender: TransportsSender, @@ -196,10 +212,10 @@ impl RemoteStateActor { local_direct_addrs, relay_mapped_addrs, discovery, - disco, connections: FxHashMap::default(), connections_close: Default::default(), path_events: Default::default(), + addr_events: Default::default(), paths: Default::default(), last_holepunch: None, selected_path: Default::default(), @@ -269,6 +285,10 @@ impl RemoteStateActor { Some((id, evt)) = self.path_events.next() => { self.handle_path_event(id, evt); } + Some((id, evt)) = self.addr_events.next() => { + trace!(?id, ?evt, "remote addrs updated, triggering holepunching"); + self.trigger_holepunching().await; + } Some(conn_id) = self.connections_close.next(), if !self.connections_close.is_empty() => { self.connections.remove(&conn_id); if self.connections.is_empty() { @@ -281,6 +301,7 @@ impl RemoteStateActor { trace!("direct address watcher disconnected, shutting down"); break; } + self.local_addrs_updated(); trace!("local addrs updated, triggering holepunching"); self.trigger_holepunching().await; } @@ -327,15 +348,6 @@ impl RemoteStateActor { RemoteStateMessage::AddConnection(handle, tx) => { self.handle_msg_add_connection(handle, tx).await; } - RemoteStateMessage::CallMeMaybeReceived(msg) => { - self.handle_msg_call_me_maybe_received(msg).await; - } - RemoteStateMessage::PingReceived(ping, src) => { - self.handle_msg_ping_received(ping, src).await; - } - RemoteStateMessage::PongReceived(pong, src) => { - self.handle_msg_pong_received(pong, src); - } RemoteStateMessage::ResolveRemote(addrs, tx) => { self.handle_msg_resolve_remote(addrs, tx); } @@ -419,11 +431,25 @@ impl RemoteStateActor { let conn_id = ConnId(conn.stable_id()); self.connections.remove(&conn_id); - // Store the connection and hook up paths events stream. - let events = BroadcastStream::new(conn.path_events()); - let stream = events.map(move |evt| (conn_id, evt)); - self.path_events.push(Box::pin(stream)); + // Hook up paths, NAT addresses and connection closed event streams. + self.path_events.push(Box::pin( + BroadcastStream::new(conn.path_events()).map(move |evt| (conn_id, evt)), + )); + self.addr_events.push(Box::pin( + BroadcastStream::new(conn.nat_traversal_updates()).map(move |evt| (conn_id, evt)), + )); self.connections_close.push(OnClosed::new(&conn)); + + // Add local addrs to the connection + let local_addrs = self + .local_direct_addrs + .get() + .iter() + .map(|d| d.addr) + .collect::>(); + Self::set_local_addrs(&conn, &local_addrs); + + // Store the connection let conn_state = self .connections .entry(conn_id) @@ -478,81 +504,6 @@ impl RemoteStateActor { .ok(); } - /// Handles [`RemoteStateMessage::CallMeMaybeReceived`]. - async fn handle_msg_call_me_maybe_received(&mut self, msg: disco::CallMeMaybe) { - event!( - target: "iroh::_events::call_me_maybe::recv", - Level::DEBUG, - remote = %self.endpoint_id.fmt_short(), - addrs = ?msg.my_numbers, - ); - for addr in msg.my_numbers { - let dst = transports::Addr::Ip(addr); - let ping = disco::Ping::new(self.local_endpoint_id); - - self.paths - .insert(dst.clone(), Source::CallMeMaybe { _0: Private }); - self.paths.disco_ping_sent(dst.clone(), ping.tx_id); - - event!( - target: "iroh::_events::ping::sent", - Level::DEBUG, - remote = %self.endpoint_id.fmt_short(), - ?dst, - ); - self.send_disco_message(dst, disco::Message::Ping(ping)) - .await; - } - } - - /// Handles [`RemoteStateMessage::PingReceived`]. - async fn handle_msg_ping_received(&mut self, ping: disco::Ping, src: transports::Addr) { - let transports::Addr::Ip(addr) = src else { - warn!("received ping via relay transport, ignored"); - return; - }; - event!( - target: "iroh::_events::ping::recv", - Level::DEBUG, - remote = %self.endpoint_id.fmt_short(), - ?src, - txn = ?ping.tx_id, - ); - let pong = disco::Pong { - tx_id: ping.tx_id, - ping_observed_addr: addr.into(), - }; - event!( - target: "iroh::_events::pong::sent", - Level::DEBUG, - remote = %self.endpoint_id.fmt_short(), - dst = ?src, - txn = ?pong.tx_id, - ); - self.send_disco_message(src.clone(), disco::Message::Pong(pong)) - .await; - - self.paths.insert(src, Source::Ping { _0: Private }); - - trace!("ping received, triggering holepunching"); - self.trigger_holepunching().await; - } - - /// Handles [`RemoteStateMessage::PongReceived`]. - fn handle_msg_pong_received(&mut self, pong: disco::Pong, src: transports::Addr) { - if self.paths.disco_pong_received(&src, pong.tx_id) { - event!( - target: "iroh::_events::pong::recv", - Level::DEBUG, - remote_endpoint = %self.endpoint_id.fmt_short(), - ?src, - txn = ?pong.tx_id, - ); - - self.open_path(&src); - } - } - /// Handles [`RemoteStateMessage::ResolveRemote`]. fn handle_msg_resolve_remote( &mut self, @@ -628,62 +579,101 @@ impl RemoteStateActor { } } + /// Sets the current local addresses to QNT's state to all connections + fn local_addrs_updated(&mut self) { + let local_addrs = self + .local_direct_addrs + .get() + .iter() + .map(|d| d.addr) + .collect::>(); + + for conn in self.connections.values().filter_map(|s| s.handle.upgrade()) { + Self::set_local_addrs(&conn, &local_addrs); + } + // todo: trace + } + + /// Sets the current local addresses to QNT's state + fn set_local_addrs(conn: &quinn::Connection, local_addrs: &BTreeSet) { + let quinn_local_addrs = match conn.get_local_nat_traversal_addresses() { + Ok(addrs) => BTreeSet::from_iter(addrs), + Err(err) => { + warn!("failed to get local nat candidates: {err:#}"); + return; + } + }; + for addr in local_addrs.difference(&quinn_local_addrs) { + if let Err(err) = conn.add_nat_traversal_address(*addr) { + warn!("failed adding local addr: {err:#}",); + } + } + for addr in quinn_local_addrs.difference(local_addrs) { + if let Err(err) = conn.remove_nat_traversal_address(*addr) { + warn!("failed removing local addr: {err:#}"); + } + } + trace!(?local_addrs, "updated local QNT addresses"); + } + /// Triggers holepunching to the remote endpoint. /// /// This will manage the entire process of holepunching with the remote endpoint. /// - /// - If there already is a direct connection, nothing happens. - /// - If there is no relay address known, nothing happens. - /// - If there was a recent attempt, it will schedule holepunching instead. - /// - Unless there are new addresses to try. - /// - The scheduled attempt will only run if holepunching has not yet succeeded by - /// then. - /// - DISCO pings will be sent to addresses recently advertised in a call-me-maybe - /// message. - /// - A DISCO call-me-maybe message advertising our own addresses will be sent. - /// - /// If a next trigger needs to be scheduled the delay until when to call this again is - /// returned. + /// - Holepunching happens on the Connection with the lowest [`ConnId`] which is a + /// client. + /// - Both endpoints may initiate holepunching if both have a client connection. + /// - Any opened paths are opened on all other connections without holepunching. + /// - If there are no changes in local or remote candidate addresses since the + /// last attempt **and** there was a recent attempt, a trigger_holepunching call + /// will be scheduled instead. async fn trigger_holepunching(&mut self) { - const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); - if self.connections.is_empty() { trace!("not holepunching: no connections"); return; } - if self - .selected_path - .get() - .map(|addr| addr.is_ip()) - .unwrap_or_default() - { - // TODO: We should ping this path to make sure it still works. Because we now - // know things could be broken. - trace!("not holepunching: already have a direct connection"); - // TODO: If the latency is kind of bad we should retry holepunching at times. + let Some(conn) = self + .connections + .iter() + .filter_map(|(id, state)| state.handle.upgrade().map(|conn| (*id, conn))) + .filter(|(_, conn)| conn.side().is_client()) + .min_by_key(|(id, _)| *id) + .map(|(_, conn)| conn) + else { + trace!("not holepunching: no client connection"); return; - } - - let remote_addrs: BTreeSet = self.remote_hp_addrs(); - let local_addrs: BTreeSet = self + }; + let remote_candidates = match conn.get_remote_nat_traversal_addresses() { + Ok(addrs) => BTreeSet::from_iter(addrs), + Err(err) => { + warn!("failed to get nat candidate addresses: {err:#}"); + return; + } + }; + let local_candidates: BTreeSet = self .local_direct_addrs .get() .iter() .map(|daddr| daddr.addr) .collect(); - let new_addrs = self + let new_candidates = self .last_holepunch .as_ref() .map(|last_hp| { // Addrs are allowed to disappear, but if there are new ones we need to // holepunch again. - trace!(?last_hp, ?local_addrs, ?remote_addrs, "addrs to holepunch?"); - !remote_addrs.is_subset(&last_hp.remote_addrs) - || !local_addrs.is_subset(&last_hp.local_addrs) + trace!( + ?last_hp, + ?local_candidates, + ?remote_candidates, + "candidates to holepunch?" + ); + !remote_candidates.is_subset(&last_hp.remote_candidates) + || !local_candidates.is_subset(&last_hp.local_candidates) }) .unwrap_or(true); - if !new_addrs { + if !new_candidates { if let Some(ref last_hp) = self.last_holepunch { let next_hp = last_hp.when + HOLEPUNCH_ATTEMPTS_INTERVAL; let now = Instant::now(); @@ -695,114 +685,40 @@ impl RemoteStateActor { } } - self.do_holepunching().await; - } - - /// Returns the remote addresses to holepunch against. - fn remote_hp_addrs(&self) -> BTreeSet { - const CALL_ME_MAYBE_VALIDITY: Duration = Duration::from_secs(30); - - self.paths - .iter() - .filter_map(|(addr, state)| match addr { - transports::Addr::Ip(socket_addr) => Some((socket_addr, state)), - transports::Addr::Relay(_, _) => None, - }) - .filter_map(|(addr, state)| { - if state - .sources - .get(&Source::CallMeMaybe { _0: Private }) - .map(|when| when.elapsed() <= CALL_ME_MAYBE_VALIDITY) - .unwrap_or_default() - || state - .sources - .get(&Source::Ping { _0: Private }) - .map(|when| when.elapsed() <= CALL_ME_MAYBE_VALIDITY) - .unwrap_or_default() - { - Some(*addr) - } else { - None - } - }) - .collect() + self.do_holepunching(conn).await; } /// Unconditionally perform holepunching. - /// - /// - DISCO pings will be sent to addresses recently advertised in a call-me-maybe - /// message. - /// - A DISCO call-me-maybe message advertising our own addresses will be sent. #[instrument(skip_all)] - async fn do_holepunching(&mut self) { - let Some(relay_addr) = self.paths.addrs().find(|addr| addr.is_relay()).cloned() else { - warn!("holepunching requested but have no relay address"); - return; - }; - let remote_addrs = self.remote_hp_addrs(); - - // Send DISCO Ping messages to all CallMeMaybe-advertised paths. - for dst in remote_addrs.iter() { - let msg = disco::Ping::new(self.local_endpoint_id); - event!( - target: "iroh::_events::ping::sent", - Level::DEBUG, - remote = %self.endpoint_id.fmt_short(), - ?dst, - txn = ?msg.tx_id, - ); - let addr = transports::Addr::Ip(*dst); - self.paths.disco_ping_sent(addr.clone(), msg.tx_id); - self.send_disco_message(addr, disco::Message::Ping(msg)) - .await; - } - - // Send the DISCO CallMeMaybe message over the relay. - let my_numbers: Vec = self + async fn do_holepunching(&mut self, conn: quinn::Connection) { + self.metrics.nat_traversal.inc(); + let local_candidates = self .local_direct_addrs .get() .iter() .map(|daddr| daddr.addr) - .collect(); - let local_addrs: BTreeSet = my_numbers.iter().copied().collect(); - let msg = disco::CallMeMaybe { my_numbers }; - event!( - target: "iroh::_events::call_me_maybe::sent", - Level::DEBUG, - remote = %self.endpoint_id.fmt_short(), - dst = ?relay_addr, - my_numbers = ?msg.my_numbers, - ); - self.send_disco_message(relay_addr, disco::Message::CallMeMaybe(msg)) - .await; - - self.last_holepunch = Some(HolepunchAttempt { - when: Instant::now(), - local_addrs, - remote_addrs, - }); - } - - /// Sends a DISCO message to the remote endpoint this actor manages. - #[instrument(skip(self), fields(remote = %self.endpoint_id.fmt_short()))] - async fn send_disco_message(&self, dst: transports::Addr, msg: disco::Message) { - let pkt = self.disco.encode_and_seal(self.endpoint_id, &msg); - let transmit = transports::OwnedTransmit { - ecn: None, - contents: pkt, - segment_size: None, - }; - let counter = match dst { - transports::Addr::Ip(_) => &self.metrics.send_disco_udp, - transports::Addr::Relay(_, _) => &self.metrics.send_disco_relay, - }; - match self.send_datagram(dst, transmit).await { - Ok(()) => { - trace!("sent"); - counter.inc(); + .collect::>(); + match conn.initiate_nat_traversal_round() { + Ok(remote_candidates) => { + let remote_candidates = remote_candidates + .iter() + .map(|addr| SocketAddr::new(addr.ip().to_canonical(), addr.port())) + .collect(); + event!( + target: "iroh::_events::qnt::init", + Level::DEBUG, + remote = %self.endpoint_id.fmt_short(), + ?local_candidates, + ?remote_candidates, + ); + self.last_holepunch = Some(HolepunchAttempt { + when: Instant::now(), + local_candidates, + remote_candidates, + }); } Err(err) => { - warn!("failed to send disco message: {err:#}"); + warn!("failed to initiate NAT traversal: {err:#}"); } } } @@ -947,6 +863,9 @@ impl RemoteStateActor { } } } + + // If the remote closed our selected path, select a new one. + self.select_path(); } PathEvent::RemoteStatus { .. } | PathEvent::ObservedAddr { .. } => { // Nothing to do for these events. @@ -1016,13 +935,14 @@ impl RemoteStateActor { } } - /// Closes any direct paths not selected. + /// Closes any direct paths not selected if we are the client. /// /// Makes sure not to close the last direct path. Relay paths are never closed /// currently, because we only have one relay path at this time. - // TODO: Need to handle this on a timer as well probably. In .select_path() we open new - // paths and immediately call this. But the new paths are probably not yet open on - // all connections. + /// + /// Only the client closes paths, just like only the client opens paths. This is to + /// avoid the client and server selecting different paths and accidentally closing all + /// paths. fn close_redundant_paths(&mut self, selected_path: &transports::Addr) { debug_assert_eq!(self.selected_path.get().as_ref(), Some(selected_path),); @@ -1039,6 +959,7 @@ impl RemoteStateActor { if let Some(path) = conn_state .handle .upgrade() + .filter(|conn| conn.side().is_client()) .and_then(|conn| conn.path(*path_id)) { trace!(?path_remote, ?conn_id, ?path_id, "closing direct path"); @@ -1079,15 +1000,7 @@ pub(crate) enum RemoteStateMessage { /// will be removed etc. #[debug("AddConnection(..)")] AddConnection(WeakConnectionHandle, oneshot::Sender), - /// Process a received DISCO CallMeMaybe message. - CallMeMaybeReceived(disco::CallMeMaybe), - /// Process a received DISCO Ping message. - #[debug("PingReceived({:?}, src: {_1:?})", _0.tx_id)] - PingReceived(disco::Ping, transports::Addr), - /// Process a received DISCO Pong message. - #[debug("PongReceived({:?}, src: {_1:?})", _0.tx_id)] - PongReceived(disco::Pong, transports::Addr), - /// Ensure we have at least one transport address for a remote. + /// Asks if there is any possible path that could be used. /// /// This adds the provided transport addresses to the list of potential paths for this remote /// and starts discovery if needed. @@ -1123,6 +1036,8 @@ pub(super) struct RemoteStateHandle { } /// Information about a holepunch attempt. +/// +/// Addresses are always stored in canonical form. #[derive(Debug)] struct HolepunchAttempt { when: Instant, @@ -1134,18 +1049,18 @@ struct HolepunchAttempt { /// /// We do not store this as a [`DirectAddr`] because this is checked for equality and we /// do not want to compare the sources of these addresses. - local_addrs: BTreeSet, + local_candidates: BTreeSet, /// The set of remote addresses which could take part in holepunching. /// - /// Like `local_addrs` we may not have used them. - remote_addrs: BTreeSet, + /// Like [`Self::local_candidates`] we may not have used them. + remote_candidates: BTreeSet, } /// Newtype to track Connections. /// /// The wrapped value is the [`quinn::Connection::stable_id`] value, and is thus only valid /// for active connections. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] struct ConnId(usize); /// State about one connection. diff --git a/iroh/src/magicsock/remote_map/remote_state/path_state.rs b/iroh/src/magicsock/remote_map/remote_state/path_state.rs index a35c9476501..ae3656a2a81 100644 --- a/iroh/src/magicsock/remote_map/remote_state/path_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state/path_state.rs @@ -6,10 +6,10 @@ use n0_error::e; use n0_future::time::Instant; use rustc_hash::FxHashMap; use tokio::sync::oneshot; -use tracing::{debug, trace, warn}; +use tracing::trace; use super::Source; -use crate::{disco::TransactionId, discovery::DiscoveryError, magicsock::transports}; +use crate::{discovery::DiscoveryError, magicsock::transports}; /// Map of all paths that we are aware of for a remote endpoint. /// @@ -74,32 +74,6 @@ impl RemotePathState { } } - /// Records a sent disco ping for a path. - pub(super) fn disco_ping_sent(&mut self, addr: transports::Addr, tx_id: TransactionId) { - let path = self.paths.entry(addr.clone()).or_default(); - path.ping_sent = Some(tx_id); - } - - /// Records a received disco pong for a path. - /// - /// Returns `true` if we have sent a ping with `tx_id` on the same path. - pub(super) fn disco_pong_received( - &mut self, - src: &transports::Addr, - tx_id: TransactionId, - ) -> bool { - let Some(state) = self.paths.get(src) else { - warn!(path = ?src, ?self.paths, "ignoring DISCO Pong for unknown path"); - return false; - }; - if state.ping_sent != Some(tx_id) { - debug!(path = ?src, ?state.ping_sent, pong_tx = ?tx_id, "ignoring unknown DISCO Pong for path"); - false - } else { - true - } - } - /// Notifies that a discovery run has finished. /// /// This will emit pending resolve requests. @@ -107,11 +81,6 @@ impl RemotePathState { self.emit_pending_resolve_requests(result.err()); } - /// Returns an iterator over all paths and their state. - pub(super) fn iter(&self) -> impl Iterator { - self.paths.iter() - } - /// Returns an iterator over the addresses of all paths. pub(super) fn addrs(&self) -> impl Iterator { self.paths.keys() @@ -154,6 +123,4 @@ pub(super) struct PathState { /// We keep track of only the latest [`Instant`] for each [`Source`], keeping the size /// of the map of sources down to one entry per type of source. pub(super) sources: HashMap, - /// The last ping sent on this path. - pub(super) ping_sent: Option, } From 1efd2b5aabba1bf1456c02a857ee4e588f1ab65c Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 25 Nov 2025 11:12:47 +0100 Subject: [PATCH 155/164] feat(iroh): introduce EndpointHooks (#3688) ## Description This adds the conecpt of hooks to the iroh endpoint. `Hooks` are structs implementing the `EndpointHooks` trait and are used to intercept the establishment of connections. Multiple hooks can be added to the endpoint, and they will be invoked for each hook in the order they have been added to the endpoint. Currently there's two methods on the `EndpointHooks` trait: * `before_connect` is invoked before an outgoing connection is started. * `after_handshake` is invoked for incoming and outgoing connections once the TLS handshake has completed Both methods return an `Outcome`, which can either be `Reject` or `Accept`. If any hook returns `Reject`, the connection or connection attempt will be rejected. The PR also adds `ConnectionInfo`, which is a struct that has information about a connection, but does not keep the connection itself alive. It allows to inspect stats and paths, and there's a `closed` method that returns a future which completes once the connection closes (without keeping the connection alive). The PR includes two examples: * `auth-hook` implements authentication for iroh protocols through a middleware and a separate authentication protocol. Individual protocols don't need to be aware of authentication at all. * `monitor-connnections` monitors incoming and outgoing connections and prints connection stats once a connection closes. ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --------- Co-authored-by: dignifiedquire Co-authored-by: ramfox --- iroh/examples/auth-hook.rs | 348 +++++++++++++++++++++ iroh/examples/monitor-connections.rs | 137 +++++++++ iroh/examples/remote-info.rs | 439 +++++++++++++++++++++++++++ iroh/src/endpoint.rs | 35 ++- iroh/src/endpoint/connection.rs | 105 ++++++- iroh/src/endpoint/hooks.rs | 168 ++++++++++ iroh/src/magicsock.rs | 7 + iroh/src/protocol.rs | 56 +++- 8 files changed, 1285 insertions(+), 10 deletions(-) create mode 100644 iroh/examples/auth-hook.rs create mode 100644 iroh/examples/monitor-connections.rs create mode 100644 iroh/examples/remote-info.rs create mode 100644 iroh/src/endpoint/hooks.rs diff --git a/iroh/examples/auth-hook.rs b/iroh/examples/auth-hook.rs new file mode 100644 index 00000000000..3201f03d53d --- /dev/null +++ b/iroh/examples/auth-hook.rs @@ -0,0 +1,348 @@ +//! Implementation of authentication using iroh hooks +//! +//! This implements an auth protocol that works with iroh hooks. +//! It allows to put authentication in front of iroh protocols. The protocols don't need any special support. +//! Authentication is handled prior to establishing the connections, over a separate connection. + +use iroh::{Endpoint, EndpointAddr, protocol::Router}; +use n0_error::{Result, StdResultExt}; + +use crate::echo::Echo; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + let server_router = accept_side(b"secret!!").await?; + server_router.endpoint().online().await; + let server_addr = server_router.endpoint().addr(); + + println!("-- no --"); + let res = connect_side_no_auth(server_addr.clone()).await; + println!("echo without auth: {:#}", res.unwrap_err()); + + println!("-- wrong --"); + let res = connect_side(server_addr.clone(), b"dunno").await; + println!("echo with wrong auth: {:#}", res.unwrap_err()); + + println!("-- correct --"); + let res = connect_side(server_addr.clone(), b"secret!!").await; + println!("echo with correct auth: {res:?}"); + + server_router.shutdown().await.anyerr()?; + + Ok(()) +} + +async fn connect_side(remote_addr: EndpointAddr, token: &[u8]) -> Result<()> { + let (auth_hook, auth_task) = auth::outgoing(token.to_vec()); + let endpoint = Endpoint::builder().hooks(auth_hook).bind().await?; + let _guard = auth_task.spawn(endpoint.clone()); + Echo::connect(&endpoint, remote_addr, b"hello there!").await +} + +async fn connect_side_no_auth(remote_addr: EndpointAddr) -> Result<()> { + let endpoint = Endpoint::bind().await?; + Echo::connect(&endpoint, remote_addr, b"hello there!").await +} + +async fn accept_side(token: &[u8]) -> Result { + let (auth_hook, auth_protocol) = auth::incoming(token.to_vec()); + let endpoint = Endpoint::builder().hooks(auth_hook).bind().await?; + + let router = Router::builder(endpoint) + .accept(auth::ALPN, auth_protocol) + .accept(echo::ALPN, Echo) + .spawn(); + + Ok(router) +} + +mod echo { + //! A bare-bones protocol with no knowledge of auth whatsoever. + + use iroh::{ + Endpoint, EndpointAddr, + endpoint::Connection, + protocol::{AcceptError, ProtocolHandler}, + }; + use n0_error::{Result, StdResultExt, anyerr}; + + #[derive(Debug, Clone)] + pub struct Echo; + + pub const ALPN: &[u8] = b"iroh-example/echo/0"; + + impl Echo { + pub async fn connect( + endpoint: &Endpoint, + remote: impl Into, + message: &[u8], + ) -> Result<()> { + let conn = endpoint.connect(remote, ALPN).await?; + let (mut send, mut recv) = conn.open_bi().await.anyerr()?; + send.write_all(message).await.anyerr()?; + send.finish().anyerr()?; + let response = recv.read_to_end(1000).await.anyerr()?; + conn.close(0u32.into(), b"bye!"); + if response == message { + Ok(()) + } else { + Err(anyerr!("Received invalid response")) + } + } + } + + impl ProtocolHandler for Echo { + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let (mut send, mut recv) = connection.accept_bi().await?; + tokio::io::copy(&mut recv, &mut send).await?; + send.finish()?; + connection.closed().await; + Ok(()) + } + } +} + +mod auth { + //! Authentication hook + + use std::{ + collections::{HashMap, HashSet, hash_map}, + sync::{Arc, Mutex}, + }; + + use iroh::{ + Endpoint, EndpointAddr, EndpointId, + endpoint::{AfterHandshakeOutcome, BeforeConnectOutcome, Connection, EndpointHooks}, + protocol::{AcceptError, ProtocolHandler}, + }; + use n0_error::{AnyError, Result, StackResultExt, StdResultExt, anyerr}; + use n0_future::task::AbortOnDropHandle; + use quinn::ConnectionError; + use tokio::{ + sync::{mpsc, oneshot}, + task::JoinSet, + }; + use tracing::debug; + + pub const ALPN: &[u8] = b"iroh-example/auth/0"; + + const CLOSE_ACCEPTED: u32 = 1; + const CLOSE_DENIED: u32 = 403; + + /// Outgoing side: Use this if you want to pre-auth outgoing connections. + pub fn outgoing(token: Vec) -> (OutgoingAuthHook, OutgoingAuthTask) { + let (tx, rx) = mpsc::channel(16); + let hook = OutgoingAuthHook { tx }; + let connector = OutgoingAuthTask { + token, + rx, + allowed_remotes: Default::default(), + pending_remotes: Default::default(), + tasks: JoinSet::new(), + }; + (hook, connector) + } + + type AuthResult = Result<(), Arc>; + + /// Hook to mount on the endpoint builder. + #[derive(Debug)] + pub struct OutgoingAuthHook { + tx: mpsc::Sender<(EndpointId, oneshot::Sender)>, + } + + impl OutgoingAuthHook { + async fn authenticate(&self, remote_id: EndpointId) -> Result<()> { + let (tx, rx) = oneshot::channel(); + self.tx + .send((remote_id, tx)) + .await + .std_context("authenticator stopped")?; + rx.await + .std_context("authenticator stopped")? + .context("failed to authenticate") + } + } + + impl EndpointHooks for OutgoingAuthHook { + async fn before_connect<'a>( + &'a self, + remote_addr: &'a EndpointAddr, + alpn: &'a [u8], + ) -> BeforeConnectOutcome { + // Don't intercept auth request themsevles + if alpn == ALPN { + BeforeConnectOutcome::Accept + } else { + match self.authenticate(remote_addr.id).await { + Ok(()) => BeforeConnectOutcome::Accept, + Err(err) => { + debug!("authentication denied: {err:#}"); + BeforeConnectOutcome::Reject + } + } + } + } + } + + /// Connector task that initiates pre-auth request. Call [`Self::spawn`] once the endpoint is built. + pub struct OutgoingAuthTask { + token: Vec, + rx: mpsc::Receiver<(EndpointId, oneshot::Sender)>, + allowed_remotes: HashSet, + pending_remotes: HashMap>>, + tasks: JoinSet<(EndpointId, Result<()>)>, + } + + impl OutgoingAuthTask { + pub fn spawn(self, endpoint: Endpoint) -> AbortOnDropHandle<()> { + AbortOnDropHandle::new(tokio::spawn(self.run(endpoint))) + } + + async fn run(mut self, endpoint: Endpoint) { + loop { + tokio::select! { + msg = self.rx.recv() => { + let Some((remote_id, tx)) = msg else { + break; + }; + self.handle_msg(&endpoint, remote_id, tx); + } + Some(res) = self.tasks.join_next(), if !self.tasks.is_empty() => { + let (remote_id, res) = res.expect("connect task panicked"); + let res = res.map_err(Arc::new); + self.handle_task(remote_id, res); + } + } + } + } + + fn handle_msg( + &mut self, + endpoint: &Endpoint, + remote_id: EndpointId, + tx: oneshot::Sender>>, + ) { + if self.allowed_remotes.contains(&remote_id) { + tx.send(Ok(())).ok(); + } else { + match self.pending_remotes.entry(remote_id) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(tx); + } + hash_map::Entry::Vacant(entry) => { + let endpoint = endpoint.clone(); + let token = self.token.clone(); + self.tasks.spawn(async move { + let res = Self::connect(endpoint, remote_id, token).await; + (remote_id, res) + }); + entry.insert(vec![tx]); + } + } + } + } + + fn handle_task(&mut self, remote_id: EndpointId, res: Result<(), Arc>) { + if res.is_ok() { + self.allowed_remotes.insert(remote_id); + } + let senders = self.pending_remotes.remove(&remote_id); + for tx in senders.into_iter().flatten() { + tx.send(res.clone()).ok(); + } + } + + async fn connect(endpoint: Endpoint, remote_id: EndpointId, token: Vec) -> Result<()> { + let conn = endpoint.connect(remote_id, ALPN).await?; + let mut stream = conn.open_uni().await.anyerr()?; + stream.write_all(&token).await.anyerr()?; + stream.finish().anyerr()?; + let reason = conn.closed().await; + if let ConnectionError::ApplicationClosed(code) = &reason + && code.error_code.into_inner() as u32 == CLOSE_ACCEPTED + { + Ok(()) + } else if let ConnectionError::ApplicationClosed(code) = &reason + && code.error_code.into_inner() as u32 == CLOSE_DENIED + { + Err(anyerr!("authentication denied by remote")) + } else { + Err(AnyError::from_std(reason)) + } + } + } + + /// Incoming side: Use this if you want to only accept connections from peers with successful pre-auth requests. + pub fn incoming(token: Vec) -> (IncomingAuthHook, AuthProtocol) { + let allowed_remotes: Arc>> = Default::default(); + let hook = IncomingAuthHook { + allowed_remotes: allowed_remotes.clone(), + }; + let protocol = AuthProtocol { + allowed_remotes, + token, + }; + (hook, protocol) + } + + /// Accept-side auth hook: Mount this onto the endpoint. + /// + /// This will reject incoming connections if the remote did not successfully authenticate before. + #[derive(Debug)] + pub struct IncomingAuthHook { + allowed_remotes: Arc>>, + } + + impl EndpointHooks for IncomingAuthHook { + async fn after_handshake<'a>( + &'a self, + conn: &'a iroh::endpoint::ConnectionInfo, + ) -> AfterHandshakeOutcome { + if conn.alpn() == ALPN + || self + .allowed_remotes + .lock() + .expect("poisoned") + .contains(&conn.remote_id()) + { + AfterHandshakeOutcome::Accept + } else { + AfterHandshakeOutcome::Reject { + error_code: 403u32.into(), + reason: b"not authenticated".to_vec(), + } + } + } + } + + /// Accept-side auth protocol. Mount this on the router to accept authentication requests. + #[derive(Debug, Clone)] + pub struct AuthProtocol { + token: Vec, + allowed_remotes: Arc>>, + } + + impl ProtocolHandler for AuthProtocol { + /// The `accept` method is called for each incoming connection for our ALPN. + /// + /// The returned future runs on a newly spawned tokio task, so it can run as long as + /// the connection lasts. + async fn accept(&self, connection: Connection) -> Result<(), AcceptError> { + let mut stream = connection.accept_uni().await?; + let token = stream.read_to_end(256).await.anyerr()?; + let remote_id = connection.remote_id(); + if token == self.token { + self.allowed_remotes + .lock() + .expect("poisoned") + .insert(remote_id); + connection.close(CLOSE_ACCEPTED.into(), b"accepted"); + } else { + connection.close(CLOSE_DENIED.into(), b"rejected"); + } + Ok(()) + } + } +} diff --git a/iroh/examples/monitor-connections.rs b/iroh/examples/monitor-connections.rs new file mode 100644 index 00000000000..465593fdac8 --- /dev/null +++ b/iroh/examples/monitor-connections.rs @@ -0,0 +1,137 @@ +use std::{sync::Arc, time::Duration}; + +use iroh::{ + Endpoint, RelayMode, Watcher, + endpoint::{AfterHandshakeOutcome, ConnectionInfo, EndpointHooks}, +}; +use n0_error::{Result, StackResultExt, StdResultExt, ensure_any}; +use n0_future::task::AbortOnDropHandle; +use tokio::{ + sync::mpsc::{UnboundedReceiver, UnboundedSender}, + task::JoinSet, +}; +use tracing::{Instrument, info, info_span}; + +const ALPN: &[u8] = b"iroh/test"; + +#[tokio::main] +async fn main() -> Result { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ) + .init(); + + let monitor = Monitor::new(); + let server = Endpoint::empty_builder(RelayMode::Disabled) + .alpns(vec![ALPN.to_vec()]) + .hooks(monitor.clone()) + .bind() + .instrument(info_span!("server")) + .await?; + let server_addr = server.addr(); + + let count = 2; + + let client_task = tokio::spawn( + async move { + let client = Endpoint::empty_builder(RelayMode::Disabled) + .bind() + .instrument(info_span!("client")) + .await?; + for _i in 0..count { + let conn = client.connect(server_addr.clone(), ALPN).await?; + let mut s = conn.accept_uni().await.anyerr()?; + let data = s.read_to_end(2).await.anyerr()?; + ensure_any!(data == b"hi", "unexpected data"); + conn.close(23u32.into(), b"bye"); + } + client.close().await; + n0_error::Ok(client) + } + .instrument(info_span!("client")), + ); + + let server_task = tokio::spawn( + async move { + for _i in 0..count { + let conn = server + .accept() + .await + .context("server endpoint closed")? + .await?; + let mut s = conn.open_uni().await.anyerr()?; + s.write_all(b"hi").await.anyerr()?; + s.finish().anyerr()?; + conn.closed().await; + } + server.close().await; + n0_error::Ok(()) + } + .instrument(info_span!("server")), + ); + client_task.await.std_context("client")?.context("client")?; + server_task.await.std_context("server")?.context("server")?; + tokio::time::sleep(Duration::from_secs(1)).await; + drop(monitor); + Ok(()) +} + +/// Our connection monitor impl. +/// +/// This here only logs connection open and close events via tracing. +/// It could also maintain a datastructure of all connections, or send the stats to some metrics service. +#[derive(Clone, Debug)] +struct Monitor { + tx: UnboundedSender, + _task: Arc>, +} + +impl EndpointHooks for Monitor { + async fn after_handshake(&self, conn: &ConnectionInfo) -> AfterHandshakeOutcome { + self.tx.send(conn.clone()).ok(); + AfterHandshakeOutcome::Accept + } +} + +impl Monitor { + fn new() -> Self { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let task = tokio::spawn(Self::run(rx).instrument(info_span!("watcher"))); + Self { + tx, + _task: Arc::new(AbortOnDropHandle::new(task)), + } + } + + async fn run(mut rx: UnboundedReceiver) { + let mut tasks = JoinSet::new(); + loop { + tokio::select! { + Some(conn) = rx.recv() => { + let alpn = String::from_utf8_lossy(conn.alpn()).to_string(); + let remote = conn.remote_id().fmt_short(); + let rtt = conn.paths().peek().iter().map(|p| p.stats().rtt).min(); + info!(%remote, %alpn, ?rtt, "new connection"); + tasks.spawn(async move { + match conn.closed().await { + Some((close_reason, stats)) => { + // We have access to the final stats of the connection! + info!(%remote, %alpn, ?close_reason, udp_rx=stats.udp_rx.bytes, udp_tx=stats.udp_tx.bytes, "connection closed"); + } + None => { + // The connection was closed before we could register our stats-on-close listener. + info!(%remote, %alpn, "connection closed before tracking started"); + } + } + }.instrument(tracing::Span::current())); + } + Some(res) = tasks.join_next(), if !tasks.is_empty() => res.expect("conn close task panicked"), + else => break, + } + while let Some(res) = tasks.join_next().await { + res.expect("conn close task panicked"); + } + } + } +} diff --git a/iroh/examples/remote-info.rs b/iroh/examples/remote-info.rs new file mode 100644 index 00000000000..6b9010cab98 --- /dev/null +++ b/iroh/examples/remote-info.rs @@ -0,0 +1,439 @@ +//! Example for using an iroh hook to collect information about remote endpoints. +//! +//! This implements a [`RemoteMap`] which collects information about all connections and paths from an iroh endpoint. +//! The remote map can be cloned and inspected from other tasks at any time. It contains both data about all +//! currently active connections, and an aggregate status for each remote that remains available even after +//! all connections to the endpoint have been closed. + +use std::time::{Duration, SystemTime}; + +use iroh::{Endpoint, EndpointAddr}; +use n0_error::{Result, StackResultExt, StdResultExt, ensure_any}; +use n0_future::IterExt; +use tracing::{Instrument, info, info_span}; + +use crate::remote_map::RemoteMap; + +const ALPN: &[u8] = b"iroh/test"; + +#[tokio::main(flavor = "multi_thread")] +async fn main() -> Result { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()), + ) + .init(); + + // Create the remote map and hook. + let (hook, remote_map) = RemoteMap::new(); + + // Bind our endpoint and install the remote map hook. + let server = Endpoint::builder() + .alpns(vec![ALPN.to_vec()]) + .hooks(hook) + .bind() + .instrument(info_span!("server")) + .await?; + // Wait for our endpoint to be fully online. + server.online().await; + let server_addr = server.addr(); + + // Spawn a task that creates `count` client endpoints that each connect to our server. + let count = 3; + let client_task = tokio::spawn(run_clients(server_addr, count)); + + // Spawn a task that prints info from the remote map while some connections are active. + // You can use this info to make decisions about remotes. + let _inspect_task = tokio::task::spawn({ + let remote_map = remote_map.clone(); + async move { + // Wait a bit. + tokio::time::sleep(Duration::from_millis(500)).await; + println!("== while connections are active == "); + log_active(&remote_map); + log_aggregate(&remote_map); + println!(); + } + }); + + // Let the server accept `count` connections in parallel. + // The server keeps all connections open for at least 500 milliseconds. + std::iter::repeat_with(async || { + let conn = server + .accept() + .await + .context("server endpoint closed")? + .await?; + info!("accepted"); + let mut s = conn.open_uni().await.anyerr()?; + // wait a bit. + tokio::time::sleep(Duration::from_millis(500)).await; + s.write_all(b"hi").await.anyerr()?; + s.finish().anyerr()?; + conn.closed().await; + info!("closed"); + n0_error::Ok(()) + }) + .take(count) + .enumerate() + .map(|(i, fut)| fut.instrument(info_span!("server-conn", %i))) + .try_join_all() + .await?; + + // Print the remote map again. + println!("== all connections closed =="); + log_active(&remote_map); + log_aggregate(&remote_map); + + server.close().await; + client_task.await.std_context("client")?.context("client")?; + + Ok(()) +} + +/// Uses the current connection info to print info about a remote. +/// +/// Uses the info about *currently active* connections, which return `None` if no connections are active. +fn log_active(remote_map: &RemoteMap) { + println!("current remote state:"); + for (id, info) in remote_map.read().iter() { + println!( + "[{}] is_active {}, connections {}, ip_path {:?}, relay_path {:?}, current_min_rtt {:?}", + id.fmt_short(), + info.is_active(), + info.connections().count(), + info.has_ip_path(), + info.has_relay_path(), + info.current_min_rtt() + ); + } +} + +/// Uses the aggregated info to print info about a remote. +/// +/// The aggregated info is updated for all connection and path changes, and stays at the latest values +/// even if all connections are closed. +fn log_aggregate(remote_map: &RemoteMap) { + println!("aggregate remote state:"); + for (id, info) in remote_map.read().iter() { + let aggregate = info.aggregate(); + println!( + "[{}] min_rtt {:?}, max_rtt {:?}, ip_path {:?}, relay_path {}, last_update {:?} ago", + id.fmt_short(), + aggregate.rtt_min, + aggregate.rtt_max, + aggregate.ip_path, + aggregate.relay_path, + SystemTime::now() + .duration_since(aggregate.last_update) + .unwrap() + ); + } +} + +async fn run_clients(server_addr: EndpointAddr, count: usize) -> Result { + std::iter::repeat_with(async || { + let client = Endpoint::builder() + .bind() + .instrument(info_span!("client")) + .await?; + let conn = client.connect(server_addr.clone(), ALPN).await?; + info!("connected"); + let mut s = conn.accept_uni().await.anyerr()?; + let data = s.read_to_end(2).await.anyerr()?; + ensure_any!(data == b"hi", "unexpected data"); + conn.close(23u32.into(), b"bye"); + info!("closed"); + client.close().await; + n0_error::Ok(()) + }) + .take(count) + .enumerate() + .map(|(i, fut)| fut.instrument(info_span!("client", %i))) + .try_join_all() + .await?; + Ok(()) +} + +mod remote_map { + //! Implementation of a remote map and hook to track information about all remote endpoints to which an iroh endpoint + //! has connections with. + + use std::{ + collections::HashMap, + sync::{Arc, RwLock, RwLockReadGuard}, + time::{Duration, SystemTime}, + }; + + use iroh::{ + EndpointId, Watcher, + endpoint::{AfterHandshakeOutcome, ConnectionInfo, EndpointHooks, PathInfo}, + }; + use n0_future::task::AbortOnDropHandle; + use tokio::{sync::mpsc, task::JoinSet}; + use tokio_stream::StreamExt; + use tracing::{Instrument, debug, info, info_span}; + + /// Information about a remote info. + #[derive(Debug, Default)] + pub struct RemoteInfo { + aggregate: Aggregate, + connections: HashMap, + } + + /// Aggregate information about a remote info. + #[derive(Debug)] + pub struct Aggregate { + /// Minimal RTT observed over all paths to this remote. + pub rtt_min: Duration, + /// Maximal RTT observed over all paths to this remote. + pub rtt_max: Duration, + /// Whether we ever had an IP path to this remote. + pub ip_path: bool, + /// Whether we ever had a relay path to this remote. + pub relay_path: bool, + /// Time this aggregate was last updated. + pub last_update: SystemTime, + } + + impl Default for Aggregate { + fn default() -> Self { + Self { + rtt_min: Duration::MAX, + rtt_max: Duration::ZERO, + ip_path: false, + relay_path: false, + last_update: SystemTime::UNIX_EPOCH, + } + } + } + + impl Aggregate { + fn update(&mut self, path: &PathInfo) { + self.last_update = SystemTime::now(); + if path.is_ip() { + self.ip_path = true; + } + if path.is_relay() { + self.relay_path = true; + } + let stats = path.stats(); + debug!("path update addr {:?} {stats:?}", path.remote_addr()); + self.rtt_min = self.rtt_min.min(stats.rtt); + self.rtt_max = self.rtt_max.max(stats.rtt); + } + } + + impl RemoteInfo { + /// Returns an aggregate of stats for this remote. + /// + /// This includes info from closed connections. + pub fn aggregate(&self) -> &Aggregate { + &self.aggregate + } + + /// Returns the minimal RTT of all currently active paths. + /// + /// Returns `None` if there are no active connections. + pub fn current_min_rtt(&self) -> Option { + self.connections() + .flat_map(|c| c.paths().get()) + .map(|path| path.stats().rtt) + .min() + } + + /// Returns whether any active connection to the remote has an active IP path. + /// + /// Returns `None` if there are no active connections. + pub fn has_ip_path(&self) -> Option { + self.connections() + .flat_map(|c| c.paths().get()) + .filter(|path| path.is_ip()) + .map(|_| true) + .next() + } + + /// Returns whether any active connection to the remote has an active relay path. + /// + /// Returns `None` if there are no active connections. + pub fn has_relay_path(&self) -> Option { + self.connections() + .flat_map(|c| c.paths().get()) + .filter(|path| path.is_relay()) + .map(|_| true) + .next() + } + + /// Returns `true` if there are active connections to this node. + pub fn is_active(&self) -> bool { + !self.connections.is_empty() + } + + /// Returns an iterator over [`ConnectionInfo`] for currently active connections to this remote. + pub fn connections(&self) -> impl Iterator { + self.connections.values() + } + } + + type RemoteMapInner = Arc>>; + + /// Contains information about remote nodes our endpoint has or had connections with. + #[derive(Clone, Debug)] + pub struct RemoteMap { + map: RemoteMapInner, + _task: Arc>, + } + + /// Hook to collect information about remote endpoints from an endpoint. + #[derive(Debug)] + pub struct RemoteMapHook { + tx: mpsc::Sender, + } + + impl EndpointHooks for RemoteMapHook { + async fn after_handshake(&self, conn: &ConnectionInfo) -> AfterHandshakeOutcome { + info!(remote=%conn.remote_id().fmt_short(), "after_handshake"); + self.tx.send(conn.clone()).await.ok(); + AfterHandshakeOutcome::Accept + } + } + + impl RemoteMap { + /// Creates a new [`RemoteMapHook`] and [`RemoteMap`]. + pub fn new() -> (RemoteMapHook, Self) { + Self::with_max_retention(Duration::from_secs(60 * 5)) + } + + /// Creates a new [`RemoteMapHook`] and [`RemoteMap`] and configure the retention time. + /// + /// `retention_time` is the time entries for remote endpoints remain in the map after the last connection has closed. + pub fn with_max_retention(retention_time: Duration) -> (RemoteMapHook, Self) { + let (tx, rx) = mpsc::channel(8); + let map = RemoteMapInner::default(); + let task = tokio::spawn( + Self::run(rx, map.clone(), retention_time) + .instrument(info_span!("remote-map-task")), + ); + let map = Self { + map, + _task: Arc::new(AbortOnDropHandle::new(task)), + }; + let hook = RemoteMapHook { tx }; + (hook, map) + } + + /// Read the current state of the remote map. + /// + /// Returns a [`RwLockReadGuard`] with the actual remote map. Don't hold over await points! + pub fn read(&self) -> RwLockReadGuard<'_, HashMap> { + self.map.read().expect("poisoned") + } + + async fn run( + mut rx: mpsc::Receiver, + map: RemoteMapInner, + retention_time: Duration, + ) { + let mut tasks = JoinSet::new(); + let mut conn_id = 0; + + // Spawn a task to clear expired entries. + let expiry_task = tasks.spawn(Self::clear_expired(retention_time, map.clone())); + + // Main loop + loop { + tokio::select! { + conn = rx.recv() => { + match conn { + Some(conn) => { + conn_id += 1; + Self::on_connection(&mut tasks, map.clone(), conn_id, conn); + }, + None => break, + } + } + Some(res) = tasks.join_next(), if !tasks.is_empty() => { + res.expect("conn close task panicked"); + } + } + } + + // Abort expiry task and join remaining tasks. + expiry_task.abort(); + while let Some(res) = tasks.join_next().await { + if let Err(err) = &res + && !err.is_cancelled() + { + res.expect("conn close task panicked"); + } + } + } + + fn on_connection( + tasks: &mut JoinSet<()>, + map: RemoteMapInner, + conn_id: u64, + conn: ConnectionInfo, + ) { + // Store conn info for full introspection possibility. + { + let mut inner = map.write().expect("poisoned"); + inner + .entry(conn.remote_id()) + .or_default() + .connections + .insert(conn_id, conn.clone()); + } + + // Track connection closing to clear up the map. + tasks.spawn({ + let conn = conn.clone(); + let map = map.clone(); + async move { + conn.closed().await; + { + let mut inner = map.write().expect("poisoned"); + let info = inner.entry(conn.remote_id()).or_default(); + info.connections.remove(&conn_id); + info.aggregate.last_update = SystemTime::now(); + } + } + .instrument(tracing::Span::current()) + }); + + // Track path changes to update stats aggregate. + tasks.spawn({ + async move { + let mut path_updates = conn.paths().stream(); + while let Some(paths) = path_updates.next().await { + { + let mut inner = map.write().expect("poisoned"); + let info = inner.entry(conn.remote_id()).or_default(); + for path in paths { + info.aggregate.update(&path); + } + } + } + } + .instrument(tracing::Span::current()) + }); + } + + async fn clear_expired( + retention_time: Duration, + map: Arc>>, + ) { + let mut interval = tokio::time::interval(retention_time); + loop { + interval.tick().await; + let now = SystemTime::now(); + let mut inner = map.write().expect("poisoned"); + inner.retain(|_remote, info| { + info.is_active() + || now.duration_since(info.aggregate().last_update).unwrap() + < retention_time + }); + } + } + } +} diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 0bff432fc14..99d6ab74369 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -18,12 +18,13 @@ use std::{ use iroh_base::{EndpointAddr, EndpointId, RelayUrl, SecretKey, TransportAddr}; use iroh_relay::{RelayConfig, RelayMap}; -use n0_error::{ensure, stack_error}; +use n0_error::{e, ensure, stack_error}; use n0_future::time::Duration; use n0_watcher::Watcher; use tracing::{debug, instrument, trace, warn}; use url::Url; +use self::hooks::EndpointHooksList; pub use super::magicsock::{ DirectAddr, DirectAddrType, PathInfo, remote_map::{PathInfoList, Source}, @@ -45,8 +46,10 @@ use crate::{ }; mod connection; +pub(crate) mod hooks; pub mod presets; +pub use hooks::{AfterHandshakeOutcome, BeforeConnectOutcome, EndpointHooks}; // Missing still: SendDatagram and ConnectionClose::frame_type's Type. pub use quinn::{ AcceptBi, AcceptUni, AckFrequencyConfig, ApplicationClose, Chunk, ClosedStream, @@ -66,8 +69,9 @@ pub use quinn_proto::{ pub use self::connection::{ Accept, Accepting, AlpnError, AuthenticationError, Connecting, ConnectingError, Connection, - ConnectionState, HandshakeCompleted, Incoming, IncomingZeroRtt, IncomingZeroRttConnection, - OutgoingZeroRtt, OutgoingZeroRttConnection, RemoteEndpointIdError, ZeroRttStatus, + ConnectionInfo, ConnectionState, HandshakeCompleted, Incoming, IncomingZeroRtt, + IncomingZeroRttConnection, OutgoingZeroRtt, OutgoingZeroRttConnection, RemoteEndpointIdError, + ZeroRttStatus, }; pub use crate::magicsock::transports::TransportConfig; @@ -92,6 +96,7 @@ pub struct Builder { insecure_skip_relay_cert_verify: bool, transports: Vec, max_tls_tickets: usize, + hooks: EndpointHooksList, } impl From for Option { @@ -154,6 +159,7 @@ impl Builder { insecure_skip_relay_cert_verify: false, max_tls_tickets: DEFAULT_MAX_TLS_TICKETS, transports, + hooks: Default::default(), } } @@ -201,6 +207,7 @@ impl Builder { #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify: self.insecure_skip_relay_cert_verify, metrics, + hooks: self.hooks, }; let msock = magicsock::MagicSock::spawn(msock_opts).await?; @@ -455,6 +462,21 @@ impl Builder { self.max_tls_tickets = n; self } + + /// Install hooks onto the endpoint. + /// + /// Endpoint hooks intercept the connection establishment process of an [`Endpoint`]. + /// + /// You can install multiple [`EndpointHooks`] by calling this function multiple times. + /// Order matters: hooks are invoked in the order they were installed onto the endpoint + /// builder. Once a hook returns reject, further processing + /// is aborted and other hooks won't be invoked. + /// + /// See [`EndpointHooks`] for details on the possible interception points in the connection lifecycle. + pub fn hooks(mut self, hooks: impl EndpointHooks + 'static) -> Self { + self.hooks.push(hooks); + self + } } /// Configuration for a [`quinn::Endpoint`] that cannot be changed at runtime. @@ -530,6 +552,8 @@ pub enum ConnectWithOptsError { /// Private source type, cannot be created publicly. source: RemoteStateActorStoppedError, }, + #[error("Connection was rejected locally")] + LocallyRejected, } #[allow(missing_docs)] @@ -688,6 +712,11 @@ impl Endpoint { options: ConnectOptions, ) -> Result { let endpoint_addr: EndpointAddr = endpoint_addr.into(); + if let BeforeConnectOutcome::Reject = + self.msock.hooks.before_connect(&endpoint_addr, alpn).await + { + return Err(e!(ConnectWithOptsError::LocallyRejected)); + } let endpoint_id = endpoint_addr.id; tracing::Span::current().record("remote", tracing::field::display(endpoint_id.fmt_short())); diff --git a/iroh/src/endpoint/connection.rs b/iroh/src/endpoint/connection.rs index ff663e0923f..6f59d7bec20 100644 --- a/iroh/src/endpoint/connection.rs +++ b/iroh/src/endpoint/connection.rs @@ -34,12 +34,13 @@ use n0_watcher::Watcher; use pin_project::pin_project; use quinn::{ AcceptBi, AcceptUni, ConnectionError, ConnectionStats, OpenBi, OpenUni, ReadDatagram, - RetryError, SendDatagramError, ServerConfig, VarInt, + RetryError, SendDatagramError, ServerConfig, Side, VarInt, WeakConnectionHandle, }; use tracing::warn; use crate::{ Endpoint, + endpoint::AfterHandshakeOutcome, magicsock::{ RemoteStateActorStoppedError, remote_map::{PathInfoList, PathsWatcher}, @@ -221,7 +222,7 @@ fn conn_from_quinn_conn( conn: quinn::Connection, ep: &Endpoint, ) -> Result< - impl Future> + Send + 'static, + impl Future> + Send + 'static, ConnectingError, > { let info = match static_info_from_conn(&conn) { @@ -241,12 +242,24 @@ fn conn_from_quinn_conn( let fut = ep .msock .register_connection(info.endpoint_id, conn.weak_handle()); + + // Check hooks + let msock = ep.msock.clone(); Ok(async move { let paths = fut.await?; - Ok(Connection { + let conn = Connection { data: HandshakeCompletedData { info, paths }, inner: conn, - }) + }; + + if let AfterHandshakeOutcome::Reject { error_code, reason } = + msock.hooks.after_handshake(&conn.to_info()).await + { + conn.close(error_code, &reason); + return Err(e!(ConnectingError::LocallyRejected)); + } + + Ok(conn) }) } @@ -315,7 +328,7 @@ pub struct Connecting { remote_endpoint_id: EndpointId, } -type RegisterWithMagicsockFut = BoxFuture>; +type RegisterWithMagicsockFut = BoxFuture>; /// In-progress connection attempt future #[derive(derive_more::Debug)] @@ -363,6 +376,8 @@ pub enum ConnectingError { /// Private source type, cannot be created publicly. source: RemoteStateActorStoppedError, }, + #[error("Connection was rejected locally")] + LocallyRejected, } impl Connecting { @@ -978,6 +993,23 @@ impl Connection { pub fn paths(&self) -> impl Watcher + Unpin + Send + Sync + 'static { self.data.paths.clone() } + + /// Returns the side of the connection (client or server). + pub fn side(&self) -> Side { + self.inner.side() + } + + /// Returns a connection info struct. + /// + /// A [`ConnectionInfo`] is a weak handle to the connection that does not keep the connection alive, + /// but does allow to access some information about the connection and to wait for the connection to be closed. + pub fn to_info(&self) -> ConnectionInfo { + ConnectionInfo { + data: self.data.clone(), + inner: self.inner.weak_handle(), + side: self.side(), + } + } } impl Connection { @@ -1033,6 +1065,69 @@ impl Connection { } } +/// Information about a connection. +/// +/// A [`ConnectionInfo`] is a weak handle to a connection that exposes some information about the connection, +/// but does not keep the connection alive. +#[derive(Debug, Clone)] +pub struct ConnectionInfo { + side: Side, + data: HandshakeCompletedData, + inner: WeakConnectionHandle, +} + +#[allow(missing_docs)] +impl ConnectionInfo { + pub fn alpn(&self) -> &[u8] { + &self.data.info.alpn + } + + pub fn remote_id(&self) -> EndpointId { + self.data.info.endpoint_id + } + + pub fn is_alive(&self) -> bool { + self.inner.upgrade().is_some() + } + + /// Returns a [`Watcher`] for the network paths of this connection. + /// + /// A connection can have several network paths to the remote endpoint, commonly there + /// will be a path via the relay server and a holepunched path. + /// + /// The watcher is updated whenever a path is opened or closed, or when the path selected + /// for transmission changes (see [`PathInfo::is_selected`]). + /// + /// The [`PathInfoList`] returned from the watcher contains a [`PathInfo`] for each + /// transmission path. + /// + /// [`PathInfo::is_selected`]: crate::magicsock::PathInfo::is_selected + /// [`PathInfo`]: crate::magicsock::PathInfo + pub fn paths(&self) -> impl Watcher + Unpin + Send + Sync + 'static { + self.data.paths.clone() + } + + /// Returns connection statistics. + /// + /// Returns `None` if the connection has been dropped. + pub fn stats(&self) -> Option { + self.inner.upgrade().map(|conn| conn.stats()) + } + + /// Returns the side of the connection (client or server). + pub fn side(&self) -> Side { + self.side + } + + /// Waits for the connection to be closed, and returns the close reason and final connection stats. + /// + /// Returns `None` if the connection has been dropped already before this call. + pub async fn closed(&self) -> Option<(ConnectionError, ConnectionStats)> { + let fut = self.inner.upgrade()?.on_closed(); + Some(fut.await) + } +} + #[cfg(test)] mod tests { use std::time::Duration; diff --git a/iroh/src/endpoint/hooks.rs b/iroh/src/endpoint/hooks.rs new file mode 100644 index 00000000000..a60dc5860c5 --- /dev/null +++ b/iroh/src/endpoint/hooks.rs @@ -0,0 +1,168 @@ +use std::pin::Pin; + +use iroh_base::EndpointAddr; +use quinn::VarInt; + +use crate::endpoint::connection::ConnectionInfo; + +type BoxFuture<'a, T> = Pin + Send + 'a>>; + +/// Outcome of [`EndpointHooks::before_connect`] +#[derive(Debug)] +pub enum BeforeConnectOutcome { + /// Accept the connect attempt. + Accept, + /// Reject the connect attempt. + Reject, +} + +/// Outcome of [`EndpointHooks::after_handshake`] +#[derive(Debug)] +pub enum AfterHandshakeOutcome { + /// Accept the connection. + Accept, + /// Reject and close the connection. + /// + /// See [`Connection::close`] for details on `error_code` and `reason`. + /// + /// [`Connection::close`]: crate::endpoint::Connection::close + Reject { + /// Error code to send with the connection close frame. + error_code: VarInt, + /// Close reason to send with the connection close frame. + reason: Vec, + }, +} + +impl AfterHandshakeOutcome { + /// Returns [`Self::Accept`]. + pub fn accept() -> Self { + Self::Accept + } + + /// Returns [`Self::Reject`]. + pub fn reject(&self, error_code: VarInt, reason: &[u8]) -> Self { + Self::Reject { + error_code, + reason: reason.to_vec(), + } + } +} + +/// EndpointHooks intercept the connection establishment process of an [`Endpoint`]. +/// +/// Use [`Builder::hooks`] to install hooks onto an endpoint. +/// +/// For each hook, all installed hooks are invoked in the order they were installed on +/// the endpoint builder. If a hook returns `Accept`, processing continues with the next +/// hook. If a hook returns `Reject`, processing is aborted and further hooks +/// are not invoked for this hook. +/// +/// ## Notes to implementers +/// +/// As hooks are stored on the endpoint, you must make sure to never store an [`Endpoint`] +/// on the hook struct itself, as this would create reference counting loop and cause the +/// endpoint to never be dropped, leaking memory. +/// +/// [`Endpoint`]: crate::Endpoint +/// [`Builder::hooks`]: crate::endpoint::Builder::hooks +pub trait EndpointHooks: std::fmt::Debug + Send + Sync { + /// Intercept outgoing connections before they are started. + /// + /// This is called whenever a new outgoing connection is initiated via [`Endpoint::connect`] + /// or [`Endpoint::connect_with_opts`]. + /// + /// If any hook returns [`BeforeConnectOutcome::Reject`], the connection attempt is aborted + /// before any packets are sent to the remote. + /// + /// [`Endpoint::connect`]: crate::Endpoint::connect + /// [`Endpoint::connect_with_opts`]: crate::Endpoint::connect_with_opts + fn before_connect<'a>( + &'a self, + _remote_addr: &'a EndpointAddr, + _alpn: &'a [u8], + ) -> impl Future + Send + 'a { + async { BeforeConnectOutcome::Accept } + } + + /// Intercept both incoming and outgoing connections once the TLS handshake has completed. + /// + /// At this point in time, we know the remote's endpoint id and ALPN. If any hook returns + /// [`AfterHandshakeOutcome::Reject`], the connection is closed with the provided error code + /// and reason. + fn after_handshake<'a>( + &'a self, + _conn: &'a ConnectionInfo, + ) -> impl Future + Send + 'a { + async { AfterHandshakeOutcome::accept() } + } +} + +pub(crate) trait DynEndpointHooks: std::fmt::Debug + Send + Sync { + fn before_connect<'a>( + &'a self, + remote_addr: &'a EndpointAddr, + alpn: &'a [u8], + ) -> BoxFuture<'a, BeforeConnectOutcome>; + fn after_handshake<'a>( + &'a self, + conn: &'a ConnectionInfo, + ) -> BoxFuture<'a, AfterHandshakeOutcome>; +} + +impl DynEndpointHooks for T { + fn before_connect<'a>( + &'a self, + remote_addr: &'a EndpointAddr, + alpn: &'a [u8], + ) -> BoxFuture<'a, BeforeConnectOutcome> { + Box::pin(EndpointHooks::before_connect(self, remote_addr, alpn)) + } + + fn after_handshake<'a>( + &'a self, + conn: &'a ConnectionInfo, + ) -> BoxFuture<'a, AfterHandshakeOutcome> { + Box::pin(EndpointHooks::after_handshake(self, conn)) + } +} + +#[derive(Debug, Default)] +pub(crate) struct EndpointHooksList { + inner: Vec>, +} + +impl EndpointHooksList { + pub(super) fn push(&mut self, hook: impl EndpointHooks + 'static) { + let hook: Box = Box::new(hook); + self.inner.push(hook); + } + + pub(super) async fn before_connect( + &self, + remote_addr: &EndpointAddr, + alpn: &[u8], + ) -> BeforeConnectOutcome { + for hook in self.inner.iter() { + match hook.before_connect(remote_addr, alpn).await { + BeforeConnectOutcome::Accept => continue, + reject @ BeforeConnectOutcome::Reject => { + return reject; + } + } + } + BeforeConnectOutcome::Accept + } + + pub(super) async fn after_handshake(&self, conn: &ConnectionInfo) -> AfterHandshakeOutcome { + for hook in self.inner.iter() { + match hook.after_handshake(conn).await { + AfterHandshakeOutcome::Accept => continue, + reject @ AfterHandshakeOutcome::Reject { .. } => { + return reject; + } + } + } + AfterHandshakeOutcome::Accept + } +} diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index ad5c5c0c8a8..209625c5424 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -57,6 +57,7 @@ use crate::net_report::QuicConfig; use crate::{ defaults::timeouts::NET_REPORT_TIMEOUT, discovery::{ConcurrentDiscovery, Discovery, DiscoveryError, EndpointData, UserData}, + endpoint::hooks::EndpointHooksList, magicsock::remote_map::PathsWatcher, metrics::EndpointMetrics, net_report::{self, IfStateDetails, Report}, @@ -140,6 +141,7 @@ pub(crate) struct Options { #[cfg(any(test, feature = "test-utils"))] pub(crate) insecure_skip_relay_cert_verify: bool, pub(crate) metrics: EndpointMetrics, + pub(crate) hooks: EndpointHooksList, } /// Handle for [`MagicSock`]. @@ -208,6 +210,7 @@ pub(crate) struct MagicSock { /// Metrics pub(crate) metrics: EndpointMetrics, + pub(crate) hooks: EndpointHooksList, } impl MagicSock { @@ -758,6 +761,7 @@ impl Handle { #[cfg(any(test, feature = "test-utils"))] insecure_skip_relay_cert_verify, metrics, + hooks, } = opts; let discovery = ConcurrentDiscovery::default(); @@ -869,6 +873,7 @@ impl Handle { local_addrs_watch: transports.local_addrs_watch(), #[cfg(not(wasm_browser))] ip_bind_addrs: transports.ip_bind_addrs(), + hooks, }); let mut endpoint_config = quinn::EndpointConfig::default(); @@ -1574,6 +1579,7 @@ mod tests { #[cfg(any(test, feature = "test-utils"))] discovery_user_data: None, metrics: Default::default(), + hooks: Default::default(), } } @@ -2006,6 +2012,7 @@ mod tests { server_config, insecure_skip_relay_cert_verify: false, metrics: Default::default(), + hooks: Default::default(), }; let msock = MagicSock::spawn(opts).await?; Ok(msock) diff --git a/iroh/src/protocol.rs b/iroh/src/protocol.rs index fcca2939fd5..70f9fdc8afc 100644 --- a/iroh/src/protocol.rs +++ b/iroh/src/protocol.rs @@ -609,7 +609,13 @@ mod tests { use quinn::ApplicationClose; use super::*; - use crate::{RelayMode, endpoint::ConnectionError}; + use crate::{ + RelayMode, + endpoint::{ + BeforeConnectOutcome, ConnectError, ConnectWithOptsError, ConnectionError, + EndpointHooks, + }, + }; #[tokio::test] async fn test_shutdown() -> Result { @@ -649,7 +655,7 @@ mod tests { } #[tokio::test] - async fn test_limiter() -> Result { + async fn test_limiter_router() -> Result { // tracing_subscriber::fmt::try_init().ok(); let e1 = Endpoint::empty_builder(RelayMode::Disabled).bind().await?; // deny all access @@ -673,6 +679,52 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_limiter_hook() -> Result { + // tracing_subscriber::fmt::try_init().ok(); + #[derive(Debug, Default)] + struct LimitHook; + impl EndpointHooks for LimitHook { + async fn before_connect<'a>( + &'a self, + _remote_addr: &'a iroh_base::EndpointAddr, + alpn: &'a [u8], + ) -> BeforeConnectOutcome { + assert_eq!(alpn, ECHO_ALPN); + + // deny all access + BeforeConnectOutcome::Reject + } + } + + let e1 = Endpoint::empty_builder(RelayMode::Disabled).bind().await?; + + let r1 = Router::builder(e1.clone()).accept(ECHO_ALPN, Echo).spawn(); + + let addr1 = r1.endpoint().addr(); + dbg!(&addr1); + let e2 = Endpoint::empty_builder(RelayMode::Disabled) + .hooks(LimitHook) + .bind() + .await?; + + println!("connecting"); + let conn_err = e2.connect(addr1, ECHO_ALPN).await.unwrap_err(); + + assert!(matches!( + conn_err, + ConnectError::Connect { + source: ConnectWithOptsError::LocallyRejected { .. }, + .. + } + )); + + r1.shutdown().await.anyerr()?; + e2.close().await; + + Ok(()) + } + #[tokio::test] async fn test_graceful_shutdown() -> Result { #[derive(Debug, Clone, Default)] From a28d1fb89d4f2128b23d4aa84ab023f830df5cc6 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Fri, 28 Nov 2025 12:44:42 +0100 Subject: [PATCH 156/164] chore: fixup wasm test --- Cargo.lock | 61 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb174c61921..ab1128e989d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,7 +95,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -106,7 +106,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1046,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2385,9 +2385,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2789,7 +2789,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2824,6 +2824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -3556,7 +3557,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3679,7 +3680,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4222,7 +4223,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4816,9 +4817,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -4829,9 +4830,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -4842,9 +4843,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4852,9 +4853,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -4865,21 +4866,29 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-bindgen-test" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc379bfb624eb59050b509c13e77b4eb53150c350db69628141abce842f2373" +checksum = "25e90e66d265d3a1efc0e72a54809ab90b9c0c515915c67cdf658689d2c22c6c" dependencies = [ + "async-trait", + "cast", "js-sys", + "libm", "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", "wasm-bindgen", "wasm-bindgen-futures", "wasm-bindgen-test-macro", @@ -4887,9 +4896,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.55" +version = "0.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085b2df989e1e6f9620c1311df6c996e83fe16f57792b272ce1e024ac16a90f1" +checksum = "7150335716dce6028bead2b848e72f47b45e7b9422f64cccdc23bedca89affc1" dependencies = [ "proc-macro2", "quote", @@ -4911,9 +4920,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -4984,7 +4993,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] From 8d568895c16cd0c2ff67b072e65f663fe7268766 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Fri, 28 Nov 2025 12:56:56 +0100 Subject: [PATCH 157/164] refactor(iroh): simplify internal transports sending (#3708) removes the need for two `send` impls --- iroh/src/magicsock/remote_map/remote_state.rs | 42 ++++++++------ iroh/src/magicsock/transports.rs | 58 +------------------ iroh/src/magicsock/transports/ip.rs | 34 ----------- iroh/src/magicsock/transports/relay.rs | 26 --------- 4 files changed, 25 insertions(+), 135 deletions(-) diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index bb451290488..797aa36b14b 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -357,23 +357,6 @@ impl RemoteStateActor { } } - async fn send_datagram( - &self, - dst: transports::Addr, - owned_transmit: OwnedTransmit, - ) -> n0_error::Result<()> { - let transmit = transports::Transmit { - ecn: owned_transmit.ecn, - contents: owned_transmit.contents.as_ref(), - segment_size: owned_transmit.segment_size, - }; - self.sender - .send(&dst, None, &transmit) - .await - .with_context(|_| format!("failed to send datagram to {dst:?}"))?; - Ok(()) - } - /// Handles [`RemoteStateMessage::SendDatagram`]. async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) { // Sending datagrams might fail, e.g. because we don't have the right transports set @@ -385,7 +368,7 @@ impl RemoteStateActor { if let Some(addr) = self.selected_path.get() { trace!(?addr, "sending datagram to selected path"); - if let Err(err) = self.send_datagram(addr.clone(), transmit).await { + if let Err(err) = send_datagram(&mut self.sender, addr.clone(), transmit).await { debug!(?addr, "failed to send datagram on selected_path: {err:#}"); } } else { @@ -396,6 +379,7 @@ impl RemoteStateActor { if self.paths.is_empty() { warn!("Cannot send datagrams: No paths to remote endpoint known"); } + for addr in self.paths.addrs() { // We never want to send to our local addresses. // The local address set is updated in the main loop so we can use `peek` here. @@ -407,7 +391,9 @@ impl RemoteStateActor { .any(|a| a.addr == *sockaddr) { trace!(%sockaddr, "not sending datagram to our own address"); - } else if let Err(err) = self.send_datagram(addr.clone(), transmit.clone()).await { + } else if let Err(err) = + send_datagram(&mut self.sender, addr.clone(), transmit.clone()).await + { debug!(?addr, "failed to send datagram: {err:#}"); } } @@ -980,6 +966,24 @@ impl RemoteStateActor { } } +fn send_datagram<'a>( + sender: &'a mut TransportsSender, + dst: transports::Addr, + owned_transmit: OwnedTransmit, +) -> impl Future> + 'a { + std::future::poll_fn(move |cx| { + let transmit = transports::Transmit { + ecn: owned_transmit.ecn, + contents: owned_transmit.contents.as_ref(), + segment_size: owned_transmit.segment_size, + }; + + Pin::new(&mut *sender) + .poll_send(cx, &dst, None, &transmit) + .map(|res| res.with_context(|_| format!("failed to send datagram to {dst:?}"))) + }) +} + /// Messages to send to the [`RemoteStateActor`]. #[derive(derive_more::Debug)] pub(crate) enum RemoteStateMessage { diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 85efb5cf2ec..7b95ec8beee 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -493,62 +493,8 @@ pub(crate) struct TransportsSender { } impl TransportsSender { - #[instrument(skip(self, transmit), fields(len = transmit.contents.len()))] - pub(crate) async fn send( - &self, - dst: &Addr, - src: Option, - transmit: &Transmit<'_>, - ) -> io::Result<()> { - let mut any_match = false; - match dst { - #[cfg(wasm_browser)] - Addr::Ip(..) => return Err(io::Error::other("IP is unsupported in browser")), - #[cfg(not(wasm_browser))] - Addr::Ip(addr) => { - for sender in &self.ip { - if sender.is_valid_send_addr(addr) { - any_match = true; - match sender.send(*addr, src, transmit).await { - Ok(()) => { - trace!("sent"); - return Ok(()); - } - Err(err) => { - warn!("ip failed to send: {:?}", err); - } - } - } - } - } - Addr::Relay(url, endpoint_id) => { - for sender in &self.relay { - if sender.is_valid_send_addr(url, endpoint_id) { - any_match = true; - match sender.send(url.clone(), *endpoint_id, transmit).await { - Ok(()) => { - trace!("sent"); - return Ok(()); - } - Err(err) => { - warn!("relay failed to send: {:?}", err); - } - } - } - } - } - } - if any_match { - Err(io::Error::other("all available transports failed")) - } else { - Err(io::Error::other( - "no transport available for this destination", - )) - } - } - #[instrument(name = "poll_send", skip(self, cx, transmit), fields(len = transmit.contents.len()))] - pub(crate) fn inner_poll_send( + pub(crate) fn poll_send( mut self: Pin<&mut Self>, cx: &mut std::task::Context, dst: &Addr, @@ -789,7 +735,7 @@ impl quinn::UdpSender for MagicSender { match this .sender - .inner_poll_send(cx, &transport_addr, quinn_transmit.src_ip, &transmit) + .poll_send(cx, &transport_addr, quinn_transmit.src_ip, &transmit) { Poll::Ready(Ok(())) => Poll::Ready(Ok(())), Poll::Ready(Err(ref err)) => { diff --git a/iroh/src/magicsock/transports/ip.rs b/iroh/src/magicsock/transports/ip.rs index e2ee9b5ffc9..dc4ab293da4 100644 --- a/iroh/src/magicsock/transports/ip.rs +++ b/iroh/src/magicsock/transports/ip.rs @@ -195,40 +195,6 @@ impl IpSender { SocketAddr::new(addr.ip().to_canonical(), addr.port()) } - pub(super) async fn send( - &self, - dst: SocketAddr, - src: Option, - transmit: &Transmit<'_>, - ) -> io::Result<()> { - let total_bytes = transmit.contents.len() as u64; - let res = self - .sender - .send(&quinn_udp::Transmit { - destination: Self::canonical_addr(dst), - ecn: transmit.ecn, - contents: transmit.contents, - segment_size: transmit.segment_size, - src_ip: src, - }) - .await; - - match res { - Ok(res) => { - match dst { - SocketAddr::V4(_) => { - self.metrics.send_ipv4.inc_by(total_bytes); - } - SocketAddr::V6(_) => { - self.metrics.send_ipv6.inc_by(total_bytes); - } - } - Ok(res) - } - Err(err) => Err(err), - } - } - pub(super) fn poll_send( mut self: Pin<&mut Self>, cx: &mut std::task::Context, diff --git a/iroh/src/magicsock/transports/relay.rs b/iroh/src/magicsock/transports/relay.rs index 6cdb2a388cb..fdaf503fd5f 100644 --- a/iroh/src/magicsock/transports/relay.rs +++ b/iroh/src/magicsock/transports/relay.rs @@ -244,32 +244,6 @@ impl RelaySender { true } - pub(super) async fn send( - &self, - dest_url: RelayUrl, - dest_endpoint: EndpointId, - transmit: &Transmit<'_>, - ) -> io::Result<()> { - let contents = datagrams_from_transmit(transmit); - - let item = RelaySendItem { - remote_endpoint: dest_endpoint, - url: dest_url.clone(), - datagrams: contents, - }; - - let Some(sender) = self.sender.get_ref() else { - return Err(io::Error::other("channel closed")); - }; - match sender.send(item).await { - Ok(_) => Ok(()), - Err(mpsc::error::SendError(_)) => Err(io::Error::new( - io::ErrorKind::ConnectionReset, - "channel to actor is closed", - )), - } - } - pub(super) fn poll_send( &mut self, cx: &mut Context, From 3f4d365740d05b4b930e3cf6c0d70d1aafa620d3 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Fri, 28 Nov 2025 14:54:42 +0100 Subject: [PATCH 158/164] refactor(iroh): avoid storing the TransportsSender (#3712) Closes #3641 --- iroh/src/magicsock.rs | 1 - iroh/src/magicsock/remote_map.rs | 5 ----- iroh/src/magicsock/remote_map/remote_state.rs | 19 ++++++++++--------- iroh/src/magicsock/transports.rs | 5 ++++- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 209625c5424..630564e3227 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -850,7 +850,6 @@ impl Handle { secret_key.public(), metrics.magicsock.clone(), direct_addrs.addrs.watch(), - transports.create_sender(), discovery.clone(), ) }; diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index 583b4916813..5410f939310 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -18,7 +18,6 @@ use self::remote_state::{RemoteStateActor, RemoteStateHandle}; use super::{ DirectAddr, MagicsockMetrics, mapped_addrs::{AddrMap, EndpointIdMappedAddr, RelayMappedAddr}, - transports::TransportsSender, }; use crate::discovery::ConcurrentDiscovery; @@ -58,7 +57,6 @@ pub(crate) struct RemoteMap { metrics: Arc, /// The "direct" addresses known for our local endpoint local_direct_addrs: n0_watcher::Direct>, - sender: TransportsSender, discovery: ConcurrentDiscovery, } @@ -68,7 +66,6 @@ impl RemoteMap { local_endpoint_id: EndpointId, metrics: Arc, local_direct_addrs: n0_watcher::Direct>, - sender: TransportsSender, discovery: ConcurrentDiscovery, ) -> Self { Self { @@ -78,7 +75,6 @@ impl RemoteMap { local_endpoint_id, metrics, local_direct_addrs, - sender, discovery, } } @@ -137,7 +133,6 @@ impl RemoteMap { self.local_direct_addrs.clone(), self.relay_mapped_addrs.clone(), self.metrics.clone(), - self.sender.clone(), self.discovery.clone(), ) .start(); diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index 797aa36b14b..a5c643c94b0 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -140,7 +140,6 @@ pub(super) struct RemoteStateActor { // /// Metrics. metrics: Arc, - sender: TransportsSender, /// Our local addresses. /// /// These are our local addresses and any reflexive transport addresses. @@ -202,7 +201,6 @@ impl RemoteStateActor { local_direct_addrs: n0_watcher::Direct>, relay_mapped_addrs: AddrMap<(RelayUrl, EndpointId), RelayMappedAddr>, metrics: Arc, - sender: TransportsSender, discovery: ConcurrentDiscovery, ) -> Self { Self { @@ -222,7 +220,6 @@ impl RemoteStateActor { scheduled_holepunch: None, scheduled_open_path: None, pending_open_paths: VecDeque::new(), - sender, discovery_stream: Either::Left(n0_future::stream::pending()), } } @@ -342,8 +339,8 @@ impl RemoteStateActor { async fn handle_message(&mut self, msg: RemoteStateMessage) { // trace!("handling message"); match msg { - RemoteStateMessage::SendDatagram(transmit) => { - self.handle_msg_send_datagram(transmit).await; + RemoteStateMessage::SendDatagram(sender, transmit) => { + self.handle_msg_send_datagram(sender, transmit).await; } RemoteStateMessage::AddConnection(handle, tx) => { self.handle_msg_add_connection(handle, tx).await; @@ -358,7 +355,11 @@ impl RemoteStateActor { } /// Handles [`RemoteStateMessage::SendDatagram`]. - async fn handle_msg_send_datagram(&mut self, transmit: OwnedTransmit) { + async fn handle_msg_send_datagram( + &mut self, + mut sender: TransportsSender, + transmit: OwnedTransmit, + ) { // Sending datagrams might fail, e.g. because we don't have the right transports set // up to handle sending this owned transmit to. // After all, we try every single path that we know (relay URL, IP address), even @@ -368,7 +369,7 @@ impl RemoteStateActor { if let Some(addr) = self.selected_path.get() { trace!(?addr, "sending datagram to selected path"); - if let Err(err) = send_datagram(&mut self.sender, addr.clone(), transmit).await { + if let Err(err) = send_datagram(&mut sender, addr.clone(), transmit).await { debug!(?addr, "failed to send datagram on selected_path: {err:#}"); } } else { @@ -392,7 +393,7 @@ impl RemoteStateActor { { trace!(%sockaddr, "not sending datagram to our own address"); } else if let Err(err) = - send_datagram(&mut self.sender, addr.clone(), transmit.clone()).await + send_datagram(&mut sender, addr.clone(), transmit.clone()).await { debug!(?addr, "failed to send datagram: {err:#}"); } @@ -996,7 +997,7 @@ pub(crate) enum RemoteStateMessage { /// operation with a bunch more copying. So it should only be used for sending QUIC /// Initial packets. #[debug("SendDatagram(..)")] - SendDatagram(OwnedTransmit), + SendDatagram(TransportsSender, OwnedTransmit), /// Adds an active connection to this remote endpoint. /// /// The connection will now be managed by this actor. Holepunching will happen when diff --git a/iroh/src/magicsock/transports.rs b/iroh/src/magicsock/transports.rs index 7b95ec8beee..a20d2d371b3 100644 --- a/iroh/src/magicsock/transports.rs +++ b/iroh/src/magicsock/transports.rs @@ -693,7 +693,10 @@ impl quinn::UdpSender for MagicSender { let sender = self.msock.remote_map.remote_state_actor(node_id); let transmit = OwnedTransmit::from(quinn_transmit); - return match sender.try_send(RemoteStateMessage::SendDatagram(transmit)) { + return match sender.try_send(RemoteStateMessage::SendDatagram( + self.sender.clone(), + transmit, + )) { Ok(()) => { trace!(dst = ?mapped_addr, dst_node = %node_id.fmt_short(), "sent transmit"); Poll::Ready(Ok(())) From 874597b807a2403d0afda93fe1bfb800f8240bca Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Fri, 28 Nov 2025 15:19:17 +0100 Subject: [PATCH 159/164] Fix the roundtrip test (#3710) ## Description Since the server was actively closing the connection it is possible that the client would not have read the response yet by the time the connection is closed. ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- iroh/src/magicsock.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 630564e3227..6032d245236 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -1619,7 +1619,6 @@ mod tests { let stats = conn.stats(); info!("stats: {:#?}", stats); - // TODO: ensure panics in this function are reported ok if matches!(loss, ExpectedLoss::AlmostNone) { for info in conn.paths().get().iter() { assert!( @@ -1630,10 +1629,10 @@ mod tests { } } - info!("close"); - conn.close(0u32.into(), b"done"); - info!("wait idle"); + conn.closed().await; + info!("closed"); ep.endpoint().wait_idle().await; + info!("idle"); Ok(()) } @@ -1684,10 +1683,10 @@ mod tests { } } - info!("close"); conn.close(0u32.into(), b"done"); - info!("wait idle"); + info!("closed"); ep.endpoint().wait_idle().await; + info!("idle"); Ok(()) } @@ -1708,26 +1707,26 @@ mod tests { let recv_endpoint_id = receiver.id(); info!("\nroundtrip: {send_endpoint_id:#} -> {recv_endpoint_id:#}"); - let receiver_task = tokio::spawn(echo_receiver(receiver, loss)); + let receiver_task = AbortOnDropHandle::new(tokio::spawn(echo_receiver(receiver, loss))); let sender_res = echo_sender(sender, recv_endpoint_id, payload, loss).await; let sender_is_err = match sender_res { Ok(()) => false, Err(err) => { - eprintln!("[sender] Error:\n{err:#?}"); + error!("[sender] Error:\n{err:#?}"); true } }; let receiver_is_err = match receiver_task.await { Ok(Ok(())) => false, Ok(Err(err)) => { - eprintln!("[receiver] Error:\n{err:#?}"); + error!("[receiver] Error:\n{err:#?}"); true } Err(joinerr) => { if joinerr.is_panic() { std::panic::resume_unwind(joinerr.into_panic()); } else { - eprintln!("[receiver] Error:\n{joinerr:#?}"); + error!("[receiver] Error:\n{joinerr:#?}"); } true } @@ -1801,6 +1800,7 @@ mod tests { rng.fill_bytes(&mut data); run_roundtrip(m1.clone(), m2.clone(), &data, ExpectedLoss::AlmostNone).await; run_roundtrip(m2.clone(), m1.clone(), &data, ExpectedLoss::AlmostNone).await; + info!("\n-- round {i} finished"); } Ok(()) From e22c0018d6aa2219442c08bd8c20887736cf92e9 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Mon, 1 Dec 2025 12:44:34 +0100 Subject: [PATCH 160/164] chore: update to quinn@main-iroh (#3716) --- Cargo.lock | 32 ++++++++++++++++---------------- Cargo.toml | 6 +++--- iroh-relay/Cargo.toml | 4 ++-- iroh/Cargo.toml | 8 ++++---- iroh/bench/Cargo.toml | 2 +- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ab1128e989d..b065aab89ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,7 +95,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -106,7 +106,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1046,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1803,7 +1803,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2225,7 +2225,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=protocol-simplification#02fd7eefcdf4263079216cecf078ff7eb49aab0f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#fc187cb1511e3905f8823ebe898cd101f4ce1d5e" dependencies = [ "bytes", "cfg_aliases", @@ -2234,7 +2234,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2245,7 +2245,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=protocol-simplification#02fd7eefcdf4263079216cecf078ff7eb49aab0f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#fc187cb1511e3905f8823ebe898cd101f4ce1d5e" dependencies = [ "bytes", "fastbloom", @@ -2268,12 +2268,12 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=protocol-simplification#02fd7eefcdf4263079216cecf078ff7eb49aab0f" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#fc187cb1511e3905f8823ebe898cd101f4ce1d5e" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -2789,7 +2789,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3249,7 +3249,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -3286,7 +3286,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", "windows-sys 0.60.2", ] @@ -3557,7 +3557,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3680,7 +3680,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4223,7 +4223,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4993,7 +4993,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e7859a75768..dbec80e8080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,9 +43,9 @@ unused-async = "warn" [patch.crates-io] -iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } -iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } -iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } +iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "main" } diff --git a/iroh-relay/Cargo.toml b/iroh-relay/Cargo.toml index 5e18ed900d9..2b506d18d36 100644 --- a/iroh-relay/Cargo.toml +++ b/iroh-relay/Cargo.toml @@ -42,8 +42,8 @@ postcard = { version = "1", default-features = false, features = [ "use-std", "experimental-derive", ] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", diff --git a/iroh/Cargo.toml b/iroh/Cargo.toml index aad59be6b98..887f3bd0d9c 100644 --- a/iroh/Cargo.toml +++ b/iroh/Cargo.toml @@ -35,9 +35,9 @@ n0-watcher = "0.6" netwatch = { version = "0.12" } pin-project = "1" pkarr = { version = "5", default-features = false, features = ["relays"] } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification", default-features = false, features = ["rustls-ring"] } -quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } -quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["rustls-ring"] } +quinn-proto = { package = "iroh-quinn-proto", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } +quinn-udp = { package = "iroh-quinn-udp", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } rand = "0.9.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls", @@ -82,7 +82,7 @@ hickory-resolver = "0.25.1" igd-next = { version = "0.16", features = ["aio_tokio"] } netdev = { version = "0.39.0" } portmapper = { version = "0.12", default-features = false } -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification", default-features = false, features = ["runtime-tokio", "rustls-ring"] } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh", default-features = false, features = ["runtime-tokio", "rustls-ring"] } tokio = { version = "1", features = [ "io-util", "macros", diff --git a/iroh/bench/Cargo.toml b/iroh/bench/Cargo.toml index 8108a22c5e1..086d1ebc31a 100644 --- a/iroh/bench/Cargo.toml +++ b/iroh/bench/Cargo.toml @@ -12,7 +12,7 @@ iroh = { path = "..", default-features = false } iroh-metrics = { version = "0.37", optional = true } n0-future = "0.3.0" n0-error = "0.1.0" -quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "protocol-simplification" } +quinn = { package = "iroh-quinn", git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } rand = "0.9.2" rcgen = "0.14" rustls = { version = "0.23.33", default-features = false, features = ["ring"] } From 783e2ef5d56ece910ba0f49c731c852929667e25 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Mon, 1 Dec 2025 13:10:32 +0100 Subject: [PATCH 161/164] refactor(iroh)!: remove Endpoint::latency (#3717) ## Description Based on #3593 This was always just a placeholder, and can now be collected using `EndpointHooks`. ## Breaking Changes - remove `Endpoint::latency` --------- Co-authored-by: varun-doshi --- iroh/src/endpoint.rs | 7 ----- iroh/src/magicsock.rs | 12 ------- iroh/src/magicsock/remote_map/remote_state.rs | 31 ------------------- 3 files changed, 50 deletions(-) diff --git a/iroh/src/endpoint.rs b/iroh/src/endpoint.rs index 99d6ab74369..48e0edb45f6 100644 --- a/iroh/src/endpoint.rs +++ b/iroh/src/endpoint.rs @@ -945,13 +945,6 @@ impl Endpoint { // // Partially they return things passed into the builder. - /// Returns the currently lowest latency for this endpoint. - /// - /// Will return `None` if we do not have any address information for the given `endpoint_id`. - pub async fn latency(&self, endpoint_id: EndpointId) -> Option { - self.msock.latency(endpoint_id).await - } - /// Returns the DNS resolver used in this [`Endpoint`]. /// /// See [`Builder::dns_resolver`]. diff --git a/iroh/src/magicsock.rs b/iroh/src/magicsock.rs index 6032d245236..e4d3ad11a09 100644 --- a/iroh/src/magicsock.rs +++ b/iroh/src/magicsock.rs @@ -391,18 +391,6 @@ impl MagicSock { }) } - // TODO: Build better info to expose to the user about remote nodes. We probably want - // to expose this as part of path information instead. - pub(crate) async fn latency(&self, eid: EndpointId) -> Option { - let remote_state = self.remote_map.remote_state_actor(eid); - let (tx, rx) = oneshot::channel(); - remote_state - .send(RemoteStateMessage::Latency(tx)) - .await - .ok(); - rx.await.unwrap_or_default() - } - /// Stores a new set of direct addresses. /// /// If the direct addresses have changed from the previous set, they are published to diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index a5c643c94b0..3a5ee1988f1 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -348,9 +348,6 @@ impl RemoteStateActor { RemoteStateMessage::ResolveRemote(addrs, tx) => { self.handle_msg_resolve_remote(addrs, tx); } - RemoteStateMessage::Latency(tx) => { - self.handle_msg_latency(tx); - } } } @@ -504,29 +501,6 @@ impl RemoteStateActor { self.trigger_discovery(); } - /// Handles [`RemoteStateMessage::Latency`]. - fn handle_msg_latency(&self, tx: oneshot::Sender>) { - let rtt = self.selected_path.get().and_then(|addr| { - for conn_state in self.connections.values() { - let Some(path_id) = conn_state.path_ids.get(&addr) else { - continue; - }; - if !conn_state.open_paths.contains_key(path_id) { - continue; - } - if let Some(rtt) = conn_state - .handle - .upgrade() - .and_then(|conn| conn.path_stats(*path_id).map(|stats| stats.rtt)) - { - return Some(rtt); - } - } - None - }); - tx.send(rtt).ok(); - } - fn handle_discovery_item(&mut self, item: Option>) { match item { None => { @@ -1018,11 +992,6 @@ pub(crate) enum RemoteStateMessage { BTreeSet, oneshot::Sender>, ), - /// Returns the current latency to the remote endpoint. - /// - /// TODO: This is more of a placeholder message currently. Check MagicSock::latency. - #[debug("Latency(..)")] - Latency(oneshot::Sender>), } /// A handle to a [`RemoteStateActor`]. From 2cf93a5abfb3e8d3c67ba63bc80911a9c4445be3 Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Tue, 2 Dec 2025 11:36:59 +0100 Subject: [PATCH 162/164] chore: update iroh-quinn (#3718) --- Cargo.lock | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b065aab89ef..8b144e4adb5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1803,7 +1803,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.1", "tokio", "tower-service", "tracing", @@ -2225,7 +2225,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#fc187cb1511e3905f8823ebe898cd101f4ce1d5e" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#21a8fdc68f5e72f354cac0222513c2f0b3e67ad1" dependencies = [ "bytes", "cfg_aliases", @@ -2234,7 +2234,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2245,7 +2245,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#fc187cb1511e3905f8823ebe898cd101f4ce1d5e" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#21a8fdc68f5e72f354cac0222513c2f0b3e67ad1" dependencies = [ "bytes", "fastbloom", @@ -2268,14 +2268,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#fc187cb1511e3905f8823ebe898cd101f4ce1d5e" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#21a8fdc68f5e72f354cac0222513c2f0b3e67ad1" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2789,7 +2789,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3249,7 +3249,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.1", "thiserror 2.0.17", "tokio", "tracing", @@ -3286,9 +3286,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.1", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -3557,7 +3557,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3680,7 +3680,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.4", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4223,7 +4223,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4993,7 +4993,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] From 51ba6998d94e03e998d10b0377b5dd4460b8d4b5 Mon Sep 17 00:00:00 2001 From: Franz Heinzmann Date: Tue, 2 Dec 2025 16:02:53 +0100 Subject: [PATCH 163/164] deps: bump quinn in feat-multipath (#3723) ## Description Bumps quinn to latest `main-iroh` and netwatch/portmapper to https://github.com/n0-computer/net-tools/pull/72 (the latter is needed because the quinn-udp version changed to 0.6 on `main-iroh`). ## Breaking Changes ## Notes & open questions ## Change checklist - [ ] Self-review. - [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant. - [ ] Tests if relevant. - [ ] All breaking changes documented. - [ ] List all breaking changes in the above "Breaking Changes" section. - [ ] Open an issue or PR on any number0 repos that are affected by this breaking change. Give guidance on how the updates should be handled or do the actual updates themselves. The major ones are: - [ ] [`quic-rpc`](https://github.com/n0-computer/quic-rpc) - [ ] [`iroh-gossip`](https://github.com/n0-computer/iroh-gossip) - [ ] [`iroh-blobs`](https://github.com/n0-computer/iroh-blobs) - [ ] [`dumbpipe`](https://github.com/n0-computer/dumbpipe) - [ ] [`sendme`](https://github.com/n0-computer/sendme) --- Cargo.lock | 38 ++++++++++++++++++-------------------- Cargo.toml | 3 ++- iroh-relay/Cargo.toml | 1 - 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b144e4adb5..1c00b3ccf76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1046,7 +1046,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1803,7 +1803,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.5.10", "tokio", "tower-service", "tracing", @@ -2225,7 +2225,7 @@ dependencies = [ [[package]] name = "iroh-quinn" version = "0.14.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#21a8fdc68f5e72f354cac0222513c2f0b3e67ad1" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#238057833af99ad7ae123dd9144436ee37a2d1e7" dependencies = [ "bytes", "cfg_aliases", @@ -2234,7 +2234,7 @@ dependencies = [ "pin-project-lite", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tokio-stream", @@ -2245,7 +2245,7 @@ dependencies = [ [[package]] name = "iroh-quinn-proto" version = "0.13.0" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#21a8fdc68f5e72f354cac0222513c2f0b3e67ad1" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#238057833af99ad7ae123dd9144436ee37a2d1e7" dependencies = [ "bytes", "fastbloom", @@ -2267,15 +2267,14 @@ dependencies = [ [[package]] name = "iroh-quinn-udp" -version = "0.5.12" -source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#21a8fdc68f5e72f354cac0222513c2f0b3e67ad1" +version = "0.6.0" +source = "git+https://github.com/n0-computer/quinn?branch=main-iroh#238057833af99ad7ae123dd9144436ee37a2d1e7" dependencies = [ "cfg_aliases", "libc", - "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2716,7 +2715,7 @@ dependencies = [ [[package]] name = "netwatch" version = "0.12.0" -source = "git+https://github.com/n0-computer/net-tools?branch=main#cd7aba545996781786b8168d49b876f0844ad3d7" +source = "git+https://github.com/n0-computer/net-tools?branch=quinn-udp-git#7fc6d4483b449739e0e7a630022afba7e9b95c55" dependencies = [ "atomic-waker", "bytes", @@ -2789,7 +2788,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3082,8 +3081,7 @@ checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portmapper" version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b575f975dcf03e258b0c7ab3f81497d7124f508884c37da66a7314aa2a8d467" +source = "git+https://github.com/n0-computer/net-tools?branch=quinn-udp-git#7fc6d4483b449739e0e7a630022afba7e9b95c55" dependencies = [ "base64", "bytes", @@ -3249,7 +3247,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.1", + "socket2 0.5.10", "thiserror 2.0.17", "tokio", "tracing", @@ -3286,9 +3284,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3557,7 +3555,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3680,7 +3678,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs 1.0.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4223,7 +4221,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dbec80e8080..656cf126d28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,8 @@ iroh-quinn = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh iroh-quinn-proto = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } iroh-quinn-udp = { git = "https://github.com/n0-computer/quinn", branch = "main-iroh" } -netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "main" } +netwatch = { git = "https://github.com/n0-computer/net-tools", branch = "quinn-udp-git" } +portmapper = { git = "https://github.com/n0-computer/net-tools", branch = "quinn-udp-git" } # iroh-quinn = { path = "../quinn/quinn" } # iroh-quinn-proto = { path = "../quinn/quinn-proto" } diff --git a/iroh-relay/Cargo.toml b/iroh-relay/Cargo.toml index 2b506d18d36..7a88b7539d2 100644 --- a/iroh-relay/Cargo.toml +++ b/iroh-relay/Cargo.toml @@ -153,7 +153,6 @@ server = [ "dep:sha1", "dep:toml", "dep:tracing-subscriber", - "quinn/log", "quinn/platform-verifier", "quinn/runtime-tokio", "iroh-metrics/service", From 7fb80b9beda1eaabfedd73309ccf471279ba4f4b Mon Sep 17 00:00:00 2001 From: ramfox Date: Tue, 2 Dec 2025 15:45:44 -0500 Subject: [PATCH 164/164] feat: prune old, inactive paths (#3666) ## Description Whenever we insert a new path, trigger pruning paths. We currently only prune IP paths, and pruning paths only occurs if we have more than 30 IP paths. We will prune any paths that did not successfully holepunch. If there are still over 30 IP paths left, then we order the "inactive" paths (paths that have been closed, but at one point holepunched), and prune the paths that were closed earliest. ## Notes and Questions - Added constants: - `MAX_IP_PATHS` = 30 - maximum IP paths per endpoint - `MAX_INACTIVE_IP_PATHS` = 10 - maximum inactive IP paths to keep - New `PathState` field: - `status` - tracks the `PathStatus` of the path - New `PathStatus` enum: - `PathStatus::Open` - is an open path - `PathStatus::Inactive(Instant)` - was opened once, but currently inactive - `PathStatus::Unusable` - we attempted to use it, but it never connected - `PathStatus::Unknown` - we don't know the status yet - New methods on `RemotePathState`: - `abandoned_path` - marks a path as abandoned with timestamp, triggered when we get the `PathEvent::Abandoned` event - `prune_paths` - triggers path pruning, occurs whenever we insert a path to the `RemotePathState` - changed `insert` to `insert_open_path` - New `prune_ip_paths` function with all the prune logic: - Only prunes if IP paths exceed `MAX_IP_PATHS` - Never prunes active paths or paths of unknown status - Always prunes failed holepunch attempts (PathStatus::Unusable) - Keeps 10 most recently inactive paths that were previously successful - Special case: if all paths failed, keeps `MAX_IP_PATHS` instead of pruning everything - Added tests for edge cases and the typical case --- iroh/src/magicsock/remote_map.rs | 5 - iroh/src/magicsock/remote_map/remote_state.rs | 26 +- .../remote_map/remote_state/path_state.rs | 449 +++++++++++++++++- 3 files changed, 448 insertions(+), 32 deletions(-) diff --git a/iroh/src/magicsock/remote_map.rs b/iroh/src/magicsock/remote_map.rs index 5410f939310..ec1ef280b27 100644 --- a/iroh/src/magicsock/remote_map.rs +++ b/iroh/src/magicsock/remote_map.rs @@ -185,11 +185,6 @@ pub enum Source { _0: Private, }, /// We established a connection on this address. - /// - /// Currently this means the path was in uses as [`PathId::ZERO`] when the a connection - /// was added to the `RemoteStateActor`. - /// - /// [`PathId::ZERO`]: quinn_proto::PathId::ZERO #[strum(serialize = "Connection")] Connection { /// private marker diff --git a/iroh/src/magicsock/remote_map/remote_state.rs b/iroh/src/magicsock/remote_map/remote_state.rs index 3a5ee1988f1..e7878e8d9c5 100644 --- a/iroh/src/magicsock/remote_map/remote_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state.rs @@ -50,16 +50,6 @@ const HOLEPUNCH_ATTEMPTS_INTERVAL: Duration = Duration::from_secs(5); mod guarded_channel; mod path_state; -// TODO: use this -// /// Number of addresses that are not active that we keep around per endpoint. -// /// -// /// See [`RemoteState::prune_direct_addresses`]. -// pub(super) const MAX_INACTIVE_DIRECT_ADDRESSES: usize = 20; - -// TODO: use this -// /// How long since an endpoint path was last alive before it might be pruned. -// const LAST_ALIVE_PRUNE_DURATION: Duration = Duration::from_secs(120); - // TODO: use this // /// The latency at or under which we don't try to upgrade to a better path. // const GOOD_ENOUGH_LATENCY: Duration = Duration::from_millis(5); @@ -461,7 +451,7 @@ impl RemoteStateActor { path.set_status(status).ok(); conn_state.add_open_path(path_remote.clone(), PathId::ZERO); self.paths - .insert(path_remote, Source::Connection { _0: Private }); + .insert_open_path(path_remote.clone(), Source::Connection { _0: Private }); self.select_path(); if path_remote_is_ip { @@ -779,7 +769,7 @@ impl RemoteStateActor { ); conn_state.add_open_path(path_remote.clone(), path_id); self.paths - .insert(path_remote, Source::Connection { _0: Private }); + .insert_open_path(path_remote.clone(), Source::Connection { _0: Private }); } self.select_path(); @@ -787,7 +777,9 @@ impl RemoteStateActor { PathEvent::Abandoned { id, path_stats } => { trace!(?path_stats, "path abandoned"); // This is the last event for this path. - conn_state.remove_path(&id); + if let Some(addr) = conn_state.remove_path(&id) { + self.paths.abandoned_path(&addr); + } } PathEvent::Closed { id, .. } | PathEvent::LocallyClosed { id, .. } => { let Some(path_remote) = conn_state.paths.get(&id).cloned() else { @@ -1073,11 +1065,13 @@ impl ConnectionState { } /// Completely removes a path from this connection. - fn remove_path(&mut self, path_id: &PathId) { - if let Some(addr) = self.paths.remove(path_id) { - self.path_ids.remove(&addr); + fn remove_path(&mut self, path_id: &PathId) -> Option { + let addr = self.paths.remove(path_id); + if let Some(ref addr) = addr { + self.path_ids.remove(addr); } self.open_paths.remove(path_id); + addr } /// Removes the path from the open paths. diff --git a/iroh/src/magicsock/remote_map/remote_state/path_state.rs b/iroh/src/magicsock/remote_map/remote_state/path_state.rs index ae3656a2a81..a9b07729e89 100644 --- a/iroh/src/magicsock/remote_map/remote_state/path_state.rs +++ b/iroh/src/magicsock/remote_map/remote_state/path_state.rs @@ -1,6 +1,6 @@ //! The state kept for each network path to a remote endpoint. -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use n0_error::e; use n0_future::time::Instant; @@ -11,6 +11,14 @@ use tracing::trace; use super::Source; use crate::{discovery::DiscoveryError, magicsock::transports}; +/// Maximum number of IP paths we keep around per endpoint. +pub(super) const MAX_IP_PATHS: usize = 30; + +/// Maximum number of inactive IP paths we keep around per endpoint. +/// +/// These are paths that at one point been opened and are now closed. +pub(super) const MAX_INACTIVE_IP_PATHS: usize = 10; + /// Map of all paths that we are aware of for a remote endpoint. /// /// Also stores a list of resolve requests which are triggered once at least one path is known, @@ -26,22 +34,54 @@ pub(super) struct RemotePathState { pending_resolve_requests: VecDeque>>, } +/// Describes the usability of this path, i.e. whether it has ever been opened, +/// when it was closed, or if it has never been usable. +#[derive(Debug, Default)] +pub(super) enum PathStatus { + /// This path is open and active. + Open, + /// This path was once opened, but was abandoned at the given [`Instant`]. + Inactive(Instant), + /// This path was never usable (we attempted holepunching and it didn't work). + Unusable, + /// We have not yet attempted holepunching, or holepunching is currently in + /// progress, so we do not know the usability of this path. + #[default] + Unknown, +} + impl RemotePathState { - /// Insert a new address into our list of potential paths. + /// Insert a new address of an open path into our list of paths. /// - /// This will emit pending resolve requests. - pub(super) fn insert(&mut self, addr: transports::Addr, source: Source) { - self.paths - .entry(addr) - .or_default() - .sources - .insert(source.clone(), Instant::now()); + /// This will emit pending resolve requests and trigger pruning paths. + pub(super) fn insert_open_path(&mut self, addr: transports::Addr, source: Source) { + let state = self.paths.entry(addr).or_default(); + state.status = PathStatus::Open; + state.sources.insert(source.clone(), Instant::now()); self.emit_pending_resolve_requests(None); + self.prune_paths(); } - /// Inserts multiple addresses into our list of potential paths. + /// Mark a path as abandoned. /// - /// This will emit pending resolve requests. + /// If this path does not exist, it does nothing to the + /// `RemotePathState` + pub(super) fn abandoned_path(&mut self, addr: &transports::Addr) { + if let Some(state) = self.paths.get_mut(addr) { + match state.status { + PathStatus::Open | PathStatus::Inactive(_) => { + state.status = PathStatus::Inactive(Instant::now()); + } + PathStatus::Unusable | PathStatus::Unknown => { + state.status = PathStatus::Unusable; + } + } + } + } + + /// Inserts multiple addresses of unknown status into our list of potential paths. + /// + /// This will emit pending resolve requests and trigger pruning paths. pub(super) fn insert_multiple( &mut self, addrs: impl Iterator, @@ -57,6 +97,7 @@ impl RemotePathState { } trace!("added addressing information"); self.emit_pending_resolve_requests(None); + self.prune_paths(); } /// Triggers `tx` immediately if there are any known paths, or store in the list of pending requests. @@ -108,6 +149,17 @@ impl RemotePathState { tx.send(result.clone()).ok(); } } + + /// Prune paths. + /// + /// Should be invoked any time we insert a new path. + /// + /// We currently only prune IP paths. For more information on the criteria + /// for when and which paths we prune, look at the [`prune_ip_paths`] function. + pub(super) fn prune_paths(&mut self) { + // right now we only prune IP paths + prune_ip_paths(&mut self.paths); + } } /// The state of a single path to the remote endpoint. @@ -123,4 +175,379 @@ pub(super) struct PathState { /// We keep track of only the latest [`Instant`] for each [`Source`], keeping the size /// of the map of sources down to one entry per type of source. pub(super) sources: HashMap, + /// The usability status of this path. + pub(super) status: PathStatus, +} + +/// Prunes the IP paths in the paths HashMap. +/// +/// Only prunes if the number of IP paths is above [`MAX_IP_PATHS`]. +/// +/// Keeps paths that are open or of unknown status. +/// +/// Always prunes paths that have unsuccessfully holepunched. +/// +/// Keeps [`MAX_INACTIVE_IP_PATHS`] of the most recently closed paths +/// that are not currently being used but have successfully been +/// holepunched previously. +/// +/// This all ensures that: +/// +/// - We do not have unbounded growth of paths. +/// - If we have many paths for this remote, we prune the paths that cannot hole punch. +/// - We do not prune holepunched paths that are currently not in use too quickly. For example, if a large number of untested paths are added at once, we will not immediately prune all of the unused, but valid, paths at once. +fn prune_ip_paths(paths: &mut FxHashMap) { + // if the total number of paths is less than the max, bail early + if paths.len() < MAX_IP_PATHS { + return; + } + + let ip_paths: Vec<_> = paths.iter().filter(|(addr, _)| addr.is_ip()).collect(); + + // if the total number of ip paths is less than the max, bail early + if ip_paths.len() < MAX_IP_PATHS { + return; + } + + // paths that were opened at one point but have previously been closed + let mut inactive = Vec::with_capacity(ip_paths.len()); + // paths where we attempted hole punching but it not successful + let mut failed = Vec::with_capacity(ip_paths.len()); + + for (addr, state) in ip_paths { + match state.status { + PathStatus::Inactive(t) => { + // paths where holepunching succeeded at one point, but the path was closed. + inactive.push((addr.clone(), t)); + } + PathStatus::Unusable => { + // paths where holepunching has been attempted and failed. + failed.push(addr.clone()); + } + _ => { + // ignore paths that are open or the status is unknown + } + } + } + + // All paths are bad, don't prune all of them. + // + // This implies that `inactive` is empty. + if failed.len() == paths.len() { + // leave the max number of IP paths + failed.truncate(paths.len().saturating_sub(MAX_IP_PATHS)); + } + + // sort the potentially prunable from most recently closed to least recently closed + inactive.sort_by(|a, b| b.1.cmp(&a.1)); + + // Prune the "oldest" closed paths. + let old_inactive = inactive.split_off(inactive.len().saturating_sub(MAX_INACTIVE_IP_PATHS)); + + // collect all the paths that should be pruned + let must_prune: HashSet<_> = failed + .into_iter() + .chain(old_inactive.into_iter().map(|(addr, _)| addr)) + .collect(); + + paths.retain(|addr, _| !must_prune.contains(addr)); +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, SocketAddrV4}, + time::Duration, + }; + + use iroh_base::{RelayUrl, SecretKey}; + use rand::SeedableRng; + + use super::*; + + fn ip_addr(port: u16) -> transports::Addr { + transports::Addr::Ip(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port).into()) + } + + fn path_state_inactive(closed: Instant) -> PathState { + PathState { + sources: HashMap::new(), + status: PathStatus::Inactive(closed), + } + } + + fn path_state_unusable() -> PathState { + PathState { + sources: HashMap::new(), + status: PathStatus::Unusable, + } + } + + #[test] + fn test_prune_under_max_paths() { + let mut paths = FxHashMap::default(); + for i in 0..20 { + paths.insert(ip_addr(i), PathState::default()); + } + + prune_ip_paths(&mut paths); + assert_eq!(20, paths.len(), "should not prune when under MAX_IP_PATHS"); + } + + #[test] + fn test_prune_at_max_paths_no_prunable() { + let mut paths = FxHashMap::default(); + // All paths are active (never abandoned), so none should be pruned + for i in 0..MAX_IP_PATHS { + paths.insert(ip_addr(i as u16), PathState::default()); + } + + prune_ip_paths(&mut paths); + assert_eq!(MAX_IP_PATHS, paths.len(), "should not prune active paths"); + } + + #[test] + fn test_prune_failed_holepunch() { + let mut paths = FxHashMap::default(); + + // Add 20 active paths + for i in 0..20 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 15 failed holepunch paths (must_prune) + for i in 20..35 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + prune_ip_paths(&mut paths); + + // All failed holepunch paths should be pruned + assert_eq!(20, paths.len()); + for i in 0..20 { + assert!(paths.contains_key(&ip_addr(i))); + } + for i in 20..35 { + assert!(!paths.contains_key(&ip_addr(i))); + } + } + + #[test] + fn test_prune_keeps_most_recent_inactive() { + let mut paths = FxHashMap::default(); + let now = Instant::now(); + + // Add 15 active paths + for i in 0..15 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 20 inactive paths with different abandon times + // Ports 15-34, with port 34 being most recently abandoned + for i in 0..20 { + let abandoned_time = now - Duration::from_secs((20 - i) as u64); + paths.insert(ip_addr(15 + i as u16), path_state_inactive(abandoned_time)); + } + + assert_eq!(35, paths.len()); + prune_ip_paths(&mut paths); + + // Should keep 15 active + 10 most recently abandoned + assert_eq!(25, paths.len()); + + // Active paths should remain + for i in 0..15 { + assert!(paths.contains_key(&ip_addr(i))); + } + + // Most recently abandoned (ports 25-34) should remain + for i in 25..35 { + assert!(paths.contains_key(&ip_addr(i)), "port {} should be kept", i); + } + + // Oldest abandoned (ports 15-24) should be pruned + for i in 15..25 { + assert!( + !paths.contains_key(&ip_addr(i)), + "port {} should be pruned", + i + ); + } + } + + #[test] + fn test_prune_mixed_must_and_can_prune() { + let mut paths = FxHashMap::default(); + let now = Instant::now(); + + // Add 15 active paths + for i in 0..15 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 5 failed holepunch paths + for i in 15..20 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + // Add 15 usable but abandoned paths + for i in 0..15 { + let abandoned_time = now - Duration::from_secs((15 - i) as u64); + paths.insert(ip_addr(20 + i as u16), path_state_inactive(abandoned_time)); + } + + assert_eq!(35, paths.len()); + prune_ip_paths(&mut paths); + + // Remove all failed paths -> down to 30 + // Keep MAX_INACTIVE_IP_PATHS, eg remove 5 usable but abandoned paths -> down to 20 + assert_eq!(20, paths.len()); + + // Active paths should remain + for i in 0..15 { + assert!(paths.contains_key(&ip_addr(i))); + } + + // Failed holepunch should be pruned + for i in 15..20 { + assert!(!paths.contains_key(&ip_addr(i))); + } + + // Most recently abandoned (ports 30-34) should remain + for i in 30..35 { + assert!(paths.contains_key(&ip_addr(i)), "port {} should be kept", i); + } + } + + #[test] + fn test_prune_non_ip_paths_not_counted() { + let mut paths = FxHashMap::default(); + + // Add 25 IP paths (under MAX_IP_PATHS) + for i in 0..25 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(0u64); + let relay_url: RelayUrl = url::Url::parse("https://localhost") + .expect("should be valid url") + .into(); + // Add 10 relay addresses + for _ in 0..10 { + let id = SecretKey::generate(&mut rng).public(); + let relay_addr = transports::Addr::Relay(relay_url.clone(), id); + paths.insert(relay_addr, PathState::default()); + } + + assert_eq!(35, paths.len()); // 25 IP + 10 relay + prune_ip_paths(&mut paths); + + // Should not prune since IP paths < MAX_IP_PATHS + assert_eq!(35, paths.len()); + } + + #[test] + fn test_prune_preserves_never_dialed() { + let mut paths = FxHashMap::default(); + + // Add 20 never-dialed paths (PathStatus::Unknown) + for i in 0..20 { + paths.insert(ip_addr(i), PathState::default()); + } + + // Add 15 failed paths to trigger pruning + for i in 20..35 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + prune_ip_paths(&mut paths); + + // Never-dialed paths should be preserved + for i in 0..20 { + assert!(paths.contains_key(&ip_addr(i))); + } + } + + #[test] + fn test_prune_all_paths_failed() { + let mut paths = FxHashMap::default(); + + // Add 40 failed holepunch paths (all paths have failed) + for i in 0..40 { + paths.insert(ip_addr(i), path_state_unusable()); + } + + assert_eq!(40, paths.len()); + prune_ip_paths(&mut paths); + + // Should keep MAX_IP_PATHS instead of pruning everything + // This prevents catastrophic loss of all path information + assert_eq!( + MAX_IP_PATHS, + paths.len(), + "should keep MAX_IP_PATHS when all paths failed" + ); + } + + #[test] + fn test_insert_open_path() { + let mut state = RemotePathState::default(); + let addr = ip_addr(1000); + let source = Source::Udp; + + assert!(state.is_empty()); + + state.insert_open_path(addr.clone(), source.clone()); + + assert!(!state.is_empty()); + assert!(state.paths.contains_key(&addr)); + let path = &state.paths[&addr]; + assert!(matches!(path.status, PathStatus::Open)); + assert_eq!(path.sources.len(), 1); + assert!(path.sources.contains_key(&source)); + } + + #[test] + fn test_abandoned_path() { + let mut state = RemotePathState::default(); + + // Test: Open goes to Inactive + let addr_open = ip_addr(1000); + state.insert_open_path(addr_open.clone(), Source::Udp); + assert!(matches!(state.paths[&addr_open].status, PathStatus::Open)); + + state.abandoned_path(&addr_open); + assert!(matches!( + state.paths[&addr_open].status, + PathStatus::Inactive(_) + )); + + // Test: Inactive stays Inactive + state.abandoned_path(&addr_open); + assert!(matches!( + state.paths[&addr_open].status, + PathStatus::Inactive(_) + )); + + // Test: Unknown goes to Unusable + let addr_unknown = ip_addr(2000); + state.insert_multiple([addr_unknown.clone()].into_iter(), Source::Relay); + assert!(matches!( + state.paths[&addr_unknown].status, + PathStatus::Unknown + )); + + state.abandoned_path(&addr_unknown); + assert!(matches!( + state.paths[&addr_unknown].status, + PathStatus::Unusable + )); + + // Test: Unusable stays Unusable + state.abandoned_path(&addr_unknown); + assert!(matches!( + state.paths[&addr_unknown].status, + PathStatus::Unusable + )); + } }