node

package
v0.16.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 43 Imported by: 0

Documentation

Overview

Package node provides a combined DMCN node that runs the identity registry and relay service in a single process.

Index

Constants

View Source
const JoinProtocol = "/dmcn/join/1.0.0"

JoinProtocol is the credential-presentation handshake (Credential PKI). On connect, a peer presents its Credential + the DAR that anchors it; the verifier checks it against a direct DNS resolution and admits the peer to the in-memory credentialSet, which gates federation participation.

Variables

View Source
var ErrNotFound = registry.ErrNotFound

ErrNotFound is returned when an address has no record on its authoritative fleet. It aliases registry.ErrNotFound so callers may check either sentinel with errors.Is.

Functions

func CredentialFilePath

func CredentialFilePath(dataDir string) string

CredentialFilePath returns the conventional persisted-credential path under a DataDir.

func LoadCredentialBundle

func LoadCredentialBundle(path string) (*identity.Credential, *identity.DomainAuthorityRecord, error)

LoadCredentialBundle reads a persisted {credential, DAR} from a credential.bin path, for the caller (main) to seed node.Config. Returns (nil, nil, nil) if absent.

func LoadOrCreateIdentityKey

func LoadOrCreateIdentityKey(path string) (crypto.PrivKey, error)

LoadOrCreateIdentityKey loads a marshaled libp2p private key from path, or, if the file does not exist, generates a new Ed25519 key and atomically publishes it at path (0600, parent directories created 0700).

A persistent identity key gives a node a stable peer ID across restarts, so that full /p2p/<peerID> multiaddrs can be configured statically (see blade.yaml and the committed dev keys in dev/keys/).

It is safe for multiple processes to call this concurrently on the same path: publication uses os.Link, which is atomic and fails if the target already exists, so exactly one generated key wins and every caller returns that key. A corrupt key file is reported as an error rather than silently regenerated — regenerating would change the peer ID and desynchronize the static multiaddrs other services have already computed from the same file.

func LoadStaticDNS

func LoadStaticDNS(path string) (map[string]domainverify.Record, error)

LoadStaticDNS reads a static _dmcn config file — a JSON object mapping domain → {fingerprint, fleet, seeds} — for Config.StaticDNS. It supplies the resolver's trust anchor + discovery seeds where there is no live DNS (dev cluster, CI) and doubles as a production operator seed-pin. An empty path returns (nil, nil).

func MarshalCredentialBundle

func MarshalCredentialBundle(cred *identity.Credential, dar *identity.DomainAuthorityRecord) ([]byte, error)

MarshalCredentialBundle serializes {credential, DAR} into the credential.bin format (a core dmcnpb.CredentialBundle). A fleet credential (which chains to a config operator root and carries no DAR) is written with an empty Dar; domain credentials carry their DAR.

func ParseRelayHint

func ParseRelayHint(hint string) (*peer.AddrInfo, error)

ParseRelayHint parses a relay hint multiaddr string into peer.AddrInfo.

Types

type BridgeEndpoint added in v0.7.0

type BridgeEndpoint struct {
	PeerID       string   // libp2p peer ID (the STORE target)
	Multiaddr    string   // the /p2p/ multiaddr from the _dmcn bridge= token
	X25519Public [32]byte // seal outbound envelopes to this
}

BridgeEndpoint is a discovered, credential-verified outbound bridge: the peer to STORE outbound-to-legacy mail to, and the X25519 key to seal that mail under.

type Config

