Implement opt-in overlay TUN runtime

This commit is contained in:
Eric Wendland 2026-05-23 02:08:51 +02:00
commit d9728a326d
15 changed files with 2315 additions and 84 deletions

377
Cargo.lock generated
View file

@ -210,6 +210,12 @@ dependencies = [
"tokio",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
@ -334,7 +340,7 @@ dependencies = [
"miniz_oxide",
"object",
"rustc-demangle",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@ -425,6 +431,19 @@ dependencies = [
"zeroize",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "borsh"
version = "1.6.1"
@ -462,6 +481,26 @@ dependencies = [
"serde",
]
[[package]]
name = "c2rust-bitfields"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcee50917f9de1a018e3f4f9a8f2ff3d030a288cffa4b18d9b391e97c12e4cfb"
dependencies = [
"c2rust-bitfields-derive",
]
[[package]]
name = "c2rust-bitfields-derive"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b457277798202ccd365b9c112ebee08ddd57f1033916c8b8ea52f222e5b715d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "cc"
version = "1.2.62"
@ -515,7 +554,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@ -1076,6 +1115,15 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@ -1142,6 +1190,9 @@ name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "fiat-crypto"
@ -1177,6 +1228,18 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be"
dependencies = [
"fastrand",
"futures-core",
"futures-sink",
"spin 0.9.8",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -1379,8 +1442,8 @@ dependencies = [
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
"windows-link 0.2.1",
"windows-result 0.4.1",
]
[[package]]
@ -1407,6 +1470,7 @@ dependencies = [
"geth-iroh",
"geth-keychain",
"geth-node",
"geth-overlay",
"geth-ssh-identity",
"geth-store",
"geth-types",
@ -1637,12 +1701,14 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"tracing",
"tun-rs",
]
[[package]]
name = "geth-overlay"
version = "0.1.0"
dependencies = [
"blake3",
"geth-types",
"serde",
"thiserror 2.0.18",
@ -1734,6 +1800,17 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "getifaddrs"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a542e1b7ac1f3d62c5777d430d66eca9cb59e813c46b86e29fa9ce94ff9a4810"
dependencies = [
"bitflags",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@ -2083,7 +2160,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
"windows-core 0.62.2",
]
[[package]]
@ -2275,7 +2352,7 @@ dependencies = [
"socket2 0.6.3",
"widestring",
"windows-registry",
"windows-result",
"windows-result 0.4.1",
"windows-sys 0.61.2",
]
@ -2712,7 +2789,7 @@ dependencies = [
"simd_cesu8",
"thiserror 2.0.18",
"walkdir",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@ -2802,6 +2879,16 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
"windows-link 0.2.1",
]
[[package]]
name = "libredox"
version = "0.1.16"
@ -2898,6 +2985,15 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@ -3021,6 +3117,27 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "netconfig-rs"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733ad7a1352f0748a620368f2ea5cd94f0e449c5108fbac86ace31df1601bf6f"
dependencies = [
"cfg-if",
"core-foundation 0.10.1",
"ipnet",
"libc",
"netlink-packet-core",
"netlink-packet-route 0.25.1",
"netlink-sys",
"nix 0.30.1",
"scopeguard",
"system-configuration-sys",
"thiserror 2.0.18",
"widestring",
"windows 0.61.3",
]
[[package]]
name = "netdev"
version = "0.38.2"
@ -3031,7 +3148,7 @@ dependencies = [
"ipnet",
"libc",
"netlink-packet-core",
"netlink-packet-route",
"netlink-packet-route 0.25.1",
"netlink-sys",
"once_cell",
"system-configuration",
@ -3059,6 +3176,18 @@ dependencies = [
"netlink-packet-core",
]
[[package]]
name = "netlink-packet-route"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ce3636fa715e988114552619582b530481fd5ef176a1e5c1bf024077c2c9445"
dependencies = [
"bitflags",
"libc",
"log",
"netlink-packet-core",
]
[[package]]
name = "netlink-proto"
version = "0.12.0"
@ -3104,7 +3233,7 @@ dependencies = [
"n0-watcher",
"netdev",
"netlink-packet-core",
"netlink-packet-route",
"netlink-packet-route 0.25.1",
"netlink-proto",
"netlink-sys",
"pin-project-lite",
@ -3115,11 +3244,37 @@ dependencies = [
"tokio-util",
"tracing",
"web-sys",
"windows",
"windows-result",
"windows 0.62.2",
"windows-result 0.4.1",
"wmi",
]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
name = "nix"
version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [
"bitflags",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -3282,7 +3437,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@ -3352,6 +3507,17 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkarr"
version = "5.0.3"
@ -3795,7 +3961,7 @@ dependencies = [
"cfg-if",
"libc",
"rustix",
"windows",
"windows 0.62.2",
]
[[package]]
@ -3911,6 +4077,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "route_manager"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "319bb478ff9aae1dc7a544fa599e9eb47e2521621dc3921fba6abcaaa312092c"
dependencies = [
"flume",
"libc",
"netlink-packet-core",
"netlink-packet-route 0.28.0",
"netlink-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rusqlite"
version = "0.32.1"
@ -5001,6 +5181,32 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tun-rs"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a88c6aea62111e48fb67b733d6bbdc2019272e6d6f7108c9b0f4cd25d715d38a"
dependencies = [
"blocking",
"byteorder",
"bytes",
"c2rust-bitfields",
"encoding_rs",
"getifaddrs",
"ipnet",
"libc",
"libloading",
"log",
"netconfig-rs",
"nix 0.31.3",
"route_manager",
"scopeguard",
"tokio",
"widestring",
"windows-sys 0.61.2",
"winreg",
]
[[package]]
name = "typenum"
version = "1.20.0"
@ -5324,16 +5530,38 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [
"windows-collections 0.2.0",
"windows-core 0.61.2",
"windows-future 0.2.1",
"windows-link 0.1.3",
"windows-numerics 0.2.0",
]
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
"windows-collections 0.3.2",
"windows-core 0.62.2",
"windows-future 0.3.2",
"windows-numerics 0.3.1",
]
[[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]]
@ -5342,7 +5570,20 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
"windows-core 0.62.2",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
@ -5353,9 +5594,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[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 0.1.3",
"windows-threading 0.1.0",
]
[[package]]
@ -5364,9 +5616,9 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
"windows-core 0.62.2",
"windows-link 0.2.1",
"windows-threading 0.2.1",
]
[[package]]
@ -5391,20 +5643,36 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "windows-link"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link 0.1.3",
]
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
"windows-core 0.62.2",
"windows-link 0.2.1",
]
[[package]]
@ -5413,9 +5681,18 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
@ -5424,7 +5701,16 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
@ -5433,7 +5719,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@ -5487,7 +5773,7 @@ version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@ -5542,7 +5828,7 @@ version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
@ -5553,13 +5839,22 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
"windows-link 0.2.1",
]
[[package]]
@ -5751,6 +6046,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if",
"windows-sys 0.59.0",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"
@ -5856,8 +6161,8 @@ dependencies = [
"log",
"serde",
"thiserror 2.0.18",
"windows",
"windows-core",
"windows 0.62.2",
"windows-core 0.62.2",
]
[[package]]

View file

@ -68,6 +68,7 @@ tokio = { version = "1", features = ["fs", "io-util", "macros", "net", "rt-multi
toml_edit = "0.25.11"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
tun-rs = { version = "2.7.7", features = ["async"] }
url = "2"
[workspace.lints.rust]

View file

@ -117,10 +117,22 @@ The bootstrap implementation provides:
`geth overlay status`,
`geth overlay plan <name> [--cidr 172.22.0.0/24]`,
`geth overlay join <name> --secret <resource-secret> [--cidr 172.22.0.0/24]`,
and `geth overlay leave <name>`. These commands expose the resource,
capability, and ALPN shape for a future Iroh-carried packet overlay inspired
by iroh-lan. The prototype does not create TUN/Wintun interfaces or route
packets yet.
`geth overlay interface-plan <name> [--platform linux|macos|windows]`,
`geth overlay up <name> [--bearer-secret <route-token>] [--mtu 1280]`,
`geth overlay down <name>`,
`geth overlay peers <name>`, `geth overlay send <name> <node>
--packet-base64 <ipv4-packet>`, `geth overlay recv <name>`, and
`geth overlay leave <name>`. Join persists local overlay membership, creates
the overlay resource when needed, assigns a deterministic virtual IP, and
stores only a BLAKE3 fingerprint of the supplied secret. If the overlay
resource already has bearer invites, join requires a bearer token with
`overlay.join`. Packet send validates IPv4 packets and carries them over the
dedicated `/geth/overlay/1` Iroh ALPN after `overlay.route` authorization.
`overlay up` creates a real L3 TUN/Wintun-style interface through `tun-rs`,
reads IPv4 packets from that interface, maps destination overlay IPs to
imported peer cards, and routes packets over `/geth/overlay/1`. Creating the
interface is explicit opt-in and may require `CAP_NET_ADMIN`, sudo, or
platform-specific network entitlements.
- `geth resource list`
- `geth resource create <kind> <name>`
- `geth keychain init [--admin-key <path>] [--signing-key <path>]`

View file

@ -189,14 +189,25 @@ Current prototype commands:
geth overlay plan home
geth overlay plan home --cidr 172.22.0.0/24
geth overlay join home --secret <resource-secret>
geth overlay interface-plan home --platform linux
geth overlay up home
geth overlay down home
geth overlay send home <node> --packet-base64 <ipv4-packet>
geth overlay recv home
geth overlay leave home
Current limits:
- join/leave are planning stubs; no TUN/Wintun interface is created yet
- host network changes must remain explicit opt-in in future versions
- overlay up creates a real TUN/Wintun-style L3 device and usually needs
privileges or host network entitlements
- host network changes are explicit opt-in only
- discovery can suggest peers, but never grants overlay access
- overlay access must be resource-authorized with overlay.join/overlay.route
- all overlay packets must be carried over Iroh, not SSH or another transport
Bearer invite flow:
geth resource create overlay home
geth secret bearer create resource:overlay:home --capability overlay.join
geth overlay join home --secret <bearer-token>
"#;
const GUIDE_SMOKE_TEST: &str = r#"Minimal smoke test:
@ -540,6 +551,37 @@ pub enum OverlayCommand {
Leave {
name: String,
},
InterfacePlan {
name: String,
#[arg(long)]
platform: Option<String>,
},
Up {
name: String,
#[arg(long)]
bearer_secret: Option<String>,
#[arg(long)]
mtu: Option<u16>,
},
Down {
name: String,
},
Peers {
name: String,
},
Send {
name: String,
node: String,
#[arg(long)]
packet_base64: String,
#[arg(long)]
bearer_secret: Option<String>,
},
Recv {
name: String,
#[arg(long)]
peek: bool,
},
}
#[derive(Debug, Subcommand)]
@ -1297,6 +1339,32 @@ fn request_for_command(command: Command) -> Result<ControlRequest> {
ControlRequest::OverlayJoin { name, secret, cidr }
}
OverlayCommand::Leave { name } => ControlRequest::OverlayLeave { name },
OverlayCommand::InterfacePlan { name, platform } => {
ControlRequest::OverlayInterfacePlan { name, platform }
}
OverlayCommand::Up {
name,
bearer_secret,
mtu,
} => ControlRequest::OverlayUp {
name,
bearer_secret,
mtu,
},
OverlayCommand::Down { name } => ControlRequest::OverlayDown { name },
OverlayCommand::Peers { name } => ControlRequest::OverlayPeers { name },
OverlayCommand::Send {
name,
node,
packet_base64,
bearer_secret,
} => ControlRequest::OverlaySend {
name,
node,
packet_base64,
bearer_secret,
},
OverlayCommand::Recv { name, peek } => ControlRequest::OverlayRecv { name, peek },
},
Command::Resource {
command: ResourceCommand::List,
@ -1989,6 +2057,12 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
ControlResponse::OverlayJoined { join } => {
println!("overlay: {}", join.plan.name);
println!("resource: {}", join.plan.resource);
println!("cidr: {}", join.network.cidr);
println!(
"virtual_ip: {}",
join.network.virtual_ip.as_deref().unwrap_or("unassigned")
);
println!("state: {:?}", join.network.state);
println!("enabled: {}", join.enabled);
println!("note: {}", join.note);
}
@ -2001,6 +2075,99 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
println!("stopped: {stopped}");
println!("note: {note}");
}
ControlResponse::OverlayInterfacePlanned { plan } => {
println!("overlay: {}", plan.name);
println!("platform: {}", plan.platform);
println!("interface: {}", plan.interface_name);
println!("cidr: {}", plan.cidr);
println!(
"virtual_ip: {}",
plan.virtual_ip.as_deref().unwrap_or("unassigned")
);
println!("requires_privileges: {}", plan.requires_privileges);
for command in plan.commands {
println!("command: {command}");
}
for note in plan.notes {
println!("note: {note}");
}
}
ControlResponse::OverlayRuntimeStarted { status } => {
println!("overlay: {}", status.name);
println!("interface: {}", status.interface_name);
println!("virtual_ip: {}", status.virtual_ip);
println!("cidr: {}", status.cidr);
println!("mtu: {}", status.mtu);
println!("packets_from_tun: {}", status.packets_from_tun);
println!("packets_to_tun: {}", status.packets_to_tun);
println!("packets_to_peers: {}", status.packets_to_peers);
if let Some(error) = status.last_error {
println!("last_error: {error}");
}
println!("note: {}", status.note);
}
ControlResponse::OverlayRuntimeStopped {
name,
stopped,
note,
} => {
println!("overlay: {name}");
println!("stopped: {stopped}");
println!("note: {note}");
}
ControlResponse::OverlayPeers { name, peers, note } => {
println!("overlay: {name}");
if peers.is_empty() {
println!("no overlay peer candidates");
} else {
for peer in peers {
println!(
"{}\t{}\t{}\t{}",
peer.node_id,
peer.endpoint_id.as_deref().unwrap_or("no-endpoint"),
peer.virtual_ip.as_deref().unwrap_or("no-virtual-ip"),
peer.state
);
}
}
println!("note: {note}");
}
ControlResponse::OverlayPacketSent {
peer_node_id,
peer_agent_id,
endpoint_id,
packet,
allowed,
reason,
note,
} => {
println!("peer: {peer_node_id}");
println!("agent: {peer_agent_id}");
println!("endpoint: {endpoint_id}");
println!("allowed: {allowed}");
println!("reason: {reason}");
if let Some(packet) = packet {
println!("packet: {}", packet.id);
println!("size_bytes: {}", packet.size_bytes);
}
println!("note: {note}");
}
ControlResponse::OverlayPackets {
name,
packets,
drained,
note,
} => {
println!("overlay: {name}");
println!("drained: {drained}");
for packet in packets {
println!(
"{}\t{}\t{}\t{} bytes",
packet.id, packet.source_node, packet.destination_node, packet.size_bytes
);
}
println!("note: {note}");
}
ControlResponse::CasAdded { hash, size_bytes } => {
println!("{hash} {size_bytes} bytes");
}

View file

@ -5,7 +5,10 @@ use geth_discovery::{DiscoveredPeer, PeerCard};
use geth_document::{DocumentResource, DocumentState};
use geth_keychain::{KeychainOp, KeychainOpSignature, NodeEnrollmentRequest, NodeRecord};
use geth_kv::{KvEntry, KvResource, KvSyncEntry};
use geth_overlay::{OverlayJoinPlan, OverlayNetworkStatus, OverlayPlan};
use geth_overlay::{
OverlayInterfacePlan, OverlayJoinPlan, OverlayNetworkStatus, OverlayPacket, OverlayPlan,
OverlayRuntimeStatus,
};
use geth_pipe::{PipeConnection, PipeListener, PipeMessage};
use geth_pubsub::PubsubMessage;
use geth_resource::ResourceDescriptor;
@ -57,6 +60,31 @@ pub enum ControlRequest {
OverlayLeave {
name: String,
},
OverlayInterfacePlan {
name: String,
platform: Option<String>,
},
OverlayUp {
name: String,
bearer_secret: Option<String>,
mtu: Option<u16>,
},
OverlayDown {
name: String,
},
OverlayPeers {
name: String,
},
OverlaySend {
name: String,
node: String,
packet_base64: String,
bearer_secret: Option<String>,
},
OverlayRecv {
name: String,
peek: bool,
},
CasAdd {
path: PathBuf,
},
@ -501,6 +529,37 @@ pub enum ControlResponse {
stopped: bool,
note: String,
},
OverlayInterfacePlanned {
plan: OverlayInterfacePlan,
},
OverlayRuntimeStarted {
status: OverlayRuntimeStatus,
},
OverlayRuntimeStopped {
name: String,
stopped: bool,
note: String,
},
OverlayPeers {
name: String,
peers: Vec<OverlayPeer>,
note: String,
},
OverlayPacketSent {
peer_node_id: String,
peer_agent_id: String,
endpoint_id: String,
packet: Option<OverlayPacket>,
allowed: bool,
reason: String,
note: String,
},
OverlayPackets {
name: String,
packets: Vec<OverlayPacket>,
drained: bool,
note: String,
},
CasAdded {
hash: BlobHash,
size_bytes: u64,
@ -990,6 +1049,14 @@ pub struct CasProvider {
pub last_seen_ms: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayPeer {
pub node_id: String,
pub endpoint_id: Option<String>,
pub virtual_ip: Option<String>,
pub state: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum PeerControlRequest {
@ -1408,6 +1475,38 @@ pub enum PipeWireResponse {
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum OverlayWireRequest {
Packet {
peer_card: PeerCard,
network: String,
packet_base64: String,
nonce: String,
bearer_proof: Option<BearerProof>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum OverlayWireResponse {
PacketAccepted {
node_id: String,
agent_id: String,
endpoint_id: String,
remote_endpoint_id: String,
packet: Option<Box<OverlayPacket>>,
allowed: bool,
reason: String,
evaluated_ops: usize,
nonce: String,
note: String,
},
Error {
message: String,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncWatermark {
pub stream: String,
@ -1518,6 +1617,28 @@ pub fn decode_pipe_wire_response(line: &str) -> Result<PipeWireResponse, Control
serde_json::from_str(line).map_err(ControlError::from)
}
pub fn encode_overlay_wire_request(request: &OverlayWireRequest) -> Result<String, ControlError> {
let mut line = serde_json::to_string(request)?;
line.push('\n');
Ok(line)
}
pub fn decode_overlay_wire_request(line: &str) -> Result<OverlayWireRequest, ControlError> {
serde_json::from_str(line).map_err(ControlError::from)
}
pub fn encode_overlay_wire_response(
response: &OverlayWireResponse,
) -> Result<String, ControlError> {
let mut line = serde_json::to_string(response)?;
line.push('\n');
Ok(line)
}
pub fn decode_overlay_wire_response(line: &str) -> Result<OverlayWireResponse, ControlError> {
serde_json::from_str(line).map_err(ControlError::from)
}
#[cfg(test)]
mod tests {
use super::*;
@ -1587,6 +1708,27 @@ mod tests {
request
);
let request = ControlRequest::OverlayUp {
name: "home-lan".to_owned(),
bearer_secret: Some("gbt_route".to_owned()),
mtu: Some(1280),
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
request
);
let request = ControlRequest::OverlaySend {
name: "home-lan".to_owned(),
node: "node:peer".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
bearer_secret: Some("gbt_invite".to_owned()),
};
assert_eq!(
decode_request(&encode_request(&request).expect("encode")).expect("decode"),
request
);
let response = ControlResponse::CasHas {
hash: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
present: true,
@ -1605,6 +1747,69 @@ mod tests {
response
);
let response = ControlResponse::OverlayJoined {
join: geth_overlay::OverlayJoinPlan {
plan: geth_overlay::plan_overlay("home-lan", None, geth_overlay::OVERLAY_ALPN)
.expect("overlay plan"),
network: geth_overlay::membership_status(
&geth_overlay::joined_overlay_membership(
"home-lan",
None,
"node:local",
"invite",
10,
)
.expect("overlay membership"),
),
enabled: true,
note: "membership persisted".to_owned(),
},
};
assert_eq!(
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
response
);
let response = ControlResponse::OverlayRuntimeStarted {
status: geth_overlay::OverlayRuntimeStatus {
name: "home-lan".to_owned(),
interface_name: "geth-homelan".to_owned(),
virtual_ip: "172.22.0.10".to_owned(),
cidr: "172.22.0.0/24".to_owned(),
mtu: 1280,
started_at_ms: 12,
packets_from_tun: 1,
packets_to_tun: 2,
packets_to_peers: 3,
last_error: None,
note: "active".to_owned(),
},
};
assert_eq!(
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
response
);
let response = ControlResponse::OverlayPackets {
name: "home-lan".to_owned(),
packets: vec![geth_overlay::OverlayPacket {
id: "overlay-packet:test".to_owned(),
network: "home-lan".to_owned(),
source_node: "node:peer".to_owned(),
destination_node: "node:local".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
size_bytes: 20,
received_at_ms: 11,
note: "received".to_owned(),
}],
drained: true,
note: "packets".to_owned(),
};
assert_eq!(
decode_response(&encode_response(&response).expect("encode")).expect("decode"),
response
);
let response = ControlResponse::NodeList {
nodes: vec![NodeRecord {
id: geth_types::NodeId::new("node:local"),
@ -2641,6 +2846,63 @@ mod tests {
response
);
let request = OverlayWireRequest::Packet {
peer_card: PeerCard {
node_id: "node:caller".into(),
agent_id: "agent:caller".into(),
endpoints: Vec::new(),
issued_at: geth_types::UnixMillis(1),
signature: geth_discovery::SignatureMetadata {
namespace: "geth.peer-card.v1@geth.local".to_owned(),
signer: "agent:caller".to_owned(),
public_key: "key".to_owned(),
signature: "sig".to_owned(),
},
},
network: "home".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
nonce: "nonce".to_owned(),
bearer_proof: Some(BearerProof {
secret: "bearer:test".into(),
resource: "resource:overlay:home".into(),
capabilities: vec!["overlay.route".into()],
nonce: "nonce".to_owned(),
response: "response".to_owned(),
}),
};
assert_eq!(
decode_overlay_wire_request(&encode_overlay_wire_request(&request).expect("encode"))
.expect("decode"),
request
);
let response = OverlayWireResponse::PacketAccepted {
node_id: "node:peer".to_owned(),
agent_id: "agent:peer".to_owned(),
endpoint_id: "endpoint:peer".to_owned(),
remote_endpoint_id: "endpoint:caller".to_owned(),
packet: Some(Box::new(geth_overlay::OverlayPacket {
id: "overlay-packet:test".to_owned(),
network: "home".to_owned(),
source_node: "node:caller".to_owned(),
destination_node: "node:peer".to_owned(),
packet_base64: "RQAAFAAAQABAAQAAqBYAAawWAAI=".to_owned(),
size_bytes: 20,
received_at_ms: 12,
note: "overlay".to_owned(),
})),
allowed: true,
reason: "direct grant".to_owned(),
evaluated_ops: 1,
nonce: "nonce".to_owned(),
note: "overlay wire".to_owned(),
};
assert_eq!(
decode_overlay_wire_response(&encode_overlay_wire_response(&response).expect("encode"))
.expect("decode"),
response
);
let request = PipeWireRequest::TcpConnect {
peer_card: PeerCard {
node_id: "node:caller".into(),

View file

@ -42,6 +42,7 @@ iroh-blobs.workspace = true
iroh-docs.workspace = true
iroh-gossip.workspace = true
swarm-discovery.workspace = true
tun-rs.workspace = true
[dev-dependencies]
rusqlite.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,7 @@ rust-version.workspace = true
license.workspace = true
[dependencies]
blake3.workspace = true
serde.workspace = true
thiserror.workspace = true
geth-types = { path = "../geth-types" }

View file

@ -34,6 +34,7 @@ pub struct OverlayPlan {
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayJoinPlan {
pub plan: OverlayPlan,
pub network: OverlayNetworkStatus,
pub enabled: bool,
pub note: String,
}
@ -57,6 +58,59 @@ pub struct OverlayNetworkStatus {
pub note: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayInterfacePlan {
pub name: String,
pub platform: String,
pub interface_name: String,
pub cidr: String,
pub virtual_ip: Option<String>,
pub requires_privileges: bool,
pub commands: Vec<String>,
pub notes: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayRuntimeStatus {
pub name: String,
pub interface_name: String,
pub virtual_ip: String,
pub cidr: String,
pub mtu: u16,
pub started_at_ms: i64,
pub packets_from_tun: u64,
pub packets_to_tun: u64,
pub packets_to_peers: u64,
pub last_error: Option<String>,
pub note: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayPacket {
pub id: String,
pub network: String,
pub source_node: String,
pub destination_node: String,
pub packet_base64: String,
pub size_bytes: usize,
pub received_at_ms: i64,
pub note: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct OverlayMembership {
pub name: String,
pub resource: ResourceId,
pub cidr: String,
pub state: OverlayState,
pub local_node_id: String,
pub virtual_ip: Option<String>,
pub secret_fingerprint: String,
pub joined_at_ms: i64,
pub updated_at_ms: i64,
pub packet_runtime: String,
}
#[derive(Debug, thiserror::Error)]
pub enum OverlayError {
#[error("invalid overlay name `{0}`")]
@ -65,6 +119,12 @@ pub enum OverlayError {
InvalidCidr(String),
#[error("overlay join requires a non-empty resource secret")]
EmptySecret,
#[error("overlay CIDR `{0}` has no usable host addresses")]
NoUsableHostAddress(String),
#[error("invalid IPv4 packet: {0}")]
InvalidIpv4Packet(String),
#[error("unsupported overlay platform `{0}`")]
UnsupportedPlatform(String),
}
pub fn validate_overlay_name(name: &str) -> Result<(), OverlayError> {
@ -104,11 +164,173 @@ pub fn validate_overlay_secret(secret: &str) -> Result<(), OverlayError> {
}
}
pub fn validate_ipv4_packet(packet: &[u8]) -> Result<(), OverlayError> {
if packet.len() < 20 {
return Err(OverlayError::InvalidIpv4Packet(
"packet shorter than IPv4 header".to_owned(),
));
}
if packet[0] >> 4 != 4 {
return Err(OverlayError::InvalidIpv4Packet(
"packet version is not IPv4".to_owned(),
));
}
let header_len = usize::from(packet[0] & 0x0f) * 4;
if header_len < 20 || header_len > packet.len() {
return Err(OverlayError::InvalidIpv4Packet(
"invalid IPv4 header length".to_owned(),
));
}
let total_len = u16::from_be_bytes([packet[2], packet[3]]) as usize;
if total_len < header_len || total_len > packet.len() {
return Err(OverlayError::InvalidIpv4Packet(
"invalid IPv4 total length".to_owned(),
));
}
Ok(())
}
pub fn ipv4_source_destination(packet: &[u8]) -> Result<(String, String), OverlayError> {
validate_ipv4_packet(packet)?;
Ok((
Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]).to_string(),
Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]).to_string(),
))
}
#[must_use]
pub fn overlay_state_key(name: &str) -> String {
format!("overlay:{name}")
}
#[must_use]
pub fn overlay_packet_key(network: &str, received_at_ms: i64, packet_id: &str) -> String {
format!("overlay-packet:{network}:{received_at_ms}:{packet_id}")
}
#[must_use]
pub fn overlay_resource_id(name: &str) -> ResourceId {
ResourceId::new(format!("resource:overlay:{name}"))
}
#[must_use]
pub fn overlay_secret_fingerprint(secret: &str) -> String {
format!("blake3:{}", blake3::hash(secret.as_bytes()))
}
#[must_use]
pub fn overlay_packet_id(
network: &str,
source_node: &str,
destination_node: &str,
packet: &[u8],
received_at_ms: i64,
) -> String {
format!(
"overlay-packet:{}",
blake3::hash(
format!("{network}\0{source_node}\0{destination_node}\0{received_at_ms}\0").as_bytes()
)
.to_hex()
) + &blake3::hash(packet).to_hex()[..16]
}
pub fn joined_overlay_membership(
name: &str,
cidr: Option<&str>,
local_node_id: &str,
secret: &str,
joined_at_ms: i64,
) -> Result<OverlayMembership, OverlayError> {
validate_overlay_secret(secret)?;
let cidr = cidr.unwrap_or(DEFAULT_OVERLAY_CIDR);
validate_overlay_name(name)?;
validate_overlay_cidr(cidr)?;
Ok(OverlayMembership {
name: name.to_owned(),
resource: overlay_resource_id(name),
cidr: cidr.to_owned(),
state: OverlayState::Joined,
local_node_id: local_node_id.to_owned(),
virtual_ip: Some(deterministic_virtual_ip(cidr, local_node_id)?),
secret_fingerprint: overlay_secret_fingerprint(secret),
joined_at_ms,
updated_at_ms: joined_at_ms,
packet_runtime:
"joined: run `geth overlay up <name>` to start the explicit TUN/Wintun runtime"
.to_owned(),
})
}
pub fn stopped_overlay_membership(
mut membership: OverlayMembership,
updated_at_ms: i64,
) -> OverlayMembership {
membership.state = OverlayState::Stopped;
membership.updated_at_ms = updated_at_ms;
membership.packet_runtime = "inactive: overlay membership stopped".to_owned();
membership
}
#[must_use]
pub fn membership_status(membership: &OverlayMembership) -> OverlayNetworkStatus {
OverlayNetworkStatus {
name: membership.name.clone(),
resource: membership.resource.clone(),
cidr: membership.cidr.clone(),
state: membership.state.clone(),
virtual_ip: membership.virtual_ip.clone(),
peers: Vec::new(),
note: membership.packet_runtime.clone(),
}
}
pub fn interface_plan(
plan: &OverlayPlan,
platform: Option<&str>,
virtual_ip: Option<&str>,
) -> Result<OverlayInterfacePlan, OverlayError> {
let platform = platform.unwrap_or(std::env::consts::OS);
let interface_name = format!("geth-{}", interface_slug(&plan.name));
let prefix = cidr_prefix(&plan.cidr)?;
let virtual_ip = virtual_ip.map(ToOwned::to_owned);
let addr = virtual_ip
.as_ref()
.map(|ip| format!("{ip}/{prefix}"))
.unwrap_or_else(|| format!("<virtual-ip>/{prefix}"));
let commands = match platform {
"linux" => vec![
format!("ip tuntap add dev {interface_name} mode tun user <user>"),
format!("ip addr add {addr} dev {interface_name}"),
format!("ip link set {interface_name} up"),
],
"macos" | "darwin" => vec![
"open a utun device from the geth daemon process".to_owned(),
format!("ifconfig <utunN> inet {} {} up", addr, plan.cidr),
],
"windows" => vec![
"install or open a Wintun adapter for the current user context".to_owned(),
format!("assign {addr} to the Wintun adapter"),
],
other => return Err(OverlayError::UnsupportedPlatform(other.to_owned())),
};
Ok(OverlayInterfacePlan {
name: plan.name.clone(),
platform: platform.to_owned(),
interface_name,
cidr: plan.cidr.clone(),
virtual_ip,
requires_privileges: true,
commands,
notes: vec![
"generated plan only; run `geth overlay up <name>` to activate host networking"
.to_owned(),
"activation is explicit and user-scoped".to_owned(),
"all overlay packets remain carried over /geth/overlay/1".to_owned(),
],
})
}
pub fn plan_overlay(
name: &str,
cidr: Option<&str>,
@ -128,7 +350,7 @@ pub fn plan_overlay(
CAPABILITY_ADMIN.to_owned(),
],
discovery: "future overlay discovery may use mDNS, peer exchange, and resource metadata; discovery remains untrusted".to_owned(),
runtime: "planned only in this prototype; no TUN/Wintun interface is created".to_owned(),
runtime: "explicit opt-in runtime available with `geth overlay up <name>`; creates a TUN/Wintun-style L3 interface".to_owned(),
security: vec![
"all overlay packets must be carried over daemon-owned Iroh connections".to_owned(),
"knowing an EndpointID or overlay name must not grant overlay access".to_owned(),
@ -137,15 +359,73 @@ pub fn plan_overlay(
],
implementation_notes: vec![
"inspired by iroh-lan's Iroh-carried packet overlay".to_owned(),
"future packet runtime should register /geth/overlay/1 on the shared geth Iroh router".to_owned(),
"future host integration may need TUN/Wintun privileges and must remain explicitly opt-in".to_owned(),
"packet runtime registers /geth/overlay/1 on the shared geth Iroh router".to_owned(),
"host integration may need TUN/Wintun privileges and remains explicitly opt-in".to_owned(),
],
})
}
#[must_use]
pub fn overlay_status_note() -> &'static str {
"overlay runtime is scaffolded but inactive; use `geth overlay plan <name>` to inspect the intended resource and capabilities"
"overlay membership is persisted locally; run `geth overlay up <name>` to start the explicit TUN/Wintun runtime"
}
pub fn deterministic_virtual_ip(cidr: &str, seed: &str) -> Result<String, OverlayError> {
let (network, prefix) = parse_ipv4_cidr(cidr)?;
let host_bits = 32_u8.saturating_sub(prefix);
let address = if host_bits == 0 {
network
} else {
let host_space = 1_u64 << host_bits;
if host_space <= 2 {
return Err(OverlayError::NoUsableHostAddress(cidr.to_owned()));
}
let hash = blake3::hash(seed.as_bytes());
let mut bytes = [0_u8; 8];
bytes.copy_from_slice(&hash.as_bytes()[..8]);
let value = u64::from_le_bytes(bytes);
let host_offset = 1 + (value % (host_space - 2));
network + host_offset as u32
};
Ok(Ipv4Addr::from(address).to_string())
}
pub fn cidr_prefix(cidr: &str) -> Result<u8, OverlayError> {
parse_ipv4_cidr(cidr).map(|(_, prefix)| prefix)
}
fn parse_ipv4_cidr(cidr: &str) -> Result<(u32, u8), OverlayError> {
let (addr, prefix) = cidr
.split_once('/')
.ok_or_else(|| OverlayError::InvalidCidr(cidr.to_owned()))?;
let addr = addr
.parse::<Ipv4Addr>()
.map_err(|_| OverlayError::InvalidCidr(cidr.to_owned()))?;
let prefix = prefix
.parse::<u8>()
.map_err(|_| OverlayError::InvalidCidr(cidr.to_owned()))?;
if prefix > 32 {
return Err(OverlayError::InvalidCidr(cidr.to_owned()));
}
let mask = if prefix == 0 {
0
} else {
u32::MAX << (32 - prefix)
};
Ok((u32::from(addr) & mask, prefix))
}
fn interface_slug(name: &str) -> String {
let slug = name
.chars()
.filter(|ch| ch.is_ascii_alphanumeric())
.take(8)
.collect::<String>();
if slug.is_empty() {
"net".to_owned()
} else {
slug
}
}
#[cfg(test)]
@ -177,6 +457,73 @@ mod tests {
assert_eq!(plan.cidr, DEFAULT_OVERLAY_CIDR);
assert!(plan.capabilities.contains(&CAPABILITY_JOIN.to_owned()));
assert!(plan.security.iter().any(|note| note.contains("Iroh")));
assert!(plan.runtime.contains("planned only"));
assert!(plan.runtime.contains("overlay up"));
}
#[test]
fn joined_membership_derives_virtual_ip_without_storing_secret() {
let membership = joined_overlay_membership(
"home",
Some("172.22.0.0/24"),
"node:local",
"invite-token",
42,
)
.expect("membership");
assert_eq!(
membership.resource,
ResourceId::new("resource:overlay:home")
);
assert_eq!(membership.virtual_ip, Some("172.22.0.168".to_owned()));
assert_ne!(membership.secret_fingerprint, "invite-token");
assert_eq!(membership.joined_at_ms, 42);
let status = membership_status(&membership);
assert_eq!(status.state, OverlayState::Joined);
assert_eq!(status.virtual_ip, membership.virtual_ip);
}
#[test]
fn tiny_overlay_cidrs_do_not_assign_hosts() {
assert!(
joined_overlay_membership("tiny", Some("10.0.0.0/31"), "node:local", "secret", 1)
.is_err()
);
}
#[test]
fn validates_ipv4_packets() {
let packet = [
0x45, 0x00, 0x00, 0x14, 0x00, 0x00, 0x40, 0x00, 64, 1, 0, 0, 172, 22, 0, 1, 172, 22, 0,
2,
];
validate_ipv4_packet(&packet).expect("valid packet");
assert!(validate_ipv4_packet(&packet[..10]).is_err());
let mut bad = packet;
bad[0] = 0x65;
assert!(validate_ipv4_packet(&bad).is_err());
let (_, destination) = ipv4_source_destination(&packet).expect("addresses");
assert_eq!(destination, "172.22.0.2");
}
#[test]
fn interface_plan_is_generated_without_mutating_host_networking() {
let plan = plan_overlay("home", None, OVERLAY_ALPN).expect("plan");
let interface =
interface_plan(&plan, Some("linux"), Some("172.22.0.10")).expect("interface plan");
assert_eq!(interface.interface_name, "geth-home");
assert!(interface.requires_privileges);
assert!(
interface
.commands
.iter()
.any(|cmd| cmd.contains("ip tuntap"))
);
assert!(
interface
.notes
.iter()
.any(|note| note.contains("generated"))
);
}
}

View file

@ -905,6 +905,14 @@ impl Store {
}
}
pub fn delete_module_state(&self, module: &str) -> Result<(), StoreError> {
self.conn.execute(
"DELETE FROM module_state WHERE module = ?1",
params![module],
)?;
Ok(())
}
pub fn list_module_states_with_prefix(
&self,
prefix: &str,
@ -1714,6 +1722,15 @@ mod tests {
.len(),
1
);
store
.delete_module_state("live-sync:node:laptop:ssh-certs")
.expect("delete state");
assert_eq!(
store
.get_module_state("live-sync:node:laptop:ssh-certs")
.expect("get deleted state"),
None
);
}
#[test]

View file

@ -24,6 +24,7 @@ geth-discovery = { path = "../geth-discovery" }
geth-iroh = { path = "../geth-iroh" }
geth-keychain = { path = "../geth-keychain" }
geth-node = { path = "../geth-node" }
geth-overlay = { path = "../geth-overlay" }
geth-ssh-identity = { path = "../geth-ssh-identity" }
geth-store = { path = "../geth-store" }
geth-types = { path = "../geth-types" }

View file

@ -1446,7 +1446,135 @@ fn overlay_plan_is_available_without_starting_packet_runtime() {
assert_eq!(plan.resource.to_string(), "resource:overlay:home-lan");
assert_eq!(plan.alpn, "/geth/overlay/1");
assert!(plan.capabilities.contains(&"overlay.join".to_owned()));
assert!(plan.runtime.contains("no TUN/Wintun"));
assert!(plan.runtime.contains("overlay up"));
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn overlay_join_persists_membership_resource_and_leave_state() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let joined = geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayJoin {
name: "home-lan".to_owned(),
secret: "local-bootstrap-secret".to_owned(),
cidr: Some("172.22.0.0/24".to_owned()),
},
)
.expect("overlay join");
match joined {
geth_control::ControlResponse::OverlayJoined { join } => {
assert!(join.enabled);
assert_eq!(join.plan.resource.to_string(), "resource:overlay:home-lan");
assert_eq!(join.network.state, geth_overlay::OverlayState::Joined);
assert!(join.network.virtual_ip.is_some());
assert!(join.note.contains("local bootstrap overlay"));
}
other => panic!("unexpected response: {other:?}"),
}
let resources = geth_node::handle_request(&node, geth_control::ControlRequest::ResourceList)
.expect("resource list");
match resources {
geth_control::ControlResponse::ResourceList { resources } => {
assert!(resources.iter().any(|resource| {
resource.id.to_string() == "resource:overlay:home-lan"
&& resource.kind == geth_types::ResourceKind::Overlay
}));
}
other => panic!("unexpected response: {other:?}"),
}
let status = geth_node::handle_request(&node, geth_control::ControlRequest::OverlayStatus)
.expect("overlay status");
match status {
geth_control::ControlResponse::OverlayStatus { networks, note } => {
assert_eq!(networks.len(), 1);
assert_eq!(networks[0].state, geth_overlay::OverlayState::Joined);
assert!(note.contains("persisted locally"));
}
other => panic!("unexpected response: {other:?}"),
}
geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayLeave {
name: "home-lan".to_owned(),
},
)
.expect("overlay leave");
let status = geth_node::handle_request(&node, geth_control::ControlRequest::OverlayStatus)
.expect("overlay status after leave");
match status {
geth_control::ControlResponse::OverlayStatus { networks, .. } => {
assert_eq!(networks.len(), 1);
assert_eq!(networks[0].state, geth_overlay::OverlayState::Stopped);
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn overlay_join_uses_bearer_invite_when_one_exists() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let resource = "resource:overlay:locked".to_owned();
geth_node::handle_request(
&node,
geth_control::ControlRequest::ResourceCreate {
kind: "overlay".to_owned(),
name: "locked".to_owned(),
},
)
.expect("create overlay resource");
let access = geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretBearerCreate {
resource: resource.clone(),
capabilities: vec!["overlay.join".to_owned()],
expires_at_ms: None,
},
)
.expect("create bearer invite");
let token = match access {
geth_control::ControlResponse::SecretBearerCreated { access } => {
access.token.expect("private bearer token").to_string()
}
other => panic!("unexpected response: {other:?}"),
};
let denied = geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayJoin {
name: "locked".to_owned(),
secret: "wrong-token".to_owned(),
cidr: None,
},
)
.expect_err("bad bearer should be rejected");
assert!(denied.to_string().contains("overlay join requires"));
let joined = geth_node::handle_request(
&node,
geth_control::ControlRequest::OverlayJoin {
name: "locked".to_owned(),
secret: token,
cidr: None,
},
)
.expect("authorized overlay join");
match joined {
geth_control::ControlResponse::OverlayJoined { join } => {
assert!(join.enabled);
assert_eq!(join.network.state, geth_overlay::OverlayState::Joined);
assert!(join.note.contains("bearer overlay.join authorization"));
}
other => panic!("unexpected response: {other:?}"),
}

View file

@ -2,7 +2,7 @@
## Status
Accepted for prototype scaffolding.
Accepted for prototype implementation.
## Context
@ -25,18 +25,32 @@ misconfigured.
## Decision
Add an optional `overlay` resource kind and reserve `/geth/overlay/1` for a
future Iroh-carried packet overlay. The initial implementation is a typed model
and CLI/control planning surface:
Add an optional `overlay` resource kind and reserve `/geth/overlay/1` for an
Iroh-carried packet overlay. The prototype implementation is a typed model,
CLI/control surface, persisted local membership record, and explicit opt-in
TUN/Wintun-style runtime:
- `geth overlay status`
- `geth overlay plan <name> [--cidr <cidr>]`
- `geth overlay join <name> --secret <resource-secret> [--cidr <cidr>]`
- `geth overlay interface-plan <name>`
- `geth overlay up <name> [--bearer-secret <route-token>] [--mtu <bytes>]`
- `geth overlay down <name>`
- `geth overlay leave <name>`
The prototype does not create TUN/Wintun interfaces, assign virtual IPs, or
route packets. Join/leave are explicit stubs that describe the intended
resource, capabilities, ALPN, and security boundaries.
Join creates or reuses `resource:overlay:<name>`, persists local membership in
`module_state`, assigns a deterministic virtual IP from the overlay CIDR, and
stores only a BLAKE3 fingerprint of the supplied secret. If the resource already
has bearer access with `overlay.join`, join requires a matching bearer token.
This keeps bearer access resource-scoped and does not enroll the caller as a
trusted node.
`overlay up` creates a real L3 TUN/Wintun-style device through `tun-rs`, assigns
the local virtual IP, reads IPv4 packets from the device, maps destination
overlay IPs to imported peer cards, and routes packets over `/geth/overlay/1`.
The serving daemon requires `overlay.route` before injecting received packets
into an active overlay runtime. If no runtime is active, received packets are
stored for inspection with `overlay recv`.
Overlay resources use capabilities:
@ -49,18 +63,15 @@ must not create a second endpoint or non-Iroh transport.
## Consequences
The CLI can now document and test the intended overlay shape without changing
host networking. This keeps the prototype safe to run on development machines
while preserving a clear path toward an iroh-lan-inspired overlay.
The CLI can now document, persist, test, and activate overlay membership.
Activating an overlay mutates host networking and can require privileges, so it
is never automatic daemon startup behavior.
Future work must add:
- persisted overlay membership/resource metadata
- resource-secret-backed join authorization
- platform-specific opt-in TUN/Wintun management
- packet routing over `/geth/overlay/1`
- peer/IP coordination through trusted resource metadata and untrusted discovery
candidates
- stronger platform packaging for Wintun/macOS entitlements where needed
No current code claims VPN-grade isolation, forward secrecy, or automatic host
network security.

View file

@ -253,12 +253,26 @@ TUN/Wintun interface are carried over the daemon-owned Iroh endpoint on
with `overlay.join`, `overlay.route`, and `overlay.admin` capabilities. The
prototype exposes `geth overlay status`, `geth overlay plan <name>`,
`geth overlay join <name> --secret <resource-secret>`, and `geth overlay leave
<name>` as planning/control stubs only. They validate names, CIDRs, resource
IDs, capabilities, and security notes, but they do not create host network
interfaces, assign virtual IPs, or route packets yet. Future implementation must
remain explicitly opt-in because TUN/Wintun setup may need platform-specific
privileges. Overlay discovery can use mDNS, peer exchange, and resource
metadata, but discovery remains untrusted and cannot grant overlay access.
<name>`. Join creates or reuses `resource:overlay:<name>`, persists local
membership in `module_state`, assigns a deterministic virtual IP from the CIDR,
and stores only a BLAKE3 fingerprint of the supplied secret. If bearer access
already exists for that overlay resource, join requires the supplied secret to
be a bearer token with `overlay.join`; this lets operators create explicit
resource-scoped overlay invites through `geth secret bearer create`. The
`geth overlay interface-plan` generates Linux, macOS, or Windows host-interface
plans for review. `geth overlay up <name>` is the explicit opt-in that creates a
real L3 TUN/Wintun-style interface through `tun-rs`, assigns the local
deterministic overlay IP, reads IPv4 packets from the interface, maps
destination overlay IPs to imported peer cards, and carries those packets over
the dedicated `/geth/overlay/1` Iroh ALPN. `geth overlay down <name>` stops the
runtime and drops the device handle. The serving daemon validates the signed
peer card against the observed EndpointID and requires `overlay.route` on
`resource:overlay:<name>` before queuing a received packet into the active
interface; if no runtime is active, the packet is persisted for `geth overlay
recv`. Interface setup may require `CAP_NET_ADMIN`, sudo, a preconfigured
`/dev/net/tun`, Wintun availability, or platform-specific network entitlements.
Overlay discovery can use mDNS, peer exchange, and resource metadata, but
discovery remains untrusted and cannot grant overlay access.
`geth-pipe` currently supports `pipe listen/connect/send/recv` against a
daemon-lifetime runtime. `geth pipe connect <name> --node <node-id>` sends an

View file

@ -584,19 +584,34 @@ Goal: add authorized stream-oriented management workflows over Iroh.
`overlay.join`, `overlay.route`, and `overlay.admin`.
- `[x]` Add CLI/control commands for `geth overlay status`, `plan`, `join`,
and `leave`.
- `[x]` Prototype commands make clear that no TUN/Wintun interface is created
and no packets are routed yet.
- `[x]` Prototype commands make clear that host networking changes require
explicit `overlay up`.
- `[x]` Tests cover overlay validation and control serialization.
- `[ ]` Persist overlay network configuration and membership state as
- `[x]` Persist overlay network configuration and membership state as
resource metadata.
- `[ ]` Implement resource-authorized overlay join using resource secrets
- `[x]` Implement resource-authorized overlay join using resource secrets
without granting node identity.
- `[ ]` Add platform-specific, opt-in TUN/Wintun interface management with
- `[x]` Store only a secret fingerprint in overlay membership state.
- `[x]` Tests cover persisted overlay join/status/leave and bearer-token
enforcement for `overlay.join`.
- `[x]` Add platform-specific, opt-in TUN/Wintun interface plans with
generated-definition tests and no privileged test requirements.
- `[ ]` Route IPv4 packets over `/geth/overlay/1` using the shared daemon
- `[x]` Route IPv4 packets over `/geth/overlay/1` using the shared daemon
Iroh endpoint.
- `[x]` Validate overlay packet routing with two-node Iroh tests and
`overlay.route` authorization checks.
- `[x]` Add candidate peer listing from untrusted peer-card metadata.
- `[x]` Implement actual TUN/Wintun-style activation as an explicit user
opt-in through `geth overlay up/down`.
- `[x]` Runtime reads validated IPv4 packets from the TUN device, routes them
to imported peer cards by deterministic overlay IP, and injects authorized
remote packets back into the device.
- `[x]` Runtime activation surfaces privilege/setup errors clearly instead of
silently falling back to a non-overlay transport.
- `[ ]` Add live peer/IP coordination over trusted resource metadata and
untrusted discovery candidates.
- `[ ]` Add packaged Windows Wintun deployment and macOS entitlement guidance
for release builds.
- `[~]` Unix socket forwarding where supported.
Acceptance criteria: