pub struct LanDiscoveryController {
slot: Arc<RwLock<Option<Arc<LanDiscovery>>>>,
desired: AtomicBool,
instance_id: String,
hostname: String,
version: String,
port: u16,
auth_key: String,
event_bus: Arc<EventBus>,
agent_cache: RwLock<HashMap<String, LanCacheEntry>>,
pubkey_cache: RwLock<HashMap<String, LanPubkeyCacheEntry>>,
pubkey_lookup_limiter: Mutex<LookupRateLimiter>,
}Expand description
Controller for live start/stop of LanDiscovery in response to setting changes.
Owns the daemon slot plus the start arguments, so toggling
network:lan_discovery from the UI (or from an external edit of
settings.json) can start or stop the daemon without restarting the
process.
Spec: docs/specs/lan-discovery-toggle.md
Fields§
§slot: Arc<RwLock<Option<Arc<LanDiscovery>>>>§desired: AtomicBoolWhat the last apply asked for: LAN discovery should be running. Set
under the slot’s write lock, so it never disagrees with the slot for
longer than one apply. It stays true when a start fails, which is how
the watchdog tells “wanted but down” (retry) from “off” (leave alone).
instance_id: String§hostname: String§version: String§port: u16§auth_key: String§event_bus: Arc<EventBus>§agent_cache: RwLock<HashMap<String, LanCacheEntry>>Short-lived cache mapping agent_id → (peer_url, auth_key). Entries expire after LAN_AGENT_CACHE_TTL_SECS to handle agent migration between peers.
pubkey_cache: RwLock<HashMap<String, LanPubkeyCacheEntry>>Separate cache mapping agent_id → LAN Ed25519 public key bytes.
SPEC_JEKT_LAN_TIER_SIGNING_2026_08_15.md §2.2 — deliberately its own
map rather than folded into agent_cache: the two are keyed by the
same agent_id but answer different questions (“which peer currently
hosts this agent” churns on process restarts/migration; “what is
this agent’s public key” is effectively static for the agent’s
lifetime), and a lookup for one is not always paired with a lookup
for the other (a message TO agent X needs agent_cache’s entry for
X; verifying a message FROM agent Y needs pubkey_cache’s entry for
Y — different agent_ids on the same inbound request).
pubkey_lookup_limiter: Mutex<LookupRateLimiter>reagentx P1 on the LAN signing PR: find_agent_lan_pubkey’s negative
cache is keyed by agent_id, so a caller holding only the lan_key
can force a fresh multi-peer fan-out on every single request just by
sending a novel random source_agent each time — the cache never
hits, and this expensive walk runs (in verify_lan_signature)
BEFORE Handler::inject_message’s own rate limiter is ever reached.
A simple global token bucket on the fan-out ITSELF (not per-agent_id
— the abuse pattern is specifically about varying the id to dodge a
per-id cache) bounds the damage regardless of how many distinct
identities are probed.
Implementations§
Source§impl LanDiscoveryController
impl LanDiscoveryController
pub fn new( instance_id: String, hostname: String, version: String, port: u16, event_bus: Arc<EventBus>, auth_key: String, ) -> Self
Sourcepub async fn find_agent(
&self,
agent_id: &str,
http: &Client,
) -> Option<(String, String)>
pub async fn find_agent( &self, agent_id: &str, http: &Client, ) -> Option<(String, String)>
Query LAN peers for which one hosts agent_id. Returns (peer_url, lan_key) for the first peer that responds 2xx to the agent-lookup
endpoint. Results — both positive and negative — are cached for
LAN_AGENT_CACHE_TTL_SECS seconds to avoid a blocking peer fan-out on
every inject for cloud-only agents.
Security:
the value broadcast in the mDNS TXT record / UDP probe response
(self.auth_key field name kept for wire back-compat with older
peer versions — see Config::lan_key’s doc comment) is now a
separate, narrowly-scoped credential, NOT the instance’s full-access
auth_key. A passive LAN listener who captures it gets standing
access to only the three LAN-forwarding routes
(lan_or_full_auth_middleware in server/mod.rs) — not the full
/agentmux/service surface this used to expose. Broadcasting
something in cleartext to the LAN is still an accepted trade-off
of this opt-in feature (mDNS/UDP have no confidentiality of their
own); this change is about shrinking what a captured value is worth,
not about hiding it in transit — see the spec’s LAN P1-1 for the
separate, not-yet-implemented “encrypt/authenticate the transport
too” hardening.
Sourcepub async fn find_agent_lan_pubkey(
&self,
agent_id: &str,
http: &Client,
) -> LanPubkeyLookup
pub async fn find_agent_lan_pubkey( &self, agent_id: &str, http: &Client, ) -> LanPubkeyLookup
Look up a claimed LAN sender’s Ed25519 public key by fanning out to
every discovered LAN peer, same query the target-agent lookup above
makes (GET /agentmux/reactive/agent?id=<agent_id>, now extended —
see handle_reactive_agent — to embed lan_public_key when the
responding peer has minted one). A SEPARATE lookup from find_agent
even though it hits the same endpoint: verifying an inbound message
FROM agent Y needs Y’s key, which is almost never the same agent_id
as whatever THIS request’s own target-agent lookup (if any) was for.
Returns the raw decoded public key bytes, or None if no peer has
one on file for this agent_id (cached negatively, same as
find_agent’s “not found” case) — callers (verify_lan_signature)
treat that identically to “no signature attempted”: nothing to check
against, not a red flag on its own.
Sourcepub fn cached_lan_uid_key(&self, agent_id: &str) -> Option<(String, Vec<u8>)>
pub fn cached_lan_uid_key(&self, agent_id: &str) -> Option<(String, Vec<u8>)>
The UID and UID key the peer that answered Self::find_agent_lan_pubkey
for agent_id gave beside its name key, while that answer is cached
(identity M4d-6).
Sourcepub fn evict_agent(&self, agent_id: &str)
pub fn evict_agent(&self, agent_id: &str)
Evict a stale cache entry (e.g. after a forward to that peer failed).
Sourcepub fn apply(&self, enabled: bool)
pub fn apply(&self, enabled: bool)
Idempotent: starts the daemon when enabled and not running, stops it
when !enabled and running. Re-entrant safe.
Holds the slot’s write lock for the entire check-and-modify transaction
to avoid a TOCTOU race between the is_running read and the slot
mutation. apply() is called from toggle clicks and setting writes —
low frequency — so briefly blocking concurrent peer-list reads is
acceptable. LanDiscovery::start() and Drop are both fast (mDNS
daemon construction + service register/unregister are local socket ops).
fn start_daemon(&self) -> Result<Arc<LanDiscovery>, String>
Sourcepub fn withdraw_instance_record(&self)
pub fn withdraw_instance_record(&self)
Remove this instance’s lan_instances record on clean process exit,
when LanDiscovery’s own Drop may never run (its tasks hold Arcs).
Leaves mDNS and the sockets alone; the process is going away.
Sourcepub fn restart_if_wanted(&self) -> bool
pub fn restart_if_wanted(&self) -> bool
Start a daemon when one is wanted but none is running: the recovery for a
rebuild whose new daemon failed to start (ReAgent P2 on #4148). Does
nothing, and returns false, when LAN is not wanted or a daemon is
already up, checked under the same write lock apply uses, so it cannot
race a switch-off.
Sourcepub fn rebuild(&self) -> bool
pub fn rebuild(&self) -> bool
Replace a RUNNING mDNS daemon with a fresh one, atomically.
Only ever replaces: if no daemon is running (LAN was switched off, or the
supervisor’s enabled && reachable gate in bootstrap/network.rs stopped
it) this does nothing and returns false, so a rebuild can never start
advertising on its own. The whole swap happens under one write lock, so
apply(false) from the supervisor cannot interleave between the stop and
the start (ReAgent P1 on #4148). The first draft called apply(false) then
apply(true), which released the lock in between and would have restarted
a daemon the setting had just turned off.
Sourcepub fn health(&self) -> Option<MdnsHealth>
pub fn health(&self) -> Option<MdnsHealth>
The running daemon’s discoverability, or None when LAN discovery is
off or failed to start.
Sourcepub fn spawn_health_watchdog(self: Arc<Self>)
pub fn spawn_health_watchdog(self: Arc<Self>)
Start the watchdog that notices an instance nobody can find and
rebuilds its mDNS daemon, the same path as switching LAN off and on
(which cleared the fault on Area54, SPEC_LAN_FIREWALL_SETUP 8.1). It
also tells the frontend, via laninstances:health, so the indicator can
say so: a host can see peers and still be invisible to them.
fn publish_health(&self, state: &str, missing: &[Ipv4Addr], rebuilds: u32)
Sourcepub fn get_instances(&self) -> Vec<LanInstance>
pub fn get_instances(&self) -> Vec<LanInstance>
Read the current peer list. Returns empty when the daemon is not running (discovery disabled or start failed).
Auto Trait Implementations§
impl !Freeze for LanDiscoveryController
impl !RefUnwindSafe for LanDiscoveryController
impl !UnwindSafe for LanDiscoveryController
impl Send for LanDiscoveryController
impl Sync for LanDiscoveryController
impl Unpin for LanDiscoveryController
impl UnsafeUnpin for LanDiscoveryController
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more