type Config struct {
	ListenAddr string // multiaddr string, e.g. "/ip4/0.0.0.0/tcp/7400"
	// AnnounceAddrs replaces the addresses this node advertises to others, when the address it
	// binds is not the address it is reachable at. That is the normal case behind NAT — a cloud
	// VM with a NAT'd public IP binds a private one — and it matters more than it looks: these
	// addresses are written into the RelayHints of every record this node provisions and into
	// its relay descriptor, so a node advertising 10.x tells other domains to deliver mail
	// somewhere they cannot reach, and nothing local ever notices.
	//
	// Set, these REPLACE the detected set rather than adding to it. A private address left in
	// the list is not harmless: a sender tries the hints it is given and waits for each to time
	// out. Empty (the default) keeps libp2p's own detection, which is right when the public
	// address is on an interface.
	AnnounceAddrs        []string
	Peers                []string // multiaddr strings of infra peers: bootstrap entry points + cluster discovery seeds
	KeystorePath         string   // path to encrypted keystore file
	Passphrase           string   // passphrase for keystore encryption
	IdentityKeyPath      string   // optional path to a persistent libp2p identity key (created if missing); empty means an ephemeral identity
	DataDir              string   // optional dir for persistent stores (registry/mailbox/transit); empty = in-memory
	Mailbox              bool     // opt into the durable mailbox role (requires DataDir); persists messages hold-until-deleted
	MailboxCapacityBytes uint64   // optional mailbox capacity in bytes (0 = unbounded), reported via STAT for load-aware placement
	StorageQuotaBytes    uint64   // optional per-account TOTAL storage quota in bytes — mail + personal-KV (0 = unbounded); node default, raised per account by a RoleQuota grant
	ClientOnly           bool     // run as a pure client: no relay server handlers, no record store (web backend, CLI commands)
	DisableOnionRelay    bool     // opt out of forwarding onion traffic for others (--no-onion-relay); relays forward by default
	// OnionForwardJitter, when > 0, adds a random per-hop delay in [0, jitter)
	// before forwarding/delivering onion packets (timing-correlation hardening).
	OnionForwardJitter time.Duration

	// AllowedPeers gates which libp2p peers may federate (connect, take part in
	// peer discovery, open relay streams). Entries are bare base58 peer IDs or
	// full multiaddrs. Empty ⇒ deny-by-default (AllowNone). A single "*" entry
	// opts into open mode (AllowAll). Peers is NOT folded in: a configured
	// bootstrap peer earns no participation rights by being dialable. In
	// Credential-PKI mode this static allow-set is only an escape hatch; trust
	// comes from presented credentials.
	AllowedPeers []string

	// DNSVerifier overrides the DAR DNS-anchoring verifier (defaults to a real DNS
	// lookup). Primarily for tests that exercise countersigned/managed identities
	// without live DNS.
	DNSVerifier registry.DNSVerifier

	// StaticDNS supplies _dmcn records (domain → {fingerprint, fleet, seeds}) that the resolver
	// consults BEFORE real DNS. It provides the trust anchor + discovery seeds in environments
	// with no live DNS (the dev cluster, CI) and doubles as an operator seed-pin / resilience
	// override in production. Keys are matched case-insensitively.
	StaticDNS map[string]domainverify.Record

	// Domain is the DMCN domain this node serves. In credential mode it is taken from the
	// node's credential; it is also used as the provisioning pin for an inert node. The
	// node announces itself as a relay provider under this domain for discovery.
	Domain string

	// BlockedDomains is an optional route-selection block-list: relays whose credential
	// names one of these domains are excluded from onion routing.
	BlockedDomains []string

	// Credential + CredentialDAR enable Credential-PKI mode: the node presents this
	// credential (anchored by this DAR) at the /dmcn/join handshake and gates federation
	// by credentials presented by peers, rather than a static peer-ID allowlist. Both must
	// be set together. A node with neither boots inert (provision-listener only) until one
	// is pushed (S2 provisioning).
	Credential    *identity.Credential
	CredentialDAR *identity.DomainAuthorityRecord

	// Credentials lets a node present SEVERAL {credential, DAR} bundles at /dmcn/join — one
	// per domain it is enrolled in (hosted multi-tenant; e.g. the web, credentialed in each
	// customer DAR with the same key). The first is the primary (relay descriptor + same-domain
	// verify); all are presented so a relay authorizes the peer per target domain. When empty,
	// the singular Credential/CredentialDAR (if set) is the one and only bundle.
	Credentials []CredentialBundle
}

Config holds configuration for a DMCN node.

type CredentialBundle

type CredentialBundle struct {
	Credential *identity.Credential
	DAR        *identity.DomainAuthorityRecord
}

CredentialBundle pairs a credential with the DAR that anchors it — the unit a node presents at /dmcn/join and loads from a credential file. A node credentialed in several domains holds one per domain.

type Node

type Node struct {
	// contains filtered or unexported fields
}

Node is a combined DMCN node running the identity registry and relay.

func New

func New(ctx context.Context, cfg Config, log ...logr.Logger) (*Node, error)

New creates and starts a new DMCN node.

func (*Node) Addrs

func (n *Node) Addrs() []string

Addrs returns the node's listen multiaddrs with peer ID included.

func (*Node) Admitted

func (n *Node) Admitted(p peer.ID) bool

