Documentation
¶
Overview ¶
Package node is the runtime that composes nodenet into a working overlay member. It owns the identity, the two routing tables, and the transport, and it runs the single dispatch loop that turns received frames into deliveries and forwards. It is the top of the dependency DAG and the public entry point other libraries build on (github.com/udisondev/nodenet/node); it holds no subsystem logic of its own — the keyspace, codec, routing tables and greedy decision all live below it — only the wiring that makes them act.
Recursive greedy forwarding ¶
A routing message addressed to a NodeID converges to that node hop by hop over live edges: at each node the dispatch loop parses the frame, checks the origination proof-of-work, and asks routing.Decide for the next hop nearest the target. Forwarding is zero-copy — the transit frame travels on in the very buffer it arrived in, the only change a one-byte TTL decrement patched in place — which the transport's borrow-Send contract (Send does not take the buffer) makes possible. Because forwarding rides live edges, a NAT node that dialed out is a full router: it forwards over the same bidirectional edge it opened.
What lives here ¶
The dispatch loop, origination (Send), the control protocol (keepalive ping/pong, routed lookups, neighbour responses, sibling-set exchange, graceful leave), the maintenance loop that keeps the live-edge set healthy under churn (failure detection, re-dial with backoff, self-lookup, keepalive, connectivity-floor fill) and the knowledge table fresh (lazy purge of contacts that exhausted the re-dial backoff, eviction probes that let live newcomers displace dead incumbents in full buckets, periodic refresh of stale buckets), and the test harness. Reflexive-address consolidation from pongs is wired here too, and the rest of NAT traversal builds on it: the routed rendezvous handshake (Rendezvous), direct-channel establishment (Connect, HolePunch, SendDirect) and the relay signalling all run on this loop, guarded by the defensive gates (admission and origination PoW, envelope signatures, freshness, rate limits) whose drops Stats exposes. The dispatch loop is single-goroutine (one select over the transport's single Inbound channel), so its scratch buffers are reused across packets with no locks and no per-packet allocation; the maintenance loop runs alongside it and shares the tables through their own locks. The maintenance policy itself (intervals, timeouts, backoff) lives beside the Maintenance type in maintenance.go.
Index ¶
- Constants
- Variables
- type ID
- type Inbound
- type Maintenance
- type Node
- func (n *Node) Bootstrap(contacts []routing.Contact)
- func (n *Node) Connect(ctx context.Context, target kad.ID) (transport.Conn, error)
- func (n *Node) Deliveries() <-chan Inbound
- func (n *Node) Edges() *routing.Edges
- func (n *Node) HolePunch(ctx context.Context, target kad.ID, hints []transport.Addr) (transport.Conn, error)
- func (n *Node) ID() kad.ID
- func (n *Node) InboundMedia() <-chan transport.MediaSession
- func (n *Node) Knowledge() *routing.Knowledge
- func (n *Node) OpenMedia(ctx context.Context, target kad.ID) (transport.MediaSession, error)
- func (n *Node) Reflexive() transport.Addr
- func (n *Node) Rendezvous(ctx context.Context, target kad.ID) (RendezvousResult, error)
- func (n *Node) Run(ctx context.Context) error
- func (n *Node) Send(target kad.ID, payload []byte) error
- func (n *Node) SendDirect(ctx context.Context, target kad.ID, payload []byte) error
- func (n *Node) Stats() Stats
- type Option
- func WithDmin(d int) Option
- func WithEdgeAdmission(f func(remote ID) bool) Option
- func WithForwardSendDeadline(d time.Duration) Option
- func WithInboundBuffer(size int) Option
- func WithMaintenance(m Maintenance) Option
- func WithMediaConsent(f func(remote ID) bool) Option
- func WithRand(r io.Reader) Option
- func WithRelay() Option
- func WithSubnetFunc(f routing.SubnetFunc) Option
- func WithoutMaintenance() Option
- type RendezvousResult
- type Stats
Constants ¶
const TypeApp wire.Type = 64
TypeApp is the application frame carried point-to-point over a direct edge. It is never routed: the dispatch loop delivers its payload to the local application via Deliveries(), attributing it to the edge's transport-authenticated remote — the edge itself is end-to-end encrypted and bound to the peer's NodeID, so no envelope or signature is needed. Type values below 64 are reserved for the core protocol; applications embedding nodenet speak their own protocol inside TypeApp payloads.
Variables ¶
var ErrEdgeRefused = errors.New("node: edge refused by local admission policy")
ErrEdgeRefused means the application's own edge-admission policy (WithEdgeAdmission) refused the peer, so the dialed connection was closed instead of becoming a live edge. The peer was reachable and cleared every verifiable gate — this is a local choice, not a network failure.
var ErrPoWUnmet = errors.New("node: peer does not meet PoW difficulty")
ErrPoWUnmet means a dialed peer's NodeID does not clear the PoW difficulty, so it is not taken as a live edge (level-2 admission-PoW). It lets a Connect/HolePunch caller tell "peer failed the work" apart from "could not reach the peer" (ErrUnroutable).
var ErrUnroutable = errors.New("node: no live edge toward target")
ErrUnroutable means Send found no live edge to launch a packet toward the target. It is the originator's view of an empty live-edge set, distinct from a packet that is launched and later dropped mid-overlay.
var ErrUnsupported = errors.New("node: operation unsupported by transport")
ErrUnsupported means the operation needs a transport capability this transport does not provide — e.g. HolePunch on the in-memory transport, which has no NAT to punch.
Functions ¶
This section is empty.
Types ¶
type ID ¶
ID re-exports kad.ID so callers of the public API can spell it node.ID without importing kad. The alias keeps the dependency one-way (node -> kad); the fundamental key type still lives at the bottom of the DAG.
type Inbound ¶
type Inbound struct {
Originator kad.ID // DeriveID(originator ed_pub) — who sent it
Payload []byte
}
Inbound is one message delivered to this node because it is the target. Payload is a copy, owned by the receiver and safe to keep past the dispatch loop's Release of the underlying packet.
type Maintenance ¶
type Maintenance struct {
Tick time.Duration // base scan cadence (fill + keepalive sweep)
KeepaliveSibling time.Duration // idle time before pinging a sibling edge
KeepaliveFinger time.Duration // idle time before pinging a finger edge (looser)
DeadSibling time.Duration // idle time before reaping a sibling edge
DeadFinger time.Duration // idle time before reaping a finger edge
SelfLookup time.Duration // how often to self-lookup for sibling discovery
SiblingExchange time.Duration // how often to exchange sibling sets
BucketRefresh time.Duration // how often to check for a stale knowledge bucket to refresh
BucketStaleAfter time.Duration // how long a populated bucket may go unrefreshed
DialTimeout time.Duration // per-dial deadline
BackoffBase time.Duration // first re-dial delay after a failed dial
BackoffMax time.Duration // ceiling on the exponential backoff
Dialers int // concurrent dial workers
}
Maintenance is the upkeep policy for both tables: how often the node scans its edges, how long an idle edge waits before a keepalive and before being declared dead, how often it self-looks-up, exchanges sibling sets and refreshes a stale knowledge bucket, and how it backs off re-dialing a peer that will not answer (with the ladder's exhaustion doubling as the knowledge table's lazy-purge signal). All of it is level-3 local policy — a deployer tunes it freely without splitting the network. Differentiated timeouts (siblings tighter than fingers) trade a little traffic for faster detection on the correctness-critical edges, per the connectivity model. The loop that acts on this policy is Node.maintainLoop (in node.go).
func DefaultMaintenance ¶
func DefaultMaintenance() Maintenance
DefaultMaintenance is the production policy: keepalive in the 15–25 s band the connectivity model calls for (siblings tighter), reap at three missed keepalives, periodic self-lookup and sibling exchange, exponential re-dial backoff.
type Node ¶
type Node struct {
// contains filtered or unexported fields
}
Node is an overlay member: an identity, the knowledge and live-edge tables, a transport, and the dispatch loop that forwards over them.
func New ¶
New builds a Node over the given identity and transport. The transport's LocalID must match the identity's NodeID (the caller wires them together — e.g. via the in-memory hub). It creates the knowledge and live-edge tables and the delivery channel; start the loop with Run.
func (*Node) Bootstrap ¶
Bootstrap seeds the knowledge table with starting contacts — the entry points and anchors the maintenance loop dials to climb off zero connectivity. It is the way in for a fresh node (and the test harness): without at least one dialable contact the fill loop has nowhere to begin. Each contact is observed as of now.
func (*Node) Connect ¶
Connect establishes a direct, authenticated live edge to target — the rendezvous → direct-channel handoff. An edge that already exists (the target connected to us first, or simultaneous Connects crossed) is returned as is — it is authenticated to the same NodeID, so a rendezvous would prove nothing new and a duplicate dial would only be folded back into it. Otherwise it discovers and verifies target's coordinates via rendezvous (so the edge it opens is to the real target, BLAKE2b(ed_pub) == target), then opens the edge: a quick direct dial for a publicly reachable peer, falling back to a hole-punch for a peer behind NAT. It returns the live edge, ErrUnroutable if no candidate could be reached, or ctx.Err() on timeout.
func (*Node) Deliveries ¶
Deliveries is the stream of messages addressed to this node. The channel closes when the node's Run returns, so a consumer ranging it unblocks on shutdown.
func (*Node) Edges ¶
Edges returns the live-edge table (the maintenance loop and tests wire edges here).
func (*Node) HolePunch ¶
func (n *Node) HolePunch(ctx context.Context, target kad.ID, hints []transport.Addr) (transport.Conn, error)
HolePunch opens a direct edge to target across NATs and registers it as an outgoing live edge. See holePunchRaw for the orchestration; the resulting edge passes the admission-PoW check before it is taken.
func (*Node) InboundMedia ¶ added in v0.2.0
func (n *Node) InboundMedia() <-chan transport.MediaSession
InboundMedia is the stream of inbound media sessions that passed the admission gates: PoW, the session caps, and the application's consent callback (WithMediaConsent — without it everything is refused). The application owns each session it takes and must Close it. The channel drains shut when the node's Run ends (on a transport without media it never yields and never closes).
func (*Node) OpenMedia ¶ added in v0.2.0
OpenMedia opens a media session to target — the foundation of a call. The session rides the path of the live overlay edge to target: if one is up, its observed address is dialed directly (same socket, same 4-tuple, the NAT mapping already proven); otherwise the full Connect cascade runs first (rendezvous → direct dial / hole-punch / relay) and the session follows the edge it established. The session is OWNED BY THE CALLER: close it when the call ends; its life never touches the edge tables. Re-establishing after path death (ErrMediaClosed) is a fresh OpenMedia. Several sessions to one peer are legal — open a second over a better path, switch, close the old one (make-before-break).
It returns ErrUnsupported if the transport has no media capability, transport.ErrMediaUnsupported if the PEER has none (the edge keeps working — fall back to overlay messaging), or the Connect cascade's error when no path exists. A media dial that fails toward a live edge's own address is treated as a liveness signal about that edge: the edge is pinged out of schedule, so a dead path is reaped and re-dialed instead of being trusted again.
func (*Node) Reflexive ¶
Reflexive returns this node's confirmed externally-visible address — the one enough distinct neighbours have agreed they saw it at — or the zero Addr until that corroboration arrives.
func (*Node) Rendezvous ¶
Rendezvous discovers and authenticates the keys and coordinates of the node R with NodeID target. It originates a signed Hello routed to target, waits for R's signed Reply (routed back), verifies it (BLAKE2b(ed_pub_R) == target and the signature, so no forwarder on the path can answer in R's place), and returns R's verified keys and coordinates. The exchanged coordinates are where the two peers would then open a DIRECT channel via hole-punching — Connect runs that handoff; Rendezvous returns once the coordinates are verified.
It blocks until the reply arrives or ctx is done (caller sets the timeout). It returns ErrUnroutable if there is no live edge to launch the hello from, or ctx.Err() on timeout/cancel. The learned contact is folded into the knowledge table.
func (*Node) Run ¶
Run is the dispatch loop: it pulls every frame off the transport's single inbound stream and handles it, until ctx is cancelled or the transport closes its stream. It is single-goroutine by design — one goroutine, one select over the inbound stream — which is what lets handle reuse scratch buffers without locks. If maintenance is enabled it also starts the maintenance loop. Run returns ctx.Err() on cancellation and nil on a clean transport shutdown — and on EVERY exit path it first stops and waits out the maintenance loop and its dial workers, and cancels the fire-and-forget punch bursts (they watch the loop's context and wind down on their own), so its return is the node's end of life, not the start of a background leak.
func (*Node) Send ¶
Send originates a routing message toward target along up to d (= routing.KMin) disjoint paths: it picks the d closest live edges as distinct first hops and sends one copy down each, every copy carrying the OTHER first hops in its avoid-set so the branches steer apart in the middle and reconverge near the target.
It returns ErrUnroutable if there is no live edge to launch from and the encode error if the payload does not fit a frame; otherwise nil. The disjoint copies are dispatched concurrently (see originate), so the call does not block on any one (possibly congested) socket and a single failing first hop does not sink the request — the surviving paths still carry it. Because origination never blocks, Send takes no context — there is nothing for one to cancel; SendDirect and Connect, which do block on the network, are the ctx-aware calls.
func (*Node) SendDirect ¶
SendDirect sends an application payload to target over a direct edge: the live edge to target if one is up, otherwise Connect (rendezvous, then direct dial / hole-punch / relay). Unlike Send, the bytes never transit other nodes — this is the conversation path; Send remains the small-control / presence path.
On the remote side the payload surfaces on Deliveries(), same as Send. Like every overlay path it is best-effort (a stalled remote consumer drops), so an application that needs reliability must acknowledge and retry at its own layer.
type Option ¶
type Option func(*Node)
Option configures a Node at construction.
func WithDmin ¶
WithDmin sets the origination-PoW difficulty (leading zero bits a sender's NodeID must clear). It is a level-1 network constant whose value the deployer picks; tests use 0 (no work required).
func WithEdgeAdmission ¶ added in v0.2.0
WithEdgeAdmission sets the application's policy gate for live edges: it is consulted with the authenticated NodeID of every peer about to become one — inbound on its first frame, outbound before registration (Connect, hole-punch, relay, the maintenance dialer) — and a false return refuses the edge: the connection is closed, an outbound attempt fails with ErrEdgeRefused, and the refusal is counted in Stats. The callback must be fast and non-blocking (it runs on the dispatch loop).
This is level-3 local policy, NOT a security boundary: a refused peer can still route messages to this node through other forwarders (filter those by Inbound.Originator) and still learns whatever any overlay member can learn; security rests on the verifiable gates — PoW, signatures, caps, rate limits — which run regardless of this policy. Mind the cost of generosity in reverse, too: every refused peer is one fewer neighbour to route over, so a broad ban list narrows this node's own connectivity.
func WithForwardSendDeadline ¶ added in v0.2.1
WithForwardSendDeadline caps how long a single peer-bound send — a forward, a control answer, a keepalive — may block on the dispatch loop before the stuck edge is dropped (a forward then falls to the next disjoint candidate) — so a slow or hostile peer cannot freeze the whole node, even under a transport configured with no send deadline. A non-positive value falls back to the transport's own Send bound. The default is generous (defaultForwardSendDeadline).
func WithInboundBuffer ¶
WithInboundBuffer sets the depth of the delivered-message channel.
func WithMaintenance ¶
func WithMaintenance(m Maintenance) Option
WithMaintenance sets the live-edge maintenance policy and enables the maintenance loop. The zero fields of m fall back to DefaultMaintenance, so a caller can tune just the intervals it cares about.
func WithMediaConsent ¶ added in v0.2.0
WithMediaConsent sets the application's gate for inbound media sessions: it is called with the authenticated NodeID of each (PoW-cleared, within-caps) caller, and only a true return admits the session to InboundMedia. Without this option every inbound session is refused — secure by default; an application that takes calls must opt in. The callback runs on the media gate goroutine and must be fast and non-blocking (answering the human's "accept the call?" belongs in the application, on the already-admitted session). level-2 self-protection.
func WithRand ¶
WithRand sets the randomness source for rendezvous nonces. Default is crypto/rand.Reader; a test can inject a deterministic reader. It does not affect any security-relevant key material (identities are derived from their seed).
func WithRelay ¶
func WithRelay() Option
WithRelay marks this node a relay volunteer: it advertises the CanRelay capability to peers and serves relay requests (splicing a tunnel for two peers that cannot hole-punch). It has effect only if the transport implements transport.Relayer.
func WithSubnetFunc ¶
func WithSubnetFunc(f routing.SubnetFunc) Option
WithSubnetFunc sets the subnet derivation the routing tables use for diversity accounting. Default is routing.NoSubnet (the in-memory transport has no IPs).
func WithoutMaintenance ¶
func WithoutMaintenance() Option
WithoutMaintenance disables the maintenance loop: the node forwards and answers control frames but does not dial, keepalive, self-lookup, or exchange on its own. Useful for tests that drive the topology by hand.
type RendezvousResult ¶
RendezvousResult is the verified outcome of a rendezvous: the keys and coordinates of the target node R, authenticated against its NodeID. EdPub hashes to Target (the anti-MITM guarantee — a forwarder cannot answer in R's place), XPub is R's static X25519 public key for sealed-box e2e, and Addrs are R's coordinates. The direct-channel handoff (dialing or hole-punching to Addrs) is Connect's job; the result stops at the verified coordinates.
type Stats ¶
type Stats struct {
DroppedSubPoW uint64 // frames from a peer/originator below the PoW threshold
DroppedStale uint64 // routed frames outside the freshness window (replayed/old)
DroppedBadSig uint64 // routed frames whose originator signature did not verify
DroppedRateLimited uint64 // control/amplifier frames shed by a rate limiter
DroppedInboundFull uint64 // conns refused (and closed) because the inbound-edge cap is reached
DroppedEdgeRefused uint64 // edges (in or out) refused by the application's WithEdgeAdmission policy
DroppedMalformed uint64 // unparseable frames from an unregistered conn (the conn is closed)
DroppedDupInbound uint64 // duplicate inbound conns for an already-edged peer (the conn is closed)
DroppedDeliverFull uint64 // delivered messages shed because the application's Deliveries buffer was full
// Inbound media-session admission refusals (per-session drops live in the
// session's own MediaStats; these count whole sessions this node refused).
DroppedMediaSubPoW uint64 // inbound media sessions from a sub-PoW identity
DroppedMediaConsent uint64 // inbound media sessions the consent gate refused (nil gate refuses all)
DroppedMediaCap uint64 // inbound media sessions past the session caps (per node / per IP) or unconsumed
}
Stats is a snapshot of a node's defensive drop counters. It exists for observability: under attack these rates spike, so an operator (or a test) can see a flood being shed instead of guessing. The counters are monotonic since start.