Skip to content

cleverfox/yggdrasil-lite

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

yggdrasil-lite

Minimal Yggdrasil client for embedded devices.

Implements the Yggdrasil/Ironwood protocol subset needed to operate as a leaf node: connect to peers, join the spanning tree, discover paths via bloom filters, and exchange end-to-end encrypted packets with any node in the network. No TUN interface, no admin API.

By default there is no transit routing. The optional transit Cargo feature turns the node into a router: it merges peer bloom filters into its advertisements, forwards path discovery, and forwards traffic between peers. Individual peers can be excluded with set_peer_no_redistribute(peer_id, true) — their keys are never redistributed to other peers (no bloom merge, no lookup forwarding toward them), while the marked peer can still use the node as an uplink. Marking all peers makes a multi-homed stub that never carries third-party traffic.

Designed for no_std + alloc environments (ESP32 with Embassy), but also builds with std for desktop testing.

Features

  • Spanning tree participation (announce, SigReq/SigRes, parent selection)
  • Bloom filter routing (advertise own key, respond to PathLookup)
  • Path discovery (PathLookup / PathNotify / PathBroken)
  • Optional transit routing (transit feature) with per-peer no_redistribute policy
  • End-to-end encrypted sessions (XSalsa20-Poly1305 with key ratcheting)
  • Metadata handshake compatible with yggdrasil-go and yggdrasil-ng
  • IPv6 address derivation from Ed25519 public key (200::/7)
  • Poll-based, synchronous API — no async runtime dependency
  • ~2500 lines of Rust, 73 unit tests

Architecture

Application
    │
    ▼
YggdrasilLite          ◄── poll-based node coordinator
├── tree.rs            ◄── spanning tree CRDT (parent selection, announces)
├── bloom.rs           ◄── bloom filter exchange (key advertisement)
├── pathfinder.rs      ◄── path cache (PathLookup/PathNotify/PathBroken)
├── session.rs         ◄── encrypted sessions (Init/Ack/Traffic + ratcheting)
├── wire.rs            ◄── frame encoding/decoding (uvarint, TLV, paths)
├── meta.rs            ◄── metadata handshake (version, key, BLAKE2b sig)
├── crypto.rs          ◄── Ed25519, X25519, XSalsa20-Poly1305
├── address.rs         ◄── IPv6 address derivation from ed25519 key
└── peer.rs            ◄── per-peer connection state

Usage

use yggdrasil_lite::{YggdrasilLite, LiteConfig, NodeEvent};

// Create a node with an Ed25519 seed
let config = LiteConfig::new(my_ed25519_seed);
let mut node = YggdrasilLite::new(config);

// After TCP+TLS connection and metadata handshake with a peer:
let peer_id = node.add_peer(peer_public_key, 0);
node.mark_handshake_done(peer_id);

// Main loop
loop {
    // Feed incoming data from the peer's TLS stream
    let events = node.handle_peer_data(peer_id, &incoming_bytes, now_ms, &mut rng);
    handle_events(&events, &mut tls_writer);

    // Periodic maintenance (~every 100ms)
    let events = node.poll(now_ms, &mut rng);
    handle_events(&events, &mut tls_writer);

    // Send encrypted data to a remote Yggdrasil node
    let events = node.send(&dest_public_key, b"hello", now_ms, &mut rng);
    handle_events(&events, &mut tls_writer);
}

fn handle_events(events: &[NodeEvent], writer: &mut impl std::io::Write) {
    for event in events {
        match event {
            NodeEvent::SendToPeer { data, .. } => {
                writer.write_all(data).unwrap();
            }
            NodeEvent::Deliver { source, data } => {
                // Decrypted application data from a remote node
                println!("Received {} bytes from {:?}", data.len(), &source[..4]);
            }
        }
    }
}

API Overview

LiteConfig

Field Type Description
private_key [u8; 32] Ed25519 signing key seed
password Option<Vec<u8>> Peering password (if peers require one)
max_sessions usize Max concurrent encrypted sessions (default: 16)
max_paths usize Max cached path entries (default: 16)

YggdrasilLite

Method Description
new(config) Create a new node
public_key() Our Ed25519 public key
address() Our Yggdrasil IPv6 address (200::/7)
subnet() Our /64 subnet (300::/7)
coords() Current tree coordinates
add_peer(key, priority) Register a peer after TLS+metadata handshake
remove_peer(peer_id) Remove a disconnected peer
mark_handshake_done(peer_id) Mark peer as fully connected
handle_peer_data(peer_id, data, now_ms, rng) Process incoming wire data from a peer
poll(now_ms, rng) Periodic maintenance (tree, bloom, keepalive, cleanup)
send(dest_key, data, now_ms, rng) Send encrypted data to a destination

