Scan the LAN, bounded to configured subnets (#257) #81
Closed
claude
wants to merge 1 commits from
overnight/netscan into overnight/smarthome
pull from: overnight/netscan
merge into: kami:overnight/smarthome
kami:master
kami:task/725-capability-ledger-and-empirical-baseline
kami:task/692-heads-path-may-equal-model-path-and-noth
kami:task/694-staticcheck-and-deadcode-are-still-not-i
kami:task/682-go-1-25-5-and-x-text-0-14-0-carry-20-rea
kami:task/674-caveats
kami:task/673-mavgpud-serves-the-model-to-the-whole-la
kami:task/487-capture-device-doc
kami:task/487-capture-device
kami:task/487-wake-word-deploy
kami:task/487-wake-word-threshold
kami:task/487-wake-word-stage-two
kami:task/671-mavwaked-registers-as-a-voice-consumer-i
kami:task/670-cut-claude-md-to-200-lines
kami:task/515-deploy-mavwaked-workpc
kami:task/669-prune-claude-md
kami:task/668-e4b-phrasing
kami:task/668-title-capital
kami:task/668-kiwix-answers-a-question-it-cannot-answe
kami:task/666-only-a-stage-0-grammar-may-take-the-pers
kami:task/487-mavwaked-has-no-wake-word-only-an-energy
kami:task/486-deploy-the-workstation-transcriber
kami:task/486-move-stt-and-tts-to-the-workstation-wher
kami:task/665-crisperwhisper-2-russian
kami:task/664-routing-heads-in-go
kami:task/662-usage-harness-source-badge
kami:task/661-post-merge-usage-rerun
kami:task/661-routing-heads-step-3-train-the-multi-hea
kami:task/660-router-prompt-destination
kami:task/659-destination-fixture
kami:task/655-query-source-is-a-routing-decision-made
kami:task/654-a-pending-clarify-has-no-way-out-neither
kami:task/654-week-of-usage-eval-docs
kami:task/649-needs-kami-telegram-is-the-only-reach-an
kami:task/643-memorystore-search-decodes-and-unmarshal
kami:task/641-two-maps-grow-for-the-process-lifetime-w
kami:task/644-mavcaldav-is-built-documented-as-running
kami:task/642-the-store-caps-sqlite-at-one-connection
kami:task/647-factenrichmentworker-walks-the-pending-q
kami:task/646-v-637-follow-up-telegram-intake-has-no-d
kami:task/638-no-deadline-survives-the-turn-path-from
kami:task/637-inbound-telegram-turns-and-corrections-f
kami:task/636-correcting-a-turn-from-telegram-and-from
kami:task/634-an-act-alias-resolves-the-verb-but-not-t
kami:task/630-one-gesture-correction-on-chat-v-628
kami:task/629-persist-the-routing-trace-and-record-it
kami:task/631-mode-inventory-written-from-the-handlers
kami:task/586-defaultfactparser-uses-hand-written-russ
kami:task/633-reconcile-the-seed-labels-with-the-handl
kami:task/627-reminder-verbs-has-no-alarm-verb-so-an-a
kami:task/626-the-classifier-seeds-teach-an-older-inte
kami:task/546-route-with-a-fine-tuned-e5-small-instead
kami:task/586-measure-the-fact-parser
kami:fix/gofmt-ecosystem-acts
kami:task/584-media-store-a-failed-write-leaks-its-bud
kami:task/518-no-write-path-for-a-backdated-event-so-t
kami:task/287-qa-voice-session-quality-polish
kami:task/492-qa-plan-reconcile
kami:task/530-sweep-tail-four-files-the-russian-sweep
kami:task/405-score-how-often-a-real-utterance-reaches
kami:task/529-money-and-list-pick-a-mechanism
kami:task/528-sweep-tail-the-three-files-on-467
kami:task/527-embedder-open-set-phrasings-stop-being-r
kami:task/526-morphology-a-dictionary-answers-the-gram
kami:task/525-lexicons-the-finite-russian-sets-move-to
kami:task/524-entity-reference-ask-nexus-about-every-l
kami:task/523-risk-tiers-take-hexis-s-tier-for-a-hexis
kami:task/521-review-pr-111-query-strings-declension-h
kami:task/491-llama-server-core-dumps-on-every-sigterm
kami:task/479-bug-an-unconfigured-capability-does-not
kami:task/467-bug-spoken-task-capture-is-dead-the-rout
kami:task/463-deploy-mavwaked-and-mavenclient-run-nowh
kami:task/480-hearing-no-shipped-client-can-start-a-re
kami:task/432-ambient-calendar-intake-is-fragile-and-p
kami:task/431-board-surface-maven-holds-the-work-board
kami:task/433-reactivehandler-has-30-fields-and-is-pas
kami:task/371-swap-the-embedder-for-an-asymmetric-retr
kami:task/408-review-31-07-split-the-30-method-coreapi
kami:task/410-review-31-07-hand-rolled-string-enums-st
kami:task/423-review-pr50-split-internal-ipc-server-go
kami:task/422-review-pr50-split-cmd-mavend-tick-go-860
kami:task/409-review-31-07-finish-moving-mavweb-markup
kami:task/482-ambient-ingest-reads-a-notification-s-ti
kami:task/444-kuma-a-fact-per-monitor-so-she-can-name
kami:task/452-capability-model-homelab-docker-restart
kami:task/449-destructive-confirm-policy-risk-tiers-no
kami:task/453-grocery-list-items-table-fourth-append-o
kami:task/399-run-the-persona-checks-inside-the-daemon
kami:task/448-bounded-follow-up-state-pending-candidat
kami:task/455-conversation-repair-name-the-misroute-co
kami:task/454-go-mod-tidy
kami:task/458-pronunciation-dictionary-for-piper
kami:task/456-command-history-read-only-query-over-exi
kami:task/457-clarification-templates-for-the-router-s
kami:task/474-query-source-ordering-feeds-and-calendar
kami:task/469-reminders-spelled-out-times-fail-the-bod
kami:task/475-bug-the-praxis-attention-capability-is-u
kami:task/481-bug-a-transient-complaint-is-stored-as-a
kami:task/476-bug-the-router-transliterates-latin-enti
kami:task/385-decide-whether-a-parked-clarify-question
kami:task/377-backfill-routines
kami:task/421-weather-geocoder
kami:task/390-no-read-path-for-delivery-attempts
kami:task/386-recall-fixture-filler-note-ids
kami:task/473-bug-morning-item-has-no-required-flag
kami:task/465-bug-make-simulate-routes-with-an-empty
kami:task/467-bug-spoken-task-capture-is-dead
kami:task/466-bug-a-pending-clarify-is-global-so-one-u
kami:task/468-bug-pattern-detect-has-no-minimum-interv
kami:task/462-bug-checkfeminine-flags-second-person-ma
kami:task/443-safekey-drops-cyrillic-so-russian-calend
kami:task/471-bug-agendaquerygrammars-covers-today-but
kami:task/383-slottext-in-clarify-answer-would-clobber
kami:task/323-qa-phraser-coverage-is-65-3-but-the-llam
kami:task/498-bug-and-x-reach-the-model-with-no-determ
kami:task/506-strings-family-6-summaries-and-reports-i
kami:task/504-strings-family-4-act-and-smart-home-repl
kami:task/503-strings-family-3-query-answers-and-gaps
kami:task/502-strings-family-2-capture-acknowledgement
kami:task/501-strings-family-1-phrasing-fallbacks-into
kami:task/397-phrasechat-and-phrasequery-hide-model-fa
kami:task/396-the-reply-path-can-t-be-tested-llmreplie
kami:task/496-recall-a-cross-language-question-loses-i
kami:task/495-bug-x-escapes-the-personal-boundary-and
kami:task/499-llama-server-holds-7-9gb-rss-for-a-1-1gb
kami:task/470-bug-a-question-writes-invented-knowledge
kami:task/493-bug-the-memory-index-stores-the-raw-utte
kami:task/490-name-the-gap-world-questions-through-the
kami:task/485-run-the-big-model-on-the-workstation-wit
kami:task/489-workstation-deploy-mavgpud-on-workpc-and
kami:task/488-workstation-a-supervisor-that-keeps-llam
kami:task/483-docs-offload-design
kami:task/483-design-offload-ml-to-the-workstation-kee
kami:task/459-docs-refresh-the-qa-plan-against-the-liv
kami:task/446-doc-reorg-tier-the-tree-retire-the-three
kami:fix/367-voice-parks-routine-accept
kami:task/365-dialogue-slots-and-router-slots-are-hand
kami:task/364-snooze-does-nothing-at-runtime-the-gate
kami:task/447-retire-progress-md-the-backlog-and-the-f
kami:task/445-session-workflow
kami:overnight/eco-versioned-traces
kami:overnight/eco-entity-refs
kami:overnight/eco-degraded-suite
kami:overnight/smarthome
kami:overnight/replay-simulator
kami:overnight/event-envelope
kami:overnight/coldstart-unlock
kami:overnight/voice-barge-in
kami:overnight/stt-golden-audio
kami:overnight/senses-speaker
kami:overnight/senses-hearing
kami:overnight/senses-media-vision
kami:overnight/mcp-tools
kami:overnight/mcp-client
kami:overnight/self-update
kami:overnight/model-swap
kami:overnight/web-crawler
kami:overnight/rss-feeds
kami:overnight/email-poller
kami:overnight/email-extract
kami:overnight/email-imap
kami:overnight/money-zenmoney
kami:overnight/task-priority
kami:overnight/task-capture
kami:overnight/behavior-profile
kami:overnight/day-plan
kami:overnight/ambient-calendar
kami:overnight/local-calendar
kami:overnight/memory-eval
kami:overnight/proactive-proposals
kami:overnight/split-voice-quiet
kami:overnight/nginx-maven-block
kami:overnight/stepup-chat-surface
kami:integration/small-batch
kami:docs/fix-drift
kami:fix/ru-wording
kami:integration/jul31
kami:overnight/resident-1.7b
kami:overnight/nudge-templates
kami:overnight/kiwix-rewrite
kami:overnight/eval-writeup
kami:overnight/fix-truncation
kami:overnight/kiwix-client
kami:overnight/ru-prompts
kami:overnight/external-data
kami:overnight/phrasing-grammar
kami:overnight/talk-eval
kami:overnight/prompt-context
kami:overnight/prompt-address
kami:overnight/eval-label-kill
kami:overnight/delivery-boundary
kami:overnight/address-check
kami:overnight/system-replies-pr
kami:overnight/clock-intent-pr
kami:overnight/embedder-backfill-pr
kami:overnight/embedder-marker-pr
kami:overnight/note-recall-pr
kami:overnight/thinking-off-pr
kami:overnight/dialogue-persist-pr
kami:overnight/persona-2p-pr
kami:overnight/clarify-expiry-pr
kami:overnight/clarify-rework
kami:overnight/phrasing
kami:overnight/bakeoff
kami:overnight/recall-margin
kami:overnight/router-on
kami:overnight/slot-extract
kami:overnight/embedder-e5
kami:overnight/router-refusal
kami:overnight/eval-rerun
kami:overnight/eval-harnesses
kami:overnight/eval-rerun-base
kami:overnight/fmt-gate
kami:overnight/routines-fire
kami:overnight/router-prompt
kami:overnight/away-leak
kami:overnight/recall-eval
kami:overnight/snooze-works
kami:overnight/clarify-wiring
kami:overnight/delivery-tests
kami:overnight/routine-accept
kami:overnight/llm-router-flag
kami:overnight/clarify-data-layer
kami:overnight/loop-rule-tests
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "overnight/netscan"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Scan the LAN, bounded to configured subnets (#257)
internal/netscan/ discovers hosts on the network Maven is configured to look at:
a TCP-connect scan (net.DialTimeout, no raw sockets, no privileges) plus a read
of the kernel's ARP cache. Wired as a read-only query source, "network", so
"какие устройства в сети?" is answered by a scan instead of by whatever old note
happens to be nearest.
Scanning is a read, but an unbounded scanner on a home LAN is noisy and easy to
point somewhere it should not go, so the package is built around four bounds:
from nowhere else, so there is no exported way to scan an arbitrary prefix
and nothing an utterance, the router, or a scanned host says can retarget
it. That is asserted directly: the test watches every address handed to the
dialer and fails if one falls outside the configured prefix. The ARP cache —
the one input the network itself populates — is filtered to the configured
range for the same reason.
larger than 1024 addresses. 8.8.8.0/24, 0.0.0.0/0 and 10.0.0.0/8 are refused
at config load, not after the packets have left.
so it looks like background traffic rather than a portscan.
and the context; a canceled scan stops dialing immediately.
Off unless configured: dark without "enabled": true, and applyDefaults
normalises a disabled block to nil. deploy/mavend.json carries it disabled.
BLUETOOTH IS NOT SHIPPED, AND IS BLOCKED, NOT SKIPPED. The plan's other half
(internal/bluetooth/, RSSI presence probes) needs a bluez stack that is not
here: bluetoothctl and hcitool are not installed, bluetoothd is not installed,
the bluetooth unit is inactive, and org.bluez is not on the system bus. hci0
exists as a kernel device and nothing can talk to it. The docker deploy is
further away still — it would need host networking, the D-Bus system socket
passed in, and CAP_NET_ADMIN. Writing an exec wrapper around a binary that does
not exist, against an output format nothing here can produce, would be a guess
dressed as a feature. It needs a decision about privileging the container before
any of it is worth writing.
Vikunja #257
internal/netscan/ discovers hosts on the network Maven is configured to look at: a TCP-connect scan (net.DialTimeout, no raw sockets, no privileges) plus a read of the kernel's ARP cache. Wired as a read-only query source, "network", so "какие устройства в сети?" is answered by a scan instead of by whatever old note happens to be nearest. Scanning is a read, but an unbounded scanner on a home LAN is noisy and easy to point somewhere it should not go, so the package is built around four bounds: - Scan takes NO target argument. The range comes from the config block and from nowhere else, so there is no exported way to scan an arbitrary prefix and nothing an utterance, the router, or a scanned host says can retarget it. That is asserted directly: the test watches every address handed to the dialer and fails if one falls outside the configured prefix. The ARP cache — the one input the network itself populates — is filtered to the configured range for the same reason. - Every configured CIDR must be private (RFC1918 / CGNAT / link-local) and no larger than 1024 addresses. 8.8.8.0/24, 0.0.0.0/0 and 10.0.0.0/8 are refused at config load, not after the packets have left. - Rate-limited to a configured connections-per-second across the whole scan, so it looks like background traffic rather than a portscan. - Bounded in total by MaxHosts, a per-connection timeout, a 20s turn budget and the context; a canceled scan stops dialing immediately. Off unless configured: dark without "enabled": true, and applyDefaults normalises a disabled block to nil. deploy/mavend.json carries it disabled. BLUETOOTH IS NOT SHIPPED, AND IS BLOCKED, NOT SKIPPED. The plan's other half (internal/bluetooth/, RSSI presence probes) needs a bluez stack that is not here: bluetoothctl and hcitool are not installed, bluetoothd is not installed, the bluetooth unit is inactive, and org.bluez is not on the system bus. hci0 exists as a kernel device and nothing can talk to it. The docker deploy is further away still — it would need host networking, the D-Bus system socket passed in, and CAP_NET_ADMIN. Writing an exec wrapper around a binary that does not exist, against an output format nothing here can produce, would be a guess dressed as a feature. It needs a decision about privileging the container before any of it is worth writing. Vikunja #257Scan(ctx)taking no target is the correct API, and it is what makes the rest of the bounds hold. There is no exported path from an utterance to a range, so the prompt-injection story is closed by shape rather than by validation. Rejecting a non-private prefix and anything wider than a /22 at config load, innetscan.Validate, means the daemon cannot be talked into a scan it would not have done at boot. TCP-connect plus an ARP-table read, with no raw sockets and no root, is the right cost for the answer. The Russian count inflection inhostWordis correct including the teens.1.
resultsis sized by hosts and written by host-port pairs. A dense subnet deadlocks the scan.Each worker sends one value per open port, so the number of sends is bounded by
len(targets) * len(s.cfg.Ports), not bylen(targets). Nothing drains the channel until afterwg.Wait()andclose(results).Walk it with
subnets: ["192.168.1.0/28"]and the default four ports.targetsyields 14 addresses, so the buffer holds 14. Six live devices answer on three ports each, which is ordinary for a router, a NAS and a couple of Pis: 18 sends. Send 15 blocks. The worker holding it never returns,wg.Wait()never returns,closeand the drain loop are never reached.The context does not save it. The goroutines are blocked on a channel send with no
selectonctx.Done(), soscanBudgetexpiring changes nothing.scanSummarynever returns, the voice turn that called it hangs for the life of the process, and 16 goroutines plus the semaphore leak with it.The test suite cannot see this: the stub dialer would have to open more ports than there are targets. Size the buffer
len(targets)*len(s.cfg.Ports), or drain in a goroutine started before the loop.2. The default scan cannot finish inside the default budget, and a truncated scan is reported as the network.
deploy/mavend.jsonships192.168.1.0/24, four ports,rate: 50,max_hosts: 256.targetsreturns 254 addresses. That is 1016 probes. The ticker interval istime.Second / 50, 20ms. 1016 × 20ms is 20.32 seconds.scanBudgetis 20 seconds.So the shipped configuration always hits the deadline, roughly 16 probes short, plus whatever the 400ms tail dials cost. The addresses are walked in ascending order, so it is always the top of the range that goes unprobed. A device parked at .250 is invisible, deterministically, and re-asking does not help.
Scanthen returnsnilfor the error on thebreak scanpath.scanSummaryhas no way to know the run was cut off and says "нашла 6 устройств" as a statement about the LAN. Return a sentinel or a bool for a truncated run and have her say "успела посмотреть не всю сеть". Either raise the default rate, or lowermax_hosts, or raisescanBudgetso the shipped numbers are self-consistent.3. With two subnets, only the first is ever scanned, silently.
targetsiteratescfg.Subnetsin order and returns as soon aslen(out) >= MaxHosts.Validatebounds each subnet atMaxPrefixHostsseparately and never looks at the sum.subnets: ["192.168.1.0/24", "192.168.2.0/24"]with the defaultmax_hostsof 256 therefore yields 254 addresses from the first subnet and 2 from the second. Configuration passes, boot logs nothing, and the second LAN is 99% dark. He asks what is on the network and gets an answer about one of the two ranges he named.Either round-robin across subnets, or make
Validatereject a configuration whose subnets sum pastMaxHosts, or say inNetScanConfigthatMaxHostsis consumed in order.4. The reply reads raw IPv4 addresses out loud.
scanSummarybuilds"192.168.1.1 (80, 443); 192.168.1.14 (22)"and that string is the spoken reply. Piper will read it as a digit stream, and this is the query path, so it goes to the voice client as well as to/chat. Six of them in one sentence is not an answer anybody can use through a speaker.The MAC is already collected and thrown away. Speak the count and the shape ("нашла 6 устройств, из них два с вебом"), and put the address list where it can be read. That is also the case for writing the scan into #283's intake journal, which it currently does not touch at all.
Smaller notes:
queryNetworkclaims the turn withhandled=truewhenh.netscanis nil, so an unconfigured box stops falling through to recall for "сколько устройств в сети?". Same shape as thequeryHomenote on PR 80, and the same fix: return"", false./events. A scan is a read, so that is defensible, but it means there is no record that Maven put packets on the LAN at 03:00 and no way to answer "when did she last scan".Host.Upis defined and never called."сети", which is inside"посетил". It needs a device noun and an ask on top, so the reachable false positives are contrived ("сколько машин я посетил?"). Whole-token matching withhomeWordwould close it, and that helper is already imported fromnetscan.go's sibling file.Validateallowsrate: 100000.Scanfloors the interval at 1ms, so the real ceiling is about 1000 connections per second. Worth a stated bound rather than a silent floor, given the package comment promises a scan looks like background traffic.Landed on master. The stack was one linear chain, so #84 carried every commit from #50 up, and master now contains this branch in full. Merging this PR on its own is an empty diff, so it is closed rather than merged. The review findings for it were fixed in the 2026-08-01 pass and are on master as commits on the stack tip, not on this branch.
Pull request closed