75 lines
3.4 KiB
Markdown
75 lines
3.4 KiB
Markdown
# Mesh Routing
|
|
|
|
## How It Works
|
|
|
|
Every Necropolis node (operator and implants) runs a circuit relay v2 service via
|
|
`libp2p.EnableRelayService()`. This means any node can relay encrypted traffic between
|
|
any two peers on the network. There is no distinction between "relay nodes" and
|
|
"client nodes". Every node is both.
|
|
|
|
Routing works in layers:
|
|
|
|
1. **Direct stream**. The implant opens a persistent `/bc/1.0.0` stream to the operator
|
|
via a direct libp2p connection. This is the primary path.
|
|
|
|
2. **PubSub fallback**. If the direct stream is unavailable, beacons and results are
|
|
published to GossipSub topics. Every node relays these topics (`Topic.Relay()`),
|
|
so even offline implants receive messages when they reconnect (GossipSub keeps the
|
|
last 10 heartbeats of history).
|
|
|
|
3. **DHT dead-drop**. When both direct streams and PubSub are unavailable, implants poll
|
|
the DHT dead-drop at `/necropolis/cmd/<id>/<nonce>` every 30s. The operator publishes
|
|
signed command envelopes to the DHT, and implants fetch them in nonce sequence.
|
|
|
|
4. **Implant-to-implant relay**. When an implant cannot reach the operator directly or
|
|
through pubsub, it routes through another implant. This happens automatically:
|
|
- Every implant advertises as a relay in the DHT under `necropolis/relay/<op-peerid>`
|
|
- Every implant discovers other relay-capable implants via DHT and connects to them
|
|
- When the direct beacon stream to the operator fails, the implant tries a circuit
|
|
relay address through each connected relay peer (`/p2p/<relay>/p2p-circuit/p2p/<op>`)
|
|
- libp2p handles the circuit negotiation; the implant just connects and opens a stream
|
|
|
|
Every node also auto-discovers relay candidates from its connected peers. Once an
|
|
implant connects to another implant (or the operator connects to an implant), libp2p's
|
|
`EnableAutoRelay` finds it as a relay candidate automatically.
|
|
|
|
## Discovery Flow
|
|
|
|
```
|
|
Operator: advertises under necropolis/<peerid> (rendezvous)
|
|
advertises under necropolis/relay/<peerid> (relay service)
|
|
finds relay peers via DHT relay namespace
|
|
|
|
Implant: finds operator via DHT rendezvous
|
|
advertises as relay under necropolis/relay/<peerid>
|
|
discovers other implants in relay namespace
|
|
connects to discovered relay peers
|
|
when direct operator connection fails, routes through relay peers
|
|
```
|
|
|
|
## Relay Health
|
|
|
|
Implants track every discovered relay peer with its connection state, last-seen time,
|
|
and fail count. Relay candidates are sorted by failCount ascending, so reliable relays are tried first. Every 30 seconds the discovery loop prunes disconnected peers. If a
|
|
relay peer drops, the next beacon attempt skips it and tries the next candidate.
|
|
|
|
## CLI
|
|
|
|
The `list` command shows which implants are acting as relays:
|
|
```
|
|
necropolis> list
|
|
0: user@hostname [linux/amd64] last=5s peer=12D3Koo... [relay]
|
|
1: admin@server [linux/amd64] last=2s peer=12D3Koo...
|
|
2 connected (1 relays)
|
|
```
|
|
|
|
## Implementation
|
|
|
|
- `pkg/transport/node.go`: `RelayRendezvousString()`, `AdvertiseRelay()`,
|
|
`FindRelayPeers()`; `EnableRelayService` in node config; `EnableAutoRelay` with
|
|
dynamic relay candidate discovery
|
|
- `implant/core/agent.go`: `advertiseRelayLoop()`, `discoverRelaysLoop()`,
|
|
`relayRendezvous()`, relay routing in `getBeaconStream()`
|
|
- `server/core/operator.go`: `advertiseRelayLoop()`, `discoverRelayPeersLoop()`,
|
|
`IsRelay` flag on `ImplantRecord`
|