This tutorial shows the library composition used by the runnable companion in
examples/lib_tutorial. Run it with:
go run ./examples/lib_tutorial
go test ./examples/lib_tutorial --raceThe example creates two in-process Yggdrasil nodes, peers them through a custom transport, attaches a VTun to each node, then sends TCP, UDP, and HTTP traffic over the Yggdrasil Core.
See lib_tutorial example for the
complete runnable code.
core.Core does not create transport networks itself. The embedding program
chooses the gonnect.Network used by carrier transports.
The tutorial example uses an in-memory loopback network so tests are deterministic:
network := loopback.NewLoopbackNetwok()Production code usually uses a native network:
network := &native.Network{}
if err := network.Up(); err != nil {
return err
}
defer network.Down()A transport implements transport.Transport. It declares one or more URL
schemes and turns a selected gonnect.Network plus URL into a net.Conn or
net.Listener.
The example wraps the built-in TCP transport with a custom metered+tcp
scheme:
type meteredTransport struct {
base transport.Transport
dials atomic.Uint64
listens atomic.Uint64
}
func (t *meteredTransport) Schemes() []string {
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)
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)
}Register transports before constructing core.Core:
manager := transport.NewManager(nil)
if err := manager.RegisterTransport(&meteredTransport{
base: transport.NewTCPTransport(),
}); err != nil {
return err
}
tlsConfig, err := core.GenerateTLSConfig(cert)
if err != nil {
return err
}
if err := manager.RegisterTransport(transport.NewTLSTransport(tlsConfig)); err != nil {
return err
}transport.Manager chooses a carrier network by host. You can set a default
network for all unmatched hosts, or leave the default as nil and route only
explicit mappings.
The example intentionally uses no default network and maps only 127.0.0.1:
manager := transport.NewManager(nil)
if err := manager.MapNetwork("127.0.0.1", network); err != nil {
return err
}Useful patterns include:
manager.SetDefaultNetwork(nativeNetwork)
_ = manager.MapNetwork("*.onion", torNetwork)
_ = manager.MapNetwork("*.i2p", i2pNetwork)
_ = manager.MapNetwork("*.loki", nil)A nil mapping disables matching hosts. Mapping changes are live: the manager
closes affected listeners and connections so new dials use the new mapping.
Generate or load identity material, then pass the prepared transport manager
into core.New:
cfg := config.GenerateConfig()
if err := cfg.GenerateSelfSignedCertificate(); err != nil {
return err
}
coreNode, err := core.New(
cfg.Certificate,
ygglogger.Discard(),
core.TransportManager{Manager: manager},
)
if err != nil {
return err
}
defer coreNode.Stop()A listener plus a peer call is enough to connect two cores:
listener, err := serverCore.Listen(mustParseURL("metered+tcp://127.0.0.1:0"), "")
if err != nil {
return err
}
peerURL := mustParseURL("metered+tcp://" + listener.Addr().String())
if err := clientCore.CallPeer(peerURL, ""); err != nil {
return err
}Use AddPeer instead of CallPeer when the link should be persistent and
retried after disconnects.
Core routes encrypted packets. VTun turns that packet stream into a userspace IPv6 network that can run normal TCP, UDP, and HTTP clients and servers.
rwc := ipv6rwc.NewReadWriteCloser(coreNode)
adapter, err := yggtun.New(rwc, ygglogger.Discard(), yggtun.InterfaceMTU(1500))
if err != nil {
return err
}
addr, ok := netip.AddrFromSlice(coreNode.Address())
if !ok {
return fmt.Errorf("invalid core address")
}
vt, err := (&vtun.Opts{
Name: "node-a",
LocalAddrs: []netip.Addr{addr},
NoLoopbackAddr: true,
NetStackOpts: &helpers.Opts{
MTU: 1500,
},
}).Build()
if err != nil {
return err
}
if err := adapter.Attach(vt, yggtun.AttachmentType("vtun")); err != nil {
return err
}After both nodes are peered and have VTun attached, use the server node's Yggdrasil IPv6 address as the listen address:
listener, err := serverVT.Listen(
context.Background(),
"tcp6",
net.JoinHostPort(serverCore.Address().String(), "0"),
)
if err != nil {
return err
}
conn, err := clientVT.Dial(context.Background(), "tcp6", listener.Addr().String())
if err != nil {
return err
}The full example accepts a TCP connection, reads ping, and replies with
tcp:ping.
Use ListenPacket on the server VTun and Dial with a UDP network on the
client VTun:
packetConn, err := serverVT.ListenPacket(
context.Background(),
"udp6",
net.JoinHostPort(serverCore.Address().String(), "0"),
)
if err != nil {
return err
}
conn, err := clientVT.Dial(context.Background(), "udp6", packetConn.LocalAddr().String())
if err != nil {
return err
}The compiled example sends ping and receives udp:ping.
HTTP needs only a VTun-backed listener and a client transport whose
DialContext uses the client VTun:
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 server.Serve(listener)
client := http.Client{
Transport: &http.Transport{
DialContext: clientVT.Dial,
},
Timeout: 10 * time.Second,
}Build the request URL from the server Core address and listener port:
target := fmt.Sprintf("http://[%s]:%s", serverCore.Address().String(), port)
resp, err := client.Get(target)autopeer.Manager combines a public-peer fetcher with a peer-management policy.
Use BUILTIN for embedded public peers list, or add remote public-peer sources
URLs when the selected network can reach them.
fetcher := autopeer.NewFetcher(ygglogger.Discard(), time.Hour)
fetcher.SetDefaultNetwork(nativeNetwork)
fetcher.SetSources([]string{autopeer.BuiltinSource})
manager := autopeer.NewManager(fetcher)
manager.SetPeerManager(coreNode)
manager.SetConfig(autopeer.ManagerConfig{
CheckInterval: time.Minute,
MinimumConnected: 2,
Countries: []string{"germany", "france", "netherlands"},
TransportSchemes: []string{"tcp", "tls"},
})
manager.Start()
defer manager.Close()The manager stays idle unless both country filters and transport-scheme filters
are configured. It uses core.AddPeer so selected peers become persistent.
Link-local autopeering is provided by ygglib/multicast. It listens for local
beacons and calls core.CallPeer for discovered nodes.
This works only with a native carrier network. Link-local IPv6 addresses and
interface-scoped listeners rely on OS network interfaces, which emulated or
proxied gonnect.Network implementations do not provide.
if network == nil || !network.IsNative() {
return fmt.Errorf("link-local autopeering requires a native carrier network")
}
mc, err := multicast.New(
multicastCoreAdapter{core: coreNode},
ygglogger.Discard(),
multicast.ProtocolVersion{
Major: core.ProtocolVersionMajor,
Minor: core.ProtocolVersionMinor,
},
multicast.MulticastInterface{
Regex: regexp.MustCompile("^(eth|en|wlan|wl).*"),
Beacon: true,
Listen: true,
Port: 0,
},
)
if err != nil {
return err
}
defer mc.Stop()The adapter is small and mirrors the daemon:
type multicastCoreAdapter struct {
core *core.Core
}
func (a multicastCoreAdapter) ListenLocal(u *url.URL, sintf string) (multicast.Listener, error) {
return a.core.ListenLocal(u, sintf)
}
func (a multicastCoreAdapter) CallPeer(u *url.URL, sintf string) error {
return a.core.CallPeer(u, sintf)
}
func (a multicastCoreAdapter) PublicKey() ed25519.PublicKey {
return a.core.PublicKey()
}