14 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5.1 520580e0e6 release v0.1.10
ci / quality (push) Failing after 10s
release / build (aarch64-unknown-linux-gnu) (push) Successful in 2m29s
release / build (x86_64-unknown-linux-gnu) (push) Successful in 2m11s
release / publish (push) Successful in 47s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 14:10:16 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 7322812524 status shows each home relay: connected, down with the error, or denied
iroh 1.3's home_relay_status, on GlobalStatus as relays (absent when
empty, so older clients parse it). The thing we lacked when fetches
timed out: whether this daemon can be reached through a relay at all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 14:08:41 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 7916e59acb release v0.1.9
ci / quality (push) Failing after 3s
release / build (aarch64-unknown-linux-gnu) (push) Successful in 2m23s
release / build (x86_64-unknown-linux-gnu) (push) Successful in 2m1s
release / publish (push) Successful in 47s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 13:58:27 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 458c2a2266 The socket unit owns /run/varde; the service names no runtime directory
RuntimeDirectoryPreserve=yes (0.1.7) made systemd privatise the
directory under DynamicUser: /run/varde became a symlink into
/run/private, 0700, and no unprivileged client - the redoal relay, a
desktop app - could connect. Without RuntimeDirectory= on the service
the socket unit creates the directory (DirectoryMode=0755), nothing
removes it on a service stop, and nothing privatises it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 13:56:47 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 a1776dbbed release v0.1.8
ci / quality (push) Failing after 9s
release / build (aarch64-unknown-linux-gnu) (push) Successful in 2m31s
release / build (x86_64-unknown-linux-gnu) (push) Successful in 2m2s
release / publish (push) Successful in 47s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 13:55:18 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 ddfb983108 iroh 1.3.0 in the lock, this time
0.1.7's bump never reached Cargo.lock: the update ran through a pipe
that closed early and cargo died before writing it. Verified in the
lock now; the suite passes on 1.3.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 13:53:41 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 0f2b0504d5 release v0.1.7
ci / quality (push) Failing after 5s
release / build (aarch64-unknown-linux-gnu) (push) Successful in 2m30s
release / build (x86_64-unknown-linux-gnu) (push) Successful in 2m16s
release / publish (push) Successful in 46s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 13:45:20 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 08fcba749a iroh 1.3, and a socket that survives a service restart
iroh 1.0.2 -> 1.3.0 (relay reconnects that keep answering, rate-limit
notices from relays, the n0 DNS resolver with configurable fallbacks,
relay connection metrics), iroh-mdns-address-lookup 0.4 -> 0.6,
n0-future 0.1 -> 0.3. No code changes were needed; the suite passes.

