惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MyScale Blog
MyScale Blog
Microsoft Azure Blog
Microsoft Azure Blog
H
Help Net Security
N
News and Events Feed by Topic
Recent Announcements
Recent Announcements
D
Docker
M
MIT News - Artificial intelligence
L
LangChain Blog
I
InfoQ
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
P
Proofpoint News Feed
博客园_首页
MongoDB | Blog
MongoDB | Blog
美团技术团队
S
Schneier on Security
G
GRAHAM CLULEY
月光博客
月光博客
有赞技术团队
有赞技术团队
Vercel News
Vercel News
Scott Helme
Scott Helme
P
Privacy International News Feed
Last Week in AI
Last Week in AI
Recorded Future
Recorded Future
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
Attack and Defense Labs
Attack and Defense Labs
Google Online Security Blog
Google Online Security Blog
Simon Willison's Weblog
Simon Willison's Weblog
量子位
S
Security @ Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
V
Visual Studio Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
NISL@THU
NISL@THU
N
Netflix TechBlog - Medium
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Spread Privacy
Spread Privacy
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
小众软件
小众软件
罗磊的独立博客
Security Archives - TechRepublic
Security Archives - TechRepublic
T
Threatpost
L
Lohrmann on Cybersecurity
www.infosecurity-magazine.com
www.infosecurity-magazine.com
S
Security Affairs
Cloudbric
Cloudbric
爱范儿
爱范儿
H
Heimdal Security Blog
PCI Perspectives
PCI Perspectives

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Yggdrasil Network as an Embedded GO Library
Ascii Moth · 2026-05-10 · via DEV Community

Yggdrasil is an experimental overlay IPv6 mesh network.
In short, it lets you build a "network on top of a network": each node gets a stable IPv6 address derived from its public key,
and that address does not depend on where the node is physically located or what external IP address it currently has.

Nodes can connect to public peers, to each other directly, or discover each other on the local network.
Once connectivity is established, ordinary TCP/UDP applications can communicate as if they were simply using another IPv6 network.

In the classic setup, Yggdrasil is a daemon that creates a virtual network interface in the operating system.

But sometimes it would be useful to embed Yggdrasil directly into an application.
For example, into Matrix clients, or into web applications.

The original yggdrasil-go is not especially convenient for that role because of leaky abstractions and strong coupling between components.
To make library-style usage easier, and to support features that were repeatedly rejected because "this is not a goal of Yggdrasil", I maintain my own compatible fork.

This article is about embedding its library part into a Go application.
But is should be usefull for work with original yggdrasil-go codebase.

What we are going to build

In this article, we will assemble a minimal setup:

┌─────────────┐               ┌─────────────┐
│ node A      │               │ node B      │
│             │   Yggdrasil   │             │
│ Core + VTun │ <───────────> │ Core + VTun │
└──────┬──────┘               └──────┬──────┘
       │                             │
       │ ordinary TCP/UDP/HTTP       │
       │ through a userspace IPv6    │
       │ stack                       │
       ▼                             ▼
  net.Listener                   http.Client

Enter fullscreen mode Exit fullscreen mode

There are two different network layers here.

The first one is the "carrier network". This is the network over which Yggdrasil nodes establish connections to each other.

In ygglib, this is represented by the Network interface.
In practice, the implementation behind it may be the normal "native" network of your operating system, a SOCKS proxy, an in-memory network emulator used for tests, or even another Yggdrasil network.

The second layer is the virtual IPv6 Yggdrasil network itself.
It appears after the nodes have started, found each other, and exchanged the required routing information.

A minimal node

Let’s start with the smallest useful example: create one Yggdrasil Core node, register TCP and TLS transports, print the node address, and exit.

package main

import (
    "fmt"

    "github.com/asciimoth/gonnect/native"
    "github.com/asciimoth/ygg/ygglib/config"
    "github.com/asciimoth/ygg/ygglib/core"
    ygglogger "github.com/asciimoth/ygg/ygglib/logger"
    "github.com/asciimoth/ygg/ygglib/transport"
)

