ratatoskr

title
ratatoskr
type
toolbox
summary
Embeds a Yggdrasil mesh node in a Golang process with a gVisor userspace stack β€” no TUN, no root
tags
go, networking, mesh, p2p
language
Go
license
LGPL-2.1
created
2026-07-23
updated
2026-07-23

Ratatoskr runs a Yggdrasil node inside your own process and hands you the results as net.Conn, net.Listener, and net.PacketConn. There is no TUN device, no root, and no separate yggdrasil daemon to supervise β€” the data path is a gvisor netstack driven by Yggdrasil core and Ironwood. ratatoskr.New builds core, the gVisor TCP/UDP stack, NodeInfo querying, and an optional managed-peer selector behind a single lifecycle, and node.DialContext drops straight into http.Transport. The module has four direct external requirements: Yggdrasil core, gVisor, golang.org/x/net, and go-socks5. Requires Golang 1.25 or newer.

It solves the same problem as asciimoth-ygg's ygglib + VTun, which is covered in yggdrasil-embedded-go, but from a different angle: instead of forking upstream Yggdrasil to make it library-shaped, Ratatoskr keeps upstream as a dependency and wraps it, then splits the surrounding services into subpackages you can import without the root facade.

Structure

The root package is a facade. Each mod/ subpackage depends on a narrow local interface rather than on ratatoskr.Obj, so you can use mod/socks with your own dialer, or mod/forward with any five-method network implementation, and never construct a node at all.

  • mod/core β€” Yggdrasil core, the gVisor NIC and sockets, identity, peers, multicast
  • mod/peermgr β€” validates peer candidates, probes them in bounded batches, keeps the lowest-latency one per protocol (MaxPerProto)
  • mod/socks β€” SOCKS5 over TCP or a Unix socket, with TCP and UDP ASSOCIATE limits
  • mod/resolver β€” resolves <64-hex>.pk.ygg, IP literals, and optionally real DNS through a supplied dialer
  • mod/forward β€” immutable TCP/UDP mappings between the local network and Yggdrasil
  • mod/ninfo β€” remote NodeInfo lookup, with concurrent queries for the same target coalesced
  • mod/probe β€” bounded breadth-first topology discovery, spanning-tree paths, route traces
  • mod/sigils β€” typed NodeInfo fragments

Sigils are the more interesting design idea. Yggdrasil NodeInfo is an open map[string]any, which makes it useless to software that wants a schema and unusable as a schema if you close it. A sigil owns a group of NodeInfo keys, validates local data, publishes those keys, and recognizes the same shape coming back from a remote node. Four ship built in: info (name, role, location, contacts), services (named ports), public (peering endpoints by transport), and inet (public Internet addresses). A key conflict between sigils, or between a sigil and Config.NodeInfo, aborts New with ErrInvalidSigils rather than publishing partial metadata. Ask and AskAddr parse the sigils they know and preserve unknown-but-valid ones untouched.

Usage

node, err := ratatoskr.New(ratatoskr.ConfigObj{
    Ctx: ctx,
    Peers: &peermgr.ConfigObj{
        Peers:       []string{"tls://peer.example:17117", "quic://peer.example:17117"},
        MaxPerProto: 1,
    },
})
if err != nil {
    panic(err)
}
defer node.Close()

client := &http.Client{Transport: &http.Transport{DialContext: node.DialContext}}

A nil Config generates random keys and disables the admin listener; for a stable identity you load a persisted config.NodeConfig (HJSON, following PrivateKeyPath) before calling New. Setting peers in both Config.Peers and ConfigObj.Peers fails with ErrPeersConflict, since two owners would be managing one peer set. Listeners come from node.Listen("tcp", ":8080") and live inside the userspace stack. Close is safe to call repeatedly, runs dependents concurrently, then the core, all under one CloseTimeout budget (10s by default); overrunning it returns ErrCloseTimedOut and lets teardown finish in the background instead of blocking the caller.

Distribution through Yggdrasil

Ratatoskr publishes itself three ways, and they are three distinct Golang module identities that must not be mixed inside one module: the canonical github.com/voluminor/ratatoskr, an HTTPS mirror at ratatoskr.space/pkg/ratatoskr, and the same mirror reachable over the mesh itself at 14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg. The mesh route needs GOPROXY pointed at the host, GOSUMDB=off (the public checksum database indexes the GitHub path, not the rewritten one), and GOINSECURE for that host because traffic inside the encrypted overlay is plain HTTP:

YGG_HOST="14cc7d57b5e70f679b851fe5b272ce17c70632ff4beb5b35ab64bc706b2485af.pk.ygg"
GOPROXY="http://${YGG_HOST}" GOSUMDB=off GOINSECURE="${YGG_HOST}/*" \
  go get "${YGG_HOST}/pkg/ratatoskr@latest"

That is the module-proxy protocol used as ordinary transport over an overlay network, the same property that cursed-bundler-go-get-ruby-gems exploits in the other direction.

Caveats

Throughput is the price of a userspace stack. The project's own benchmark against a direct Docker path on one host puts TCP at 198 MiB/s median versus 4,809 MiB/s (4.1%) and UDP at 44.5 MiB/s versus 440 MiB/s (10.1%, and that is receiver goodput under 24% packet loss, not lossless capacity). Profiles put the cost in Yggdrasil and Ironwood cryptography, gVisor packet handling, syscalls, and copies β€” not in Ratatoskr's own control code, which is the useful part of the result but also means there is no fix available at this layer.

Core().EnableAdmin is documented as deliberately unhardened: it passes straight through to upstream Yggdrasil's admin implementation, which can call os.Exit(1) on a bad address or a socket cleanup failure, has no authentication, no request-size cap, and no per-connection deadline, and can leave keepalive connections alive past DisableAdmin. Treat access to it as full process control.

In mod/forward, MaxTCPConnections == 0 and MaxUDPSessions == 0 mean unlimited, so a mapping reachable by untrusted nodes will happily allocate goroutines per accepted connection and per UDP source. Set both explicitly at any public boundary.

CI runs on Golang 1.26.5 and tests release source on Linux, macOS, and Windows, with compile checks across 25 GOOS/GOARCH combinations including FreeBSD, OpenBSD, and NetBSD. Unix sockets and some Yggdrasil transports are platform-specific. Note also that the development branch does not track generated files β€” a checkout needs the bootstrap in CONTRIBUTING.md, while tagged releases ship the generated target package and a complete go.mod.

Repo

github.com/voluminor/ratatoskr β€” Go, LGPL-2.1. Single-author project, first releases in 2026.