varde.service now keeps /run/varde across restarts
(RuntimeDirectoryPreserve=yes): stopping the service removed the
directory and with it the socket unit's file, leaving varde.socket
"active" on an unlinked path after any service restart. The install
notes say to restart the socket, which cycles both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 13:43:43 +02:00
Bendik Aagaard LynghaugandClaude Opus 5.5 6ca859c691 release v0.1.6
ci / quality (push) Failing after 5s
release / build (aarch64-unknown-linux-gnu) (push) Successful in 2m25s
release / build (x86_64-unknown-linux-gnu) (push) Successful in 2m0s
release / publish (push) Successful in 48s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:30:44 +02:00
Bendik Aagaard LynghaugandClaude Opus 5.5 c2b9167e1e Pushes name the peer by ticket, and an allowance can confirm its push
PeerAllowPush and Push take a blob ticket the peer issued as well as a
NodeId; the ticket's addresses are remembered, so the push dials
directly. A peer allowed to push may observe too: the sender confirms a
push by observing it, and without that every allowance-only push
reported failure though it had landed. The allowance test now fails
instead of skipping on anything but an unreachable peer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:29:03 +02:00
Bendik Aagaard LynghaugandClaude Opus 5.5 4d9985c9cb release v0.1.5
ci / quality (push) Failing after 5s
release / build (aarch64-unknown-linux-gnu) (push) Successful in 2m26s
release / build (x86_64-unknown-linux-gnu) (push) Successful in 2m18s
release / publish (push) Successful in 47s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:24:51 +02:00
Bendik Aagaard LynghaugandClaude Opus 5.5 2063d11890 PeerAllowPush: consent to exchange pushes, for a while, and nothing else
PeerTrust syncs a peer's pins and serves it everything - too much to
grant a stranger. An app that wants two peers sharing something (two
phones at the same place) to hand each other new content allows pushes
for ttl_secs (at most a day): both sides may push, the receiver pins,
and the allowance lapses on its own. Asking again extends it;
PeerUntrust ends it. varde-ctl peer allow-push.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:21:50 +02:00
Bendik Aagaard LynghaugandClaude Opus 5.5 0006a188e3 release v0.1.4
ci / quality (push) Failing after 8s
release / build (aarch64-unknown-linux-gnu) (push) Successful in 2m26s
release / build (x86_64-unknown-linux-gnu) (push) Successful in 2m6s
release / publish (push) Successful in 48s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:08:52 +02:00
Bendik Aagaard LynghaugandClaude Opus 5.5 687af8bd2a Put and Read: content on the socket, for clients that share no files
The system daemon runs as its own user behind ProtectHome, so it can
neither read a file a desktop app wrote for Add nor write one back for
Materialize - redoal on the system varde failed every get with EACCES.
Put imports base64 bytes carried in the request and Read returns a raw
blob's bytes in the reply, both up to MAX_INLINE_BYTES (16 MiB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:08:50 +02:00
15 changed files with 565 additions and 395 deletions
Generated
+149 -371
View File
@@ -431,9 +431,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
@@ -581,16 +581,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@@ -631,24 +621,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-channel"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.22"
@@ -722,41 +694,6 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.118",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.118",
]
[[package]]
name = "data-encoding"
version = "2.11.0"
@@ -814,65 +751,13 @@ version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.118",
]
[[package]]
name = "derive_more"
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.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl 2.1.1",
]
[[package]]
name = "derive_more-impl"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
"unicode-xid",
"derive_more-impl",
]
[[package]]
@@ -966,12 +851,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "either"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
[[package]]
name = "embedded-io"
version = "0.4.0"
@@ -1406,35 +1285,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hickory-net"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
dependencies = [
"async-trait",
"bytes",
"cfg-if",
"data-encoding",
"futures-channel",
"futures-io",
"futures-util",
"h2",
"hickory-proto",
"http",
"idna",
"ipnet",
"jni 0.22.4",
"rand 0.10.2",
"rustls",
"thiserror 2.0.18",
"tinyvec",
"tokio",
"tokio-rustls",
"tracing",
"url",
]
[[package]]
name = "hickory-proto"
version = "0.26.1"
@@ -1446,43 +1296,13 @@ dependencies = [
"ipnet",
"jni 0.22.4",
"once_cell",
"prefix-trie",
"rand 0.10.2",
"ring",
"thiserror 2.0.18",
"tinyvec",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
dependencies = [
"cfg-if",
"futures-util",
"hickory-net",
"hickory-proto",
"ipconfig",
"ipnet",
"jni 0.22.4",
"moka",
"ndk-context",
"once_cell",
"parking_lot",
"rand 0.10.2",
"resolv-conf",
"rustls",
"smallvec",
"system-configuration",
"thiserror 2.0.18",
"tokio",
"tokio-rustls",
"tracing",
]
[[package]]
name = "http"
version = "1.4.2"
@@ -1703,12 +1523,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "identity-hash"
version = "0.1.0"
@@ -1802,15 +1616,12 @@ name = "ipnet"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
dependencies = [
"serde",
]
[[package]]
name = "iroh"
version = "1.0.2"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fca9b4b462c343ff88fc0af4096c186f939b602a0bc08723536ef2c31c93971"
checksum = "885787b892b5e2507c701f132ecbd45d2bad4bbb75157a19427087c16dadb833"
dependencies = [
"backon",
"blake3",
@@ -1818,11 +1629,10 @@ dependencies = [
"cfg_aliases",
"ctutils",
"data-encoding",
"derive_more 2.1.1",
"derive_more",
"ed25519-dalek",
"futures-util",
"getrandom 0.4.3",
"hickory-resolver",
"http",
"ipnet",
"iroh-base",
@@ -1830,7 +1640,7 @@ dependencies = [
"iroh-metrics",
"iroh-relay",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"n0-watcher",
"netwatch",
"noq",
@@ -1859,14 +1669,14 @@ dependencies = [
[[package]]
name = "iroh-base"
version = "1.0.3"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6be73e16ee21c923aca9b3121aaa0db936f7c7ecc156ff47b8dac944c68d59a8"
checksum = "6ae9092be76c9f5429776ab3394f5a5a78dc19dc2524267e884a5b9781546c3f"
dependencies = [
"curve25519-dalek",
"data-encoding",
"data-encoding-macro",
"derive_more 2.1.1",
"derive_more",
"ed25519-dalek",
"getrandom 0.4.3",
"n0-error",
@@ -1889,7 +1699,7 @@ dependencies = [
"chrono",
"constant_time_eq",
"data-encoding",
"derive_more 2.1.1",
"derive_more",
"genawaiter",
"getrandom 0.4.3",
"hex",
@@ -1901,7 +1711,7 @@ dependencies = [
"iroh-util",
"irpc",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"nested_enum_utils",
"noq",
"postcard",
@@ -1919,18 +1729,17 @@ dependencies = [
[[package]]
name = "iroh-dns"
version = "1.0.2"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516e4eedc38e33ab69a6bd325520332dc3d67b25454e2d590ebb84a25240dd9a"
checksum = "80276761b56e904e26ea71d0863beef0bb8d5ab6b639cbd1338e4b615209e3a1"
dependencies = [
"arc-swap",
"cfg_aliases",
"derive_more 2.1.1",
"hickory-resolver",
"derive_more",
"iroh-base",
"n0-dns-resolver",
"n0-error",
"n0-future 0.3.2",
"ndk-context",
"n0-future",
"portable-atomic",
"rand 0.10.2",
"rustls",
@@ -1956,17 +1765,17 @@ dependencies = [
[[package]]
name = "iroh-mdns-address-lookup"
version = "0.4.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c1f941695731bc3999d8c13d7e94d50e229427da81ac957eec3818264f17559"
checksum = "7afb03c296022fcb724a7bb76dbb8dcca6aac4f4eb65df161e084b7276c93d8c"
dependencies = [
"data-encoding",
"derive_more 2.1.1",
"derive_more",
"futures-util",
"iroh",
"iroh-base",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"n0-watcher",
"swarm-discovery",
"tokio",
@@ -1976,9 +1785,9 @@ dependencies = [
[[package]]
name = "iroh-metrics"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291065721ad7c477b972e581bbc528df031dc8eb5e39fe1ff3300ae5dfb157ef"
checksum = "8ede55536349842337f7cdc63012ec33dc637945f3259c98a0763aac364cdf09"
dependencies = [
"iroh-metrics-derive",
"itoa",
@@ -2003,17 +1812,16 @@ dependencies = [
[[package]]
name = "iroh-relay"
version = "1.0.2"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8149bb6a57126225a07d6928846d82dcedfd24ea0f863ef7b2eb475e1d726354"
checksum = "ee116a8233f84980574bb8d11e4517503080cd2c3605bafca04ee9c35d708bd9"
dependencies = [
"blake3",
"bytes",
"cfg_aliases",
"data-encoding",
"derive_more 2.1.1",
"derive_more",
"getrandom 0.4.3",
"hickory-resolver",
"http",
"http-body-util",
"hyper",
@@ -2023,7 +1831,7 @@ dependencies = [
"iroh-metrics",
"lru",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"noq",
"noq-proto",
"num_enum",
@@ -2042,7 +1850,6 @@ dependencies = [
"tokio-websockets",
"tracing",
"url",
"vergen-gitcl",
"webpki-roots",
"ws_stream_wasm",
]
@@ -2054,7 +1861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da53233419ca36bf521ed45683b7748366f9b233032891eefc2d70567a84ac54"
dependencies = [
"data-encoding",
"derive_more 2.1.1",
"derive_more",
"iroh-base",
"n0-error",
"postcard",
@@ -2067,10 +1874,10 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20e41eb982f15230c55f0a70a74a514360e1f565b07861924fd0e8db172b3d00"
dependencies = [
"derive_more 2.1.1",
"derive_more",
"iroh",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"tokio",
"tracing",
]
@@ -2085,7 +1892,7 @@ dependencies = [
"futures-util",
"irpc-derive",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"noq",
"postcard",
"rcgen",
@@ -2326,20 +2133,28 @@ dependencies = [
]
[[package]]
name = "moka"
version = "0.12.15"
name = "n0-dns-resolver"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
checksum = "535c99c9a786bee714155d2ee4fc2477405c2c43536e1bc6a51de09a0d1448f7"
dependencies = [
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"equivalent",
"parking_lot",
"portable-atomic",
"smallvec",
"tagptr",
"uuid",
"derive_more",
"ipconfig",
"jni 0.22.4",
"lru",
"n0-error",
"n0-future",
"ndk-context",
"rand 0.10.2",
"reqwest",
"rustc-hash",
"rustls",
"simple-dns",
"system-configuration",
"tokio",
"tokio-rustls",
"tracing",
"webpki-roots",
]
[[package]]
@@ -2363,27 +2178,6 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "n0-future"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb0e5d99e681ab3c938842b96fcb41bf8a7bb4bfdb11ccbd653a7e83e06c794"
dependencies = [
"cfg_aliases",
"derive_more 1.0.0",
"futures-buffered",
"futures-lite",
"futures-util",
"js-sys",
"pin-project",
"send_wrapper",
"tokio",
"tokio-util",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-time",
]
[[package]]
name = "n0-future"
version = "0.3.2"
@@ -2391,7 +2185,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe"
dependencies = [
"cfg_aliases",
"derive_more 2.1.1",
"derive_more",
"futures-buffered",
"futures-lite",
"futures-util",
@@ -2411,9 +2205,9 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbc618745ad0b7414b149d0517ad8b5573b2fb4d4e2717add3d2446ce1fdd826"
dependencies = [
"derive_more 2.1.1",
"derive_more",
"n0-error",
"n0-future 0.3.2",
"n0-future",
]
[[package]]
@@ -2448,9 +2242,9 @@ dependencies = [
"libc",
"mac-addr",
"ndk-context",
"netlink-packet-core",
"netlink-packet-route",
"netlink-sys",
"netlink-packet-core 0.8.2",
"netlink-packet-route 0.31.0",
"netlink-sys 0.8.8",
"objc2",
"objc2-core-foundation",
"objc2-core-wlan",
@@ -2461,6 +2255,30 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "netdev"
version = "0.46.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c52c2584961c68f5a6e3356797344061f818c93b8acc9cd6d7e284795e563e"
dependencies = [
"block2",
"dispatch2",
"dlopen2",
"ipnet",
"jni 0.21.1",
"libc",
"mac-addr",
"ndk-context",
"netlink-packet-core 0.9.0",
"netlink-packet-route 0.33.0",
"netlink-sys 0.9.0",
"objc2-core-foundation",
"objc2-system-configuration",
"once_cell",
"plist",
"windows-sys 0.61.2",
]
[[package]]
name = "netlink-packet-core"
version = "0.8.2"
@@ -2470,6 +2288,12 @@ dependencies = [
"paste",
]
[[package]]
name = "netlink-packet-core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d6e2daf9a8c2e21302714706860a0e6be02e03d17294ecc66b354ea7d4059dd"
[[package]]
name = "netlink-packet-route"
version = "0.31.0"
@@ -2479,7 +2303,20 @@ dependencies = [
"bitflags",
"libc",
"log",
"netlink-packet-core",
"netlink-packet-core 0.8.2",
]
[[package]]
name = "netlink-packet-route"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59d48ffc8b73a506e1ff0878528c72668f2bfbc3d875b6be890a96be5d0d5576"
dependencies = [
"bitflags",
"libc",
"log",
"netlink-packet-core 0.9.0",
"zerocopy",
]
[[package]]
@@ -2492,8 +2329,8 @@ dependencies = [
"futures-channel",
"futures-util",
"log",
"netlink-packet-core",
"netlink-sys",
"netlink-packet-core 0.8.2",
"netlink-sys 0.8.8",
"thiserror 2.0.18",
]
@@ -2511,26 +2348,38 @@ dependencies = [
]
[[package]]
name = "netwatch"
version = "0.19.1"
name = "netlink-sys"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d9cbe01741347ef750d743d6690603f5eed8341e679fb51c8e629337aa11976"
checksum = "c99d38e00b420df49e940fe0826e667c3dad82ac68eb4c2bbf754c1c14a83a10"
dependencies = [
"bytes",
"libc",
"log",
]
[[package]]
name = "netwatch"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39da9cad5f23aa43401f09497d3b280e51b5feba115d9ffbf38ca35d6ff95e96"
dependencies = [
"atomic-waker",
"bytes",
"cfg_aliases",
"derive_more 2.1.1",
"derive_more",
"ipnet",
"js-sys",
"libc",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"n0-watcher",
"netdev",
"netlink-packet-core",
"netlink-packet-route",
"netdev 0.45.0",
"netdev 0.46.3",
"netlink-packet-core 0.8.2",
"netlink-packet-route 0.31.0",
"netlink-proto",
"netlink-sys",
"netlink-sys 0.8.8",
"noq-udp",
"objc2-core-foundation",
"objc2-system-configuration",
@@ -2559,13 +2408,13 @@ dependencies = [
[[package]]
name = "noq"
version = "1.0.1"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bf95190af1bd4a00a10e8255ca0c8ddd9e9a9f5e79151d7a7eb6d56aff5dc89"
checksum = "b78be567e796cfa74bb9bdc4117790af2d505eb887019cf8803244353eb09d89"
dependencies = [
"bytes",
"cfg_aliases",
"derive_more 2.1.1",
"derive_more",
"noq-proto",
"noq-udp",
"pin-project-lite",
@@ -2581,13 +2430,13 @@ dependencies = [
[[package]]
name = "noq-proto"
version = "1.1.1"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa7b5ccd819a9c68a0d955e67a881032d09b1a17219b1f90b0997a0888e1a15"
checksum = "7c1e5b6fe668491eca022f745a0a9402585626c73a7b839b3424ace15d6a9c8f"
dependencies = [
"aes-gcm",
"bytes",
"derive_more 2.1.1",
"derive_more",
"enum-assoc",
"fastbloom",
"getrandom 0.4.3",
@@ -2610,9 +2459,9 @@ dependencies = [
[[package]]
name = "noq-udp"
version = "1.0.1"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3137a52df66c20090a889828d1c655f21f52294cba64e5c4fbb04fc83eee7c8e"
checksum = "4dc50afea61aff926e150a36c31f06094608848e114a3fe667d76ec233163b1c"
dependencies = [
"cfg_aliases",
"libc",
@@ -2686,15 +2535,6 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "objc2"
version = "0.6.4"
@@ -2984,19 +2824,19 @@ dependencies = [
[[package]]
name = "portmapper"
version = "0.19.1"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3713e4977408279158444a18c1a01ac9bf2e7eaf1fbfd1a19ac9cd18d90721"
checksum = "ca97242f016e090a25330613bc2382aab65eb22f9149dbe84d8f8e711d49a530"
dependencies = [
"base64",
"bytes",
"derive_more 2.1.1",
"derive_more",
"hyper-util",
"igd-next",
"iroh-metrics",
"libc",
"n0-error",
"n0-future 0.3.2",
"n0-future",
"netwatch",
"num_enum",
"rand 0.10.2",
@@ -3070,17 +2910,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prefix-trie"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
dependencies = [
"either",
"ipnet",
"num-traits",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
@@ -3381,12 +3210,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "resolv-conf"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
[[package]]
name = "ring"
version = "0.17.14"
@@ -3481,7 +3304,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation 0.10.1",
"core-foundation",
"core-foundation-sys",
"jni 0.22.4",
"log",
@@ -3574,7 +3397,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags",
"core-foundation 0.10.1",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -3767,9 +3590,9 @@ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "simple-dns"
version = "0.11.3"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a75cbde1bf934313596a004973e462f9a82caa814dcf1a5f507bdf51597eeb4"
checksum = "2b6f884fa9a8d48101774bfbd3aeb81e968dd22cffd19a372da69f183db22c1a"
dependencies = [
"bitflags",
]
@@ -3973,12 +3796,12 @@ dependencies = [
[[package]]
name = "system-configuration"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
checksum = "501336eb7ba9e417300a6a0fa985721065467aa83a6dcf0422a8e43e4c0328fa"
dependencies = [
"bitflags",
"core-foundation 0.9.4",
"core-foundation",
"system-configuration-sys",
]
@@ -3992,12 +3815,6 @@ dependencies = [
"libc",
]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -4005,7 +3822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"getrandom 0.3.4",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@@ -4068,9 +3885,7 @@ checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
dependencies = [
"deranged",
"js-sys",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde_core",
"time-core",
@@ -4477,7 +4292,6 @@ version = "1.23.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea5fab0d6c3c01ae70085a09cb03d4c7a1d6314e2b3e075392783396d724ca0a"
dependencies = [
"getrandom 0.4.3",
"js-sys",
"serde_core",
"wasm-bindgen",
@@ -4491,7 +4305,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "varde-ctl"
version = "0.1.3"
version = "0.1.10"
dependencies = [
"anyhow",
"clap",
@@ -4504,14 +4318,15 @@ dependencies = [
[[package]]
name = "varde-daemon"
version = "0.1.3"
version = "0.1.10"
dependencies = [
"anyhow",
"base64",
"iroh",
"iroh-blobs",
"iroh-mdns-address-lookup",
"irpc",
"n0-future 0.1.3",
"n0-future",
"postcard",
"proptest",
"reflink-copy",
@@ -4529,49 +4344,12 @@ dependencies = [
[[package]]
name = "varde-proto"
version = "0.1.3"
version = "0.1.10"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "vergen"
version = "9.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75"
dependencies = [
"anyhow",
"derive_builder",
"rustversion",
"vergen-lib",
]
[[package]]
name = "vergen-gitcl"
version = "9.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9"
dependencies = [
"anyhow",
"derive_builder",
"rustversion",
"time",
"vergen",
"vergen-lib",
]
[[package]]
name = "vergen-lib"
version = "9.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569"
dependencies = [
"anyhow",
"derive_builder",
"rustversion",
]
[[package]]
name = "version_check"
version = "0.9.5"
+1 -1
View File
@@ -3,7 +3,7 @@ resolver = "2"
members = ["varde-proto", "varde-daemon", "varde-ctl"]
[workspace.package]
version = "0.1.3"
version = "0.1.10"
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://project.uhhm.no/uhhm/varde"
+2 -2
View File
@@ -5,6 +5,6 @@ post_install() {
}
post_upgrade() {
echo ":: Restart a running daemon to pick up the new version:"
echo " systemctl --user try-restart varde.service (and/or: systemctl try-restart varde.service)"
echo ":: Restart a running daemon to pick up the new version - the socket, not the service:"
echo " systemctl try-restart varde.socket; systemctl --user try-restart varde.socket"
}
+7 -1
View File
@@ -15,7 +15,13 @@ Restart=on-failure
# Hardening
DynamicUser=yes
StateDirectory=varde
RuntimeDirectory=varde
# No RuntimeDirectory= here: varde.socket creates /run/varde for its
# socket file, and the service only uses the activation socket. With
# RuntimeDirectory= on the service, stopping it removed the socket file
# from under the socket unit (0.1.6 and earlier), and preserving it
# (0.1.7/0.1.8) made systemd privatise the directory under DynamicUser -
# /run/varde -> /run/private/varde, 0700 - so no unprivileged client
# could connect at all.
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
+3
View File
@@ -5,6 +5,9 @@ Description=varde API socket
[Socket]
ListenStream=/run/varde/varde.sock
SocketMode=0666
# The directory is this unit's, world-readable, so clients of any user
# reach the socket (see varde.service for why the service must not own it).
DirectoryMode=0755
[Install]
WantedBy=sockets.target
+23 -1
View File
@@ -73,7 +73,8 @@ enum Command {
/// Peer trust operations
#[command(subcommand)]
Peer(PeerCommand),
/// Push fully-present content to a trusted peer
/// Push fully-present content to a trusted peer, or one allowed to
/// exchange pushes
Push {
/// BLAKE3 hash (hex)
hash: String,
@@ -116,6 +117,14 @@ enum PeerCommand {
/// iroh NodeId (z-base-32)
node_id: String,
},
/// Allow pushes with a peer for a while, without trusting it
AllowPush {
/// iroh NodeId (z-base-32)
node_id: String,
/// How long, in seconds (at most a day)
#[arg(long, default_value_t = 1800)]
ttl: u64,
},
/// List trusted peers
List,
}
@@ -187,6 +196,9 @@ fn to_request(command: Command) -> Result<Request> {
},
Command::Peer(PeerCommand::Trust { node_id }) => Request::PeerTrust { node_id },
Command::Peer(PeerCommand::Untrust { node_id }) => Request::PeerUntrust { node_id },
Command::Peer(PeerCommand::AllowPush { node_id, ttl }) => {
Request::PeerAllowPush { node_id, ttl_secs: ttl }
}
Command::Peer(PeerCommand::List) => Request::PeerList {},
Command::Push { hash, node_id } => Request::Push { hash, node_id },
Command::Gc => Request::Gc {},
@@ -246,6 +258,7 @@ fn print_human(data: Option<&ResponseData>) {
match data {
ResponseData::Added { hash, bytes } => println!("added {hash} ({bytes} bytes)"),
ResponseData::Pinned { hash } => println!("pinned {hash}"),
ResponseData::Bytes { data } => println!("{data}"),
ResponseData::Done {} => println!("ok"),
ResponseData::Materialized { bytes, reflinked } => {
let how = if *reflinked { "reflinked" } else { "copied" };
@@ -259,6 +272,15 @@ fn print_human(data: Option<&ResponseData>) {
println!("store: {}", s.store_path);
println!("pins: {}", s.pins);
println!("peers: {} connected", s.connected_peers);
for r in &s.relays {
let how = match (&r.auth_denied, &r.last_error, r.connected) {
(Some(reason), _, _) => format!("denied: {reason}"),
(None, Some(err), false) => format!("down: {err}"),
(None, None, false) => "connecting".to_string(),
(None, _, true) => "connected".to_string(),
};
println!("relay: {} {how}", r.url);
}
}
ResponseData::HashStatus(h) => {
println!("hash: {}", h.hash);
+4 -2
View File
@@ -16,11 +16,13 @@ required-features = ["socket-api"]
varde-proto = { workspace = true }
iroh = { workspace = true }
iroh-blobs = { workspace = true }
# Request::Put / Read carry content inline, base64.
base64 = "0.22"
# mDNS/local-network address lookup left iroh core in 1.0.
iroh-mdns-address-lookup = "0.4"
iroh-mdns-address-lookup = "0.6"
# Interop with iroh-blobs streams and channels; both are already in the
# dependency tree via iroh-blobs.
n0-future = "0.1"
n0-future = "0.3"
irpc = "0.17"
# Feature-gated D-Bus client for NetworkManager metered status.
zbus = { version = "5", optional = true, default-features = false, features = ["tokio"] }
+71 -8
View File
@@ -335,6 +335,26 @@ impl Daemon {
}
}
/// The peer a request names: a NodeId, or a blob ticket from it (whose
/// addresses are remembered, so dialing it needs no discovery). An app
/// that knows peers by the tickets they hand out - redoal does - can
/// allow and push without unpacking them.
fn peer_of(&self, peer: &str) -> Result<iroh::EndpointId, OpError> {
if let Ok(id) = peer.parse::<iroh::EndpointId>() {
return Ok(id);
}
match crate::transfer::parse_ticket(peer) {
Ok((_, _, addr)) => {
let id = addr.id;
self.transfer.remember(addr);
Ok(id)
}
Err(_) => Err(OpError::InvalidArgument(format!(
"{peer:?} is neither a node id nor a ticket"
))),
}
}
/// Trusted peers seen on the LAN recently.
fn present_trusted_peers(&self) -> Vec<PeerInfo> {
let peers = self.lan_peers.lock().expect("lan peers lock");
@@ -371,6 +391,39 @@ impl Daemon {
bytes,
})
}
Request::Put { data } => {
use base64::Engine as _;
let bytes = base64::engine::general_purpose::STANDARD
.decode(data.as_bytes())
.map_err(|e| OpError::InvalidArgument(format!("put: data is not base64: {e}")))?;
if bytes.len() as u64 > varde_proto::MAX_INLINE_BYTES {
return Err(OpError::InvalidArgument(format!(
"put: {} bytes, more than {} inline; add it by path",
bytes.len(),
varde_proto::MAX_INLINE_BYTES
)));
}
let (hash, bytes) = self.store.add_bytes(bytes).await?;
self.meta.record_format(&hash.to_hex(), StoredFormat::Raw)?;
Ok(ResponseData::Added {
hash: hash.to_hex().to_string(),
bytes,
})
}
Request::Read { hash } => {
use base64::Engine as _;
let parsed = parse_hash(&hash)?;
if self.meta.format_of(&parsed.to_hex()) == StoredFormat::HashSeq {
return Err(OpError::InvalidArgument(format!(
"read: {} is a collection; materialize it",
parsed.to_hex()
)));
}
let bytes = self.store.read_bytes(parsed, varde_proto::MAX_INLINE_BYTES).await?;
Ok(ResponseData::Bytes {
data: base64::engine::general_purpose::STANDARD.encode(bytes),
})
}
Request::Pin { hash, policy } => {
let parsed = parse_hash(&hash)?;
self.meta.pin(&parsed.to_hex(), policy)?;
@@ -433,6 +486,7 @@ impl Daemon {
store_path: self.config.store_dir.display().to_string(),
pins: self.meta.pins().len() as u64,
connected_peers: connected,
relays: self.transfer.relay_status(),
}))
}
Request::Status { hash: Some(hash) } => {
@@ -533,24 +587,33 @@ impl Daemon {
let parsed: iroh::EndpointId = node_id
.parse()
.map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?;
if !self.meta.untrust_peer(&parsed.to_string())? {
let was_trusted = self.meta.untrust_peer(&parsed.to_string())?;
let was_allowed = self.meta.disallow_push(&parsed.to_string())?;
if !was_trusted && !was_allowed {
return Err(OpError::NotFound(format!("{parsed} was not trusted")));
}
Ok(ResponseData::Done {})
}
Request::PeerAllowPush { node_id, ttl_secs } => {
let parsed = self.peer_of(&node_id)?;
let ttl = ttl_secs.min(varde_proto::MAX_PUSH_TTL_SECS);
self.meta
.allow_push(&parsed.to_string(), crate::meta::unix_now() + ttl)?;
debug!(peer = %parsed, ttl, "pushes allowed");
Ok(ResponseData::Done {})
}
Request::PeerList {} => Ok(ResponseData::Peers {
peers: self.present_trusted_peers(),
}),
Request::Push { hash, node_id } => {
let parsed = parse_hash(&hash)?;
let peer: iroh::EndpointId = node_id
.parse()
.map_err(|e| OpError::InvalidArgument(format!("invalid node id: {e}")))?;
// Consent, sending side: we only push to peers we trust.
// (The receiver independently requires that it trusts us.)
if !self.meta.is_trusted(&peer.to_string()) {
let peer = self.peer_of(&node_id)?;
// Consent, sending side: we only push to peers we trust or
// hold a push allowance for. (The receiver independently
// requires the same of us.)
if !self.meta.may_push(&peer.to_string()) {
return Err(OpError::InvalidArgument(format!(
"{peer} is not a trusted peer; trust it before pushing"
"{peer} is neither trusted nor allowed to exchange pushes; trust it or allow pushes first"
)));
}
let (_, _, complete) = self.store.presence(parsed).await?;
+60
View File
@@ -40,6 +40,10 @@ struct MetaState {
/// Trusted peer NodeIds (z-base-32).
#[serde(default)]
trusted_peers: BTreeSet<String>,
/// Peers allowed to push to us and be pushed to, until a unix time
/// (`PeerAllowPush`): consent for that one thing, lapsing on its own.
#[serde(default)]
push_peers: BTreeMap<String, u64>,
/// Hashes (hex) the user shared via ticket export: standing consent
/// to serve them to anyone, like an `open_lan` pin.
#[serde(default)]
@@ -164,6 +168,31 @@ impl Meta {
.clone()
}
/// Allow pushes with `node_id` until `until` (unix seconds). Extends,
/// never shortens, an allowance already held; expired ones are
/// dropped on the way.
pub fn allow_push(&self, node_id: &str, until: u64) -> Result<()> {
let now = unix_now();
self.mutate(|s| {
s.push_peers.retain(|_, u| *u > now);
let entry = s.push_peers.entry(node_id.to_string()).or_insert(0);
*entry = (*entry).max(until);
})
}
/// Whether pushes with `node_id` are allowed now: a trusted peer, or
/// one holding an unexpired push allowance.
pub fn may_push(&self, node_id: &str) -> bool {
let state = self.state.lock().expect("meta lock poisoned");
state.trusted_peers.contains(node_id)
|| state.push_peers.get(node_id).is_some_and(|u| *u > unix_now())
}
/// End a push allowance. Returns whether there was one.
pub fn disallow_push(&self, node_id: &str) -> Result<bool> {
self.mutate(|s| s.push_peers.remove(node_id).is_some())
}
pub fn is_trusted(&self, node_id: &str) -> bool {
self.state
.lock()
@@ -173,6 +202,14 @@ impl Meta {
}
}
/// Seconds since the unix epoch.
pub fn unix_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -201,4 +238,27 @@ mod tests {
assert!(meta.unpin("h").unwrap());
assert!(!meta.is_pinned("h"));
}
#[test]
fn a_push_allowance_lapses_and_never_shortens() {
let dir = tempfile::tempdir().unwrap();
let meta = Meta::load(dir.path()).unwrap();
let now = unix_now();
assert!(!meta.may_push("p"));
meta.allow_push("p", now + 600).unwrap();
assert!(meta.may_push("p"));
assert!(!meta.is_trusted("p"), "a push allowance is not trust");
meta.allow_push("p", now + 10).unwrap();
assert!(meta.may_push("p"), "a shorter ask does not cut it short");
meta.allow_push("q", now.saturating_sub(1)).unwrap();
assert!(!meta.may_push("q"), "expired");
// Survives a reload.
let again = Meta::load(dir.path()).unwrap();
assert!(again.may_push("p"));
assert!(again.disallow_push("p").unwrap());
assert!(!again.may_push("p"));
// Full trust allows pushes too.
again.trust_peer("t").unwrap();
assert!(again.may_push("t"));
}
}
+18 -8
View File
@@ -111,6 +111,8 @@ pub struct Gate {
#[derive(Debug, Clone)]
struct ConnInfo {
trusted: bool,
/// Trusted, or holding a push allowance (`PeerAllowPush`).
may_push: bool,
endpoint_id: Option<String>,
}
@@ -142,6 +144,11 @@ pub fn provider_events(gate: Gate) -> EventSender {
.endpoint_id
.map(|id| gate.meta.is_trusted(&id.to_string()))
.unwrap_or(false);
let may_push = m
.inner
.endpoint_id
.map(|id| gate.meta.may_push(&id.to_string()))
.unwrap_or(false);
let ClientConnected {
connection_id,
endpoint_id,
@@ -151,6 +158,7 @@ pub fn provider_events(gate: Gate) -> EventSender {
connection_id,
ConnInfo {
trusted,
may_push,
endpoint_id: endpoint_id.map(|id| id.to_string()),
},
);
@@ -192,11 +200,12 @@ pub fn provider_events(gate: Gate) -> EventSender {
ProviderMessage::ObserveRequestReceived(m) => {
// Presence is information too: gate it like data.
// Bitfields are metadata, not payload, so trusted
// peers may observe even while serving is off or the
// connection is metered (push confirmation needs it).
// peers - and peers allowed to push, whose push is
// confirmed by observing it - may observe even while
// serving is off or the connection is metered.
let trusted = conns
.get(&m.inner.connection_id)
.map(|i| i.trusted)
.map(|i| i.trusted || i.may_push)
.unwrap_or(false);
let allowed = trusted
|| allow(&gate, &conns, m.inner.connection_id, [m.inner.request.hash]);
@@ -208,12 +217,13 @@ pub fn provider_events(gate: Gate) -> EventSender {
m.tx.send(verdict).await.ok();
}
ProviderMessage::PushRequestReceived(m) => {
// Inbound writes: trusted peers only, deferred while
// metered (RateLimited invites a retry later).
// Inbound writes: trusted peers and push allowances
// only, deferred while metered (RateLimited invites a
// retry later).
let info = conns.get(&m.inner.connection_id);
let trusted = info.map(|i| i.trusted).unwrap_or(false);
debug!(hash = %m.inner.request.hash.to_hex(), trusted, "push request received");
let verdict = if !trusted {
let may_push = info.map(|i| i.may_push).unwrap_or(false);
debug!(hash = %m.inner.request.hash.to_hex(), may_push, "push request received");
let verdict = if !may_push {
Err(AbortReason::Permission)
} else if gate.metered.is_metered() {
Err(AbortReason::RateLimited)
+24
View File
@@ -228,6 +228,30 @@ impl BlobStore {
/// Export `hash` to `dest`. Returns bytes written and whether at least
/// one file was reflinked.
/// Import `data` as one raw blob (`Request::Put`).
pub async fn add_bytes(&self, data: Vec<u8>) -> Result<(Hash, u64), OpError> {
let size = data.len() as u64;
let tag = self.store.add_bytes(data).temp_tag().await.map_err(internal)?;
info!(hash = %tag.hash().to_hex(), size, "imported bytes");
Ok((tag.hash(), size))
}
/// The whole content of a complete raw blob (`Request::Read`), when it
/// is no larger than `limit`.
pub async fn read_bytes(&self, hash: Hash, limit: u64) -> Result<Vec<u8>, OpError> {
match self.store.blobs().status(hash).await.map_err(internal)? {
BlobStatus::Complete { size } if size > limit => Err(OpError::InvalidArgument(format!(
"{} is {size} bytes, more than {limit} can be read inline; materialize it",
hash.to_hex()
))),
BlobStatus::Complete { .. } => {
let bytes = self.store.blobs().get_bytes(hash).await.map_err(internal)?;
Ok(bytes.to_vec())
}
_ => Err(OpError::NotFound(format!("{} is not complete in the store", hash.to_hex()))),
}
}
pub async fn materialize(
&self,
hash: Hash,
+26 -1
View File
@@ -12,7 +12,7 @@ use std::time::Duration;
use anyhow::{Context, Result};
use iroh::address_lookup::memory::MemoryLookup;
use iroh::protocol::Router;
use iroh::{Endpoint, EndpointAddr, EndpointId, RelayMode, SecretKey};
use iroh::{Endpoint, EndpointAddr, EndpointId, RelayMode, SecretKey, Watcher as _};
use iroh_blobs::api::downloader::{DownloadOptions, Downloader, Shuffled, SplitStrategy};
use iroh_blobs::protocol::GetRequest;
use iroh_blobs::{BlobFormat, BlobsProtocol, Hash, HashAndFormat};
@@ -156,6 +156,22 @@ impl Transfer {
self.endpoint.id().to_string()
}
/// How each home relay is doing right now: connected, or why not.
/// Empty with relays disabled.
pub fn relay_status(&self) -> Vec<varde_proto::RelayInfo> {
self.endpoint
.home_relay_status()
.get()
.iter()
.map(|r| varde_proto::RelayInfo {
url: r.url().to_string(),
connected: r.is_connected(),
last_error: r.last_error().map(|e| format!("{e:#}")),
auth_denied: r.auth_denied_reason().map(str::to_string),
})
.collect()
}
/// The endpoint's identity key (also signs our announcements).
pub fn secret_key(&self) -> SecretKey {
self.endpoint.secret_key().clone()
@@ -298,6 +314,15 @@ impl Transfer {
Ok(ticket.to_string())
}
/// Teach the endpoint where a peer can be reached (the addresses a
/// ticket carries), so a push dials it directly instead of waiting on
/// discovery.
pub fn remember(&self, addr: EndpointAddr) {
if !addr.addrs.is_empty() {
self.known_addrs.add_endpoint_info(addr);
}
}
/// Queue a background fetch of `content` from `providers`. Emits
/// progress events and [`Event::PinComplete`] when fully verified.
pub fn spawn_fetch(&self, content: HashAndFormat, providers: Vec<EndpointAddr>) {
+58
View File
@@ -422,3 +422,61 @@ fn push_rejected_without_receiver_trust() {
// And the receiver holds none of it.
assert!(!is_complete(&b.socket, &hash));
}
/// A push allowance is enough on both sides: two daemons that only
/// allowed pushes with each other (strangers sharing a place, in redoal)
/// exchange content without trusting each other - and the allowance
/// grants nothing else, so neither lists the other as a trusted peer.
#[test]
fn push_allowance_is_enough_and_is_not_trust() {
let _serial = MDNS_SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let dir_a = tempfile::tempdir().unwrap();
let dir_b = tempfile::tempdir().unwrap();
let a = support::spawn_daemon_with_env(dir_a.path(), &[("VARDE_DISCOVERY", "true")]);
let b = support::spawn_daemon_with_env(dir_b.path(), &[("VARDE_DISCOVERY", "true")]);
let mut client_a = support::Client::connect(&a.socket);
let mut client_b = support::Client::connect(&b.socket);
let id_a = node_id(&mut client_a);
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("voice.bin");
let payload = vec![7u8; 120_000];
std::fs::write(&src, &payload).unwrap();
let hash = add_file(&mut client_a, &src);
pin(&mut client_a, &hash, false);
// A knows B by a ticket B handed out (as redoal peers do), not by
// its bare node id.
let b_src = dir.path().join("b-own.bin");
std::fs::write(&b_src, vec![1u8; 1_000]).unwrap();
let b_hash = add_file(&mut client_b, &b_src);
let b_ticket = match client_b.request(&Request::TicketExport { hash: b_hash }).data {
Some(ResponseData::Ticket { ticket }) => ticket,
other => panic!("unexpected ticket reply: {other:?}"),
};
assert!(client_a.request(&Request::PeerAllowPush { node_id: b_ticket.clone(), ttl_secs: 600 }).ok);
assert!(client_b.request(&Request::PeerAllowPush { node_id: id_a, ttl_secs: 600 }).ok);
match client_a.request(&Request::PeerList {}).data {
Some(ResponseData::Peers { peers }) => assert!(peers.is_empty(), "an allowance is not trust: {peers:?}"),
other => panic!("unexpected peer list: {other:?}"),
}
// The peer's address arrives by mdns; retry until it does.
let deadline = Instant::now() + Duration::from_secs(30);
let reply = loop {
let reply = client_a.request(&Request::Push { hash: hash.clone(), node_id: b_ticket.clone() });
if reply.ok || Instant::now() > deadline {
break reply;
}
std::thread::sleep(Duration::from_millis(500));
};
if !reply.ok {
let message = reply.error.map(|e| e.message).unwrap_or_default();
// Only an unreachable peer is the environment's fault; any other
// refusal is a real failure.
assert!(message.contains("connect"), "push failed: {message}");
eprintln!("SKIP: push never reached the peer (multicast unavailable?): {message}");
return;
}
assert!(wait_complete(&b.socket, &hash, Duration::from_secs(60)), "pushed content not complete on receiver");
}
+42
View File
@@ -276,3 +276,45 @@ fn reflink_when_supported() {
drop(daemon);
let _ = std::fs::remove_dir_all(&base);
}
/// Put and Read carry the bytes on the socket: a client that shares no
/// filesystem with the daemon (a desktop app and the system daemon, which
/// runs as its own user behind ProtectHome) stores and reads back without
/// a path either side must reach. A Put blob reads back by Materialize too.
#[test]
fn bytes_round_trip_inline() {
use base64::Engine as _;
let b64 = base64::engine::general_purpose::STANDARD;
let daemon = support::spawn_daemon();
let mut client = support::Client::connect(&daemon.socket);
let payload: Vec<u8> = (0..50_000u32).flat_map(|i| i.to_le_bytes()).collect();
let reply = client.request(&Request::Put { data: b64.encode(&payload) });
assert!(reply.ok, "put failed: {:?}", reply.error);
let hash = match reply.data {
Some(ResponseData::Added { hash, bytes }) => {
assert_eq!(bytes, payload.len() as u64);
hash
}
other => panic!("unexpected put reply: {other:?}"),
};
let reply = client.request(&Request::Read { hash: hash.clone() });
assert!(reply.ok, "read failed: {:?}", reply.error);
match reply.data {
Some(ResponseData::Bytes { data }) => assert_eq!(b64.decode(data).unwrap(), payload),
other => panic!("unexpected read reply: {other:?}"),
}
let dir = tempfile::tempdir().unwrap();
let dest = dir.path().join("out.bin");
materialize(&mut client, &hash, &dest, MaterializeMode::Copy).unwrap();
assert_eq!(std::fs::read(&dest).unwrap(), payload);
// Not base64: refused, not stored.
let reply = client.request(&Request::Put { data: "not base64!".into() });
assert_eq!(reply.error.expect("error body").code, ErrorCode::InvalidArgument);
// Unknown hash: not found.
let reply = client.request(&Request::Read { hash: "0".repeat(64) });
assert!(!reply.ok);
}
+77
View File
@@ -20,6 +20,13 @@ use serde::{Deserialize, Serialize};
/// Bumped on incompatible changes; reported in [`GlobalStatus`].
pub const PROTOCOL_VERSION: u32 = 2;
/// The longest a [`Request::PeerAllowPush`] allowance may last: a day.
pub const MAX_PUSH_TTL_SECS: u64 = 24 * 60 * 60;
/// The most bytes a [`Request::Put`] or a [`Request::Read`] reply carries
/// inline. Larger content goes through `Add`/`Materialize` and a path.
pub const MAX_INLINE_BYTES: u64 = 16 * 1024 * 1024;
/// A request from a client to the daemon. One JSON object per line.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
@@ -61,6 +68,24 @@ pub enum Request {
#[serde(default)]
mode: MaterializeMode,
},
/// Import bytes carried in the request, base64 (standard alphabet),
/// at most [`MAX_INLINE_BYTES`]. Replies [`ResponseData::Added`].
///
/// For a client that shares no filesystem with the daemon: the system
/// daemon runs as its own user with `ProtectHome`, so it can neither
/// read a file a desktop app wrote for `Add` nor write one back for
/// `Materialize`. Added in varde 0.1.4.
Put {
/// The content, base64.
data: String,
},
/// Read a complete raw blob back in the reply, base64, at most
/// [`MAX_INLINE_BYTES`]. Replies [`ResponseData::Bytes`]. The
/// counterpart of [`Request::Put`]; added in varde 0.1.4.
Read {
/// Hash (hex) of the blob.
hash: String,
},
/// Query daemon status, globally or for one hash.
Status {
/// If set, report on this hash; otherwise report global status.
@@ -94,6 +119,20 @@ pub enum Request {
/// The peer's iroh NodeId (z-base-32).
node_id: String,
},
/// Allow pushes between this daemon and a peer for `ttl_secs` (at most
/// [`MAX_PUSH_TTL_SECS`]): we may push to it, and accept what it pushes
/// to us. Nothing else - unlike [`Request::PeerTrust`] it neither
/// syncs the peer's pins nor serves it our content - so an app can
/// grant it to strangers it shares something with for a while (two
/// phones at the same place) and let it lapse. Asking again extends
/// it; [`Request::PeerUntrust`] ends it early. Added in varde 0.1.5.
PeerAllowPush {
/// The peer's iroh NodeId (z-base-32), or a blob ticket it issued
/// (varde 0.1.6+), whose addresses are then used to reach it.
node_id: String,
/// How long the allowance lasts, in seconds.
ttl_secs: u64,
},
/// List trusted peers and their current connectivity.
PeerList {},
/// Push fully-present content to a trusted peer, unprompted.
@@ -104,6 +143,9 @@ pub enum Request {
/// policy), so the content survives gc and shows up in `List`.
Push {
/// Hash (hex) of the fully-present blob or HashSeq root to push.
///
/// The receiver below may be a NodeId or, from varde 0.1.6, a blob
/// ticket it issued.
hash: String,
/// The receiving peer's iroh NodeId (z-base-32).
node_id: String,
@@ -228,6 +270,11 @@ pub enum ResponseData {
/// Whether at least one file was reflinked rather than copied.
reflinked: bool,
},
/// A blob's content, base64. Reply to [`Request::Read`].
Bytes {
/// The content, base64 (standard alphabet).
data: String,
},
/// Global daemon status. Reply to [`Request::Status`] without a hash.
Status(GlobalStatus),
/// Per-hash status. Reply to [`Request::Status`] with a hash.
@@ -277,6 +324,27 @@ pub struct GlobalStatus {
pub pins: u64,
/// Number of connected trusted peers.
pub connected_peers: u64,
/// The home relays this endpoint uses and how each is doing. Empty
/// when relays are off (LAN-only) or on a daemon before 0.1.10.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub relays: Vec<RelayInfo>,
}
/// One home relay's connection state (iroh's `home_relay_status`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RelayInfo {
/// The relay URL.
pub url: String,
/// Whether the endpoint holds a connection to it right now.
pub connected: bool,
/// The most recent connection error, when not connected and an
/// attempt has failed; for a person to read, not to match on.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_error: Option<String>,
/// The relay's reason when it denied this endpoint's authentication:
/// the one failure retrying will not cure.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_denied: Option<String>,
}
/// Status of one hash.
@@ -431,4 +499,13 @@ mod tests {
_ => panic!("wrong variant"),
}
}
#[test]
fn a_status_from_before_relays_still_parses() {
let old = r#"{"version":"0.1.9","protocol":2,"store_path":"/s","pins":1,"connected_peers":0}"#;
let status: GlobalStatus = serde_json::from_str(old).unwrap();
assert!(status.relays.is_empty());
let back = serde_json::to_string(&status).unwrap();
assert!(!back.contains("relays"), "empty stays absent for old clients: {back}");
}
}