NodeEvent

Variant Description
SendToPeer { peer_id, data } Write data to the peer's TLS stream
Deliver { source, data } Decrypted application data from source

Metadata Handshake

The caller is responsible for TCP+TLS connection setup. After TLS, exchange metadata using meta::Metadata:

use yggdrasil_lite::meta::Metadata;

// Encode and send our metadata
let meta = Metadata::new(node.public_key().clone(), 0);
let encoded = meta.encode(&signing_key, password);
tls_writer.write_all(&encoded)?;

// Read and decode peer metadata
let (peer_meta, consumed) = Metadata::decode(&received_bytes, password)?;
assert!(peer_meta.check()); // verify protocol compatibility

Transport

yggdrasil-lite is transport-agnostic. It operates on framed byte streams — the caller provides TCP+TLS and feeds raw bytes in/out. This makes it usable with:

  • Desktop: std::net::TcpStream + rustls
  • Embassy: embassy-net::TcpSocket + embedded-tls
  • Any other runtime that provides a byte stream

Examples

Desktop: lite_node

A complete desktop integration test node with TLS, metadata handshake, and a userspace TCP/HTTP server running on the Yggdrasil overlay.

# 1. Start a yggdrasil-ng node
cargo run -p yggdrasil -- --config yggdrasil.conf --loglevel debug
# Note the TLS listen address (e.g. tls://0.0.0.0:2020)

# 2. Run the lite node
cargo run --example lite_node -p yggdrasil-lite -- 127.0.0.1:2020

# 3. Test from the yggdrasil-ng host
curl -6 --max-time 30 "http://[<lite_ipv6>]:80/hello"

Desktop: tun bridge (Linux / macOS)

Bridges the overlay to a real kernel TUN interface, so the host's own stack can reach the mesh (ping6, curl -6, ssh, …) — not just a single userspace HTTP endpoint. Needs root to create the device and add a route.

# Connect to a yggdrasil-ng / yggstack TLS peer and bring up the bridge
sudo cargo run --release -p yggdrasil-tun -- 127.0.0.1:2020

# From this host, reach any node on the mesh:
ping6 <a-yggdrasil-address>

See examples/tun/ for details. It uses two small APIs for address-initiated traffic: YggdrasilLite::lookup() (start path discovery from a partial key derived from an IPv6 address) and YggdrasilLite::resolve() (map a learned address back to a full public key).

ESP32: Yggdrasil TCP-UART Bridge

A firmware for ESP32 (C6 and and 32D) that bridges TCP connections over the Yggdrasil mesh to a hardware UART. The device connects to WiFi, establishes a TLS connection to Yggdrasil peers, and listens for TCP and ICMPv6 on its overlay IPv6 address. Target chip is selected via Cargo features.

See examples/esp32/ for the full project. Quick start:

cd examples/esp32

# Edit .cargo/config.toml with your WiFi and peer settings
# Then build and flash:

# ESP32-C6 (RISC-V)
cargo run --release --target riscv32imac-unknown-none-elf --features esp32c6

# ESP32 (Xtensa) — requires esp toolchain
cargo +esp run --release --target xtensa-esp32-none-elf --features esp32 --no-default-features

On boot the device prints its Yggdrasil IPv6 address. You can then connect to it from anywhere on the mesh:

# Ping over Yggdrasil (via yggstack SOCKS5 proxy)
ping6 -x 127.0.0.1:1080 <esp-ipv6>

# TCP to UART bridge
nc -X 5 -x 127.0.0.1:1080 <esp-ipv6> 2000

A web UI is available at http://192.168.4.1 (connect to the YggBridge AP) for configuring WiFi credentials and Yggdrasil peer addresses.

End-to-End Test

An automated E2E test verifies the full stack: yggstack → yggdrasil-ng → lite_node → HTTP.

YGGSTACK_BIN=/path/to/yggstack bash tests/e2e.sh

The test starts a yggstack node with a SOCKS5 proxy, connects a lite_node to it, and verifies HTTP reachability over the Yggdrasil overlay via curl.

Building

# Desktop (std) — tests and examples
cargo test
cargo run --example lite_node -- 127.0.0.1:12345

# ESP32-C6 (no_std) — requires nightly toolchain
cd examples/esp32
cargo build --release --target riscv32imac-unknown-none-elf --features esp32c6

# ESP32 (no_std) — requires esp toolchain
cargo +esp build --release --target xtensa-esp32-none-elf --features esp32 --no-default-features

License

MPL-2.0

About

No description, website, or topics provided.

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors