Skip to main content

Module lan_discovery

Module lan_discovery 

Source
Expand description

LAN instance discovery via mDNS/DNS-SD.

Each AgentMux backend advertises itself as _agentmux._tcp.local. and continuously browses for peers. Discovered instances are tracked in memory and broadcast to frontend clients via EventBus.

Re-exports§

pub use events::LanDiscoverability;
pub use events::LanDiscoveryError;
pub use events::LanInstanceList;

Modules§

agent_names 🔒
Reading an instance’s /agentmux/reactive/agent-names answer: the names a LAN peer’s poll (and the Swarm’s ask of this machine’s other channels) list, and each listed agent’s state and summary line, for the Swarm’s remote rows (SPEC_SWARM_OTHER_HOSTS_AND_CHANNELS_2026_10_02.md §3.4 Phase 4).
events 🔒
The LAN discovery events’ payloads (mps_events: laninstances, :error, :health).
lan_instances 🔒
A host-global record of this machine’s LAN-enabled instances, so the one instance that holds the UDP probe port can list the others as siblings in its reply (docs/specs/SPEC_LAN_FLEET_FEED_2026_10_03.md §3, which links the mobile spec that owns the wire contract).
udp_peers 🔒
Desktop-to-desktop discovery that does not depend on mDNS (docs/specs/SPEC_LAN_FIREWALL_SETUP_2026_10_01.md §4.7 item 5, docs/specs/SPEC_LAN_UDP_PEER_DISCOVERY_2026_10_02.md).

Structs§

LanCacheEntry 🔒
LanDiscovery
LanDiscoveryController
Controller for live start/stop of LanDiscovery in response to setting changes.
LanInstance
LanPubkeyCacheEntry 🔒
LookupRateLimiter 🔒
Minimal token bucket, refilled once per second — same shape as backend::reactive::handler::RateLimiter, duplicated locally rather than widening that one’s visibility across modules for a single caller.

Enums§

LanPubkeyLookup 🔒
Outcome of find_agent_lan_pubkey. Three states, not two — reagentx P0 follow-up on the LAN signing PR: collapsing “the lookup was skipped (rate-limited)” into the same None/“not found” outcome as “genuinely no peer has this key” let an attacker exhaust LAN_PUBKEY_LOOKUP_RATE_LIMIT with junk lookups, then slip a forged signature for a REAL agent’s identity through as unverified (benign) instead of a verification failure (forced sensitive). By the time this function is ever called, verify_lan_signature has already confirmed a lan_sig was actually presented — so “we didn’t check” must be distinguishable from “there was nothing to check.”

Constants§

