<?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>Sun, 07 Jun 2026 16:59:19 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[3a: A Format and Tooling for Creating ASCII Animations]]></title><description><![CDATA[Most people in tech have probably seen ASCII art at some point, and it has been written about plenty of times already, but let’s do the obligatory intro anyway.
ASCII art is a way to draw using plain ]]></description><link>https://hash.moth.contact/3a-a-format-and-tooling-for-creating-ascii-animations</link><guid isPermaLink="true">https://hash.moth.contact/3a-a-format-and-tooling-for-creating-ascii-animations</guid><category><![CDATA[#asciiart]]></category><category><![CDATA[cli]]></category><dc:creator><![CDATA[Ascii Moth]]></dc:creator><pubDate>Fri, 15 May 2026 16:28:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ff3f1af239332df4b947ff/2e7e0427-07ca-4f83-aeb8-7b23ce27241d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most people in tech have probably seen ASCII art at some point, and it has been written about plenty of times already, but let’s do the obligatory intro anyway.</p>
<p>ASCII art is a way to draw using plain text. Instead of pixels, you have characters. Instead of a graphics editor, any text editor will do. And instead of a viewer, there is usually <code>cat</code> and a terminal.</p>
<pre><code class="language-text"> /\_/\
( o.o )
 &gt; ^ &lt;
</code></pre>
<p>Historically, "ASCII art" is often used as an umbrella term for almost any kind of text-based art, even when it has long since escaped the boundaries of ASCII itself and started using Unicode, ANSI colors, box-drawing characters, Braille symbols, terminal control sequences, and so on. I will use the term in that broader sense too: an image or animation that can be stored, edited, and displayed as text.</p>
<h2>Why Yet Another Format?</h2>
<p>About five years ago, before my interest in ricing and OS customization had fully evaporated, I really wanted to add an animated ASCII logo of my distro to <code>neofetch</code>, mostly for the sake of nice-looking GIFs on <a href="https://www.reddit.com/r/unixporn/">r/unixporn</a>.</p>
<p>I had already seen examples of animated fetch tools, and for some reason I assumed there must be some common format for that kind of thing.</p>
<p>As it turned out, it wasnt. (Well, technically it was, but we will get to that near the end).</p>
<p>The examples I found were usually custom scripts with hardcoded strings containing <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#colors">ANSI colors</a> and a <code>sleep</code> between frames. There was no "standard". And if there is no such format that it should be created.</p>
<p>That is how <code>Animated ASCII Art</code> appeared. The characteristic difference between <code>3a</code> and a good old plaintext file with ANSI escape codes is that <code>3a</code> preserves formatting in a text editor: the art looks the same in the editor as it does when rendered. This means you can work with it directly, without needing a specialized editor like <a href="https://github.com/cmang/durdraw/">DurDraw</a>.</p>
<p>For a long time, the project existed as a somewhat ambiguous format description, a minimal implementation without many features, and literally a couple of artworks for which the whole thing had been created in the first place.</p>
<p>Recently I finally cleaned it all up: rewrote the specification, brought the CLI tool into a more usable state, split out libraries, added conversion to different formats, and organized a <a href="https://github.com/asciimoth/openascii">small collection</a> of openly licensed ASCII animations.</p>
<h2>File Structure</h2>
<p>A <code>3a</code> file consists of blocks.</p>
<p>Each block starts with a line containing the block name, beginning with <code>@</code>. The first block is always <code>@3a</code>; it is the header with metadata. The main block with frames is called <code>@body</code>.</p>
<p>A minimal file without colors or metadata may look like this:</p>
<pre><code class="language-text">@3a

@body
  &lt;=&gt;\      
  ,..\\..,  
 ' //     '
|          |
|          |
 '.__.~._.'
</code></pre>
<p>If there are multiple frames, they are separated by blank lines:</p>
<pre><code class="language-text">@3a
title just an apple
delay 300
loop yes

@body
  &lt;=&gt;\      
  ,..\\..,  
 ' //     '
|          |
|          |
 '.__.~._.'

  &lt;=&gt;\      
  ,..\\..,  
 ' //,_.--'
