Files
Necropolis-C2/docs/p2p-model.md
T
2026-07-07 04:50:23 +01:00

182 lines
7.8 KiB
Markdown

# The IP-less / P2P Model
## The Core Idea
necropolis has no server, no domain, no static IP address.
In traditional C2 (Sliver, Covenant, Cobalt Strike), the implant has a hardcoded IP or
domain pointing to a VPS or redirector. That IP/domain is a single point of failure:
block it, seize the server, or sinkhole the DNS, and the operation is cut off.
necropolis instead uses **cryptographic addressing** and the **libp2p peer-to-peer network**.
Instead of "connect to 1.2.3.4:443", the implant says "find the peer whose public key matches
this hash, regardless of where it is on the planet." The network handles the discovery and
routing — the operator could be on a laptop behind NAT in a coffee shop, and as long as it's
connected to the libp2p network, implants will find it.
## How Discovery Works (Step by Step)
### 1. Bootstrap — The Only Time You Touch a Known Address
When an implant or operator first starts, it needs to find *someone* on the libp2p network.
It connects to a set of **public bootstrap peers** — the same ones used by IPFS, run by
Protocol Labs:
```
/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN
/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa
...
```
These are the *only* hardcoded addresses in the binary. Their sole job is to hand you a
"phone book" (the DHT routing table) and then you never need them again. Because they're
public IPFS infrastructure used by thousands of unrelated peers, necropolis traffic is
indistinguishable from normal IPFS traffic.
The operator binary also ships with IP-based fallback multiaddrs (see
`pkg/transport/bootstrap_ip_fallbacks.go`) so it can bypass DNS entirely if needed.
### 2. DHT — The Distributed Phone Book
Once connected to any peer, the node joins the **Kademlia DHT** (Distributed Hash Table) —
a global, decentralised key-value store spread across every participating node.
The DHT is used for two things in necropolis: **rendezvous-based discovery** and **command dead-drops**.
```
Operator starts: Advertises itself under key "necropolis/<operator-peerid>"
Implant starts: Looks up key "necropolis/<operator-peerid>" in the DHT
Gets back the operator's current multiaddresses
Connects directly
```
If the operator can't publish a rendezvous or the implant can't find it, the operator
publishes signed command envelopes under `/necropolis/cmd/<id>/<nonce>` in the DHT.
Implants poll these keys sequentially every 30 seconds as a dead-drop: the operator
writes, the implant reads, no direct connection needed.
This is the **only** discovery mechanism. There is no polling, no DNS lookup, no hardcoded
endpoint. The operator can change IPs, move between networks, or go through NAT — and the
DHT always has the current address.
### 3. Relay — Getting Through NAT
If the operator is behind NAT (no public IP, no port forwarding), a direct DHT connection
may fail. libp2p handles this with **circuit relay**:
1. The operator connects to a relay peer (public libp2p node)
2. The relay gives the operator a "virtual address" (`/p2p/<relay-id>/p2p-circuit/p2p/<op-id>`)
3. The operator advertises this relay address in the DHT
4. The implant connects to the relay, which forwards traffic to the operator
Crucially, the relay sees **only encrypted bytes**. It cannot read messages, authenticate
as the operator, or modify traffic. It is a dumb pipe.
Once the implant and operator have a relay connection, libp2p attempts **hole-punching**
to upgrade to a direct connection (bypassing the relay entirely). This happens automatically
and transparently.
```
Phase 1: Implant -> Relay -> Operator (relayed, slow)
Phase 2: Implant <-> Operator (direct, fast, after hole-punch)
```
### 4. Persistent Stream — The "Beacon" Without Polling
Once connected, the implant opens a **persistent bidirectional stream** to the operator
(protocol ID `/bc/1.0.0`). This is NOT polling — it's an always-open TCP-like pipe over
the p2p network.
```
Implant sends Z1 (beacon register) on the stream every 10-15 seconds
Operator sends commands back on the same stream (or via separate /bc/1.0.0/cmd stream)
A keepalive goroutine writes cover traffic every 5 seconds to prevent relay timeout
```
If the stream dies (network blip, relay restart), the implant:
1. Re-discovers the operator via DHT
2. Opens a new persistent stream
3. Resumes normal operation
If streams and PubSub are both unavailable, the implant falls back to DHT dead-drop
polling — it reads signed command envelopes from `/necropolis/cmd/<id>/<nonce>` every 30s
and processes them through the same command handler.
There is no beacon URL, no HTTP callback, no DNS tick. The entire communication is a
single long-lived libp2p stream.
## What the Network Actually Sees
To an observer (ISP, relay operator, IDS), necropolis traffic looks like this:
```
Peer 12D3KooW... connected to Peer 12D3KooX...
Traffic: Noise-encrypted bytes (indistinguishable from any other libp2p traffic)
Topics: /b/<hash>/bx, /c/<hash>/cx (opaque strings, no identifying info)
```
There is no way to distinguish necropolis traffic from:
- An IPFS node syncing content
- A libp2p-based chat application
- Any other application built on the libp2p stack
## Operational Implications
### What You Don't Need
| Traditional C2 | necropolis |
|---|---|
| A VPS or cloud server | Nothing — use the public libp2p network |
| A domain name | No DNS needed |
| A static IP address | Any network, even behind NAT |
| A redirector / CDN | Relays are free and already exist |
| TLS certificates | libp2p Noise handles encryption |
| Firewall rules | No inbound ports needed |
| DNS records | No DNS at all |
### What You Need
1. **Internet access** — the only requirement. The operator needs outbound connectivity to
reach the libp2p network (any port, any protocol the network allows).
2. **An operator key** — generated on first run and stored at `~/.necropolis/operator.key`.
This is the root of trust. No key = no access.
3. **The operator's public key** — needed to build implants. Export it once, embed it in
each implant at build time.
### Failure Modes
| Scenario | What Happens |
|---|---|
| Operator goes offline | Implants keep beaconing, detecting disconnect after ~30s. They re-register when operator comes back. |
| DHT is slow | Implants retry every 15s. The operator also discovers implants via inbound beacon streams. |
| Relay goes down | Implant reconnects via DHT to a new relay. Hole-punching happens automatically. |
| Bootstrap peers unreachable | Implants retry. The binary also ships IP-based fallback addrs to bypass DNS. |
| Operator changes network | The DHT is updated within minutes. Implants re-discover via the next discovery cycle. |
## The Role of PubSub (Fallback)
Direct streams are the primary communication channel. **PubSub (GossipSub) is only a fallback**
for when direct streams are unavailable. Topics follow this pattern:
```
/b/<operator-peerid>/bx — Beacons (implant -> operator, fallback)
/c/<operator-peerid>/cx — Commands (operator -> implant, fallback)
/b/<operator-peerid>/tx/<id> — Per-implant task topics
```
In normal operation, PubSub is unused. The implant talks to the operator over the
persistent stream, and the operator sends commands over a separate stream.
PubSub only activates if the persistent stream cannot be established or re-established.
## Why This Works at Scale
The libp2p network has millions of active peers (IPFS alone). necropolis traffic uses the
same Noise encryption as every other libp2p connection. Cover traffic masks beacon
intervals. Stream multiplexing and DHT lookups look identical to any other libp2p
application. The operator's address changes, so there is no single IP to correlate.
This is the fundamental difference from any HTTP-based C2: there is no destination
to block because the destination is a cryptographic identity, not a network address.