HEALTH_TICK 🔒
How often the watchdog looks at discoverability. Two consecutive undiscoverable ticks are needed before a rebuild (lan_mdns_health::STRIKES_BEFORE_REBUILD).
LAN_AGENT_CACHE_TTL_SECS 🔒
LAN_AGENT_NAMES_MAX_COUNT 🔒
Defense in depth past the byte cap above: even a response that fits in 64 KiB could be an adversarially compact array of many short strings.
LAN_AGENT_NAMES_MAX_NAME_LEN 🔒
A legitimate agent name is short; anything absurdly long is more likely an attempt to waste memory than a real name, so it’s dropped rather than truncated (truncating could quietly merge two distinct long names).
LAN_AGENT_NAMES_MAX_RESPONSE_BYTES 🔒
Hard cap on a single peer’s /agentmux/reactive/agent-names response body (Codex P1 on PR #3102): the lan_key a peer advertised authenticates THIS client TO that peer, not the peer to us — anything that can fake an mDNS advertisement for _agentmux._tcp.local makes it into self.instances and gets polled by this loop. Enforced while reading, not via Content-Length (a malicious peer can omit or lie about that while still streaming an unbounded chunked body). 64 KiB is generous for a plain name list — real fleets are agents-per-machine, not per-thousand.
LAN_AGENT_NAMES_REFRESH_SECS 🔒
How often each known LAN peer is asked for its agent-name list. Peers are few (one per machine) and the request is a single small GET, so this is cheap; it’s deliberately slower than LAN_AGENT_CACHE_TTL_SECS because this list is for display (“who lives on that host”), not for delivery routing — find_agent still does its own authoritative lookup.
LAN_PEER_QUERY_TIMEOUT_SECS 🔒
LAN_PEER_STALE_TIMEOUT_CEIL_SECS 🔒
Ceiling on other_ttl_secs-driven staleness (ReAgent P1, PR #3301): other_ttl_secs is taken directly from a peer’s own mDNS advertisement, which this file already treats as adversarial elsewhere (see LAN_AGENT_NAMES_MAX_* above — “anything that can fake an mDNS advertisement… makes it into self.instances”). Without a cap, a spoofed peer advertising other_ttl near u32::MAX (~136 years) would be honored by both get_instances() and the periodic GC, permanently pinning it as “alive” — defeating the entire self-pruning design this exists for. 2x the RFC 6762 §10 / mdns-sd 0.12 default (4500s) comfortably covers every legitimate cadence actually observed on this LAN (narko/starpower’s ~46min gaps) with margin, while bounding attacker-controlled input the same way the byte/count/length caps above already do.
LAN_PEER_STALE_TIMEOUT_FLOOR_SECS 🔒
How long a peer is kept after its most recent ServiceResolved before get_instances() treats it as gone. mdns-sd’s own ServiceRemoved event is NOT used for this (see the ServiceRemoved arm of handle_event below) — live logs showed it firing on ordinary TTL churn seconds before the same peer re-resolved, and every re-resolution after a remove() came back with a blank TXT record, so deleting on ServiceRemoved permanently wiped a live peer’s hostname/instance_id/version/auth_key for no real disappearance.
LAN_PUBKEY_LOOKUP_RATE_LIMIT 🔒
Fan-outs per second find_agent_lan_pubkey will perform before failing closed (returning None without querying any peer). Generous for legitimate traffic — LAN jekts to previously-unseen senders are rare once agents have been talking a while, and the 60s positive/negative cache absorbs repeat lookups for the SAME agent_id — while still bounding the worst case to a small, fixed number of outbound requests per second regardless of how many distinct agent_ids are probed.
SERVICE_TYPE 🔒
UDP_DISCOVERY_PORT 🔒
UDP broadcast discovery fallback (Layer 2), for LANs where mDNS multicast is filtered (common on corporate/guest WiFi). Mobile clients broadcast a small JSON probe to this port; any listening desktop instance unicasts a response straight back to the sender’s source address.
UDP_PROBE_TYPE 🔒
Wire-protocol type value a probe datagram must carry.
UDP_PROTOCOL_VERSION 🔒
Wire-protocol version. Bump alongside the mobile client if the schema changes; is_valid_probe rejects anything else.
UDP_RESPONDER_RETRY 🔒
How often an instance that could not bind UDP_DISCOVERY_PORT tries again, so another LAN-enabled channel on this host takes over the probe port when its holder stops (SPEC_LAN_FLEET_FEED_2026_10_03.md §3).
UDP_RESPONSE_TYPE 🔒
Wire-protocol type value this responder replies with.

Functions§

is_lan_source 🔒
Trust-boundary check for the UDP responder: is addr reachable only from a private/link-local network, i.e. plausibly on this LAN? Unlike mDNS (link-local multicast, structurally non-routable off-subnet), this socket binds 0.0.0.0 and would otherwise answer any routable unicast probe with auth_key — an internet-facing disclosure oracle / UDP reflection vector if the port is ever reachable externally (port forward, hairpin NAT). Mirrors the private/loopback/link-local classification used for the opposite purpose (SSRF egress checks) in drone/executor/blocks/api.rs::is_reserved_v4/is_reserved_v6, minus the broadcast/unspecified/multicast cases that don’t apply to a source addr.
is_probe_version_match 🔒
Match the probe’s v field against UDP_PROTOCOL_VERSION, accepting either JSON integer or float encoding. Some JSON serializers (e.g. a client whose numeric type is inferred as double) emit a whole number like 1 as 1.0; Value::as_u64() alone returns None for that representation, which would silently drop an otherwise-conformant probe.
is_self_resolution 🔒
Pure predicate behind LanDiscovery::resolves_to_this_instance, split out so it can be tested without standing up an mDNS ServiceDaemon.
is_valid_probe 🔒
Validate a received UDP datagram as an in-protocol discovery probe. Anything that fails to parse as JSON, or does not carry the exact type/v fields, is treated as noise (unrelated LAN traffic or a future/older protocol version) and silently ignored by the caller.
mdns_hostname 🔒
Normalize an OS hostname into a valid mDNS host name by appending the .local. suffix that mdns-sd’s ServiceInfo::new requires. Idempotent — already-normalized inputs pass through unchanged. We also strip any trailing dot first so "foo.local" doesn’t end up as "foo.local..local.".
mdns_instance_label 🔒
Build this instance’s DNS-SD instance name — the first label of the service fullname. Must be BOTH unique per running instance AND free of dots.
monitor_loop 🔒
Reads the daemon’s monitor channel and records the IPv4 addresses it announced our service on. Ends when the daemon shuts down (the channel disconnects). Runs on a blocking thread, like the browse loop.
peer_staleness_window_secs 🔒
A peer is stale once last_seen exceeds ITS OWN advertised other_ttl — the standards-defined lifetime of the PTR/TXT records that make up an mDNS announcement — not one fixed cutoff shared by every peer regardless of how often it actually re-announces. Clamped between LAN_PEER_STALE_TIMEOUT_FLOOR_SECS (guards an implausibly small advertised TTL) and LAN_PEER_STALE_TIMEOUT_CEIL_SECS (guards an adversarially large one — other_ttl_secs is untrusted peer input).
probe_response_json 🔒
Build the JSON response payload for a valid probe. Pure/free function (no &self) so it is trivially testable without spinning up a full LanDiscovery (which owns a real mDNS ServiceDaemon). LanDiscovery’s production loop calls this via build_probe_response.
query_peers_concurrently 🔒
Query EVERY eligible LAN peer concurrently for agent_id, yielding results as they arrive.
record_daemon_event 🔒
Folds one daemon event into the set of IPv4 addresses we announced on.

Type Aliases§

PeerQueryOutcome 🔒
One in-flight GET /agentmux/reactive/agent?id=… against a single peer.