Documentation
¶
Overview ¶
Package node provides a combined DMCN node that runs the identity registry and relay service in a single process.
Index ¶
- Constants
- Variables
- func CredentialFilePath(dataDir string) string
- func LoadCredentialBundle(path string) (*identity.Credential, *identity.DomainAuthorityRecord, error)
- func LoadOrCreateIdentityKey(path string) (crypto.PrivKey, error)
- func LoadStaticDNS(path string) (map[string]domainverify.Record, error)
- func MarshalCredentialBundle(cred *identity.Credential, dar *identity.DomainAuthorityRecord) ([]byte, error)
- func ParseRelayHint(hint string) (*peer.AddrInfo, error)
- type BridgeEndpoint
- type Config
- type CredentialBundle
- type Node
- func (n *Node) Addrs() []string
- func (n *Node) Admitted(p peer.ID) bool
- func (n *Node) Close() error
- func (n *Node) ComputeRelayHints(_ context.Context, _ string, _ int, _ []string) ([]string, error)
- func (n *Node) ConnectPeer(addr string) error
- func (n *Node) Credential() *identity.Credential
- func (n *Node) FanOutRecord(ctx context.Context, kind dmcnpb.RecordKind, record []byte) (int, error)
- func (n *Node) FetchRemovalUnion(ctx context.Context, dar *identity.DomainAuthorityRecord, address string) (*identity.AddressRemovalRecord, error)
- func (n *Node) Host() host.Host
- func (n *Node) IsCredentialed() bool
- func (n *Node) IssuerKeyPair() (*identity.IdentityKeyPair, error)
- func (n *Node) Keystore() *keystore.Keystore
- func (n *Node) Lookup(ctx context.Context, address string) (*identity.IdentityRecord, error)
- func (n *Node) LookupDAR(ctx context.Context, domain string) (*identity.DomainAuthorityRecord, error)
- func (n *Node) MailFilterStore() *relay.PersistentMailFilterStore
- func (n *Node) MailboxFilterPub() [32]byte
- func (n *Node) MergeStaticDNS(m map[string]domainverify.Record)
- func (n *Node) PeerID() peer.ID
- func (n *Node) PublishBlocklist(ctx context.Context, bl *identity.CredentialBlockList) (int, error)
- func (n *Node) PublishDAR(ctx context.Context, dar *identity.DomainAuthorityRecord) (int, error)
- func (n *Node) PublishIdentity(ctx context.Context, rec *identity.IdentityRecord) (int, error)
- func (n *Node) PublishRemoval(ctx context.Context, rm *identity.AddressRemovalRecord) (int, error)
- func (n *Node) PublishRoster(ctx context.Context, roster *identity.FleetRoster) (int, error)
- func (n *Node) Records() *relay.RecordStore
- func (n *Node) Registry() *registry.Registry
- func (n *Node) Relay() *relay.Relay
- func (n *Node) RelayDirectory(ctx context.Context) []identity.RelayDescriptor
- func (n *Node) RelayHints() []string
- func (n *Node) RelayX25519Priv() [32]byte
- func (n *Node) RelayX25519Pub() [32]byte
- func (n *Node) ReserveRelayHints(_ context.Context, _ string, _ string, _ int, _ []string) ([]string, error)
- func (n *Node) Resolve(ctx context.Context, address string) (*identity.IdentityRecord, error)
- func (n *Node) ResolveBridge(ctx context.Context, senderDomain string) (*BridgeEndpoint, error)
- func (n *Node) ResolveDAR(ctx context.Context, domain string) (*identity.DomainAuthorityRecord, error)
- func (n *Node) ResolveRelayDescriptor(ctx context.Context, peerID string) (*identity.RelayDescriptor, error)
- func (n *Node) SendOnion(ctx context.Context, senderAddr string, senderKP *identity.IdentityKeyPair, ...) ([32]byte, error)
- func (n *Node) SendOnionPreSigned(ctx context.Context, senderAddr string, senderSig []byte, ...) ([32]byte, error)
- func (n *Node) SetDescriptorCredential(cred *identity.Credential)
- func (n *Node) SetStaticDNS(m map[string]domainverify.Record)
- func (n *Node) WaitForPeers(ctx context.Context, timeout time.Duration) error
Constants ¶
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 ¶
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 ¶
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 ¶
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.
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 (*Node) Admitted ¶
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 ¶
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 ¶
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 ¶
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) IsCredentialed ¶
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 ¶
Keystore returns the encrypted keystore. May be nil if no keystore path was configured.
func (*Node) Lookup ¶
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 ¶
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) PublishBlocklist ¶
PublishBlocklist replicates a signed CredentialBlockList to the fleet (revocation).
func (*Node) PublishDAR ¶
PublishDAR replicates a signed DomainAuthorityRecord to the fleet (domain onboarding / rotation).
func (*Node) PublishIdentity ¶
PublishIdentity replicates a signed IdentityRecord to the fleet (registration / rotation).
func (*Node) PublishRemoval ¶
PublishRemoval replicates a signed AddressRemovalRecord (tombstone) to the fleet.
func (*Node) PublishRoster ¶
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) 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 ¶
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 ¶
RelayX25519Priv returns this node's onion-routing private key, used by the onion-forward handler to peel its layer.
func (*Node) RelayX25519Pub ¶
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 ¶
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:
- Resolve the mailbox domain's _dmcn TXT → fingerprint (trust anchor) + optional fleet= deferral.
- Follow the deferral to the fleet domain's _dmcn TXT → seed endpoints (bootstrap, round-robin).
- 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
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 ¶
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.