feat(w7 p6): opt-in n0 DNS discovery for Discoverable presence
Wire the Discoverable presence posture to n0 DNS publish/lookup, the last
core piece of W7 (friends-first contacts). When a friend moves networks and
their saved address goes stale, they flip Discoverable to publish their
current address; everyone else resolves it by node id. Asymmetric: only the
mover publishes.
- src/discovery.rs (pure seam, +3 tests): lookup_plan(network_mode, want_publish)
-> LookupPlan { resolver, publisher }. Relay-capable modes always resolve and
publish only when Discoverable; DirectOnly (the explicit no-server posture)
gets neither, overriding the toggle. DISCOVERY_TIMEBOX = 30 min.
- apply_discovery (core edge): clears + reinstalls the bound endpoint's
address-lookup services at runtime (no endpoint rebuild). memory-lookup always;
n0 PkarrResolver + DnsAddressLookup when resolver; PkarrPublisher when publisher.
Toggling publish off drops the publisher (republish task ends; TTL-30s record
expires). build_net_stack now binds uniformly with Minimal + per-mode relay and
installs discovery via apply_discovery (drops the per-mode presets::N0 build).
- Toggle + time-box: SetPresenceMode re-applies discovery and arms/cancels a
discovery_deadline; a select! branch fires at the deadline -> revert to Normal,
stop publishing, and emit UiEvent::PresenceModeReverted so the GUI mirrors and
persists it. Re-selecting Discoverable restarts the clock.
Decisions (user, 2026-06-16): 30-min auto-revert (not sticky); resolver always
on in relay-capable modes so a stationary friend in Normal can look up a mover.
266 lib tests green, clippy clean (--all-targets). Runtime smoke-tested: the new
Minimal+apply_discovery path binds and runs with no error/panic for both Normal
and Discoverable startup postures. Cross-network publish->lookup and the live
30-min revert still want a 2-machine field test (P7).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+25
-3
@@ -193,9 +193,31 @@ Rejoin is best-effort (works only while the room is still live + reachable throu
|
|||||||
stored ticket — reliability is P6/the member-ticket floor, not this list).
|
stored ticket — reliability is P6/the member-ticket floor, not this list).
|
||||||
**Screenshot-verified** (seeded config → 3 recents render with correct ages + fallback).
|
**Screenshot-verified** (seeded config → 3 recents render with correct ages + fallback).
|
||||||
|
|
||||||
### P6 — Opt-in discovery — Small
|
### P6 — Opt-in discovery — ✅ DONE 2026-06-16
|
||||||
Wire the *discoverable* state to n0 DNS publish (default off, time-boxed). Lookup
|
Wires the *Discoverable* presence state to n0 DNS publish (default off, 30-min
|
||||||
path for finding a discoverable friend whose saved address went stale.
|
time-boxed) plus an always-on lookup path. **Decisions (user, 2026-06-16):** 30-min
|
||||||
|
auto-revert (not sticky); resolver always on in relay-capable modes (the stationary
|
||||||
|
"looker" is usually in Normal, so lookups must work there).
|
||||||
|
- **Pure seam (`src/discovery.rs`, +3 tests):** `lookup_plan(network_mode, want_publish)
|
||||||
|
→ LookupPlan { resolver, publisher }` — relay modes always resolve + publish only when
|
||||||
|
Discoverable; **`DirectOnly` gets neither** (the explicit no-server posture overrides
|
||||||
|
the toggle). `DISCOVERY_TIMEBOX = 30 min`.
|
||||||
|
- **iroh edge (`apply_discovery` in `core/mod.rs`):** at runtime, on the bound endpoint,
|
||||||
|
`clear()` + reinstall the address-lookup services — memory-lookup always, n0 `PkarrResolver`
|
||||||
|
+ `DnsAddressLookup` when `resolver`, `PkarrPublisher` when `publisher`. **No endpoint
|
||||||
|
rebuild** — toggling publish off drops the publisher (its republish task ends; the
|
||||||
|
TTL-30s record expires). `build_net_stack` now binds uniformly with `Minimal` + per-mode
|
||||||
|
relay and calls `apply_discovery` (the old per-mode `presets::N0` build is gone), seeded
|
||||||
|
by the startup presence mode.
|
||||||
|
- **Toggle + time-box (core loop):** `SetPresenceMode` re-applies discovery and arms/cancels
|
||||||
|
a `discovery_deadline`; a `tokio::select!` branch fires at the deadline → revert to Normal +
|
||||||
|
stop publishing + `UiEvent::PresenceModeReverted` so the GUI mirrors/persists it (status:
|
||||||
|
"Discoverable timed out — back to Normal"). Re-selecting Discoverable restarts the clock.
|
||||||
|
- **Verified:** 266 lib tests green, clippy clean (`--all-targets`); runtime smoke-tested —
|
||||||
|
app binds + runs the new `Minimal`+`apply_discovery` path with no error/panic, both Normal
|
||||||
|
and Discoverable startup postures (the `PkarrPublisher` build path). ⚠️ **The actual cross-
|
||||||
|
network publish→lookup (a friend whose saved addr went stale resolving via n0 DNS) and the
|
||||||
|
live 30-min auto-revert need a 2-machine field test (P7).**
|
||||||
|
|
||||||
### P7 — Security review + 2-machine field test — Small–Medium
|
### P7 — Security review + 2-machine field test — Small–Medium
|
||||||
Surface: the friends-only listener (confirm non-friends are truly dropped pre-any
|
Surface: the friends-only listener (confirm non-friends are truly dropped pre-any
|
||||||
|
|||||||
@@ -735,6 +735,15 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
UiEvent::FriendPresence { id, presence } => {
|
UiEvent::FriendPresence { id, presence } => {
|
||||||
state.friend_presence.insert(id, presence);
|
state.friend_presence.insert(id, presence);
|
||||||
}
|
}
|
||||||
|
UiEvent::PresenceModeReverted { mode } => {
|
||||||
|
// The Discoverable time-box elapsed; core dropped us back to
|
||||||
|
// `mode` (Normal) and stopped publishing. Mirror + persist so the
|
||||||
|
// presence picker reflects it, and tell the user why it changed.
|
||||||
|
state.config.presence_mode = mode;
|
||||||
|
state.config.save();
|
||||||
|
state.status_message =
|
||||||
|
"Discoverable timed out — back to Normal".to_string();
|
||||||
|
}
|
||||||
UiEvent::Error(err) => {
|
UiEvent::Error(err) => {
|
||||||
state.status_message = format!("Error: {}", err);
|
state.status_message = format!("Error: {}", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,5 +116,11 @@ pub enum UiEvent {
|
|||||||
/// joinable gathering (with a one-click ticket). Emitted by the outbound ping
|
/// joinable gathering (with a one-click ticket). Emitted by the outbound ping
|
||||||
/// scheduler; absence of a recent event = treat as offline.
|
/// scheduler; absence of a recent event = treat as offline.
|
||||||
FriendPresence { id: EndpointId, presence: FriendPresence },
|
FriendPresence { id: EndpointId, presence: FriendPresence },
|
||||||
|
/// The Discoverable time-box elapsed (W7 P6): the core auto-reverted our presence
|
||||||
|
/// posture to the carried `mode` (always `Normal`) and stopped publishing. The
|
||||||
|
/// GUI must mirror + persist this so its presence picker stops showing
|
||||||
|
/// Discoverable. Distinct from a user-driven change so the GUI knows to update
|
||||||
|
/// without having issued the command itself.
|
||||||
|
PresenceModeReverted { mode: PresenceMode },
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-32
@@ -405,46 +405,73 @@ impl NetStack {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Install the n0 DNS address-lookup services for a discovery `plan` (W7 P6), at
|
||||||
|
/// runtime, on an already-bound endpoint. The in-memory lookup (server-free, fed by
|
||||||
|
/// tickets + gossip) is always re-added; the n0 DNS *resolver* (`PkarrResolver` +
|
||||||
|
/// `DnsAddressLookup`, mirroring the `N0` preset) is added when `plan.resolver`; the
|
||||||
|
/// n0 DNS *publisher* (`PkarrPublisher`) when `plan.publisher`.
|
||||||
|
///
|
||||||
|
/// Idempotent and reversible: it clears the whole service set and reinstalls exactly
|
||||||
|
/// what the plan wants, so flipping `publisher` off simply drops the publisher (its
|
||||||
|
/// republish task ends when the last clone is dropped, and the already-published
|
||||||
|
/// record TTL-expires within ~30s) without an endpoint rebuild and without disturbing
|
||||||
|
/// resolution. The brief clear→re-add window is a few synchronous calls; presence
|
||||||
|
/// toggles are rare, so a concurrent dial racing it is not a practical concern.
|
||||||
|
fn apply_discovery(
|
||||||
|
endpoint: &Endpoint,
|
||||||
|
memory_lookup: &iroh::address_lookup::memory::MemoryLookup,
|
||||||
|
plan: crate::discovery::LookupPlan,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
use iroh::address_lookup::{
|
||||||
|
AddressLookupBuilder, dns::DnsAddressLookup,
|
||||||
|
pkarr::{PkarrPublisher, PkarrResolver},
|
||||||
|
};
|
||||||
|
let services = endpoint.address_lookup()?;
|
||||||
|
services.clear();
|
||||||
|
// Always keep the local, server-free lookup (this is what ticket/gossip dialing
|
||||||
|
// depends on — it must survive every posture, including DirectOnly).
|
||||||
|
services.add(memory_lookup.clone());
|
||||||
|
if plan.resolver {
|
||||||
|
services.add(PkarrResolver::n0_dns().into_address_lookup(endpoint)?);
|
||||||
|
services.add(DnsAddressLookup::n0_dns().into_address_lookup(endpoint)?);
|
||||||
|
}
|
||||||
|
if plan.publisher {
|
||||||
|
services.add(PkarrPublisher::n0_dns().into_address_lookup(endpoint)?);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the persistent network stack for the given identity + relay/discovery
|
/// Build the persistent network stack for the given identity + relay/discovery
|
||||||
/// posture. Binds the endpoint (the per-`NetworkMode` build hoisted out of the old
|
/// posture. Binds the endpoint (the per-`NetworkMode` build hoisted out of the old
|
||||||
/// per-join path), spawns one gossip instance + one router accepting gossip and
|
/// per-join path), spawns one gossip instance + one router accepting gossip and
|
||||||
/// audio, and kicks off `online()` in the background so app launch isn't blocked on
|
/// audio, and kicks off `online()` in the background so app launch isn't blocked on
|
||||||
/// the relay handshake.
|
/// the relay handshake. `publish` is whether the presence posture is `Discoverable`
|
||||||
|
/// at build time (W7 P6) — it seeds the initial n0 DNS publish state.
|
||||||
async fn build_net_stack(
|
async fn build_net_stack(
|
||||||
secret_key: SecretKey,
|
secret_key: SecretKey,
|
||||||
network_mode: NetworkMode,
|
network_mode: NetworkMode,
|
||||||
memory_lookup: iroh::address_lookup::memory::MemoryLookup,
|
memory_lookup: iroh::address_lookup::memory::MemoryLookup,
|
||||||
friends_handler: crate::presence_net::Handler,
|
friends_handler: crate::presence_net::Handler,
|
||||||
|
publish: bool,
|
||||||
) -> Result<NetStack, anyhow::Error> {
|
) -> Result<NetStack, anyhow::Error> {
|
||||||
// Build the endpoint per the configured relay/discovery posture. All postures
|
// Bind with only the relay posture baked in (`Minimal` = crypto provider only,
|
||||||
// keep the in-memory address lookup (fed by tickets and gossip); they differ in
|
// relay on/off per mode); n0 DNS discovery is installed uniformly below via
|
||||||
// whether n0's relay and DNS presence beacon are used. `Minimal` sets only the
|
// `apply_discovery` so the Discoverable publish toggle is independent of the
|
||||||
// mandatory crypto provider and deliberately omits the n0 DNS publish/resolve.
|
// network mode and can be flipped later at runtime. The in-memory lookup is added
|
||||||
let endpoint = match network_mode {
|
// here so dialing works even before the first `apply_discovery` (which re-adds it).
|
||||||
NetworkMode::N0Full => {
|
let relay_mode = match network_mode {
|
||||||
Endpoint::builder(presets::N0)
|
NetworkMode::DirectOnly => RelayMode::Disabled,
|
||||||
|
NetworkMode::RelayNoDiscovery | NetworkMode::N0Full => RelayMode::Default,
|
||||||
|
};
|
||||||
|
let endpoint = Endpoint::builder(presets::Minimal)
|
||||||
.secret_key(secret_key.clone())
|
.secret_key(secret_key.clone())
|
||||||
|
.relay_mode(relay_mode)
|
||||||
.address_lookup(memory_lookup.clone())
|
.address_lookup(memory_lookup.clone())
|
||||||
.bind()
|
.bind()
|
||||||
.await
|
.await?;
|
||||||
}
|
|
||||||
NetworkMode::RelayNoDiscovery => {
|
// Install the n0 DNS services for the current (mode, publish) posture.
|
||||||
Endpoint::builder(presets::Minimal)
|
apply_discovery(&endpoint, &memory_lookup, crate::discovery::lookup_plan(network_mode, publish))?;
|
||||||
.secret_key(secret_key.clone())
|
|
||||||
.relay_mode(RelayMode::Default)
|
|
||||||
.address_lookup(memory_lookup.clone())
|
|
||||||
.bind()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
NetworkMode::DirectOnly => {
|
|
||||||
Endpoint::builder(presets::Minimal)
|
|
||||||
.secret_key(secret_key.clone())
|
|
||||||
.relay_mode(RelayMode::Disabled)
|
|
||||||
.address_lookup(memory_lookup.clone())
|
|
||||||
.bind()
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
|
|
||||||
// Bring the endpoint online in the background so launch isn't blocked on the
|
// Bring the endpoint online in the background so launch isn't blocked on the
|
||||||
// relay handshake; joins/sends just work once it's up (online() is idempotent).
|
// relay handshake; joins/sends just work once it's up (online() is idempotent).
|
||||||
@@ -702,7 +729,8 @@ async fn run_core_loop(
|
|||||||
// it (subscribe a topic + bind the audio router on join, clear on leave); it's
|
// it (subscribe a topic + bind the audio router on join, clear on leave); it's
|
||||||
// rebuilt only when the network mode or identity changes. Moving `memory_lookup`
|
// rebuilt only when the network mode or identity changes. Moving `memory_lookup`
|
||||||
// in — all later access is via `net.memory_lookup`.
|
// in — all later access is via `net.memory_lookup`.
|
||||||
let mut net = match build_net_stack(secret_key.clone(), network_mode, memory_lookup, friends_handler.clone()).await {
|
let initial_publish = presence_mode.lock().unwrap().publishes_to_discovery();
|
||||||
|
let mut net = match build_net_stack(secret_key.clone(), network_mode, memory_lookup, friends_handler.clone(), initial_publish).await {
|
||||||
Ok(stack) => stack,
|
Ok(stack) => stack,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Only a local socket bind can fail here (the relay handshake is
|
// Only a local socket bind can fail here (the relay handshake is
|
||||||
@@ -719,6 +747,11 @@ async fn run_core_loop(
|
|||||||
// "applies on next join" semantics while keeping the endpoint up while idle.
|
// "applies on next join" semantics while keeping the endpoint up while idle.
|
||||||
let mut net_rebuild_pending = false;
|
let mut net_rebuild_pending = false;
|
||||||
|
|
||||||
|
// When Discoverable is on, the instant it auto-reverts to Normal (W7 P6 time-box).
|
||||||
|
// `None` = not Discoverable, no pending revert. Set on SetPresenceMode(Discoverable),
|
||||||
|
// cleared on any other posture, and consumed by the select! branch below.
|
||||||
|
let mut discovery_deadline: Option<tokio::time::Instant> = None;
|
||||||
|
|
||||||
// Tell the GUI the loaded friends list (it renders from this, no longer owning
|
// Tell the GUI the loaded friends list (it renders from this, no longer owning
|
||||||
// it). Snapshot under the lock, then release it before the async send.
|
// it). Snapshot under the lock, then release it before the async send.
|
||||||
let initial_snapshot = friends.lock().unwrap().list().to_vec();
|
let initial_snapshot = friends.lock().unwrap().list().to_vec();
|
||||||
@@ -758,6 +791,27 @@ async fn run_core_loop(
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// W7 P6 time-box: Discoverable auto-reverts to Normal after DISCOVERY_TIMEBOX
|
||||||
|
// so a publish beacon never stands indefinitely. The branch is disabled
|
||||||
|
// (`if` guard) unless a deadline is armed; `unwrap_or_else` is unreachable
|
||||||
|
// belt-and-braces. On fire: stop publishing, drop to Normal, tell the GUI.
|
||||||
|
_ = tokio::time::sleep_until(
|
||||||
|
discovery_deadline.unwrap_or_else(tokio::time::Instant::now),
|
||||||
|
), if discovery_deadline.is_some() => {
|
||||||
|
discovery_deadline = None;
|
||||||
|
*presence_mode.lock().unwrap() = crate::presence::PresenceMode::Normal;
|
||||||
|
let plan = crate::discovery::lookup_plan(network_mode, false);
|
||||||
|
if let Err(e) = apply_discovery(&net.endpoint, &net.memory_lookup, plan) {
|
||||||
|
crate::log_msg(&format!("discovery: time-box revert failed: {e:#}"));
|
||||||
|
}
|
||||||
|
crate::log_msg("discovery: Discoverable time-box elapsed → reverting to Normal");
|
||||||
|
let _ = ui_tx
|
||||||
|
.send(UiEvent::PresenceModeReverted {
|
||||||
|
mode: crate::presence::PresenceMode::Normal,
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match cmd {
|
match cmd {
|
||||||
CoreCommand::Join { name, ticket, room_name, input_device, output_device, echo_cancellation, avatar } => {
|
CoreCommand::Join { name, ticket, room_name, input_device, output_device, echo_cancellation, avatar } => {
|
||||||
@@ -782,7 +836,8 @@ async fn run_core_loop(
|
|||||||
if net_rebuild_pending {
|
if net_rebuild_pending {
|
||||||
let lookup = net.memory_lookup.clone();
|
let lookup = net.memory_lookup.clone();
|
||||||
net.shutdown().await;
|
net.shutdown().await;
|
||||||
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone()).await?;
|
let publish = presence_mode.lock().unwrap().publishes_to_discovery();
|
||||||
|
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone(), publish).await?;
|
||||||
net_rebuild_pending = false;
|
net_rebuild_pending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1410,7 +1465,8 @@ async fn run_core_loop(
|
|||||||
if net_rebuild_pending {
|
if net_rebuild_pending {
|
||||||
let lookup = net.memory_lookup.clone();
|
let lookup = net.memory_lookup.clone();
|
||||||
net.shutdown().await;
|
net.shutdown().await;
|
||||||
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone()).await?;
|
let publish = presence_mode.lock().unwrap().publishes_to_discovery();
|
||||||
|
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone(), publish).await?;
|
||||||
net_rebuild_pending = false;
|
net_rebuild_pending = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1523,7 +1579,8 @@ async fn run_core_loop(
|
|||||||
if active_session.is_none() {
|
if active_session.is_none() {
|
||||||
let lookup = net.memory_lookup.clone();
|
let lookup = net.memory_lookup.clone();
|
||||||
net.shutdown().await;
|
net.shutdown().await;
|
||||||
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone()).await?;
|
let publish = presence_mode.lock().unwrap().publishes_to_discovery();
|
||||||
|
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone(), publish).await?;
|
||||||
} else {
|
} else {
|
||||||
net_rebuild_pending = true;
|
net_rebuild_pending = true;
|
||||||
}
|
}
|
||||||
@@ -1555,7 +1612,8 @@ async fn run_core_loop(
|
|||||||
if active_session.is_none() {
|
if active_session.is_none() {
|
||||||
let lookup = net.memory_lookup.clone();
|
let lookup = net.memory_lookup.clone();
|
||||||
net.shutdown().await;
|
net.shutdown().await;
|
||||||
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone()).await?;
|
let publish = presence_mode.lock().unwrap().publishes_to_discovery();
|
||||||
|
net = build_net_stack(secret_key.clone(), network_mode, lookup, friends_handler.clone(), publish).await?;
|
||||||
} else {
|
} else {
|
||||||
net_rebuild_pending = true;
|
net_rebuild_pending = true;
|
||||||
}
|
}
|
||||||
@@ -1592,6 +1650,21 @@ async fn run_core_loop(
|
|||||||
|
|
||||||
CoreCommand::SetPresenceMode(mode) => {
|
CoreCommand::SetPresenceMode(mode) => {
|
||||||
*presence_mode.lock().unwrap() = mode;
|
*presence_mode.lock().unwrap() = mode;
|
||||||
|
// W7 P6: re-apply n0 DNS discovery for the new posture (publish on iff
|
||||||
|
// Discoverable). Runtime — no endpoint rebuild; clears + reinstalls the
|
||||||
|
// address-lookup services. The resolver stays on regardless so we can
|
||||||
|
// still look up moved friends.
|
||||||
|
let plan = crate::discovery::lookup_plan(network_mode, mode.publishes_to_discovery());
|
||||||
|
if let Err(e) = apply_discovery(&net.endpoint, &net.memory_lookup, plan) {
|
||||||
|
crate::log_msg(&format!("discovery: apply failed: {e:#}"));
|
||||||
|
}
|
||||||
|
// Arm (Discoverable) or cancel (any other posture) the auto-revert
|
||||||
|
// time-box. Re-selecting Discoverable restarts the clock.
|
||||||
|
discovery_deadline = if mode == crate::presence::PresenceMode::Discoverable {
|
||||||
|
Some(tokio::time::Instant::now() + crate::discovery::DISCOVERY_TIMEBOX)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
CoreCommand::SetRecordingMode(mode) => {
|
CoreCommand::SetRecordingMode(mode) => {
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
//! Opt-in discovery posture (W7 P6) — the pure policy that turns a (network mode,
|
||||||
|
//! "want to be Discoverable?") pair into *which* n0 DNS address-lookup services to
|
||||||
|
//! run, plus the Discoverable time-box. The actual iroh wiring (clearing and
|
||||||
|
//! reinstalling the endpoint's address-lookup services) lives in `core`; this module
|
||||||
|
//! is the small, testable decision surface so the policy can be unit-tested without
|
||||||
|
//! standing up an endpoint.
|
||||||
|
//!
|
||||||
|
//! The model (from `docs/contacts-plan.md` P6, decided 2026-06-16):
|
||||||
|
//! - **Resolving is always allowed on relay-capable modes** — a stationary friend
|
||||||
|
//! (typically in `Normal`) must be able to look up a friend who moved networks. A
|
||||||
|
//! resolve is a DNS query to n0 that publishes nothing; it only fires when a saved
|
||||||
|
//! address is stale and the dial falls through to discovery.
|
||||||
|
//! - **Publishing is gated on `Discoverable`** and asymmetric: only the mover
|
||||||
|
//! publishes their address to n0 DNS; everyone else just looks it up.
|
||||||
|
//! - **`DirectOnly` is the explicit no-server posture** — neither resolve nor publish
|
||||||
|
//! ever touches n0 there, regardless of the Discoverable toggle.
|
||||||
|
|
||||||
|
use crate::config::NetworkMode;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// How long `Discoverable` stays on before auto-reverting to `Normal`. Discovery is
|
||||||
|
/// a publish-to-n0 beacon, so it auto-expires rather than standing indefinitely; the
|
||||||
|
/// user can re-toggle to restart the clock. (Open decision #3 settled: 30-minute
|
||||||
|
/// time-box, not sticky-until-off.)
|
||||||
|
pub const DISCOVERY_TIMEBOX: Duration = Duration::from_secs(30 * 60);
|
||||||
|
|
||||||
|
/// Which n0 DNS address-lookup services to install for a posture. The in-memory
|
||||||
|
/// lookup (fed by tickets + gossip, fully server-free) is always present and is
|
||||||
|
/// deliberately *not* represented here — these flags are only about n0 DNS.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct LookupPlan {
|
||||||
|
/// Resolve a moved friend's address by their (stable) node id via n0 DNS. A
|
||||||
|
/// passive query — publishes nothing — and only fires when a saved address is
|
||||||
|
/// stale enough that the dial falls through to discovery.
|
||||||
|
pub resolver: bool,
|
||||||
|
/// Publish *our* current address to n0 DNS so friends can find us after we moved
|
||||||
|
/// networks. The one privacy-costly bit; on only while `Discoverable`.
|
||||||
|
pub publisher: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The n0 DNS services wanted for a `(network_mode, want_publish)` pair. Pure.
|
||||||
|
///
|
||||||
|
/// `want_publish` is `true` exactly when the presence posture is `Discoverable`
|
||||||
|
/// (see [`crate::presence::PresenceMode::publishes_to_discovery`]).
|
||||||
|
pub fn lookup_plan(network_mode: NetworkMode, want_publish: bool) -> LookupPlan {
|
||||||
|
match network_mode {
|
||||||
|
// The explicit serverless posture: no n0 contact at all, even to resolve.
|
||||||
|
// A Discoverable toggle here is intentionally inert.
|
||||||
|
NetworkMode::DirectOnly => LookupPlan { resolver: false, publisher: false },
|
||||||
|
// Relay-capable: always resolve (so a stationary friend can find a mover);
|
||||||
|
// publish only when the user opted into Discoverable.
|
||||||
|
NetworkMode::RelayNoDiscovery | NetworkMode::N0Full => {
|
||||||
|
LookupPlan { resolver: true, publisher: want_publish }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relay_modes_always_resolve_and_publish_only_when_wanted() {
|
||||||
|
for mode in [NetworkMode::RelayNoDiscovery, NetworkMode::N0Full] {
|
||||||
|
assert_eq!(
|
||||||
|
lookup_plan(mode, false),
|
||||||
|
LookupPlan { resolver: true, publisher: false },
|
||||||
|
"{mode:?}: resolve always on, no publish when not Discoverable"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lookup_plan(mode, true),
|
||||||
|
LookupPlan { resolver: true, publisher: true },
|
||||||
|
"{mode:?}: Discoverable adds publish on top of resolve"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn direct_only_never_touches_n0_even_when_discoverable() {
|
||||||
|
assert_eq!(
|
||||||
|
lookup_plan(NetworkMode::DirectOnly, false),
|
||||||
|
LookupPlan { resolver: false, publisher: false }
|
||||||
|
);
|
||||||
|
// The serverless posture overrides the Discoverable request entirely.
|
||||||
|
assert_eq!(
|
||||||
|
lookup_plan(NetworkMode::DirectOnly, true),
|
||||||
|
LookupPlan { resolver: false, publisher: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn timebox_is_thirty_minutes() {
|
||||||
|
assert_eq!(DISCOVERY_TIMEBOX, Duration::from_secs(1800));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ pub mod screenshare;
|
|||||||
pub mod sanitize;
|
pub mod sanitize;
|
||||||
pub mod avatar;
|
pub mod avatar;
|
||||||
pub mod recents;
|
pub mod recents;
|
||||||
|
pub mod discovery;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|||||||
Reference in New Issue
Block a user