Documentation
¶
Overview ¶
Package irohmesh is a transport substrate over go-iroh: the endpoint bring-up, connection, discovery, and bootstrap ritual that every go-iroh mesh consumer otherwise hand-rolls, plus the two primitives layered on top of it — ed25519 node identity and signed-JSON gossip.
The substrate fences go-iroh: callers see net.Conn streams and a backend-agnostic Discovery, never the underlying iroh.Conn or gossip types. Bind a NodeKey as the endpoint identity, Serve an application ALPN, dial a peer by ticket, and run a protocol over a plain net.Conn.
The blob content-addressed store lives in github.com/tmc/mlx-go-iroh/blob and the manifest pull-with-hub-fallback in github.com/tmc/mlx-go-iroh/manifest. Both depend only on go-iroh.
irohmesh has no mlx-go dependency: its name marks the ecosystem family, not a dependency. The only imports are go-iroh, golang.org/x, and the standard library, so a consumer pins go-iroh once through this module.
Index ¶
- Constants
- Variables
- func ParseBootstrap(s string) (netaddr.EndpointAddr, error)
- func ParseBootstraps(values []string) ([]netaddr.EndpointAddr, error)
- func PublishSigned(ctx context.Context, topic *gossip.Topic, k NodeKey, payload []byte) error
- func RosterBootstrap(rosterPubsBase64 []string, self key.EndpointID) ([]netaddr.EndpointAddr, error)
- func TicketFromAddr(id key.EndpointID, addr netip.AddrPort) string
- func VerifiedEnvelopes(topic *gossip.Topic) iter.Seq2[SignedEnvelope, error]
- func Verify(pub ed25519.PublicKey, msg, sig []byte) bool
- type Config
- type Conn
- type Discovery
- type Endpoint
- func (e *Endpoint) Addr() netaddr.EndpointAddr
- func (e *Endpoint) Close() error
- func (e *Endpoint) Connect(ctx context.Context, ticket, alpn string) (*Conn, error)
- func (e *Endpoint) ConnectAddr(ctx context.Context, addr netaddr.EndpointAddr, alpn string) (*Conn, error)
- func (e *Endpoint) ConnectID(ctx context.Context, id key.EndpointID, alpn string) (*Conn, error)
- func (e *Endpoint) DialableAddr() netaddr.EndpointAddr
- func (e *Endpoint) Endpoint() *iroh.Endpoint
- func (e *Endpoint) ID() key.EndpointID
- func (e *Endpoint) LocalAddr() netip.AddrPort
- func (e *Endpoint) LocalTicket() string
- func (e *Endpoint) LookupServices() *iroh.AddressLookupServices
- func (e *Endpoint) ResolveAddr(ctx context.Context, id key.EndpointID) (netaddr.EndpointAddr, error)
- func (e *Endpoint) Serve(appALPN string, h Handler, disc Discovery) error
- func (e *Endpoint) Subscribe(ctx context.Context, topic gossip.TopicID, bootstrap []netaddr.EndpointAddr) (*gossip.Topic, error)
- func (e *Endpoint) Ticket(addr netip.AddrPort) string
- func (e *Endpoint) WaitOnline(ctx context.Context) error
- type Handler
- type NodeKey
- type SignedEnvelope
Examples ¶
Constants ¶
const MaxGossipFrame = 4096
MaxGossipFrame is the maximum size of a single gossip message payload. The go-iroh gossip layer caps frames here, which is what forces control messages to stay small (heartbeats, commands) and pushes anything large (checkpoints) onto the blob path. SignEnvelope asserts the signed payload fits.
Variables ¶
var DefaultDiscoveryTopic = gossip.DefaultDiscoveryTopic
DefaultDiscoveryTopic is the gossip topic endpoints publish and resolve addressing on by default. It reuses go-iroh's default discovery topic; a namespaced topic is a one-line change at NewGossipDiscovery call sites.
var ErrInvalid = errors.New("irohmesh: invalid")
ErrInvalid reports invalid endpoint input or state.
Functions ¶
func ParseBootstrap ¶
func ParseBootstrap(s string) (netaddr.EndpointAddr, error)
ParseBootstrap parses one bootstrap string into an EndpointAddr. It accepts three forms:
- "endpointID@kind:value" — a transport address in go-iroh's kind-prefixed form, e.g. "id@ip:127.0.0.1:9000" or a relay or custom address.
- "endpointID@host:port" — a bare IP socket address, shorthand for the ip: form, e.g. "id@127.0.0.1:9000".
- "endpointID" — a bare global identity with no transport address. It is resolved at dial time through the endpoint's lookup services (pkarr or DNS), so it dials only from an endpoint bound with Config.Pkarr or another resolver; a gossip/LAN-only endpoint yields no address for it.
The "@" forms yield a seed the swarm can dial immediately, like a roster entry that already carries an address; the bare form yields an addr-less EndpointAddr, like a RosterBootstrap entry, whose address discovery supplies.
Example ¶
ExampleParseBootstrap parses an endpointID@transportAddr seed string.
package main
import (
"fmt"
irohmesh "github.com/tmc/mlx-go-iroh"
)
func main() {
// A fixed endpoint id (lowercase hex of an ed25519 public key) and a direct
// IP transport address.
const seed = "632c9edea77a6d157bcff57d92adf6bb7ac99a543a3709eb4f27c2b884f93a72@ip:1.2.3.4:5678"
addr, err := irohmesh.ParseBootstrap(seed)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(addr.Addrs()[0])
}
Output: ip:1.2.3.4:5678
func ParseBootstraps ¶
func ParseBootstraps(values []string) ([]netaddr.EndpointAddr, error)
ParseBootstraps parses a list of bootstrap strings (see ParseBootstrap for the accepted forms). A single malformed entry fails the whole list, since a seed list is operator input where a typo should surface, not be silently dropped.
func PublishSigned ¶
PublishSigned signs payload with k and broadcasts it to the topic. It is the send half of the signed-gossip pattern: the application serializes its own message (JSON or binary), this wraps and broadcasts it under the size bound.
func RosterBootstrap ¶
func RosterBootstrap(rosterPubsBase64 []string, self key.EndpointID) ([]netaddr.EndpointAddr, error)
RosterBootstrap maps a roster's ed25519 public keys (base64 std encoding) to the EndpointAddrs a gossip swarm seeds its join from. self is omitted so a node never bootstraps off itself. The addresses are intentionally empty: under the unified-key model a roster pubkey IS an endpoint id, so the swarm dials members it discovers an address for, and the allow-list is the roster itself. A row that does not decode to a valid ed25519 key is skipped — fail-soft on one malformed row rather than refusing the whole mesh.
func TicketFromAddr ¶ added in v0.3.0
func TicketFromAddr(id key.EndpointID, addr netip.AddrPort) string
TicketFromAddr mints an endpointticket for a raw id and IP address, without a bound endpoint. It is the operator-tool and test form: code that knows a peer's id and host:port but has no live Endpoint (for which Endpoint.Ticket and Endpoint.LocalTicket mint from the endpoint's own id). The result is a ticket a connector can dial without discovery.
func VerifiedEnvelopes ¶
VerifiedEnvelopes iterates the topic's received messages, decoding and verifying each as a SignedEnvelope and yielding only those that pass. A message that does not decode, fails verification, or is not a received-content event is skipped, so the iterator yields exactly the authenticated payloads. It ends when the topic's event stream ends or ctx is cancelled; a transport error from the underlying stream is yielded with a zero envelope.
Types ¶
type Config ¶
type Config struct {
BindAddr string
SecretKey string
Identity ed25519.PrivateKey
Relay bool
Pkarr bool
PkarrRelayURL string
PublishAllAddrs bool
}
Config is the validated set of go-iroh endpoint inputs Bind needs.
Identity, when set, is the node's ed25519 key used directly as the go-iroh endpoint key, so the wire EndpointID is the same key a roster records and a ledger signs with. It takes precedence over SecretKey, which remains for a string-encoded key (e.g. an operator environment variable). When neither is set, Bind generates an ephemeral key.
BindAddr is an "ip:port" address; an empty value uses the go-iroh default (an ephemeral port on all interfaces).
Relay and Pkarr enable global reachability and default to false, so a Config that leaves them unset binds a gossip/LAN endpoint exactly as before. With Pkarr set, Bind registers a pkarr publisher and resolver on the endpoint's lookup services and starts republishing the node's own address as it changes, so Endpoint.ResolveAddr and Endpoint.ConnectID then reach a peer by bare EndpointID across the internet. With Relay set, relay mode is enabled so NAT'd peers connect (relay first, then a hole-punched direct upgrade); a globally reachable node sets both.
PkarrRelayURL overrides the pkarr relay to publish to and resolve from; an empty value uses the number0 production relay. PublishAllAddrs publishes direct IP addresses in addition to relay addresses; the default publishes relay addresses only (go-iroh's RelayOnlyFilter), enough for reachability and leaking fewer addresses. Both apply only when Pkarr is set.
The zero Config is usable: it binds an ephemeral gossip/LAN endpoint with a generated identity.
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn is one accepted or dialed connection. It exposes the stream operations a protocol needs as net.Conn streams, so go-iroh stays fenced inside this package and callers run their protocol over a plain io.ReadWriteCloser.
func (*Conn) AcceptStream ¶
AcceptStream accepts the peer's next bidirectional stream as a net.Conn.
func (*Conn) OpenStream ¶
OpenStream opens a bidirectional stream as a net.Conn. The connector side opens; the listener side accepts the matching stream with Conn.AcceptStream.
func (*Conn) RemoteID ¶
func (c *Conn) RemoteID() key.EndpointID
RemoteID returns the verified endpoint id of the peer, used to attribute the connection to a known node.
type Discovery ¶
type Discovery interface {
// Start joins the discovery topic and begins publishing/resolving. It blocks
// until ctx is cancelled, like the underlying gossip backend.
Start(ctx context.Context) error
// Publish advertises this node's endpoint addressing to the swarm.
Publish(data dns.EndpointData)
// Resolve subscribes to a peer's addressing, yielding its iroh.Item each
// time the swarm learns a fresher address for it.
Resolve(ctx context.Context, id key.EndpointID) iter.Seq2[iroh.Item, error]
}
Discovery is the backend-agnostic endpoint-discovery seam. Its method set mirrors go-iroh's gossip discovery so the backend is a private implementation detail: Publish advertises this node's addressing, and Resolve is a subscription that re-emits a peer's addressing as it changes.
Resolve returning iter.Seq2[iroh.Item, error] leaks the iroh.Item type, the one deliberate seam leak; discovery is intrinsically about go-iroh addressing.
func NewGossipDiscovery ¶
func NewGossipDiscovery(ep *Endpoint, topic gossip.TopicID, bootstrap []netaddr.EndpointAddr) Discovery
NewGossipDiscovery builds a gossip-backed Discovery over a bound endpoint for the given topic. The returned Discovery carries the gossip protocol handler internally; Endpoint.Serve registers it under gossip.ALPN on the shared router, so the same endpoint serves both the application ALPN and gossip.ALPN. bootstrap is the swarm's reachable seed peers (see RosterBootstrap and ParseBootstraps).
The gossip discovery is registered as a resolver on the endpoint's lookup services, so Endpoint.ResolveAddr and Endpoint.ConnectID can dial a peer known only by id once the swarm has learned its address.
type Endpoint ¶
type Endpoint struct {
// contains filtered or unexported fields
}
Endpoint is a bound go-iroh endpoint. After Endpoint.Serve it also owns the iroh.Router that runs the accept loop for the application and gossip ALPNs. Construct with Bind; the zero value is unusable.
func Bind ¶
Bind validates cfg and binds a go-iroh endpoint WITHOUT registering ALPNs: the router started by Endpoint.Serve owns the accept loop, so the endpoint must not already be listening. Endpoint.Connect (the dial path) works on a bound endpoint with no Serve. The caller owns Endpoint.Close.
func (*Endpoint) Addr ¶
func (e *Endpoint) Addr() netaddr.EndpointAddr
Addr returns the endpoint's advertised address (identity plus any known transport addresses), the form a peer dials.
func (*Endpoint) Close ¶
Close shuts the endpoint down. It first stops the pkarr publish loop and publisher when Config.Pkarr was set, then shuts the transport: after Serve through router.Shutdown (which cancels the accept loop, runs handler Shutdown hooks, and closes the endpoint), otherwise the endpoint directly. Shutdown is idempotent, so a redundant Close is safe.
func (*Endpoint) Connect ¶
Connect dials the peer named by ticket on alpn and returns the resulting *Conn. ticket is an endpointticket-encoded peer address that already carries dialable transport addresses. The caller owns Close on the returned connection. To dial a peer known only by its id, use Endpoint.ConnectID.
func (*Endpoint) ConnectAddr ¶
func (e *Endpoint) ConnectAddr(ctx context.Context, addr netaddr.EndpointAddr, alpn string) (*Conn, error)
ConnectAddr dials addr on alpn. addr must carry at least one transport address (IP, relay, or custom); a bare-id addr fails, since go-iroh's Connect does not resolve addresses before dialing. Use Endpoint.ConnectID to resolve a bare id first.
func (*Endpoint) ConnectID ¶
ConnectID resolves a bare endpoint id through the lookup services and dials the resulting address on alpn. It is the global-discovery dial path: a peer is reached by id alone, with discovery supplying the address. When the only resolver is gossip, the peer must be reachable through the discovery swarm.
func (*Endpoint) DialableAddr ¶ added in v0.1.1
func (e *Endpoint) DialableAddr() netaddr.EndpointAddr
DialableAddr returns this endpoint's id paired with its bound local address, a dialable seed a peer can bootstrap from without discovery.
func (*Endpoint) Endpoint ¶
Endpoint exposes the underlying go-iroh endpoint for the blob and manifest layers, which need it to open blob streams. It is the one deliberate seam leak, scoped to this module's own sub-packages; application code stays on the fenced Conn/Discovery surface.
func (*Endpoint) ID ¶
func (e *Endpoint) ID() key.EndpointID
ID returns the bound endpoint's identity key.
func (*Endpoint) LocalAddr ¶ added in v0.1.1
LocalAddr returns the endpoint's bound UDP address. Combined with Endpoint.ID via netaddr.NewEndpointAddr(id).WithIP(localAddr), it forms a dialable seed address for a peer on a known host — the loopback and seed-list case.
func (*Endpoint) LocalTicket ¶
LocalTicket returns the ticket for this endpoint at its actual bound local address, so a node can publish its dial address without the caller knowing the resolved port (the ephemeral-bind case).
func (*Endpoint) LookupServices ¶
func (e *Endpoint) LookupServices() *iroh.AddressLookupServices
LookupServices returns the endpoint's address-lookup registry, so a caller can add a pkarr, DNS, or in-memory resolver beyond the gossip discovery that NewGossipDiscovery registers. Resolvers added here back Endpoint.ResolveAddr and Endpoint.ConnectID.
func (*Endpoint) ResolveAddr ¶
func (e *Endpoint) ResolveAddr(ctx context.Context, id key.EndpointID) (netaddr.EndpointAddr, error)
ResolveAddr resolves a bare endpoint id to a dialable address through the endpoint's address-lookup services (gossip discovery, and any pkarr or DNS resolver the caller added). It returns the first result whose address is non-empty, or an error if no service yields one. It exists because go-iroh's Connect consults lookup services only after a connection is established, so a peer known only by id cannot be dialed without resolving first.
func (*Endpoint) Serve ¶
Serve starts the iroh.Router on this endpoint: it registers appALPN to h and, when disc carries a gossip handler, gossip.ALPN to it — one router serving both ALPNs. disc may be nil for an endpoint with no discovery. Serve returns once the accept loop is running in the background; it does not block. It must be called at most once, on an endpoint bound without ALPNs. After Serve, Endpoint.Close shuts the router (and the endpoint) down.
func (*Endpoint) Subscribe ¶ added in v0.1.1
func (e *Endpoint) Subscribe(ctx context.Context, topic gossip.TopicID, bootstrap []netaddr.EndpointAddr) (*gossip.Topic, error)
Subscribe joins the gossip topic and returns it for signed publish/receive via PublishSigned and VerifiedEnvelopes. bootstrap seeds the swarm. The topic shares the endpoint's gossip instance, so Endpoint.Serve must register the gossip ALPN (it does whenever a gossip Discovery or any Subscribe is in use). The caller closes the returned topic.
func (*Endpoint) Ticket ¶
Ticket returns an endpointticket-encoded address for this endpoint at addr, so a connector can dial it without discovery. Used to point one endpoint at another on a known address (e.g. a loopback test or a seed list).
func (*Endpoint) WaitOnline ¶ added in v0.2.0
WaitOnline blocks until the endpoint has registered with a relay, so its published address carries a reachable relay URL, then publishes that complete address immediately rather than waiting for the next address change. A node that advertises its EndpointID for global discovery should wait for this first: until it is online, a remote peer resolving the id gets an address with no relay and cannot connect. It returns ctx.Err() if ctx is done first, and ErrInvalid on an endpoint bound without Config.Pkarr (nothing publishes).
type Handler ¶
Handler runs one accepted connection to completion. The endpoint wraps the go-iroh connection as a *Conn before calling it, so a handler never sees go-iroh. ctx is cancelled when the endpoint shuts down. The handler owns the connection lifetime and must Close it; a returned error closes only that connection and is not fatal to the accept loop.
type NodeKey ¶
type NodeKey struct {
// contains filtered or unexported fields
}
NodeKey is a node's long-lived ed25519 identity. Its public key is the node's id on the mesh — the same key the go-iroh endpoint binds with (so the wire EndpointID equals the node id) and the key a signed receipt or heartbeat verifies against.
NodeKey exposes a generic NodeKey.Sign for callers that just need a signature, and the raw ed25519 key via NodeKey.Ed25519/NodeKey.Public for callers that own their payload framing (e.g. a domain-separated receipt signer). The zero value is unusable; construct with GenerateNodeKey or LoadOrCreate.
Example ¶
ExampleNodeKey signs a message and verifies it against the node's public key.
package main
import (
"crypto/ed25519"
"fmt"
irohmesh "github.com/tmc/mlx-go-iroh"
)
func main() {
seed := make([]byte, ed25519.SeedSize) // all-zero seed, for a deterministic example
k, err := irohmesh.NodeKeyFromSeed(seed)
if err != nil {
fmt.Println(err)
return
}
msg := []byte("hello mesh")
sig := k.Sign(msg)
fmt.Println(irohmesh.Verify(k.Public(), msg, sig))
}
Output: true
func GenerateNodeKey ¶
GenerateNodeKey returns a fresh random NodeKey.
func LoadOrCreate ¶
LoadOrCreate loads the NodeKey whose seed is stored at path, creating and persisting a fresh one if the file does not exist. The seed is written as hex with 0600 permissions; intermediate directories are created. This is the stable-identity path: a node calls it once at startup with a path under its home directory.
func NodeKeyFromEd25519 ¶
func NodeKeyFromEd25519(priv ed25519.PrivateKey) (NodeKey, error)
NodeKeyFromEd25519 wraps an existing ed25519 private key, so a caller that already manages an identity key can present it on the mesh.
func NodeKeyFromSeed ¶
NodeKeyFromSeed returns the NodeKey deterministically derived from seed, which must be ed25519.SeedSize bytes.
func (NodeKey) Ed25519 ¶
func (k NodeKey) Ed25519() ed25519.PrivateKey
Ed25519 returns the underlying ed25519 private key, for callers that sign their own domain-separated payloads (e.g. a receipt signer that owns its framing). The returned key aliases the NodeKey's material; do not mutate it.
func (NodeKey) ID ¶
func (k NodeKey) ID() key.EndpointID
ID returns the node's go-iroh endpoint id, equal to its ed25519 public key.
type SignedEnvelope ¶
type SignedEnvelope struct {
NodeID []byte `json:"node_id"` // ed25519 public key of the signer
Payload []byte `json:"payload"` // opaque application bytes
Sig []byte `json:"sig"` // signature over envelopeDomain||Payload
}
SignedEnvelope is a gossip message carrying an application payload signed by the sending node. The shared layer owns only the envelope (identity + signature + size bound); Payload is OPAQUE bytes the application serializes however it likes — JSON for new payloads, or a domain-tagged binary form for a payload whose signature must be reproducible across implementations. The envelope never inspects or re-serializes Payload, so no domain type leaks into this module and a binary signed payload survives byte-for-byte.
The wire form is the JSON encoding of this struct; NodeID, Payload, and Sig are base64 by encoding/json's []byte handling. (The JSON is the outer transport framing only — it does not touch the bytes the signature covers.)
func SignEnvelope ¶
func SignEnvelope(k NodeKey, payload []byte) (SignedEnvelope, error)
SignEnvelope wraps payload in an envelope signed by k. payload is treated as opaque bytes: it is signed and carried verbatim, never re-serialized. It returns an error if the resulting wire message would exceed MaxGossipFrame, so an oversized control message fails at the sender rather than being silently dropped by the gossip layer.
Example ¶
package main
import (
"fmt"
irohmesh "github.com/tmc/mlx-go-iroh"
)
func main() {
k, err := irohmesh.GenerateNodeKey()
if err != nil {
panic(err)
}
env, err := irohmesh.SignEnvelope(k, []byte("heartbeat"))
if err != nil {
panic(err)
}
fmt.Println(env.Verify() == nil)
fmt.Printf("%s\n", env.Payload)
}
Output: true heartbeat
func (SignedEnvelope) Verify ¶
func (e SignedEnvelope) Verify() error
Verify reports whether the envelope is well formed and its signature is valid for NodeID over the payload. A verified envelope means the bytes in Payload were signed by the holder of NodeID's key; it says nothing about the contents.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package blob is a content-addressed byte store over go-iroh blobs: hash some bytes to get a Hash, serve them to peers, and pull them back BAO-verified so a corrupt or hostile peer cannot substitute different bytes.
|
Package blob is a content-addressed byte store over go-iroh blobs: hash some bytes to get a Hash, serve them to peers, and pull them back BAO-verified so a corrupt or hostile peer cannot substitute different bytes. |
|
Package manifest pulls a content-addressed set of blobs from peers over go-iroh blobs, falling back to a central hub when no peer can serve one.
|
Package manifest pulls a content-addressed set of blobs from peers over go-iroh blobs, falling back to a central hub when no peer can serve one. |
|
Package meshtest provides test helpers for irohmesh-based meshes: reserving a loopback address to bind, and deriving a deterministic identity from a seed byte so a test's EndpointID is reproducible.
|
Package meshtest provides test helpers for irohmesh-based meshes: reserving a loopback address to bind, and deriving a deterministic identity from a seed byte so a test's EndpointID is reproducible. |