func main() {
    // The config contains the node identity.
    // For the example, we generate a new self-signed certificate on every run.
    cfg := config.GenerateConfig()
    if err := cfg.GenerateSelfSignedCertificate(); err != nil {
        panic(err)
    }

    // native.Network is the normal operating-system network.
    // It will be used to open carrier connections to other peers.
    network := &native.Network{}
    if err := network.Up(); err != nil {
        panic(err)
    }
    defer network.Down()

    // Transport manager registers transport implementations (tcp, tls, ws, etc.)
    // and maps addresses to the carrier network they should use.
    manager := transport.NewManager(network)

    // Plain tcp:// transport.
    if err := manager.RegisterTransport(transport.NewTCPTransport()); err != nil {
        panic(err)
    }

    // tls:// transport uses our node certificate.
    tlsConfig, err := core.GenerateTLSConfig(cfg.Certificate)
    if err != nil {
        panic(err)
    }
    if err := manager.RegisterTransport(transport.NewTLSTransport(tlsConfig)); err != nil {
        panic(err)
    }

    // Create the Core itself.
    // Logging is disabled here to keep the example small.
    node, err := core.New(
        cfg.Certificate,
        ygglogger.Discard(),
        core.TransportManager{Manager: manager},
    )
    if err != nil {
        panic(err)
    }
    defer node.Stop()

    // This is the IPv6 address of the node inside the Yggdrasil network.
    fmt.Println(node.Address())
}

Enter fullscreen mode Exit fullscreen mode

Transports

A transport in ygglib owns one or more URL schemes and provides methods for dialing outgoing connections and listening for incoming ones.

Transports are registered in a concrete node instance at runtime. The library part includes transports for tcp://... and tls://..., while the daemon also implements quic, ws/wss, and unix.

You can write your own transports too.

For demonstration, let’s wrap an existing transport and add a bit of behavior around it. For example, we can count dial/listen operations and name our scheme metered+tcp.

package main

import (
    "context"
    "net/url"
    "sync/atomic"

    "github.com/asciimoth/ygg/ygglib/transport"
)

type meteredTransport struct {
    // All real work is delegated to the plain TCP transport.
    base transport.Transport

    // Counters are only here for demonstration.
    dials   atomic.Uint64
    listens atomic.Uint64
}

func (t *meteredTransport) Schemes() []string {
    // Now the manager can handle URLs like metered+tcp://127.0.0.1:1234.
    return []string{"metered+tcp"}
}

func (t *meteredTransport) Dial(
    ctx context.Context,
    network transport.Network,
    u *url.URL,
    opts transport.Options,
) (transport.Conn, error) {
    t.dials.Add(1)

    // The base TCP transport does not understand our metered+tcp scheme,
    // so we rewrite it to tcp before delegating.
    return t.base.Dial(ctx, network, rewriteScheme(u, "tcp"), opts)
}

func (t *meteredTransport) Listen(
    ctx context.Context,
    network transport.Network,
    u *url.URL,
    opts transport.Options,
) (transport.Listener, error) {
    t.listens.Add(1)
    return t.base.Listen(ctx, network, rewriteScheme(u, "tcp"), opts)
}

func (t *meteredTransport) Dials() uint64 {
    return t.dials.Load()
}

func (t *meteredTransport) Listens() uint64 {
    return t.listens.Load()
}

func rewriteScheme(u *url.URL, scheme string) *url.URL {
    clone := *u
    clone.Scheme = scheme
    return &clone
}

Enter fullscreen mode Exit fullscreen mode

This transport is registered in exactly the same way as the built-in ones:

manager := transport.NewManager(nil)

metered := &meteredTransport{
    base: transport.NewTCPTransport(),
}

if err := manager.RegisterTransport(metered); err != nil {
    return err
}

Enter fullscreen mode Exit fullscreen mode

Network mapping

transport.Manager can use one default network:

manager := transport.NewManager(defaultNetwork)

Enter fullscreen mode Exit fullscreen mode

But you can also explicitly describe which network should be used for which hosts.

// All connections to 127.0.0.1 will go through our loopback/native network.
if err := manager.MapNetwork("127.0.0.1", localNetwork); err != nil {
    return err
}

Enter fullscreen mode Exit fullscreen mode

This lets you route connections to the outside world through different carrier networks:

manager.SetDefaultNetwork(nativeNetwork)

// Tor addresses can go through a SOCKS network.
_ = manager.MapNetwork("*.onion", torNetwork)

// I2P can be handled in the same way.
_ = manager.MapNetwork("*.i2p", i2pNetwork)

// Some zones can be blocked explicitly.
_ = manager.MapNetwork("*.loki", nil)

Enter fullscreen mode Exit fullscreen mode

A nil mapping means that matching addresses must be blocked.
One more important detail: mapping changes are live. If you change the network for a host, the manager closes affected listeners and connections, so new ones will go through the new network.

Two nodes in one process

A single node is not very interesting by itself. Let’s create two Core instances and connect them to each other.

First, it is useful to move node creation into a function:

