Skip to content

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 -iUDP supplies 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. lsof supplies that richer state, including CLOSE_WAIT, FIN_WAIT_*, SYN_SENT and ESTABLISHED, 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:

python3 -m shadowclaw --config config/shadowclaw.sanctioned-example.json

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.

Next