Go library for embedding a Yggdrasil node into an application. The network stack runs in userspace on top of gVisor netstack — no TUN interface, root access, or external dependencies required.
- Userspace stack. TCP/UDP over gVisor netstack, no OS privileges.
- Standard Go interfaces.
DialContext,Listen,ListenPacket— compatible withnet.Conn,net.Listener,http.Transport, etc. core.Interfaceas a contract. Packagessocks,peermgr, and the rootratatoskrdepend on the interface, not oncore.Objimplementation. You can plug in your own implementation for testing or custom transports.
yggstack is a ready-made binary for end users
(SOCKS proxy, TCP/UDP forwarding via CLI flags). ratatoskr is a library for developers:
a node is created with ratatoskr.New(), everything is controlled through the Go API.
- Installation
- Quick start
- Architecture
- Root package API
- Configuration
- Snapshot types
- Errors
- Thread safety
- Lifecycle
- Usage examples
- Modules
- Example applications
- Supported platforms
go get github.com/voluminor/ratatoskrMinimum Go version: 1.24.
Create a node, connect to the network, and make an HTTP request:
package main
import (
"context"
"fmt"
"io"
"net/http"
"github.com/voluminor/ratatoskr"
"github.com/voluminor/ratatoskr/mod/peermgr"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
node, err := ratatoskr.New(ratatoskr.ConfigObj{
Ctx: ctx,
Peers: &peermgr.ConfigObj{
Peers: []string{
"tls://peer1.example.com:17117",
"tls://peer2.example.com:17117",
},
MaxPerProto: 1,
},
})
if err != nil {
panic(err)
}
defer node.Close()
fmt.Println("Network address:", node.Address())
client := &http.Client{
Transport: &http.Transport{
DialContext: node.DialContext,
},
}
resp, err := client.Get("http://[200:abcd::1]:8080/api")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}flowchart TB
App[Application]
subgraph ratatoskr["ratatoskr (root package)"]
Obj["Obj — facade"]
SOCKS["SOCKS5 proxy"]
PeerMgr["Peer Manager"]
NInfo["ninfo — Ask / AskAddr"]
end
subgraph core["mod/core"]
CoreObj["core.Obj"]
Netstack["netstack — userspace TCP/UDP"]
end
subgraph sigils["mod/sigils"]
SigilCore["sigil_core — NodeInfo assembly"]
Sigils["inet, info, public, services"]
end
subgraph external["External dependencies"]
YggCore["yggdrasil-go/core"]
gVisor["gVisor netstack"]
end
App --> Obj
Obj -->|" core.Interface "| CoreObj
Obj --> SOCKS
Obj --> PeerMgr
Obj --> NInfo
NInfo -->|" getNodeInfo "| YggCore
SOCKS -->|" DialContext "| CoreObj
PeerMgr -->|" AddPeer / RemovePeer "| CoreObj
SigilCore --> CoreObj
Sigils --> SigilCore
CoreObj --> Netstack
Netstack --> gVisor
Netstack -->|" IPv6 packets "| YggCore
ratatoskr.Obj embeds core.Interface — all network methods (DialContext, Listen,
ListenPacket, Address, Subnet, PublicKey, etc.) are available directly. SOCKS5 proxy,
peer manager, and ninfo are optional components controlled through Obj methods.
func New(cfg ConfigObj) (*Obj, error)Creates and starts a Yggdrasil node. Returns *Obj — a facade with full capabilities.
flowchart LR
New["New(cfg)"] --> SG{Sigils set?}
SG -->|Yes| SC["sigil_core.New() → NodeInfo"]
SG -->|No| Core
SC --> Core["Start core"]
Core --> NI["ninfo.New()"]
NI --> PM{Peers set?}
PM -->|Yes| Start["peermgr.Start()"]
PM -->|No| Ready["Obj ready"]
Start --> Ready
Ready -->|" Ctx != nil "| Watch["goroutine: <-Ctx.Done() → Close()"]
- If
cfg.Config == nil— random keys are generated - If
cfg.Logger == nil— logs are discarded (noop logger) - If
cfg.Sigils != nil— NodeInfo is assembled from sigils;Config.NodeInfois used as the base - If
cfg.Peers != nil— peer manager is started;cfg.Config.Peersmust be empty - If
cfg.Ctx != nil— node shuts down automatically on context cancellation
After a successful New call, all core.Interface network methods are available through Obj:
| Method | Description |
|---|---|
DialContext(ctx, net, addr) |
Outgoing TCP/UDP connection via Yggdrasil |
Listen(net, addr) |
TCP listener on the Yggdrasil network |
ListenPacket(net, addr) |
UDP listener |
Address() |
Node IPv6 address (200::/7) |
Subnet() |
/64 subnet |
PublicKey() |
ed25519 public key |
MTU() |
Stack MTU |
GetPeers() |
Peer list with metrics |
AddPeer(uri) |
Add a peer |
RemovePeer(uri) |
Remove a peer |
EnableMulticast(logger) |
mDNS discovery on local network |
DisableMulticast() |
Stop multicast |
EnableAdmin(addr) |
Admin socket (unix/tcp) |
DisableAdmin() |
Stop admin socket |
For details on network operations, components, and NIC — see mod/core/README.md.
func (o *Obj) EnableSOCKS(cfg SOCKSConfigObj) error
func (o *Obj) DisableSOCKS() errorEnableSOCKS starts the SOCKS5 proxy. The resolver is created automatically based on cfg.Nameserver.
DisableSOCKS stops the proxy; idempotent.
stateDiagram-v2
[*] --> Created: New()
Created --> Active: EnableSOCKS()
Active --> Created: DisableSOCKS()
Active --> Active: EnableSOCKS() → error
Created --> Created: DisableSOCKS() → no-op
The Enable → Disable → Enable cycle is supported. For details — see mod/socks/README.md.
func (o *Obj) PeerManagerActive() []string
func (o *Obj) PeerManagerOptimize() errorThe peer manager is enabled via ConfigObj.Peers when calling New. If Peers == nil — methods
return nil / ErrPeerManagerNotEnabled.
| Method | Description |
|---|---|
PeerManagerActive() |
Current active peers (copy); nil if manager is not used |
PeerManagerOptimize() |
Force peer re-evaluation (blocks until completion) |
When MinPeers > 0 (active mode only), a background watch detects peer loss: if the number of active
peers stays at or below the threshold for several consecutive checks, an unscheduled re-evaluation
is triggered automatically.
For details on modes, batching, MinPeers watch, and peer validation — see mod/peermgr/README.md.
func (o *Obj) RetryPeers()Triggers immediate reconnection of disconnected peers. Works independently of the peer manager —
calls core.RetryPeersNow() directly.
func (o *Obj) Ask(ctx context.Context, key ed25519.PublicKey) (*ninfo.AskResultObj, error)
func (o *Obj) AskAddr(ctx context.Context, addr string) (*ninfo.AskResultObj, error)Query a remote node's NodeInfo. Ask takes a public key, AskAddr takes an address string
(64-char hex, <hex>.pk.ygg, [ipv6]:port, or bare IPv6). Returns parsed metadata,
software info, and measured RTT.
If the remote node uses ratatoskr, the response is automatically split into sigils — each
known sigil goes into AskResultObj.Node.Sigils, remaining keys go into Extra.
result, err := node.AskAddr(ctx, "200:abcd::1")
if err != nil {
log.Fatal(err)
}
fmt.Printf("RTT: %s, version: %s\n", result.RTT, result.Node.Version)
if result.Software != nil {
fmt.Printf("Software: %s %s\n", result.Software.Name, result.Software.Version)
}
for name, sigil := range result.Node.Sigils {
fmt.Printf("Sigil %s: %v\n", name, sigil.Params())
}Internally, ninfo is always created during New(). If ConfigObj.Sigils is set, the sigils
are imported into ninfo as parsers for responses. For details — see mod/ninfo/README.md.
type ConfigObj struct {
// ...
Sigils []sigils.Interface
}Sigils are typed data blocks for NodeInfo. Each sigil owns a set of keys in NodeInfo
and can write/read them. When passed via ConfigObj.Sigils:
sigil_core.New()assembles NodeInfo from the baseConfig.NodeInfoand the provided sigils- The result is written to
Config.NodeInfobefore starting the core - The same sigils are imported into
ninfoas parsers forAsk/AskAddr
node, err := ratatoskr.New(ratatoskr.ConfigObj{
Ctx: ctx,
Sigils: []sigils.Interface{
info.New("my-node", "My cool Yggdrasil node"),
public.New(ed25519.PublicKey(pk)),
inet.New("192.168.1.1", 8080),
},
})Built-in sigils: info, public, inet, services. You can create your own by implementing
sigils.Interface. For details — see mod/sigils/README.md and
mod/sigils/sigil_core/README.md.
func (o *Obj) Snapshot() SnapshotObjCollects full node state in a single call:
flowchart LR
Snapshot --> Addr["Address, Subnet, PublicKey, MTU"]
Snapshot --> RST["RSTDropped"]
Snapshot --> Peers["GetPeers() → []PeerSnapshotObj"]
Snapshot --> Active["PeerManagerActive() → []string"]
Snapshot --> SOCKS["IsEnabled, Addr, IsUnix"]
Returns SnapshotObj with JSON tags — can be serialized directly for /status or /metrics.
func (o *Obj) Close() errorStops all components. Idempotent (sync.Once) — safe for repeated and concurrent calls.
Shutdown order:
flowchart TD
Close --> PM["peermgr.Stop()"]
PM --> S1["Disable SOCKS"]
S1 --> S15["ninfo.Close()"]
S15 --> S2["core.Close() — multicast, admin, listeners, core.Stop, NIC, gVisor"]
S2 --> Done["Done"]
Collects errors from all components via errors.Join.
Node creation parameters.
| Field | Type | Default | Description |
|---|---|---|---|
Ctx |
context.Context |
nil |
Parent context; on cancellation — automatic Close(). nil — manual control |
Config |
*config.NodeConfig |
nil |
Yggdrasil configuration. nil — random keys |
Logger |
yggcore.Logger |
nil |
Logger. nil — logs are discarded |
CoreStopTimeout |
time.Duration |
0 |
core.Stop() timeout. 0 — no limit |
Peers |
*peermgr.ConfigObj |
nil |
Peer manager. nil — peers from Config.Peers. Non-nil + Config.Peers → error |
Sigils |
[]sigils.Interface |
nil |
Sigils for NodeInfo. nil — not used. Combines with Config.NodeInfo |
SOCKS5 proxy parameters.
| Field | Type | Default | Description |
|---|---|---|---|
Addr |
string | required | TCP "127.0.0.1:1080" or Unix "/tmp/ygg.sock" (/ or . — Unix) |
Nameserver |
string | "" |
DNS on the Yggdrasil network. "[ipv6]:port". Empty — .pk.ygg only |
Verbose |
bool | false |
Log each SOCKS connection |
MaxConnections |
int | 0 |
Max concurrent connections. 0 — unlimited |
| Field | Type | Description |
|---|---|---|
Address |
string |
Node IPv6 address |
Subnet |
string |
/64 subnet |
PublicKey |
string |
ed25519 public key (hex) |
MTU |
uint64 |
Stack MTU |
RSTDropped |
int64 |
Dropped RST packet counter |
Peers |
[]PeerSnapshotObj |
State of each peer |
ActivePeers |
[]string |
Peers selected by manager (omitempty) |
SOCKS |
SOCKSSnapshotObj |
SOCKS5 proxy state |
| Field | Type | Description |
|---|---|---|
URI |
string |
Connection URI |
Up |
bool |
Connected |
Inbound |
bool |
Inbound connection |
Key |
string |
Peer public key (hex) |
Latency |
time.Duration |
Latency |
Cost |
uint64 |
Route cost |
RXBytes |
uint64 |
Bytes received |
TXBytes |
uint64 |
Bytes sent |
Uptime |
time.Duration |
Connection uptime |
LastError |
string |
Last error (omitempty) |
LastErrorTime |
time.Time |
Time of last error |
| Field | Type | Description |
|---|---|---|
Enabled |
bool |
Proxy is running |
Addr |
string | Address (omitempty) |
IsUnix |
bool |
Unix socket (omitempty) |
| Variable | Description |
|---|---|
ErrPeersConflict |
Config.Peers and Peers manager are set simultaneously |
ErrPeerManagerNotEnabled |
Peer manager method called but manager is not enabled |
Errors from core.Interface (ErrNotAvailable, etc.) are described in mod/core/README.md.
All public methods of Obj are safe for concurrent use.
| Method / group | Guarantee |
|---|---|
DialContext, Listen, ListenPacket |
Thread-safe; netstack via atomic.Pointer |
EnableSOCKS / DisableSOCKS |
Mutex-protected |
EnableMulticast / EnableAdmin |
Protected by sync.RWMutex |
AddPeer / RemovePeer |
Delegate to yggdrasil-go/core (thread-safe) |
PeerManagerActive |
Returns a copy; mutex-protected |
PeerManagerOptimize |
Blocks; serialized via optimizeMu |
Ask / AskAddr |
Thread-safe; network call in a goroutine, cancellable via ctx |
Close |
Idempotent (sync.Once) |
Snapshot |
Thread-safe; collects data from thread-safe methods |
flowchart TD
START([Creation]) --> NEW["ratatoskr.New(cfg)"]
NEW --> SG{Sigils set?}
SG -->|Yes| SC["sigil_core.New() → NodeInfo"]
SG -->|No| CORE
SC --> CORE["core.New() — Yggdrasil + netstack + NIC"]
CORE --> NI["ninfo.New()"]
NI --> PM{Peers set?}
PM -->|Yes| PMSTART["peermgr.New() + Start()"]
PM -->|No| READY
PMSTART --> READY([Node ready])
READY -->|optionally| SOCKS["EnableSOCKS()"]
READY -->|optionally| MC["EnableMulticast()"]
READY -->|optionally| ADM["EnableAdmin()"]
READY -->|optionally| PEERS["AddPeer() / RemovePeer()"]
READY -->|optionally| ASK["Ask() / AskAddr()"]
SOCKS --> READY
MC --> READY
ADM --> READY
PEERS --> READY
ASK --> READY
READY --> CLOSE["Close()"]
CLOSE --> S1["peermgr.Stop()"]
S1 --> S2["Disable SOCKS"]
S2 --> S25["ninfo.Close()"]
S25 --> S3["core.Close()"]
S3 --> DONE([Done])
Three ways to shut down:
// 1. Explicit Close()
defer node.Close()
// 2. Via context
ctx, cancel := context.WithCancel(context.Background())
node, _ = ratatoskr.New(ratatoskr.ConfigObj{Ctx: ctx})
cancel() // → Close() automatically
// 3. Via OS signal
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
node, _ = ratatoskr.New(ratatoskr.ConfigObj{Ctx: ctx})
<-ctx.Done()client := &http.Client{
Transport: &http.Transport{
DialContext: node.DialContext,
},
}
resp, err := client.Get("http://[200:abcd::1]:8080/api/v1/status")ln, err := node.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer ln.Close()
fmt.Printf("http://[%s]:8080/\n", node.Address())
http.Serve(ln, handler)pc, err := node.ListenPacket("udp", ":9000")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
buf := make([]byte, 1500)
for {
n, addr, err := pc.ReadFrom(buf)
if err != nil {
break
}
pc.WriteTo(buf[:n], addr)
}err = node.EnableSOCKS(ratatoskr.SOCKSConfigObj{
Addr: "127.0.0.1:1080",
Nameserver: "[200:abcd::1]:53",
Verbose: true,
MaxConnections: 128,
})
defer node.DisableSOCKS()
// curl --proxy socks5h://127.0.0.1:1080 http://example.pk.ygg/Unix socket:
err = node.EnableSOCKS(ratatoskr.SOCKSConfigObj{
Addr: "/tmp/ygg-socks.sock",
})SOCKS5 proxy that routes Yggdrasil addresses (200::/7) through the node
and everything else through the regular network:
import (
"context"
"net"
"github.com/voluminor/ratatoskr/mod/resolver"
"github.com/voluminor/ratatoskr/mod/socks"
)
// split dialer: Yggdrasil addresses → node, everything else → direct
dial := func (ctx context.Context, network, addr string) (net.Conn, error) {
host, _, _ := net.SplitHostPort(addr)
if ip := net.ParseIP(host); ip != nil && ip[0]&0xfe == 0x02 { // 200::/7
return node.DialContext(ctx, network, addr)
}
return (&net.Dialer{}).DialContext(ctx, network, addr)
}
srv := socks.New(dialerFunc(dial))
srv.Enable(socks.EnableConfigObj{
Addr: "127.0.0.1:1080",
Resolver: resolver.New(node, "[200:abcd::1]:53"), // DNS over Yggdrasil
Logger: logger,
})
defer srv.Disable()
// dialerFunc adapts a function to proxy.ContextDialer
type dialerFunc func (ctx context.Context, network, addr string) (net.Conn, error)
func (f dialerFunc) DialContext(ctx context.Context, n, a string) (net.Conn, error) {
return f(ctx, n, a)
}Can be used as a system-wide SOCKS5 proxy — regular internet traffic passes through unaffected, only Yggdrasil addresses are routed through the node:
# Yggdrasil IPv6 — routed through the node
curl --proxy socks5h://127.0.0.1:1080 http://[200:b0aa:c535:89fb:4c73:bbd:c30b:2665]/
# .pk.ygg domain — resolver converts to 200::/7, then routed through the node
curl --proxy socks5h://127.0.0.1:1080 http://a7aa9d653b0259c67a211e7a6ccd281219db1246c75e4ebcf9edbdbdaff55924.pk.ygg/
# Regular internet — goes directly, bypassing Yggdrasil
curl --proxy socks5h://127.0.0.1:1080 https://example.com/node, err := ratatoskr.New(ratatoskr.ConfigObj{
Ctx: ctx,
Peers: &peermgr.ConfigObj{
Peers: []string{
"tls://peer1.example.com:17117",
"tls://peer2.example.com:17117",
"quic://peer3.example.com:17117",
},
ProbeTimeout: 10 * time.Second,
RefreshInterval: 5 * time.Minute,
MaxPerProto: 1,
BatchSize: 2,
},
})
active := node.PeerManagerActive()
node.PeerManagerOptimize() // force re-evaluationPassive mode (add all without selection):
Peers: &peermgr.ConfigObj{
Peers: peers,
MaxPerProto: -1,
}snap := node.Snapshot()
data, _ := json.MarshalIndent(snap, "", " ")
fmt.Println(string(data))// mDNS peer discovery on local network
if err := node.EnableMulticast(logger); err != nil {
log.Fatal(err)
}
defer node.DisableMulticast()
// Admin socket
if err := node.EnableAdmin("unix:///tmp/ygg-admin.sock"); err != nil {
log.Fatal(err)
}
defer node.DisableAdmin()type slogAdapter struct{ l *slog.Logger }
func (a slogAdapter) Infof(f string, v ...interface{}) { a.l.Info(fmt.Sprintf(f, v...)) }
func (a slogAdapter) Infoln(v ...interface{}) { a.l.Info(fmt.Sprint(v...)) }
func (a slogAdapter) Warnf(f string, v ...interface{}) { a.l.Warn(fmt.Sprintf(f, v...)) }
func (a slogAdapter) Warnln(v ...interface{}) { a.l.Warn(fmt.Sprint(v...)) }
func (a slogAdapter) Errorf(f string, v ...interface{}) { a.l.Error(fmt.Sprintf(f, v...)) }
func (a slogAdapter) Errorln(v ...interface{}) { a.l.Error(fmt.Sprint(v...)) }
func (a slogAdapter) Debugf(f string, v ...interface{}) { a.l.Debug(fmt.Sprintf(f, v...)) }
func (a slogAdapter) Debugln(v ...interface{}) { a.l.Debug(fmt.Sprint(v...)) }
func (a slogAdapter) Printf(f string, v ...interface{}) { a.l.Info(fmt.Sprintf(f, v...)) }
func (a slogAdapter) Println(v ...interface{}) { a.l.Info(fmt.Sprint(v...)) }
func (a slogAdapter) Traceln(v ...interface{}) {}
node, _ := ratatoskr.New(ratatoskr.ConfigObj{
Logger: slogAdapter{l: slog.Default()},
})| Module | Description |
|---|---|
mod/core |
Core: Yggdrasil node, netstack, NIC, multicast, admin |
mod/peermgr |
Peer manager: probing, best selection, rotation, MinPeers watch |
mod/socks |
SOCKS5 proxy (TCP/Unix), connection limit |
mod/resolver |
Resolver: .pk.ygg, IP literals, DNS via Yggdrasil |
mod/forward |
TCP/UDP forwarding between local network and Yggdrasil |
mod/probe |
Topology exploration (BFS), route tracing |
mod/settings |
Config loading, parsing, and saving (WIP — schema not finalized) |
mod/sigils |
Typed NodeInfo blocks (info, services, public, inet) |
mod/ninfo |
Remote NodeInfo querying and parsing, parse sigil management |
Ready-made examples in cmd/embedded/:
| Example | Description |
|---|---|
http |
HTTP server on Yggdrasil |
tiny-http |
Minimal HTTP server |
tiny-chat |
Chat over Yggdrasil |
mobile |
Mobile platform example |
Also cmd/yggstack/ — yggstack implementation built on ratatoskr.
Tests run on Linux (amd64, arm64), macOS (arm64), and Windows (amd64). Cross-compilation is verified on every PR for 25 targets:
| OS | Architectures |
|---|---|
| Linux | amd64, arm64, armv7, armv6, 386, riscv64, mips64, mips64le, mips, mipsle, ppc64, ppc64le, s390x |
| Windows | amd64, arm64, 386 |
| macOS | amd64, arm64 |
| FreeBSD | amd64, arm64, 386 |
| OpenBSD | amd64, arm64 |
| NetBSD | amd64, arm64 |