host

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: May 7, 2026 License: MPL-2.0 Imports: 39 Imported by: 0

Documentation

Overview

Package host is the Phase 2 SDK: DHT + QUIC, nat-sense, multi-candidate endpoints (2b), optional UPnP, Happy Eyeballs dial, Publish/Resolve/Connect/Accept.

A Host manages the node-level resources (DHT, QUIC transport). One or more agents (each identified by a unique Address) share the same QUIC listener via TLS SNI routing (spec Phase 2a "mux" module).

When Config.QUICListenAddr is empty, DHT and QUIC share one UDP port via UDPMux (spec target). When QUICListenAddr is set, QUIC uses a separate socket — recommended until mux is hardened on all platforms.

Index

Constants

View Source
const ALPNQuic = "a2al-quic-1"
View Source
const DefaultConnectStagger = 250 * time.Millisecond

DefaultConnectStagger is the delay between starting each QUIC dial (Happy Eyeballs).

View Source
const DefaultICEStagger = 2 * time.Second

DefaultICEStagger is the delay before starting the ICE path when racing direct QUIC against ICE in parallel. This gives direct QUIC a head start: on open networks (FullCone / public IP) the handshake completes well under 1 s, so the ICE path is never needed. On NAT-restricted networks where direct QUIC cannot succeed, ICE kicks in after this delay and the overall connection time is stagger + ICE setup (~2 s) rather than HandshakeIdleTimeout (10 s) + ICE setup.

Variables

View Source
var ErrNoAgent = errors.New("a2al/host: target agent not registered")

ErrNoAgent is returned by runICESession when the signaling hub reports that the target agent is not currently registered ("noagent" frame). Callers may retry after a brief delay to ride out a callee reconnect window.

Functions

func FirstQUICAddr

func FirstQUICAddr(er *protocol.EndpointRecord) (*net.UDPAddr, error)

FirstQUICAddr returns the first quic:// (or legacy udp://) endpoint as a UDP address.

func PeerAddressFromConn

func PeerAddressFromConn(tlsPeerCerts []*x509.Certificate) (a2al.Address, error)

PeerAddressFromConn extracts the remote peer's AID from a QUIC connection's TLS state (works after mutual TLS handshake). For Phase 3 delegated agents, it returns the AID from the delegation extension rather than the op-key-derived address.

func QUICDialTargets

func QUICDialTargets(er *protocol.EndpointRecord) ([]*net.UDPAddr, error)

QUICDialTargets returns ordered, deduplicated UDP addresses from quic:// / udp:// entries.

Types

type AgentConn

type AgentConn struct {
	quic.Connection
	// Local is the agent Address that was targeted (agent-route frame, or SNI fallback).
	Local a2al.Address
	// Remote is the connecting peer's Address (from mutual TLS client cert).
	Remote a2al.Address
}

AgentConn wraps a QUIC connection with the resolved peer and local agent identities.

type Config

type Config struct {
	KeyStore crypto.KeyStore
	// ListenAddr is the DHT UDP bind address, e.g. ":5001".
	// Currently resolved as udp4; dual-stack (udp / "[::]:port") is planned.
	ListenAddr string
	// QUICListenAddr, if non-empty, is a separate UDP bind for QUIC.
	// Same udp4 constraint as ListenAddr.
	QUICListenAddr   string
	PrivateKey       ed25519.PrivateKey
	MinObservedPeers int
	FallbackHost     string
	// DisableUPnP skips IGD port mapping for the QUIC UDP port (Phase 2b).
	DisableUPnP bool

	// ICESignalURL is the primary WebSocket base URL for ICE signaling (single URL, backward compat).
	// Superseded by ICESignalURLs when that field is non-empty.
	ICESignalURL string
	// ICESignalURLs lists WebSocket base URLs for ICE signaling (multi-center support).
	// When non-empty, supersedes ICESignalURL. The first URL is also written to
	// EndpointPayload.Signal (CBOR key 3) for backward compatibility with old nodes.
	ICESignalURLs []string
	// ICESTUNURLs lists stun: URIs for ICE gathering; empty means default public STUN when no TURN is configured.
	ICESTUNURLs []string
	// ICETURNURLs lists turn: URIs with embedded credentials for ICE relay (legacy format).
	// Use TURNServers for new deployments; both fields are processed when set.
	ICETURNURLs []string
	// TURNServers lists TURN relay servers with structured credential configuration.
	// Supports Static, HMAC (coturn use-auth-secret), and REST API credential types.
	// Credentials are resolved per ICE session and never published to the DHT.
	TURNServers []TURNServer
	// ICEPublishTurns is retained for decoding old records; new nodes do not publish turns[].
	// Deprecated: callee-pays TURN relay addresses are exchanged via trickle ICE, not the DHT.
	ICEPublishTurns []string
	// Logger is forwarded to the DHT node for diagnostic logging (reply failures, RPC retries).
	// If nil, slog.Default() is used.
	Logger *slog.Logger
	// SeenPeersPath is forwarded to the DHT node for seenPeers persistence (spec §7.3).
	// Empty disables persistence.
	SeenPeersPath string
	// ICENetworkTypes lists the ICE network types used for candidate gathering.
	// Defaults to {ice.NetworkTypeUDP4} (IPv4-only) when nil or empty.
	// To enable IPv6 ICE, set to {ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} after
	// dual-stack socket support is implemented (Layer 1 of the IPv6 support plan).
	ICENetworkTypes []ice.NetworkType
}

Config wires DHT + QUIC.

IPv6 note: currently Host binds udp4 sockets only. The Transport interface, protocol wire format (NodeInfo.IP 4/16 bytes, observed_addr 6/18 bytes), and endpoint URL model ("quic://[v6]:port") are all IPv6-ready. Dual-stack requires changing listenUDP4 in New() — either "udp" dual-stack or separate v4+v6 listeners — and adding v6 candidate collection in candidates.go. No interface or data-model changes are expected.

type DHTpunchPool added in v0.1.8

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

DHTpunchPool implements dht.PunchTransport for the host layer.

Lifecycle: create with newDHTpunchPool before dht.NewNode, inject into dht.Config.PunchTransport, then call bind(host) after the Host is fully initialised. This two-phase setup avoids the dht.Node / Host circular init.

func (*DHTpunchPool) ConnCount added in v0.1.8

func (p *DHTpunchPool) ConnCount() int

ConnCount returns the number of active Mode B connections (for diagnostics).

func (*DHTpunchPool) HandleIncomingPunch added in v0.1.8

func (p *DHTpunchPool) HandleIncomingPunch(ctx context.Context, callerNodeID a2al.NodeID, callerLogicalAddr a2al.Address, signalBase, room string)

HandleIncomingPunch is called by the daemon ICE listener when a Mode B punch incoming is received (fr.Target == nodeAddr). It runs ICE as controlled, accepts a QUIC connection, and calls OnPunchComplete on the DHT node.

func (*DHTpunchPool) HasConn added in v0.1.8

func (p *DHTpunchPool) HasConn(nodeID a2al.NodeID) bool

HasConn implements dht.PunchTransport. Returns true if an active Mode B QUIC connection exists for nodeID.

func (*DHTpunchPool) Punch added in v0.1.8

func (p *DHTpunchPool) Punch(nodeID a2al.NodeID, er *protocol.EndpointRecord, priority int)

Punch implements dht.PunchTransport. Spawns a goroutine that dials ICE → QUIC (Mode B) and calls OnPunchComplete.

func (*DHTpunchPool) SendTo added in v0.1.8

func (p *DHTpunchPool) SendTo(ctx context.Context, nodeID a2al.NodeID, msg []byte) (bool, error)

SendTo implements dht.PunchTransport. Looks up the Mode B connection for nodeID; if found, opens a QUIC stream and writes msg. Returns (false, nil) when no connection is available so the DHT falls back to UDP transparently.

type Host

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

Host is the Phase 2a runtime.

Identity layering strategy:

  • DHT and signaling transport use node identity.
  • QUIC mutual-TLS uses agent identity certificates (including delegated agent cert extensions when applicable).
  • Each QUIC connection represents a (localAgent, remoteAgent) pair.
  • The gateway relies on AgentConn.Remote as the authenticated caller AID.

func New

func New(cfg Config) (*Host, error)

New creates a Host with one initial agent identity from cfg.KeyStore.

func (*Host) Accept

func (h *Host) Accept(ctx context.Context) (*AgentConn, error)

Accept waits for an incoming QUIC connection and returns an AgentConn.

Agent routing priority:

  1. Agent-route control stream (first stream: magic + 21-byte Address) — canonical.
  2. TLS SNI (Address hex) — fast-path hint when not camouflaged.
  3. Default to the host's own Address.

Remote peer AID is extracted from the mutual TLS client certificate.

func (*Host) AcceptICEViaSignal

func (h *Host) AcceptICEViaSignal(ctx context.Context, localAgent, expectRemote a2al.Address, signalBase string) (*AgentConn, error)

AcceptICEViaSignal is the controlled (callee) side: WebSocket ICE signaling on signalBase, then QUIC-over-ICE. expectRemote is the caller's agent address.

Same lifetime semantics as connectViaICESignal — resources are freed when the AgentConn's underlying QUIC connection closes.

func (*Host) Address

func (h *Host) Address() a2al.Address

func (*Host) BuildEndpointPayload

func (h *Host) BuildEndpointPayload(ctx context.Context) (protocol.EndpointPayload, error)

BuildEndpointPayload builds ordered, deduplicated quic:// candidates (Phase 2b). UPnP discovery and external IP probing (STUN + HTTP) run concurrently.

func (*Host) Close

func (h *Host) Close() error

func (*Host) Connect

func (h *Host) Connect(ctx context.Context, expectRemote a2al.Address, udpAddr *net.UDPAddr) (quic.Connection, error)

Connect dials the remote agent over QUIC with mutual TLS. After the QUIC handshake, it opens a control stream and sends the agent-route frame (4-byte magic + 21-byte target Address) so the server can route the connection even when TLS SNI is camouflaged.

func (*Host) ConnectFromRecord

func (h *Host) ConnectFromRecord(ctx context.Context, expectRemote a2al.Address, er *protocol.EndpointRecord) (_ quic.Connection, err error)

ConnectFromRecord dials expectRemote using the following strategy:

  • When both direct QUIC targets and a Signal URL are available and the NAT type does not mandate skipping direct: race direct QUIC against ICE in parallel (see connectRace). Direct gets a DefaultICEStagger head start.
  • When only direct targets are available (no Signal): Happy Eyeballs only.
  • When direct must be skipped or no targets exist (Signal required): ICE only.

The host's default agent identity is used for mutual TLS.

On any failure the locally-cached endpoint record for expectRemote is transparently invalidated so the next Resolve fetches fresh data.

func (*Host) ConnectFromRecordFor

func (h *Host) ConnectFromRecordFor(ctx context.Context, localAgent, expectRemote a2al.Address, er *protocol.EndpointRecord) (_ quic.Connection, err error)

ConnectFromRecordFor dials as localAgent (must be registered) toward expectRemote. Uses the same race/direct/ICE strategy as ConnectFromRecord.

On failure the locally-cached endpoint record for expectRemote is transparently invalidated (same as ConnectFromRecord).

func (*Host) DHTLocalAddr

func (h *Host) DHTLocalAddr() *net.UDPAddr

func (*Host) DHTpunchPool added in v0.1.8

func (h *Host) DHTpunchPool() *DHTpunchPool

func (*Host) DebugHTTPHandler

func (h *Host) DebugHTTPHandler() http.Handler

DebugHTTPHandler returns an http.Handler serving /debug/host (Phase 2 state) and delegates /debug/identity, /debug/routing, /debug/store, /debug/stats to the underlying DHT node.

func (*Host) DecryptMailboxRecords added in v0.1.7

func (h *Host) DecryptMailboxRecords(agentAddr a2al.Address, recs []protocol.SignedRecord) ([]protocol.MailboxMessage, error)

DecryptMailboxRecords decrypts the given raw SignedRecords using the private key of agentAddr. Callers that obtained records from an out-of-band source (e.g. fallback infrastructure) can use this to reuse the standard decryption path without triggering a DHT query.

func (*Host) EffectiveICESignalBase added in v0.1.4

func (h *Host) EffectiveICESignalBase() string

EffectiveICESignalBase returns the primary ICE signaling WebSocket base URL published in endpoint records: explicit config overrides bootstrap-derived value.

func (*Host) EffectiveICESignalURLs added in v0.1.7

func (h *Host) EffectiveICESignalURLs() []string

EffectiveICESignalURLs returns the full ordered list of ICE signaling base URLs. Callee subscribers should open one /signal connection per URL. Callers should try each URL in order until one succeeds.

func (*Host) FindRecords

func (h *Host) FindRecords(ctx context.Context, target a2al.Address, recType uint8) ([]protocol.SignedRecord, error)

FindRecords runs iterative FIND_VALUE for the given RecType filter (0 = all types).

func (*Host) InvalidateNetworkCaches added in v0.1.4

func (h *Host) InvalidateNetworkCaches()

InvalidateNetworkCaches clears short-lived external-network caches so subsequent endpoint building uses fresh probes after network changes.

func (*Host) LocalUDPAddr

func (h *Host) LocalUDPAddr() *net.UDPAddr

func (*Host) Node

func (h *Host) Node() *dht.Node

func (*Host) ObserveFromPeers

func (h *Host) ObserveFromPeers(ctx context.Context, seeds []net.Addr)

func (*Host) ObserveFromRouting added in v0.1.4

func (h *Host) ObserveFromRouting(ctx context.Context, n int) int

ObserveFromRouting samples current routing-table candidates and performs passive observed_addr collection from them.

func (*Host) PollMailbox

func (h *Host) PollMailbox(ctx context.Context) ([]protocol.MailboxMessage, error)

PollMailbox aggregates mailbox records for the host default AID (spec §4.4–4.6).

func (*Host) PollMailboxForAgent

func (h *Host) PollMailboxForAgent(ctx context.Context, agentAddr a2al.Address) ([]protocol.MailboxMessage, error)

PollMailboxForAgent aggregates and decrypts mailbox records for agentAddr.

func (*Host) PublishEndpoint

func (h *Host) PublishEndpoint(ctx context.Context, seq uint64, ttl uint32) error

func (*Host) PublishEndpointBuilt added in v0.1.3

func (h *Host) PublishEndpointBuilt(ctx context.Context, ep protocol.EndpointPayload, seq uint64, ttl uint32) error

PublishEndpointBuilt publishes the node endpoint using a pre-built EndpointPayload. Use this when the payload has already been constructed (e.g. to avoid a redundant probe).

func (*Host) PublishEndpointForAgent

func (h *Host) PublishEndpointForAgent(ctx context.Context, agentAddr a2al.Address, seq uint64, ttl uint32) error

PublishEndpointForAgent publishes an endpoint record signed by the given registered agent. For Phase 3 delegated agents (registered via RegisterDelegatedAgent), the record embeds the DelegationProof so DHT nodes can verify the operational key's authority.

func (*Host) PublishRecord

func (h *Host) PublishRecord(ctx context.Context, rec protocol.SignedRecord) error

PublishRecord pushes a signed sovereign record (RecType 0x01–0x0F) to the DHT. Returns an error if rec is not a sovereign-category record; use PublishTopicRecord / host mailbox APIs for other categories.

func (*Host) QUICLocalAddr

func (h *Host) QUICLocalAddr() *net.UDPAddr

func (*Host) RegisterAgent

func (h *Host) RegisterAgent(addr a2al.Address, priv ed25519.PrivateKey) error

RegisterAgent adds an additional agent identity to this host's SNI router. Incoming connections with TLS ServerName matching addr will be served with the corresponding certificate. Returns an error if the address is already registered.

func (*Host) RegisterDelegatedAgent

func (h *Host) RegisterDelegatedAgent(addr a2al.Address, opPriv ed25519.PrivateKey, delegationCBOR []byte) error

RegisterDelegatedAgent adds a Phase 3 agent whose operational key is authorized by a DelegationProof (delegationCBOR). The proof is embedded in endpoint records so DHT nodes can verify the authority of the operational key independently.

func (*Host) RegisterTopic

func (h *Host) RegisterTopic(ctx context.Context, topic string, entry protocol.TopicPayload, ttl uint32) error

RegisterTopic publishes one topic registration for the host default identity (spec §5.7).

func (*Host) RegisterTopicForAgent

func (h *Host) RegisterTopicForAgent(ctx context.Context, agentAddr a2al.Address, topic string, entry protocol.TopicPayload, ttl uint32) error

RegisterTopicForAgent signs and stores a topic record for a registered agent (delegation-aware).

func (*Host) RegisterTopics

func (h *Host) RegisterTopics(ctx context.Context, topics []string, base protocol.TopicPayload, ttl uint32) error

RegisterTopics registers under multiple topic strings for the default identity (spec §5.7).

func (*Host) RegisterTopicsForAgent

func (h *Host) RegisterTopicsForAgent(ctx context.Context, agentAddr a2al.Address, topics []string, base protocol.TopicPayload, ttl uint32) error

RegisterTopicsForAgent is RegisterTopics for a registered agent address.

func (*Host) RegisteredAgents

func (h *Host) RegisteredAgents() []a2al.Address

RegisteredAgents returns the addresses of all registered agents.

func (*Host) Resolve

func (h *Host) Resolve(ctx context.Context, target a2al.Address) (*protocol.EndpointRecord, error)

func (*Host) RunNATProbe added in v0.1.4

func (h *Host) RunNATProbe(ctx context.Context)

RunNATProbe performs an AutoNAT-style active reachability test to classify NAT type. At most one probe runs at a time (TryLock); concurrent callers return immediately.

Classification logic:

QUIC bind IP is public WAN              → sense.RecordBindPublic(true); return (no probe needed)
probe echo received from ≥1 candidate   → sense.RecordProbeResult(true)  [Full Cone / cloud NAT]
no echo despite known external address  → sense.RecordProbeResult(false) [Restricted]

func (*Host) SearchTopic

func (h *Host) SearchTopic(ctx context.Context, topic string) ([]protocol.TopicEntry, error)

SearchTopic runs AggregateRecords on the topic key and returns verified entries (spec §5.5).

func (*Host) SearchTopics

func (h *Host) SearchTopics(ctx context.Context, topics []string) ([]protocol.TopicEntry, error)

SearchTopics returns agents registered on all given topics (intersection by AID) (spec §5.5). The returned TopicEntry values are taken from the first topic's results; fields from subsequent topics are not merged.

func (*Host) SendMailbox

func (h *Host) SendMailbox(ctx context.Context, recipient a2al.Address, msgType uint8, body []byte) error

SendMailbox encrypts a message for recipient using the host default identity (spec §4.4–4.6).

func (*Host) SendMailboxForAgent

func (h *Host) SendMailboxForAgent(ctx context.Context, agentAddr, recipient a2al.Address, msgType uint8, body []byte) error

SendMailboxForAgent encrypts and stores a mailbox record signed by the given registered agent. Delegated agents use SignRecordDelegated (same authority model as PublishEndpointForAgent).

func (*Host) Sense

func (h *Host) Sense() *natsense.Sense

func (*Host) SetActiveSignalURLs added in v0.1.8

func (h *Host) SetActiveSignalURLs(urls []string)

SetActiveSignalURLs records the set of signal hub URLs with live connections. Called by ice_listener when the active set changes; BuildEndpointPayload uses this list (instead of all candidates) to avoid publishing stale hub addresses.

func (*Host) SetBeaconStatsProvider added in v0.1.7

func (h *Host) SetBeaconStatsProvider(fn func() map[string]any)

SetBeaconStatsProvider registers fn to supply optional extra key-value fields merged into GET /debug/stats (e.g. metrics for the high-capacity auxiliary DHT path). Omitted when fn is nil; fields omitted when the callback returns nil or an empty map.

func (*Host) SetDerivedICESignalURLs added in v0.1.8

func (h *Host) SetDerivedICESignalURLs(urls []string)

SetDerivedICESignalURLs sets bootstrap-derived signal hub candidates. Explicit config (ICESignalURLs / ICESignalURL) always wins and is not overwritten.

func (*Host) SetSignalStatsProvider added in v0.1.4

func (h *Host) SetSignalStatsProvider(f func() map[string]any)

SetSignalStatsProvider merges hub stats into GET /debug/stats under "signal".

func (*Host) StartDebugHTTP

func (h *Host) StartDebugHTTP(addr string) (stop func(), err error)

StartDebugHTTP listens on addr and serves /debug/* JSON for both DHT and Phase 2 host state. Returns a stop function.

func (*Host) SymmetricNATReachabilityHint

func (h *Host) SymmetricNATReachabilityHint() string

SymmetricNATReachabilityHint returns a user-facing note when NAT looks symmetric. Phase 2b does not guarantee inbound QUIC from arbitrary peers; TURN is deferred.

func (*Host) UnregisterAgent

func (h *Host) UnregisterAgent(addr a2al.Address)

UnregisterAgent removes an agent from the SNI router (the default agent created at New() cannot be unregistered).

type TURNCredentialType added in v0.1.4

type TURNCredentialType int

TURNCredentialType selects how TURN credentials are obtained per session.

const (
	// TURNCredentialStatic uses a fixed username and password stored in config.
	TURNCredentialStatic TURNCredentialType = iota
	// TURNCredentialHMAC generates time-limited credentials from a shared secret
	// using the coturn use-auth-secret mechanism:
	//   username = strconv.FormatInt(unix_expiry, 10) + ":" + Username
	//   password = base64(HMAC-SHA1(Credential, username))
	TURNCredentialHMAC
	// TURNCredentialRESTAPI fetches short-lived credentials from an HTTP endpoint
	// before each ICE session. The response must be JSON with "username" and
	// "password" (or "credential") fields. Covers Twilio, Metered.ca, etc.
	TURNCredentialRESTAPI
)

type TURNServer added in v0.1.4

type TURNServer struct {
	// URL is the TURN server address without embedded credentials,
	// e.g. "turn:turn.example.com:3478?transport=udp".
	URL string
	// CredentialType selects the credential acquisition method.
	CredentialType TURNCredentialType
	// Username is the static username (Static) or base username prefix (HMAC).
	Username string
	// Credential is the static password (Static), the HMAC shared secret (HMAC),
	// or the Authorization header value for the REST API request (RESTAPI).
	Credential string
	// CredentialURL is the HTTP endpoint that returns short-lived credentials (RESTAPI only).
	CredentialURL string
}

TURNServer describes a TURN relay server and how to obtain credentials for it. Credentials are resolved fresh per ICE session; they are never published to the DHT.

Jump to

Keyboard shortcuts

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