Admitted reports whether a peer is permitted to federate (participate) with this node — the deny-by-default participation gate. That is exactly the federation policy: the peer is in the static allow-set (the dev `*` / explicit bootstrap escape hatch) OR it presented a valid credential at /dmcn/join. With neither, false.

func (*Node) Close

func (n *Node) Close() error

Close shuts down the node, stopping the relay and registry. The persistent datastore is closed last (the node owns it; nothing must write after).

func (*Node) ComputeRelayHints

func (n *Node) ComputeRelayHints(_ context.Context, _ string, _ int, _ []string) ([]string, error)

ComputeRelayHints returns the node's own relay hint(s) — a single self-host node is the only home for its domain's mailboxes. k/exclude are ignored.

func (*Node) ConnectPeer

func (n *Node) ConnectPeer(addr string) error

ConnectPeer connects to a peer by multiaddr string.

func (*Node) Credential

func (n *Node) Credential() *identity.Credential

Credential returns the node's own membership credential (the one it presents at /dmcn/join), or nil if it holds none.

func (*Node) FanOutRecord

func (n *Node) FanOutRecord(ctx context.Context, kind dmcnpb.RecordKind, record []byte) (int, error)

FanOutRecord replicates a marshaled record of the given kind to every reachable fleet node (full replication). It stores locally first if this node serves records, then PutRecords to each peer. Returns the number of nodes (incl. self) that accepted. Best-effort: unreachable/rejecting nodes are logged and skipped; an all-fail is an error.

func (*Node) FetchRemovalUnion added in v0.5.0

func (n *Node) FetchRemovalUnion(ctx context.Context, dar *identity.DomainAuthorityRecord, address string) (*identity.AddressRemovalRecord, error)

FetchRemovalUnion builds the authoritative removal record for an address by querying EVERY reachable fleet node and unioning the tombstones each one holds.

It exists because the naive "fetch the prior record, append, re-sign" pattern is unsafe in two independent ways. First, a record fetched from ONE unverified peer and then re-signed with the domain root turns the rotation ceremony into a root-signature ORACLE: a hostile node returns a record naming a different address, and the admin mints a genuinely root-signed tombstone for an address they never touched — precisely the artifact the rebind gate treats as authoritative. Second, removal records are an append-only set enforced by the store, so rebuilding from one peer's possibly-stale copy would permanently split a divergent fleet.

Every candidate is therefore verified against the domain's DAR and checked to name THIS address before its bindings are merged. Unreachable or unverifiable peers are skipped, never fatal.

func (*Node) Host

func (n *Node) Host() host.Host

Host returns the underlying libp2p host.

func (*Node) IsCredentialed

func (n *Node) IsCredentialed() bool

IsCredentialed reports whether the node holds a membership credential (credential mode).

func (*Node) IssuerKeyPair

func (n *Node) IssuerKeyPair() (*identity.IdentityKeyPair, error)

IssuerKeyPair returns an identity.IdentityKeyPair backed by the node's single libp2p Ed25519 key — the same key that is the node's peer ID and the Subject of its join credential. Used to issue (sign) sub-credentials (address/routing) when this peer's own credential carries the matching grant, so issuance uses one key, not a separate online signing key. Only the Ed25519 halves are populated; Credential.Sign uses only Ed25519, so the X25519 halves are intentionally left zero.

func (*Node) Keystore

func (n *Node) Keystore() *keystore.Keystore

Keystore returns the encrypted keystore. May be nil if no keystore path was configured.

func (*Node) Lookup

func (n *Node) Lookup(ctx context.Context, address string) (*identity.IdentityRecord, error)

Lookup resolves an address's IdentityRecord, RecordStore-first (a serving node's own authoritative records) then the DNS-seeded fleet resolver — the unified read entry point for the services. Returns ErrNotFound when the address's fleet has no record for it.

func (*Node) LookupDAR

func (n *Node) LookupDAR(ctx context.Context, domain string) (*identity.DomainAuthorityRecord, error)

LookupDAR resolves a domain's DAR RecordStore-first then via the fleet resolver (see Lookup).

func (*Node) MailFilterStore

func (n *Node) MailFilterStore() *relay.PersistentMailFilterStore

MailFilterStore returns the node's recipient filter store (nil unless this node hosts mailboxes). Used by the owner-facing API to get/put sealed filter blobs.

func (*Node) MailboxFilterPub

func (n *Node) MailboxFilterPub() [32]byte

MailboxFilterPub returns the relay's mailbox X25519 public key — clients seal their filter list to this (alongside their own key) so the mailbox can enforce it. Zero for non-mailbox nodes.

func (*Node) MergeStaticDNS

func (n *Node) MergeStaticDNS(m map[string]domainverify.Record)

MergeStaticDNS adds/overwrites entries in the static _dmcn map, preserving existing ones. It lets a serving node register its OWN domain's anchor at boot without clobbering operator-configured peer domains (the federation seed-pin). Not safe for concurrent use — call during setup.

func (*Node) PeerID

func (n *Node) PeerID() peer.ID

PeerID returns the node's libp2p peer ID.

func (*Node) PublishBlocklist

func (n *Node) PublishBlocklist(ctx context.Context, bl *identity.CredentialBlockList) (int, error)

PublishBlocklist replicates a signed CredentialBlockList to the fleet (revocation).

func (*Node) PublishDAR

func (n *Node) PublishDAR(ctx context.Context, dar *identity.DomainAuthorityRecord) (int, error)

PublishDAR replicates a signed DomainAuthorityRecord to the fleet (domain onboarding / rotation).

func (*Node) PublishIdentity

func (n *Node) PublishIdentity(ctx context.Context, rec *identity.IdentityRecord) (int, error)

PublishIdentity replicates a signed IdentityRecord to the fleet (registration / rotation).

func (*Node) PublishRemoval

func (n *Node) PublishRemoval(ctx context.Context, rm *identity.AddressRemovalRecord) (int, error)

PublishRemoval replicates a signed AddressRemovalRecord (tombstone) to the fleet.

func (*Node) PublishRoster

func (n *Node) PublishRoster(ctx context.Context, roster *identity.FleetRoster) (int, error)

PublishRoster replicates a signed FleetRoster to the fleet.

func (*Node) Records

func (n *Node) Records() *relay.RecordStore

Relay returns the relay service. Records returns the node's local authoritative record store (the fleet-resolution source of truth), or nil for a pure client. Registration/assignment paths populate it.

func (*Node) Registry

func (n *Node) Registry() *registry.Registry

Registry returns the identity verification registry.

func (*Node) Relay

func (n *Node) Relay() *relay.Relay

func (*Node) RelayDirectory

func (n *Node) RelayDirectory(ctx context.Context) []identity.RelayDescriptor

RelayDirectory gathers RelayDescriptors for the relays this node can route onion traffic through. Candidates are the currently-connected peers + the configured infra peers (a client seeded with the full DMCN_NODE_PEERS list reaches the whole fleet); each candidate's descriptor is fetched from its own node via the fleet op. (There is no global relay enumeration; enumerating relays this node never connected to via the fleet roster is deferred.)

In credential mode each candidate must carry a valid `node` credential bound to its own peer ID (the relay-trust gate sits at route selection rather than at peer admission) and must not belong to a blocked domain. Best-effort: unreachable/invalid peers are skipped.

func (*Node) RelayHints

func (n *Node) RelayHints() []string

RelayHints returns the node's own addresses, suitable for populating IdentityRecord.RelayHints. Advertisement is decoupled from the infra peer list: a record advertises only the relay(s) that actually host its mailbox, not every peer the node connects to. (Smarter multi-relay placement is a planned follow-up.)

func (*Node) RelayX25519Priv

func (n *Node) RelayX25519Priv() [32]byte

RelayX25519Priv returns this node's onion-routing private key, used by the onion-forward handler to peel its layer.

func (*Node) RelayX25519Pub

func (n *Node) RelayX25519Pub() [32]byte

RelayX25519Pub returns this node's onion-routing public key (advertised in its RelayDescriptor).

func (*Node) ReserveRelayHints

func (n *Node) ReserveRelayHints(_ context.Context, _ string, _ string, _ int, _ []string) ([]string, error)

ReserveRelayHints is the reserve-before-commit variant; on a single node the mailbox is always local, so it returns the node's own hint(s) with no remote reservation handshake.

func (*Node) Resolve

func (n *Node) Resolve(ctx context.Context, address string) (*identity.IdentityRecord, error)

