app: listen to what the segment says unprompted
server-release / image (push) Successful in 14s
server-release / release (push) Successful in 31s

Four passive collectors join long mode: SSDP (passive NOTIFY plus paced
M-SEARCH from the capture socket, so unicast replies land in the same
capture), LLMNR, NetBIOS-NS and WS-Discovery. All are periodic and sparse
by nature, which is exactly why they belong to a window rather than a
probe - a thirty-second run mostly hears silence and would report an
empty network as confidently as a quiet one.

NetBIOS reports unsupported on the app tier and says why: UDP 137 is
privileged. The decoder and evidence shape are tested and waiting for the
Shizuku tier; a recorded reason beats a missing test.

Silence without a multicast lock or a group join is PARTIAL, never OK -
that case is a fact about this app, not about the network. Hostnames,
banners and device UUIDs are classified in core-privacy so the anonymizer
treats them like every other identifier rather than letting a neighbour's
device model ride out in an upload.

No active WSD Probe and no NBSTAT sweep: the app listens to what a
network broadcasts, it does not announce itself to strangers or
interrogate its neighbours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrambossek
2026-08-02 14:58:41 +02:00
co-authored by Claude Opus 5
parent 93125a4b1d
commit 9ee7d554a6
11 changed files with 1800 additions and 4 deletions
@@ -40,6 +40,10 @@ object Classification {
listOf(
"addr", "address", "gateway", "dst", "src", "servers", "server", "resolver",
"next_hop", "via", "public_ip", "observed_ip",
// Who sent each passive-discovery announcement (SSDP / LLMNR / NetBIOS / WS-Discovery).
// Family-agnostic on purpose: the same field carries a dotted quad from a v4 group and
// a link-local from ff02::c, and the v6 transform hands dotted quads to the v4 path.
"source_ip",
).forEach { put(it, LogicalType.IP6) }
listOf("mac", "hw_addr", "gateway_mac", "router_mac", "sender_mac", "peer_mac")
@@ -52,13 +56,28 @@ object Classification {
"friendly_name", "server_name", "sni", "cname", "search_domain", "device_name",
// Plural and prefixed variants the models actually use.
"search_domains", "private_dns_hostname", "domains", "hostnames",
// Names a device shouts at the whole segment. An LLMNR question and a NetBIOS
// registration are hostnames in every sense that matters here — they name a machine on
// somebody's home network — so they get the same per-label treatment as any other.
"netbios_name",
).forEach { put(it, LogicalType.FQDN) }
listOf("session_id", "credential", "token", "device_id", "android_id", "serial", "imsi", "iccid")
.forEach { put(it, LogicalType.OPAQUE_ID) }
listOf(
"session_id", "credential", "token", "device_id", "android_id", "serial", "imsi", "iccid",
// Discovery identities. A UPnP USN and a WS-Discovery endpoint UUID are stable,
// globally unique per device and frequently derived from a serial number — exactly the
// thing that lets two uploads be recognised as the same household.
"usn", "device_uuid",
).forEach { put(it, LogicalType.OPAQUE_ID) }
listOf("notes", "detail", "raw", "excerpt", "location", "model_description")
.forEach { put(it, LogicalType.FREETEXT) }
listOf(
"notes", "detail", "raw", "excerpt", "location", "model_description",
// The make/model a device volunteers, and the URLs it points at. `server_banner` is
// deliberately not named `server`, which the family-agnostic address block already
// claims — a SERVER header run through the address transform would be mangled into
// nonsense while protecting nothing.
"server_banner", "product_hint", "wsd_types", "wsd_xaddrs",
).forEach { put(it, LogicalType.FREETEXT) }
}
/**
@@ -79,6 +98,13 @@ object Classification {
private val droppedKeys = setOf(
"ssdp_responders", "upnp", "neighbors", "arp_table", "scan_results",
"nearby_networks", "peers", "raw_dump", "dumpsys",
// The long run's passive-discovery inventories. Same argument as `ssdp_responders`, only
// more so: these are minutes of everything the segment said about itself — device models,
// hostnames, printers, who is looking for whom. Even fully pseudonymized the *shape* of a
// household is a fingerprint, no metric depends on the list (the counts live in `metrics`,
// which survives), and the collectors' own status fields stay behind to say the capture
// worked. Dropping beats mangling.
"ssdp_devices", "llmnr_queries", "netbios_names", "wsd_devices",
)
fun typeOf(key: String, path: List<String>): LogicalType? {