func newCore(manager *transport.Manager) (*core.Core, error) {
    // In a real application, the key should usually be persisted between runs.
    // Here we generate a new one to keep the example self-contained.
    cfg := config.GenerateConfig()
    if err := cfg.GenerateSelfSignedCertificate(); err != nil {
        return nil, err
    }

    return core.New(
        cfg.Certificate,
        ygglogger.Discard(),
        core.TransportManager{Manager: manager},
    )
}

Enter fullscreen mode Exit fullscreen mode

Now create the server and the client:

network := loopback.NewLoopbackNetwok()

metered := &meteredTransport{
    base: transport.NewTCPTransport(),
}

manager := transport.NewManager(nil)
if err := manager.MapNetwork("127.0.0.1", network); err != nil {
    return err
}
if err := manager.RegisterTransport(metered); err != nil {
    return err
}

serverCore, err := newCore(manager)
if err != nil {
    return err
}
defer serverCore.Stop()

clientCore, err := newCore(manager)
if err != nil {
    return err
}
defer clientCore.Stop()

Enter fullscreen mode Exit fullscreen mode

In this example, both nodes use the same manager and the same loopback network. In a real application, each node will usually live in its own process, with its own manager and its own network.

For one Core to accept a connection from another Core, we need to open a listener:

listenURL, err := url.Parse("metered+tcp://127.0.0.1:0")
if err != nil {
    return err
}

listener, err := serverCore.Listen(listenURL, "")
if err != nil {
    return err
}

Enter fullscreen mode Exit fullscreen mode

Port 0 means "choose any free port".

Now the client can connect to the server:

peerURL, err := url.Parse("metered+tcp://" + listener.Addr().String())
if err != nil {
    return err
}

if err := clientCore.CallPeer(peerURL, ""); err != nil {
    return err
}

Enter fullscreen mode Exit fullscreen mode

CallPeer opens a single connection to a peer. If you need a persistent connection with reconnects after failures, use AddPeer instead.

At this point, the two Core instances are already connected. But you still cannot put a normal http.Client directly on top of core.Core.

Core routes Yggdrasil packets. It does not provide the familiar net.Listener/net.Conn interface for user TCP connections.

For that, we need a "tun".

VTun

The normal Yggdrasil daemon creates a system TUN interface. But for an embedded library, we want to keep everything inside the process.

In this fork, that is done through an embedded userspace TCP/IP stack.

Core gives us a stream of IPv6 packets (L3), and VTun turns it into an L4 interface that can be used almost like a normal Go network.

Create a VTun for one Core:

import (
    "fmt"
    "net/netip"

    "github.com/asciimoth/gonnect-netstack/helpers"
    "github.com/asciimoth/gonnect-netstack/vtun"
    "github.com/asciimoth/ygg/ygglib/core"
    "github.com/asciimoth/ygg/ygglib/ipv6rwc"
    ygglogger "github.com/asciimoth/ygg/ygglib/logger"
    yggtun "github.com/asciimoth/ygg/ygglib/tun"
)

func newVTun(name string, coreNode *core.Core) (*vtun.VTun, *yggtun.TunAdapter, error) {
    // ipv6rwc adapts core.Core to an io.ReadWriteCloser-like interface
    // for reading and writing IPv6 packets.
    rwc := ipv6rwc.NewReadWriteCloser(coreNode)

    // TunAdapter connects Yggdrasil Core to a concrete TUN/VTun implementation.
    adapter, err := yggtun.New(
        rwc,
        ygglogger.Discard(),
        yggtun.InterfaceMTU(1500),
    )
    if err != nil {
        _ = rwc.Close()
        return nil, nil, err
    }

    // The Core address is the IPv6 address of the node inside the Yggdrasil network.
    addr, ok := netip.AddrFromSlice(coreNode.Address())
    if !ok {
        _ = adapter.Stop()
        _ = rwc.Close()
        return nil, nil, fmt.Errorf("invalid core address")
    }

    // VTun lives in process memory and provides Dial/Listen/ListenPacket.
    vt, err := (&vtun.Opts{
        Name:           name,
        LocalAddrs:     []netip.Addr{addr},
        NoLoopbackAddr: true,
        NetStackOpts: &helpers.Opts{
            MTU: 1500,
        },
    }).Build()
    if err != nil {
        _ = adapter.Stop()
        _ = rwc.Close()
        return nil, nil, err
    }

    // Attach VTun to the Core packet stream.
    if err := adapter.Attach(vt, yggtun.AttachmentType("vtun")); err != nil {
        _ = vt.Close()
        _ = adapter.Stop()
        _ = rwc.Close()
        return nil, nil, err
    }

    return vt, adapter, nil
}

Enter fullscreen mode Exit fullscreen mode

Now create one VTun for each Core:

serverVT, serverAdapter, err := newVTun("server", serverCore)
if err != nil {
    return err
}
defer serverAdapter.Stop()
defer serverVT.Close()

clientVT, clientAdapter, err := newVTun("client", clientCore)
if err != nil {
    return err
}
defer clientAdapter.Stop()
defer clientVT.Close()

Enter fullscreen mode Exit fullscreen mode

Now we have two in-process IPv6 networks connected through Yggdrasil Core. And we can use ordinary networking primitives on top of them.

TCP over VTun

Let’s start with a simple TCP echo-like exchange. The server listens on its Yggdrasil IPv6 address, and the client connects through its VTun.

func tcpPing(clientVT, serverVT *vtun.VTun, serverCore *core.Core) (string, error) {
    // Listen for TCP inside the Yggdrasil network.
    // The address comes from serverCore, and the port is selected automatically.
    listener, err := serverVT.Listen(
        context.Background(),
        "tcp6",
        net.JoinHostPort(serverCore.Address().String(), "0"),
    )
    if err != nil {
        return "", err
    }
    defer listener.Close()

    serverErr := make(chan error, 1)

    go func() {
        conn, err := listener.Accept()
        if err != nil {
            serverErr <- err
            return
        }
        defer conn.Close()

        buf := make([]byte, 64)
        n, err := conn.Read(buf)
        if err != nil {
            serverErr <- err
            return
        }

        // Reply with the same payload, but with a prefix.
        _, err = conn.Write([]byte("tcp:" + string(buf[:n])))
        serverErr <- err
    }()

    // The client connects to the listener address through its VTun.
    conn, err := clientVT.Dial(context.Background(), "tcp6", listener.Addr().String())
    if err != nil {
        return "", err
    }
    defer conn.Close()

    _ = conn.SetDeadline(time.Now().Add(10 * time.Second))

    if _, err := conn.Write([]byte("ping")); err != nil {
        return "", err
    }

    buf := make([]byte, 64)
    n, err := conn.Read(buf)
    if err != nil {
        return "", err
    }

    if err := <-serverErr; err != nil {
        return "", err
    }

    return string(buf[:n]), nil
}

Enter fullscreen mode Exit fullscreen mode

The result is:

tcp:ping

Enter fullscreen mode Exit fullscreen mode

From the outside, this looks almost like ordinary TCP code. The main difference is that Dial and Listen come not from the standard-library net package, but from the VTun object.

UDP over VTun

The UDP version is almost the same, except that the server uses ListenPacket.

func udpPing(clientVT, serverVT *vtun.VTun, serverCore *core.Core) (string, error) {
    packetConn, err := serverVT.ListenPacket(
        context.Background(),
        "udp6",
        net.JoinHostPort(serverCore.Address().String(), "0"),
    )
    if err != nil {
        return "", err
    }
    defer packetConn.Close()

    serverErr := make(chan error, 1)

    go func() {
        buf := make([]byte, 64)

        // For UDP, we need the sender address
        // so we can send a response back.
        n, addr, err := packetConn.ReadFrom(buf)
        if err != nil {
            serverErr <- err
            return
        }

        _, err = packetConn.WriteTo([]byte("udp:"+string(buf[:n])), addr)
        serverErr <- err
    }()

    conn, err := clientVT.Dial(
        context.Background(),
        "udp6",
        packetConn.LocalAddr().String(),
    )
    if err != nil {
        return "", err
    }
    defer conn.Close()

    _ = conn.SetDeadline(time.Now().Add(10 * time.Second))

    if _, err := conn.Write([]byte("ping")); err != nil {
        return "", err
    }

    buf := make([]byte, 64)
    n, err := conn.Read(buf)
    if err != nil {
        return "", err
    }

    if err := <-serverErr; err != nil {
        return "", err
    }

    return string(buf[:n]), nil
}

Enter fullscreen mode Exit fullscreen mode

Result:

udp:ping

Enter fullscreen mode Exit fullscreen mode

So, for ordinary application code, Yggdrasil does not really change much. We just use a different Dial/ListenPacket, and then continue working with standard net.Conn and net.PacketConn interfaces.

HTTP over VTun

Since TCP works, HTTP does not require anything special either. The server needs a listener from serverVT, and the client needs an http.Transport whose DialContext points to clientVT.Dial.

