<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Mothware Notes]]></title><description><![CDATA[Things I am doing. Mostly software, decentralized networks, a bit of ASCII art and sometimes chtonic DIY.]]></description><link>https://hash.moth.contact</link><image><url>https://cdn.hashnode.com/uploads/logos/69ff3f1af239332df4b947ff/512ae5ab-5b1d-40e1-aef2-34dd0e6d47f0.jpg</url><title>Mothware Notes</title><link>https://hash.moth.contact</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 09 May 2026 19:03:07 GMT</lastBuildDate><atom:link href="https://hash.moth.contact/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Yggdrasil Network as an Embedded Go Library]]></title><description><![CDATA[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 ]]></description><link>https://hash.moth.contact/yggdrasil-network-as-an-embedded-go-library</link><guid isPermaLink="true">https://hash.moth.contact/yggdrasil-network-as-an-embedded-go-library</guid><category><![CDATA[Go Language]]></category><category><![CDATA[networking]]></category><category><![CDATA[ipv6]]></category><category><![CDATA[decentralization]]></category><category><![CDATA[Mesh Networking]]></category><category><![CDATA[yggdrasil]]></category><dc:creator><![CDATA[Ascii Moth]]></dc:creator><pubDate>Sat, 09 May 2026 16:58:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ff3f1af239332df4b947ff/52680a16-6645-4fed-b0ad-fb697de959fc.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a href="https://yggdrasil-network.github.io/">Yggdrasil</a> 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.</p>
<p>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.</p>
<p>In the classic setup, Yggdrasil is a daemon that creates a <a href="https://en.wikipedia.org/wiki/TUN/TAP">virtual network interface</a> in the operating system.</p>
<p>But sometimes it would be useful to embed Yggdrasil directly into an application.
For example, into <a href="https://github.com/matrix-org/dendrite">Matrix clients</a>, or into <a href="https://asciimoth.github.io/ygg/">web applications</a>.</p>
<p>The original <a href="https://github.com/yggdrasil-network/yggdrasil-go">yggdrasil-go</a> 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 <a href="https://github.com/asciimoth/ygg#what-changed-from-upstream">features</a> that were repeatedly rejected because <a href="https://github.com/yggdrasil-network/yggdrasil-go/issues/1060#issuecomment-1712613462">"this is not a goal of Yggdrasil"</a>, I maintain my own compatible <a href="https://github.com/asciimoth/ygg">fork</a>.</p>
<p>This article is about embedding its library part into a Go application.
But is should be usefull for work with original yggdrasil-go codebase.</p>
<h1>What we are going to build</h1>
<p>In this article, we will build two Yggdrasil nodes running inside the same Go process.</p>
<p>Each node consists of two layers:</p>
<p>a Yggdrasil Core, responsible for peer connectivity and packet routing
a VTun, which exposes the Yggdrasil IPv6 network as a userspace TCP/IP stack</p>
<p>The two nodes communicate with each other through a carrier network.</p>
<p>On top of the virtual IPv6 network created by Yggdrasil, we will run ordinary TCP, UDP, and HTTP applications using familiar Go networking primitives like net.Listener, net.Conn, and http.Client.</p>
<p>By the end of the article, we will have TCP, UDP, and HTTP communication working entirely inside one process, without creating a system TUN interface.</p>
<h1>A minimal node</h1>
<p>Let’s start with the smallest useful example: create one Yggdrasil Core node, register TCP and TLS transports, print the node address, and exit.</p>
<pre><code class="language-go">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 := &amp;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())
}
</code></pre>
<h1>Transports</h1>
<p>A transport in <code>ygglib</code> owns one or more URL schemes and provides methods for dialing outgoing connections and listening for incoming ones.</p>
<p>Transports are registered in a concrete node instance at runtime. The library part includes transports for <code>tcp://...</code> and <code>tls://...</code>, while the <a href="https://github.com/asciimoth/ygg/tree/develop/yggd">daemon</a> also implements <code>quic</code>, <code>ws</code>/<code>wss</code>, and <code>unix</code>.</p>
<p>You can write your own transports too.</p>
<p>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 <code>metered+tcp</code>.</p>
<pre><code class="language-go">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 &amp;clone
}
</code></pre>
<p>This transport is registered in exactly the same way as the built-in ones:</p>
<pre><code class="language-go">manager := transport.NewManager(nil)

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

if err := manager.RegisterTransport(metered); err != nil {
	return err
}
</code></pre>
<h1>Network mapping</h1>
<p><code>transport.Manager</code> can use one default network:</p>
<pre><code class="language-go">manager := transport.NewManager(defaultNetwork)
</code></pre>
<p>But you can also explicitly describe which network should be used for which hosts.</p>
<pre><code class="language-go">// 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
}
</code></pre>
<p>This lets you route connections to the outside world through different carrier networks:</p>
<pre><code class="language-go">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)
</code></pre>
<p>A <code>nil</code> 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.</p>
<h1>Two nodes in one process</h1>
<p>A single node is not very interesting by itself. Let’s create two Core instances and connect them to each other.</p>
<p>First, it is useful to move node creation into a function:</p>
<pre><code class="language-go">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},
	)
}
</code></pre>
<p>Now create the server and the client:</p>
<pre><code class="language-go">network := loopback.NewLoopbackNetwok()

