Plane B — shadow egress¶
Plane B answers where is this process talking? It is the plane the OpenTelemetry Collector cannot supply on macOS, and therefore the reason a sensor exists at all.
Acquisition¶
Plane B combines an event source with a state source on macOS:
- PKTAP observes connection initiation as it happens. Apple's packet-tap metadata carries the originating process name and pid. TCP is observed at the outbound SYN; QUIC/HTTP-3 at its long-header handshake packet. A connection that opens and closes between two polls is therefore still evidence.
lsof -iTCP -iUDPsupplies settled socket state. A SYN or QUIC Initial proves contact, not whether the peer answered, how many bytes crossed, or how long the socket lived.lsofsupplies that richer state, includingCLOSE_WAIT,FIN_WAIT_*,SYN_SENTandESTABLISHED, and wins when both sources report the same pid and peer.
UDP is not optional detail: asking lsof for TCP alone made every QUIC connection invisible to this plane, not graded down but absent. On the development host, adding UDP contributed one connected socket beside roughly fifty TCP sockets, and that one socket was HTTP/3 to a public provider on port 443.
The banner states which claim the running sensor can make: observed (PKTAP) + polled (lsof) or polled (lsof) -- brief connections may be missed. Liveness is not coverage, and the two must not look alike.
Root changes what you see¶
| Privilege | Process table | Sockets |
|---|---|---|
| Unprivileged | Every process, every user | Your user only |
| Root | Every process, every user | Every process, every user |
A finding needs both halves for the same pid. So an unprivileged sensor watches the whole machine compute and only its own share of it talk — enough to validate the detector, not enough to police a machine. That is stated in the banner rather than implied.
Attribution: three sources, three confidences¶
An IP address is not a provider. Turning one into the other is the hard part, and the sensor is explicit about how well it managed.
Source 1
`dns`
A captured DNS answer from tcpdump on port 53. The process asked for a name, the resolver returned this address, and the sensor saw both.
- Exact. The only exact source.
- Requires root
- Enabled with
--dns-sniffer
Source 2
`catalog`
Forward-resolving the 92 catalog domains and matching the result against observed peers.
- Probabilistic
- No privilege required
- Index accumulates across refreshes
Source 3
`ptr`
Reverse DNS on the peer address.
- Weak. Often generic CDN names
- No privilege required
enable_reverse_dns
Every endpoint in a finding carries its attribution_source, so an analyst can weigh the evidence rather than being handed a verdict. A signal supported by evidence with confidence below 0.9 contributes proportionally less than its nominal weight.
Shared CDN addresses can misattribute
Several providers sit on the same Cloudflare ranges, and provider APIs sit behind anycast CDNs that hand out a rotating subset of a large pool. Forward-resolving the catalog will not always cover the address a process actually used. The exact answer requires the DNS sniffer, which requires root.
Exfil-surface detection retains every DNS and catalog candidate for a shared address rather than only the generic winner. This prevents a catalogued surface such as gist.github.com disappearing when github.com shares the edge and wins generic attribution. Normal provider scoring still receives one deterministic winner.
Candidate preservation is not permission to guess. When surface and non-surface names compete for one address, the surface observation is explicitly ambiguous and discounted to corroboration-only confidence. It cannot seed an inter-agent external channel, and durable endpoint evidence carries all candidate hostnames instead of claiming one provider.
Signals¶
Withheld from the public documentation
Exact weights, thresholds, and defaults are kept out of the published site: together they are enough to tune activity to sit below the reporting floor. They are in the repository, beside the code that applies them, for operators who need to reason about a score.
Category sets the weight¶
shadow_ai_egress is not one number. Reaching a frontier API is a different fact from reaching a model hub, and the weight says so.
Withheld from the public documentation
Exact weights, thresholds, and defaults are kept out of the published site: together they are enough to tune activity to sit below the reporting floor. They are in the repository, beside the code that applies them, for operators who need to reason about a score.
Unknown providers¶
The detector does not depend on the catalog. Four hostname heuristics identify inference-shaped endpoints belonging to nobody in it:
| Heuristic | Example match |
|---|---|
| Inference-shaped API subdomain | api.somellm.example |
| Inference-shaped hostname label | llm.example.com, embeddings.example.com |
| Model family name in the hostname | llama-serve.example.net, qwen.example.io |
The .ai TLD |
newstartup.ai |
This is what makes the control survive a vendor that launches next week without
making every vanity .ai website equivalent to an inference API. The match
confidence and the hostname-attribution confidence answer different questions
and are both applied: a bare .ai TLD stays below the reporting floor alone,
while an API/inference/model-family shape can clear it. Parallel sockets to one
host fold into one signal; distinct shaped hosts remain distinct evidence.
Withheld from the public documentation
Exact weights, thresholds, and defaults are kept out of the published site: together they are enough to tune activity to sit below the reporting floor. They are in the repository, beside the code that applies them, for operators who need to reason about a score.
The gateway-bypass contrast¶
sanctioned_endpoints declares your approved AI path. It is a label, not a mute: traffic there is still recorded, still appears in the inventory, and simply scores as sanctioned_ai_egress (15, informational) rather than as shadow use.
What the label buys you is the contrast. A process that reaches a provider directly while that gateway exists earns gateway_bypass — the difference between "someone used AI" and "someone evaded the control".
Mixed traffic is not exculpatory
Bypass fires even when the process also used the gateway. A process that demonstrably knows the approved path and still sends some requests around it is the strongest evasion signal available here. Treating any gateway use as a free pass would make evasion trivial to hide behind.
Compare the two shipped configs to watch labelling change the outcome:
Withheld from the public documentation
Exact weights, thresholds, and defaults are kept out of the published site: together they are enough to tune activity to sit below the reporting floor. They are in the repository, beside the code that applies them, for operators who need to reason about a score.