CommunityArt & Designgithub.com

voluminor/skill-yggdrasil

Agent skill for building Yggdrasil mesh-network apps in Go (via the ratatoskr library): embedded clients, HTTP/TCP/UDP servers, SOCKS5, port forwarding, peer management, NodeInfo/sigils — userspace, no TUN or root.

Works withClaude Code~Codex CLI~Cursor
npx skills add voluminor/skill-yggdrasil

Ask in your favorite AI

Open a new chat with this agent skill pre-loaded.

Documentation

Yggdrasil Networking In Go

Overview

Use this skill to build Go programs that connect to and serve on the Yggdrasil end-to-end-encrypted IPv6 mesh: clients, HTTP/TCP/UDP servers, SOCKS5 proxies, port forwarders, and nodes that publish discovery metadata. The node runs in userspace on a gVisor netstack — no TUN device, no root, no external yggdrasil daemon, and no yggstack binary. You dial and listen over Yggdrasil with standard Go interfaces (net.Conn, net.Listener, net.PacketConn, http.Transport).

The embedding library is github.com/voluminor/ratatoskr: create the node with ratatoskr.New and drive everything through its Go API. Scope and stability of its packages are summarized below.

Design For A Constrained Mesh

Yggdrasil is an end-to-end-encrypted overlay mesh, not a datacenter fabric. Links are high-latency, low-bandwidth, lossy, and intermittent; a peer reachable now may be gone a second later, and traffic may cross many hops. Build every Yggdrasil solution as if for a weak, unstable connection. This is not optional polish — it is the default shape of correct code here.

  • Never unbounded. A deadline on every dial and request (context.WithTimeout); bounded retries; bounded queues. No infinite waits.
  • Cap concurrency. Limit in-flight work: SOCKS MaxConnections, forward SetMaxUDPSessions, http.Transport.MaxConnsPerHost/MaxIdleConnsPerHost, your own worker pools. Both default to unlimited — set real limits.
  • Chunk large transfers, verify, resume. Never move a big file in one shot. Split into small chunks (e.g. HTTP Range), verify each (size/checksum), and make it resumable so a dropped link costs one chunk, not the whole transfer.
  • Retry with backoff + jitter, idempotently. Expect mid-operation disconnects. Make operations resumable/idempotent; exponential backoff with jitter; cap attempts.
  • Keep payloads small. Avoid chatty protocols and polling; compress; use compact encodings. Keep NodeInfo/sigils well under the 16 KB limit.
  • Size buffers to the link. Use node.MTU() for packet buffers (userspace MTU is dynamic, floored at 1280).
  • Degrade, don't hammer. On failure back off and return partial results; never aggressively retry a peer or flood the mesh.

A naive "serve a big file over net/http" or "one giant request/response" is the wrong shape. The right shape is bounded, chunked, resumable, fault-tolerant by default. See the constrained-mesh transfer pattern in references/integrations.md.

Source Of Truth

The active module is authoritative — not memory, not stale README text, not this skill. Before writing version-specific guidance, inspect what the project actually depends on:

go list -m github.com/voluminor/ratatoskr   # selected version
go doc github.com/voluminor/ratatoskr       # root API as compiled
go doc github.com/voluminor/ratatoskr/mod/peermgr ConfigObj

To read the real source of the selected version, resolve it from the module cache instead of guessing a path:

go list -m -f '{{.Dir}}' github.com/voluminor/ratatoskr

Do not freeze the minimum Go version from README text — it can lag behind the go.mod directive. Read the active go.mod of the selected Ratatoskr version.

Default Workflow

  1. Identify the application shape: embedded node, HTTP client/server, raw TCP/UDP, SOCKS5, port forwarding, peer management, or NodeInfo/sigils.
  2. Use the root package github.com/voluminor/ratatoskr for applications. Reach into mod/core only through the core.Interface contract; avoid *core.Obj/UnsafeCore() unless a low-level package genuinely requires it.
  3. Own a context.Context, pass it as ratatoskr.ConfigObj.Ctx, and still defer node.Close() after a successful start. Both are needed.
  4. Configure peers in exactly one place:
    • Static Yggdrasil peers: cfg.Config.Peers
    • Ratatoskr peer manager: ratatoskr.ConfigObj.Peers (then leave cfg.Config.Peers empty, or New returns ErrPeersConflict).
  5. Use node.DialContext for outbound TCP/UDP and node.Listen / node.ListenPacket for services inside Yggdrasil.
  6. Bracket IPv6 addresses in URLs and host-port strings: http://[200:abcd::1]:8080, never http://200:abcd::1:8080.
  7. Treat the link as weak: put a deadline on every call, cap concurrency, and chunk/verify/resume large transfers (see Design For A Constrained Mesh).
  8. When you generate code into a real project, add a compile check or focused test rather than assuming it builds.

