Yggdrasil Network as an Embedded Go Library
Yggdrasil Network as an Embedded Go Library
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. Yggdrasil 是一个实验性的覆盖型 IPv6 网状网络。简而言之,它允许你在“网络之上构建网络”:每个节点都会获得一个由其公钥派生的稳定 IPv6 地址,该地址不依赖于节点的物理位置或其当前的外部 IP 地址。
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. 节点可以连接到公共对等点、彼此直接连接,或在本地网络中发现对方。一旦建立连接,普通的 TCP/UDP 应用程序就可以像使用其他 IPv6 网络一样进行通信。
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. 在经典配置中,Yggdrasil 是一个在操作系统中创建虚拟网络接口的守护进程。但有时将 Yggdrasil 直接嵌入到应用程序中会很有用,例如嵌入到 Matrix 客户端或 Web 应用程序中。
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. 由于抽象泄露和组件之间的高度耦合,原始的 yggdrasil-go 在此角色中并不特别方便。为了使库式用法更简单,并支持那些因“这不是 Yggdrasil 的目标”而被反复拒绝的功能,我维护了自己的兼容分支。
This article is about embedding its library part into a Go application. But it should be useful for work with original yggdrasil-go codebase. 本文旨在介绍如何将其库部分嵌入到 Go 应用程序中。不过,这些内容对于使用原始 yggdrasil-go 代码库也同样适用。
What we are going to build
我们将构建什么
In this article, we will assemble a minimal setup: 在本文中,我们将组装一个最小化的设置:
┌─────────────┐ ┌─────────────┐
│ node A │ │ node B │
│ │ │ │
│ Yggdrasil │ │ Yggdrasil │
│ Core + VTun │ <──────> │ Core + VTun │
└──────┬──────┘ └──────┬──────┘
│ │
│ ordinary TCP/UDP/HTTP │
│ through a userspace IPv6
│ stack │
▼ ▼
net.Listener http.Client
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. 这里有两个不同的网络层。第一层是“载体网络”(carrier network)。这是 Yggdrasil 节点之间建立连接的网络。在 ygglib 中,它由 Network 接口表示。在实践中,其背后的实现可以是操作系统的常规“原生”网络、SOCKS 代理、用于测试的内存网络模拟器,甚至是另一个 Yggdrasil 网络。
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. 第二层是虚拟的 IPv6 Yggdrasil 网络本身。它在节点启动、发现彼此并交换所需的路由信息后出现。
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. 让我们从最小的实用示例开始:创建一个 Yggdrasil Core 节点,注册 TCP 和 TLS 传输,打印节点地址,然后退出。
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())
}
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. ygglib 中的传输层拥有一个或多个 URL 方案,并提供用于拨号外向连接和监听入站连接的方法。传输层在运行时注册到具体的节点实例中。
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.
该库部分包含了 tcp://... 和 tls://... 的传输实现,而守护进程还实现了 quic、ws/wss 和 unix。你也可以编写自己的传输层。
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.
为了演示,让我们包装一个现有的传输层并为其添加一些行为。例如,我们可以统计拨号/监听操作,并将我们的方案命名为 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
}
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
}