|   /  {    
|   \_,''-.
 '.__.~._.'

  &lt;=&gt;\      
  ,..\\..,  
 ',--,_.--'
    }  {    
  ,-,_,''-.
 '.__.~._.'
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69ff3f1af239332df4b947ff/0ea913db-206b-48e5-b29e-6cb699b7129f.gif" alt="" style="display:block;margin:0 auto" />

<p>A few fields appeared in the header here:</p>
<ul>
<li><p><code>title</code> - the title of the art;</p>
</li>
<li><p><code>delay</code> - delay between frames in milliseconds;</p>
</li>
<li><p><code>loop</code> - whether the animation should loop.</p>
</li>
</ul>
<p>If <code>delay</code> is not specified, the default is 50 ms. If <code>loop</code> is not specified, the animation is considered looped. So simple artworks do not need to be bloated with metadata unless there is a reason for it.</p>
<p>The header can also contain authors, original author, source link, license, editor, preview frame, tags, and so on. For example:</p>
<pre><code class="language-text">@3a
title just an apple
author ASCIIMoth
license CC0-1.0
src https://github.com/asciimoth/openascii
delay 300
loop yes
preview 0
#apple #fruit #food
</code></pre>
<p>Comments, in places where they are allowed, start with <code>;;</code> and take the whole line.</p>
<h2>Colors</h2>
<p>The most important idea behind colored <code>3a</code> is that text and colors are stored in separate columns, not mixed together.</p>
<p>Instead of inserting ANSI escape codes directly into the art, each text line is followed by a color line of the same length, consisting of color names.</p>
<pre><code class="language-text">@3a
colors yes

@body
  &lt;=&gt;\      112228111111
  ,..\\..,  111118811111
 ' //     ' 111991111111
|          |111111111111
|          |111111111111
 '.__.~._.' 111111811111
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69ff3f1af239332df4b947ff/f7a08190-be59-4a66-ac2e-f7a36fa88d62.gif" alt="" style="display:block;margin:0 auto" />

<p>The built-in names represent standard 4-bit ANSI colors:</p>
<pre><code class="language-text">0 black
1 red
2 green
3 yellow
4 blue
5 magenta
6 cyan
7 white
8 bright black / gray
9 bright red
a bright green
b bright yellow
c bright blue
d bright magenta
e bright cyan
f bright white
_ default (depends on the terminal)
</code></pre>
<p>You can also define custom colors:</p>
<pre><code class="language-text">@3a
colors yes
col r fg:196
col l fg:bright-green
col b fg:94
</code></pre>
<p>A color can be an ANSI color by name, a 256-color code, or an RGB hex value. For terminal output, this is later mapped to ANSI sequences; for SVG, PNG, GIF, WebP, and MP4, it is mapped to the corresponding graphical representation.</p>
<p>If the color channel is the same for all frames, it does not have to be repeated every time. For that there is the <code>@color-pin</code> block, which defines one "pinned" color-channel frame for the whole animation.</p>
<p>And conversely, if the text stays the same and only colors change, <code>@text-pin</code> can be used.</p>
<pre><code class="language-text">@3a
title apple with pinned colors
colors yes
delay 300

@color-pin
112228111111
111118811111
111991111111
111111111111
111111111111
111111811111

@body
  &lt;=&gt;\      
  ,..\\..,  
 ' //     '
|          |
|          |
 '.__.~._.'

  &lt;=&gt;\      
  ,..\\..,  
 ' //,_.--'
|   /  {    
|   \_,''-.
 '.__.~._.'

  &lt;=&gt;\      
  ,..\\..,  
 ',--,_.--'
    }  {    
  ,-,_,''-.
 '.__.~._.'
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/69ff3f1af239332df4b947ff/0410fc90-5003-4a50-a760-dff53cfdfe40.gif" alt="" style="display:block;margin:0 auto" />

<h2>aaa</h2>
<p>The main tool for working with <code>3a</code> is the <a href="https://github.com/asciimoth/aaa">aaa</a> CLI utility.</p>
<p>At the top level, it has several command groups:</p>
<pre><code class="language-text">list         List builtin art
gen          Generate new art
play         Play art in terminal
fetch        Show system info side by side with animated logo
preview      Show art preview
edit         Editing subcommands
convert      Format conversion subcommands
from-text    Constructs art from plain text with ANSI color escape codes
completions  Generate shell completions
</code></pre>
<p>To simply play a <code>.3a</code> file in the terminal:</p>
<pre><code class="language-bash">aaa play ./apple.3a
</code></pre>
<p>If the file has colors, <code>aaa</code> converts the color channel into ANSI escape sequences at output time.</p>
<p>To show a preview - one frame, by default frame zero:</p>
<pre><code class="language-bash">aaa preview apple.3a
</code></pre>
<p>And the fetch command, the reason this whole thing started:</p>
<pre><code class="language-bash">aaa fetch distro_nixos_big.3a
</code></pre>
<p><a class="embed-card" href="https://asciinema.org/a/ZtzhhTOmVWCAmrfz">https://asciinema.org/a/ZtzhhTOmVWCAmrfz</a></p>

<p>By default, <code>aaa</code> tries to use one of the already installed fetch tools: <code>neofetch</code>, <code>fastfetch</code>, <code>screenfetch</code>, <code>nitch</code>, <code>profetch</code>, <code>leaf</code>, or <code>fetch-scm</code>. So the system information comes from the usual fetch tooling, while the logo comes from the <code>3a</code> file.</p>
<h3>Generation and Editing</h3>
<p>A template can be created with <code>gen</code>:</p>
<pre><code class="language-bash">aaa gen &gt; apple.3a
</code></pre>
<p>After that, the file can be opened and edited manually. But some operations can also be done through <code>aaa edit</code>, which is especially useful from scripts.</p>
<p>Set the title:</p>
<pre><code class="language-bash">aaa edit ./apple.3a title "just an apple" &gt; apple2.3a
</code></pre>
<p>Add tags:</p>
<pre><code class="language-bash">aaa edit ./apple.3a tag-add apple fruit food &gt; apple2.3a
</code></pre>
<p>Change the license:</p>
<pre><code class="language-bash">aaa edit ./apple.3a license CC0-1.0 &gt; apple2.3a
</code></pre>
<p>Besides metadata, there are frame operations too: duplicate a frame, delete one, swap frames, reverse frame order, cut out a range, deduplicate repeated frames, shift the animation forward or backward, and so on.</p>
<h2>Conversion</h2>
<p>The second major part of <code>aaa</code> is conversion.</p>
<p>The list of currently supported targets looks like this:</p>
<pre><code class="language-text">to-frames  ANSI-colored frames separated by blank lines
to-cast    asciicast v2
to-dur     durdraw format
to-json    JSON document
to-ttyrec  ttyrec
to-png     PNG image
to-gif     GIF animation
to-webp    WebP animation
to-mp4     MP4 video
to-svg     SVG animation
to3a       print art back in 3a format
</code></pre>
<p>For example, to make a GIF:</p>
<pre><code class="language-bash">aaa convert apple.3a to-gif &gt; apple.gif
</code></pre>
<p>Or an <a href="https://asciinema.org/">asciinema</a> cast:</p>
<pre><code class="language-bash">aaa convert apple.3a to-cast &gt; apple.cast
asciinema play apple.cast
</code></pre>
<p>The normalization command is useful on its own:</p>
<pre><code class="language-bash">aaa convert apple.3a to3a &gt; normalized.3a
</code></pre>
<p>It reads the file and prints it back in the <code>3a</code> format. This is convenient for checking how the parser understands the file and for bringing artworks to a more uniform shape.</p>
<p>Another practical scenario is taking an existing artwork with ANSI sequences and converting it into <code>3a</code>:</p>
<pre><code class="language-bash">cat old-logo.txt | aaa from-text &gt; logo.3a
</code></pre>
<h2>Libraries</h2>
<p><code>aaa</code> is built on top of the <a href="https://github.com/asciimoth/rs3a">rs3a</a> Rust library.</p>
<p>It can read and write the new <code>3a</code> format, partially read the legacy version, edit art programmatically, and export it to SVG, asciicast v2, and ANSI-colored text.</p>
<pre><code class="language-rust">use rs3a::{Art, font::Font, CSSColorMap};
use std::fs::File;
use std::io::Write;

fn main() {
    let mut art = Art::from_file("./examples/dna.3a").unwrap();

    let color_pair = "fg:black bg:yellow".parse().unwrap();
    let color = art.search_or_create_color_map(color_pair);

    for frame in 0..art.frames() {
        art.print(frame, 0, 0, &amp;format!("{}", frame), Some(Some(color)));
    }

    art.to_file("./examples/edited_dna.3a").unwrap();

    let mut output = File::create("./examples/dna.svg").unwrap();
    write!(
        output,
        "{}",
        art.to_svg_frames(&amp;CSSColorMap::default(), &amp;Font::default())
    ).unwrap();
}
</code></pre>
<p>There are also <a href="https://github.com/asciimoth/py3a">py3a</a> and <a href="https://github.com/asciimoth/go3a">go3a</a>, but they are less feature-complete and are mostly useful for parsing.</p>
<h2>OpenASCII</h2>
<p>The last part of the "ecosystem" is <a href="https://github.com/asciimoth/openascii">openascii</a>.</p>
<p>It is a tiny collection of ASCII art, mostly mine, in the <code>3a</code> format, under permissive or copyleft licenses, with a web player based on <a href="https://asciinema.org/">asciinema</a>.</p>
<p>I would be happy if something from the collection turns out useful for your <a href="https://openascii.moth.contact/#resources">CLI tools / ASCII games / etc.</a>, or if you happen to have <a href="https://github.com/asciimoth/openascii/pulls">something to contribute</a>.</p>
<h2>Alternatives</h2>
<p>After the fact, I did eventually discover that several other attempts to do something similar had already existed. But they either solved a slightly different problem, or never got far enough.</p>
<p>The most noteworthy one is the terminal ASCII animation editor <a href="https://github.com/cmang/durdraw/">DurDraw</a>. It has its own format (gzipped JSON) and can use it in its built-in fetch command.</p>
<p>Still:</p>
<ul>
<li><p>the format has exactly one very tangled implementation;</p>
</li>
<li><p>the format documentation contradicts the de facto implementation;</p>
</li>
<li><p>editing that spaghetti of nested JSON by hand in a normal text editor is not much easier than the "reference" plaintext-plus-ANSI-codes approach.</p>
</li>
</ul>
<p>That said, had DurDraw been a little more famous and a little less buggy back then, maybe <code>3a</code> would never have happened. However, <code>aaa</code> now supports conversion between <code>.3a</code> and <code>.dur</code>.</p>
]]></content:encoded></item><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>