Module Scope And Stability

Yggdrasil addresses are derived from the node key, so the network surface is small and stable. Prefer the parts the library treats as finished; flag the rest explicitly instead of building on it.

PackageStatusUse it through
root ratatoskrStable, primary APINew, EnableSOCKS, Ask/AskAddr, Snapshot, Close
mod/coreStable contractthe core.Interface methods embedded on the node
mod/peermgrStableratatoskr.ConfigObj.Peers
mod/socksStablenode.EnableSOCKS (do not instantiate directly)
mod/resolverStablecreated automatically by EnableSOCKS; direct use is advanced
mod/forwardStableforward.NewAdd*Start
mod/sigils (+ info, services, public, inet)Stableratatoskr.ConfigObj.Sigils
mod/ninfoStablenode.Ask / node.AskAddr
mod/probeAdvanced / unstable — requires UnsafeCore(), API may changeonly when topology/route tracing is explicitly required
mod/settingsWork in progress — schema not finalizedavoid; use yggdrasil-go/src/config directly
cmd/*Example apps, not importable library APIread as patterns, never import

Root vs submodules: default to ratatoskr.New (core + SOCKS + peer manager

  • ninfo + sigils behind one Close). Drop to mod/core via core.New only for the smallest surface (raw dial/listen), or compose submodules around core.Interface for custom transports and tests. Every submodule (peermgr/socks/forward/resolver/ninfo) takes a core.Interface. See references/overview.md.

System Sigils — Publish Here First

NodeInfo is how a node describes itself to the network. Four built-in ("system") sigils are parsed automatically by every node, so data you put in them is recognized by other participants. Reach for these before inventing a custom sigil; pass them via ratatoskr.ConfigObj.Sigils. Keep each tiny — they share one 16 KB NodeInfo budget.

SigilWhat belongs hereWhy use it
infoNode identity: name, type, location, contacts, descriptionLets others know who/what your node is.
servicesPorts you expose inside Yggdrasil, as name → portService discovery without port scanning.
publicYour public peering URIs grouped by networkLets others peer with you directly.
inetYour real-world addresses (domains/IPs)Maps your Yggdrasil node to clearnet identity.

Put discoverable, standard data in these so the whole network can read it. Only define a custom sigil for data none of the four cover — and remember only peers running your code will parse it. Constructors validate and return errors; handle them before ratatoskr.New. Details, exact limits, typed access, and custom-sigil authoring: references/nodeinfo-sigils.md and references/custom-sigils.md.

Reference Map

Read only the files needed for the current task:

Common Mistakes

MistakeCorrection
Starting an external Yggdrasil daemon for an embedded appUse ratatoskr.New; it runs a userspace gVisor netstack.
Recommending yggstack for library integrationyggstack is an end-user binary. Use Ratatoskr for Go API control.
Setting both cfg.Config.Peers and ConfigObj.PeersChoose static peers or the peer manager; never both → ErrPeersConflict.
Passing Ctx but skipping Close (or vice versa)Do both: cancellation triggers shutdown, Close releases resources.
Building URLs without IPv6 bracketsUse fmt.Sprintf("http://[%s]:%d", ip, port).
Creating ninfo.New for basic queriesUse node.Ask / node.AskAddr; the root object already builds ninfo.
Assembling or registering sigils dynamically at runtimeSigils are assembled strictly in code; duplicate or overwriting keys are hard errors by design, not silent merges. Publish via ConfigObj.Sigils; read your own keys from res.Node.Extra.
Reaching for UnsafeCore() / mod/probe / mod/settings by defaultStay on the root API; these are advanced or work-in-progress.
Hardcoding a Go version from READMERead the active go.mod of the selected module version.

Validation

Keep SKILL.md as the navigation layer and put heavy API detail in references/. Forward-test changes against the scenarios in evals/evals.json before publishing.

Related Skills