func httpPing(clientVT, serverVT *vtun.VTun, serverCore *core.Core) (string, error) {
    listener, err := serverVT.Listen(
        context.Background(),
        "tcp6",
        net.JoinHostPort(serverCore.Address().String(), "0"),
    )
    if err != nil {
        return "", err
    }

    server := &http.Server{
        Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
            _, _ = io.WriteString(w, "http:pong")
        }),
        ReadHeaderTimeout: 10 * time.Second,
    }

    go func() {
        // http.ErrServerClosed during Shutdown is expected,
        // so we do not log it in this minimal example.
        _ = server.Serve(listener)
    }()

    defer func() {
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        _ = server.Shutdown(ctx)
    }()

    _, port, err := net.SplitHostPort(listener.Addr().String())
    if err != nil {
        return "", err
    }

    // An IPv6 address in a URL must be wrapped in square brackets.
    target := fmt.Sprintf("http://[%s]:%s", serverCore.Address().String(), port)

    client := http.Client{
        Transport: &http.Transport{
            // The HTTP client opens TCP connections
            // through our VTun instead of net.Dialer.
            DialContext: clientVT.Dial,
        },
        Timeout: 10 * time.Second,
    }

    resp, err := client.Get(target)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }

    return string(body), nil
}

Enter fullscreen mode Exit fullscreen mode

Result:

http:pong

Enter fullscreen mode Exit fullscreen mode

Autopeering

So far we connected nodes manually: one listens, the other calls CallPeer.

That is enough for tests, but a normal application usually wants to connect to the global network automatically.

For that, there is autopeer.Manager.

It fetches public peer lists, filters the results, and adds suitable addresses to Core.

func configurePublicAutopeering(coreNode *core.Core, network transport.Network) *autopeer.Manager {
    // Fetcher can retrieve public peer lists.
    // BUILTIN is the built-in list and does not require a separate URL.
    fetcher := autopeer.NewFetcher(ygglogger.Discard(), time.Hour)
    fetcher.SetDefaultNetwork(network)
    fetcher.SetSources([]string{autopeer.BuiltinSource})

    manager := autopeer.NewManager(fetcher)
    manager.SetPeerManager(coreNode)

    manager.SetConfig(autopeer.ManagerConfig{
        CheckInterval: time.Minute,

        // If there are fewer than two connected peers,
        // the manager will try to add new ones.
        MinimumConnected: 2,

        // For the example, limit the search to a few countries.
        Countries: []string{
            "germany",
            "france",
            "netherlands",
        },

        // And only these transport schemes.
        TransportSchemes: []string{"tcp", "tls"},
    })

    return manager
}

Enter fullscreen mode Exit fullscreen mode

The manager is started explicitly:

autopeering := configurePublicAutopeering(coreNode, nativeNetwork)
autopeering.Start()
defer autopeering.Close()

Enter fullscreen mode Exit fullscreen mode

It is worth noting that the manager does nothing by default until country and transport scheme filters are configured explicitly.

Internally, it uses core.AddPeer, not CallPeer, so selected peers become persistent and will be reconnected after failures.

Link-local autopeering

Besides public peers, there is also automatic discovery on the local network.

This is handled by the ygglib/multicast package. It listens for local multicast announcements and calls core.CallPeer for discovered nodes.

There is one limitation though: link-local autopeering requires a real network. In-memory loopback networks, SOCKS clients, and other virtual implementations do not have the low-level OS interfaces required for it.

A minimal setup looks like this:

func startLinkLocalAutopeering(
    coreNode *core.Core,
    network transport.Network,
    ifacePattern string,
) (*multicast.Multicast, error) {
    if network == nil || !network.IsNative() {
        return nil, fmt.Errorf("link-local autopeering requires a native carrier network")
    }

    return multicast.New(
        coreNode,
        ygglogger.Discard(),
        multicast.ProtocolVersion{
            Major: core.ProtocolVersionMajor,
            Minor: core.ProtocolVersionMinor,
        },
        multicast.MulticastInterface{
            // For example: ^(eth|en|wlan|wl).*
            Regex: regexp.MustCompile(ifacePattern),
            Beacon: true,
            Listen: true,
            Port:   0,
        },
    )
}

Enter fullscreen mode Exit fullscreen mode

Usage:

mc, err := startLinkLocalAutopeering(
    coreNode,
    nativeNetwork,
    "^(eth|en|wlan|wl).*",
)
if err != nil {
    return err
}
defer mc.Stop()

Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope that the more modular approach implemented in this fork will make more people interested in experimenting with Yggdrasil as a component of larger systems, instead of only using it as a standalone daemon.