7.8 KiB
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:
- The operator connects to a relay peer (public libp2p node)
- The relay gives the operator a "virtual address" (
/p2p/<relay-id>/p2p-circuit/p2p/<op-id>) - The operator advertises this relay address in the DHT
- 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:
- Re-discovers the operator via DHT
- Opens a new persistent stream
- 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
-
Internet access — the only requirement. The operator needs outbound connectivity to reach the libp2p network (any port, any protocol the network allows).
-
An operator key — generated on first run and stored at
~/.necropolis/operator.key. This is the root of trust. No key = no access. -
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.