Skip to main content

LanDiscoveryController

Struct LanDiscoveryController 

Source
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: AtomicBool

What 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

Source

pub fn new( instance_id: String, hostname: String, version: String, port: u16, event_bus: Arc<EventBus>, auth_key: String, ) -> Self

Source

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.

Source

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.

Source

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).

Source

pub fn evict_agent(&self, agent_id: &str)

Evict a stale cache entry (e.g. after a forward to that peer failed).

Source

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).

Source

fn start_daemon(&self) -> Result<Arc<LanDiscovery>, String>

Source

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.

Source

pub fn is_wanted(&self) -> bool

Should a daemon be running right now, per the last apply?

Source

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.

Source

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.

Source

pub fn health(&self) -> Option<MdnsHealth>

The running daemon’s discoverability, or None when LAN discovery is off or failed to start.

Source

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.

Source

fn publish_health(&self, state: &str, missing: &[Ipv4Addr], rebuilds: u32)

Source

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§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert 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>

Convert 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)

Convert &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)

Convert &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
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts 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
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more