Resolve looks up an address's IdentityRecord authoritatively via DNS discovery + the domain's fleet-served signed records, verifying entirely against the domain's DNS fingerprint:

  1. Resolve the mailbox domain's _dmcn TXT → fingerprint (trust anchor) + optional fleet= deferral.
  2. Follow the deferral to the fleet domain's _dmcn TXT → seed endpoints (bootstrap, round-robin).
  3. Dial a seed over libp2p, fetch the mailbox DAR + the IdentityRecord, and verify both against the mailbox fingerprint (self-sig, DNS anchor, signed fleet_domain == DNS fleet=, credential chain). A wrong/hostile fleet is DoS-only — it cannot produce fingerprint-anchored records.

Seeds are tried in order (failover); a definitive not-found from a DAR-verified fleet node is authoritative and returned immediately.

func (*Node) ResolveBridge added in v0.7.0

func (n *Node) ResolveBridge(ctx context.Context, senderDomain string) (*BridgeEndpoint, error)

ResolveBridge discovers a credential-verified outbound bridge for senderDomain.

It reads the domain's `_dmcn` `bridge=` endpoints, resolves each candidate's self-anchored RelayDescriptor by peer ID, and returns the first whose `bridge` credential verifies. A candidate that cannot present one is skipped, not trusted: the errors are collected so the caller can say why nothing worked rather than just that nothing did.

func (*Node) ResolveDAR

func (n *Node) ResolveDAR(ctx context.Context, domain string) (*identity.DomainAuthorityRecord, error)

ResolveDAR resolves + verifies a domain's DomainAuthorityRecord authoritatively via its fleet (DNS discovery → dial seed → fetch → anchor). The resolving counterpart to registry.LookupDomainAuthority.

func (*Node) ResolveRelayDescriptor

func (n *Node) ResolveRelayDescriptor(ctx context.Context, peerID string) (*identity.RelayDescriptor, error)

ResolveRelayDescriptor fetches a relay's onion descriptor from the relay's own node via the fleet op (the descriptor is self-anchored — signed by the node's libp2p key, recoverable from the peer ID — so it is verified without trusting the server). Returns ErrNotFound when absent.

func (*Node) SendOnion

func (n *Node) SendOnion(ctx context.Context, senderAddr string, senderKP *identity.IdentityKeyPair, recipientRec *identity.IdentityRecord, env *message.EncryptedEnvelope, relaxed bool) ([32]byte, error)

SendOnion delivers a (split) envelope to the recipient via a fixed 3-hop onion route whose exit is the recipient's relay. relaxed drops subnet diversity for small dev clusters. There is no silent downgrade to direct delivery — if a route can't be built or forwarded, it errors. Returns the envelope hash on success.

func (*Node) SendOnionPreSigned

func (n *Node) SendOnionPreSigned(ctx context.Context, senderAddr string, senderSig []byte, recipientRec *identity.IdentityRecord, env *message.EncryptedEnvelope, relaxed bool) ([32]byte, error)

SendOnionPreSigned is SendOnion for a caller that already holds the sender's signature over the envelope hash (the web backend, where the browser signs and the server never has the user's key). The onion layers themselves use only ephemeral keys + relay public keys, so no user key material is involved here.

func (*Node) SetDescriptorCredential added in v0.7.0

func (n *Node) SetDescriptorCredential(cred *identity.Credential)

SetDescriptorCredential sets the credential this node advertises in its RelayDescriptor and republishes immediately.

It exists because some credentials cannot be known at node.New time. A self-hosted daemon receives its domain's authority record over the wire after startup, and only then can load the `bridge` credential anchored on it — but the descriptor was already published, so without a republish the node would advertise no credential and be undiscoverable as a bridge until the 30-minute refresh.

A peer that is both a relay and a bridge should hold ONE credential carrying both roles, so replacing a credential that has roles the new one lacks is almost always a misconfiguration rather than an intent, and says so.

func (*Node) SetStaticDNS

func (n *Node) SetStaticDNS(m map[string]domainverify.Record)

SetStaticDNS installs/replaces the static _dmcn records the resolver consults before real DNS. Useful when the fleet's seed multiaddrs are only known after the nodes are listening (dev cluster generation, tests). Set once before the node serves lookups.

func (*Node) WaitForPeers

func (n *Node) WaitForPeers(ctx context.Context, timeout time.Duration) error

WaitForPeers blocks until the node has at least one connected mesh peer, or the timeout elapses. A short-lived process (e.g. a CLI command) that has just dialed its bootstrap node needs a moment before the connection is up; calling this before a resolve/publish avoids racing the dial.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL