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 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) Endpoint() *iroh.Endpoint
- func (e *Endpoint) ID() key.EndpointID
- 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) Ticket(addr netip.AddrPort) string
- 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 endpointID@transportAddr bootstrap string into a dialable EndpointAddr. Unlike RosterBootstrap's addr-less ids, this yields a seed the swarm can dial immediately.
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 endpointID@transportAddr bootstrap strings. 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 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
}
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).
The zero Config is usable: it binds an ephemeral 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. After Serve it routes through router.Shutdown, which cancels the accept loop, runs handler Shutdown hooks, and closes the endpoint. Before Serve it closes 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) 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) 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.
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. |