metered := &amp;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()
</code></pre>
<p>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.</p>
<p>For one Core to accept a connection from another Core, we need to open a listener:</p>
<pre><code class="language-go">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
}
</code></pre>
<p>Port <code>0</code> means "choose any free port".</p>
<p>Now the client can connect to the server:</p>
<pre><code class="language-go">peerURL, err := url.Parse("metered+tcp://" + listener.Addr().String())
if err != nil {
	return err
}

if err := clientCore.CallPeer(peerURL, ""); err != nil {
	return err
}
</code></pre>
<p><code>CallPeer</code> opens a single connection to a peer. If you need a persistent connection with reconnects after failures, use <code>AddPeer</code> instead.</p>
<p>At this point, the two Core instances are already connected. But you still cannot put a normal <code>http.Client</code> directly on top of <code>core.Core</code>.</p>
<p>Core routes Yggdrasil packets. It does not provide the familiar <code>net.Listener</code>/<code>net.Conn</code> interface for user TCP connections.</p>
<p>For that, we need a "tun".</p>
<h1>VTun</h1>
<p>The normal Yggdrasil daemon creates a system TUN interface. But for an embedded library, we want to keep everything inside the process.</p>
<p>In this fork, that is done through an embedded userspace TCP/IP stack.</p>
<p>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.</p>
<p>Create a VTun for one Core:</p>
<pre><code class="language-go">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 := (&amp;vtun.Opts{
		Name:           name,
		LocalAddrs:     []netip.Addr{addr},
		NoLoopbackAddr: true,
		NetStackOpts: &amp;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
}
</code></pre>
<p>Now create one VTun for each Core:</p>
<pre><code class="language-go">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()
</code></pre>
<p>Now we have two in-process IPv6 networks connected through Yggdrasil Core. And we can use ordinary networking primitives on top of them.</p>
<h1>TCP over VTun</h1>
<p>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.</p>
<pre><code class="language-go">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 &lt;- err
			return
		}
		defer conn.Close()

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

		// Reply with the same payload, but with a prefix.
		_, err = conn.Write([]byte("tcp:" + string(buf[:n])))
		serverErr &lt;- 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 := &lt;-serverErr; err != nil {
		return "", err
	}

	return string(buf[:n]), nil
}
</code></pre>
<p>The result is:</p>
<pre><code class="language-text">tcp:ping
</code></pre>
<p>From the outside, this looks almost like ordinary TCP code. The main difference is that <code>Dial</code> and <code>Listen</code> come not from the standard-library <code>net</code> package, but from the VTun object.</p>
<h1>UDP over VTun</h1>
<p>The UDP version is almost the same, except that the server uses <code>ListenPacket</code>.</p>
<pre><code class="language-go">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 &lt;- err
			return
		}

		_, err = packetConn.WriteTo([]byte("udp:"+string(buf[:n])), addr)
		serverErr &lt;- 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 := &lt;-serverErr; err != nil {
		return "", err
	}

	return string(buf[:n]), nil
}
</code></pre>
<p>Result:</p>
<pre><code class="language-text">udp:ping
</code></pre>
<p>So, for ordinary application code, Yggdrasil does not really change much. We just use a different <code>Dial</code>/<code>ListenPacket</code>, and then continue working with standard <code>net.Conn</code> and <code>net.PacketConn</code> interfaces.</p>
<h1>HTTP over VTun</h1>
<p>Since TCP works, HTTP does not require anything special either. The server needs a listener from <code>serverVT</code>, and the client needs an <code>http.Transport</code> whose <code>DialContext</code> points to <code>clientVT.Dial</code>.</p>
<pre><code class="language-go">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 := &amp;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: &amp;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
}
</code></pre>
<p>Result:</p>
<pre><code class="language-text">http:pong
</code></pre>
<h1>Autopeering</h1>
<p>So far we connected nodes manually: one listens, the other calls <code>CallPeer</code>.</p>
<p>That is enough for tests, but a normal application usually wants to connect to the global network automatically.</p>
<p>For that, there is <code>autopeer.Manager</code>.</p>
<p>It fetches public peer lists, filters the results, and adds suitable addresses to Core.</p>
<pre><code class="language-go">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
}
</code></pre>
<p>The manager is started explicitly:</p>
<pre><code class="language-go">autopeering := configurePublicAutopeering(coreNode, nativeNetwork)
autopeering.Start()
defer autopeering.Close()
</code></pre>
<p>It is worth noting that the manager does nothing by default until country and transport scheme filters are configured explicitly.</p>
<p>Internally, it uses <code>core.AddPeer</code>, not <code>CallPeer</code>, so selected peers become persistent and will be reconnected after failures.</p>
<h1>Link-local autopeering</h1>
<p>Besides public peers, there is also automatic discovery on the local network.</p>
<p>This is handled by the <code>ygglib/multicast</code> package. It listens for local multicast announcements and calls <code>core.CallPeer</code> for discovered nodes.</p>
<p>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.</p>
<p>A minimal setup looks like this:</p>
<pre><code class="language-go">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,
		},
	)
}
</code></pre>
<p>Usage:</p>
<pre><code class="language-go">mc, err := startLinkLocalAutopeering(
	coreNode,
	nativeNetwork,
	"^(eth|en|wlan|wl).*",
)
if err != nil {
	return err
}
defer mc.Stop()
</code></pre>
<h1>Conclusion</h1>
<p>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.</p>
]]></content:encoded></item></channel></rss>