How it Works

In-depth information on AXL's architecture, encryption, peering, privacy model, wire format, and node internals.

The Mental Model

This is the single most important thing to understand about AXL, and it's counterintuitive if you're coming from traditional client-server architecture:

Your application code never touches the network.

It runs on your machine and only ever talks to http://127.0.0.1:9002 which is the local HTTP interface exposed by your AXL node. Your node handles all peer-to-peer transport, encryption, and routing behind the scenes.

Both sides run the full stack independently. If two people want to communicate, each person runs:

  1. Their own AXL node (the Go binary)

  2. Their own copy of the application

Nobody "connects to" the other person's application. Each application talks only to its own local node. The nodes talk to each other over the encrypted mesh.

      Your Machine                                                  Their Machine
┌──────────────────────┐                                       ┌──────────────────────┐
│  [Your App]          │                                       │  [Their App]         │
│       ↕ HTTP         │                                       │       ↕ HTTP         │
│  [AXL node :9002]    │             ◄── mesh ──►              │  [AXL node :9002]    │
└──────────────────────┘                                       └──────────────────────┘

"Exposing a service" doesn't mean what it usually means. In traditional web development, exposing a service means binding to a port and accepting remote connections. But in AXL, it means: [1] your node is running, [2] your application is running locally, and [3] remote nodes send messages to your public key.

The Yggdrasil network routes those messages to your node, which queues them for your application. Your application is never directly reachable from the outside. The node is the only thing with a network presence.

Therefore, there is nothing to deploy; you run your application on your laptop, and as long as your node is up, other nodes can reach you by your public key, so long as you share it.

Architecture

The Go binary (node) contains four layers:

Layer

What It Does

HTTP Interface

Local server on 127.0.0.1:9002. Your application's single point of contact with the node.

Multiplexer

Routes inbound TCP messages to the correct handler based on envelope fields. Unmatched messages go to a queue your app can read.

gVisor TCP

Userspace TCP/IP stack. No TUN device, no root privileges. Listens on port 7000 for inbound peer connections.

Yggdrasil Core

Manages the ed25519 keypair, derives a deterministic IPv6 address, and peers over TLS/TCP with other nodes.

The MCP Router and A2A Server are optional, separate Python processes for structured request/response protocols. See Building Applications & Examples for details.

Startup Flow

Here is what happens when you run ./node -config node-config.json:

  1. CLI flags are parsed (-config, optional -listen override).

  2. The config file is read for both Yggdrasil settings (Peers, Listen, PrivateKeyPath) and node settings (ports, limits, router URLs).

    1. Yggdrasil core starts. It connects to configured peers, joins the spanning tree, logs your IPv6 address and public key.

    2. gVisor TCP stack starts and listens on tcp_port (default 7000) for inbound connections from other nodes.

    3. HTTP server starts then binds to bridge_addr:api_port (default 127.0.0.1:9002).

The Yggdrasil Network

AXL uses Yggdrasil, an encrypted IPv6 overlay network, for all peer-to-peer transport.

  • Overlay network. Yggdrasil runs on top of the regular internet (or LAN). It creates an encrypted mesh between all participating nodes.

  • Spanning tree routing. Peers form a spanning tree. Each node gets a deterministic IPv6 address derived from its public key. Routing happens along the tree without no centralized routing table.

  • Identity = public key. Nodes are identified by their 64-character hex-encoded ed25519 public key, not by IP or hostname.

  • Encrypted by default. All connections use TLS, and Yggdrasil adds end-to-end encryption on top (see Encryption below).

Peering

To join the network, your node connects to at least one other node. This is configured in the Peers array in node-config.json.

Bootstrap Peers

Bootstrap peers are nodes that accept inbound connections and help route traffic. They're entry points into the mesh, not controllers and as such they cannot read your message content. They just relay encrypted bytes.

Once connected to a bootstrap peer, your node can reach any other node in the mesh by their public key. The mesh handles all of the routing work so you don't need direct connectivity to every node.

Running a Public Node (Being a Bootstrap Peer)

If you want other nodes to connect to you, you can run a public node. Public nodes help route traffic through the mesh: they don't store other nodes' data or have access to their messages, they just forward encrypted bytes.

The more public nodes in the network, the more resilient it becomes.

To set one up:

  1. Expose a TCP port to the network (LAN or internet).

  2. Add a Listen address to your config:

  1. Share your IP and port with others. They add tls://YOUR_IP:9001 to their Peers.

Use a persistent identity (PrivateKeyPath) so your key doesn't change across restarts.

Keep the HTTP interface port (9002) locked to localhost. Only the peering port should be exposed.

LAN vs. Internet Peering

  • On a LAN (same network): Trivial. Both nodes just need each other's LAN IP and listen port. There is no port forwarding or firewall adjustments that need to be made with this route.

  • Over the internet: The listening node must expose its TCP port to the public internet (port forwarding, cloud VM with open port, etc.). Outbound-only nodes don't need to expose anything however: they can still communicate with each other because traffic is routed through public nodes in the mesh.

Peer Discovery

Running the following command returns:

  • our_public_key: your node's identity

  • our_ipv6: your node's Yggdrasil IPv6 address

  • peers: directly connected peers

  • tree: the spanning tree as your node sees it

There is no built-in service registry.

You can see public keys in the tree, but you can't look up who owns them or what services they run. Keys and service names must be exchanged directly between people.

Encryption: Two Layers

There are two distinct layers of encryption. Understanding both matters.

Layer 1: Peering Transport (TLS)

The tls:// URIs in your config establish encrypted links between directly connected peers. This is hop-by-hop meaning it secures the connection between your node and the peer it's directly connected to, which is standard TLS.

Layer 2: End-to-End Payload Encryption (Yggdrasil)

Separately, Yggdrasil encrypts all traffic between source and destination using keys derived from both nodes' ed25519 keypairs. This is end-to-end so if Node A sends a message to Node C and it routes through Node B, Node B sees only ciphertext it cannot decrypt.

Both of these matter because in a mesh network, your traffic may pass through nodes you don't control. Layer 1 protects the link whereas Layer 2 protects the payload across the entire path, regardless of how many hops it takes.

So even though a different node could 'see' that there is communication happening, it can't figure out what is being communicated.

Security & Privacy

What routing nodes can and cannot see is important to understanding security and privacy of anything connected to a node, coming from a node, being received by a node, etc.

What Nodes Can & Cannot See

  • Routing nodes can see:

    • That your node exists and your IP address (you connected to them directly).

    • That two nodes are communicating, including the [1] source and [2] destination public keys.

    • When communication happens, how frequently, and the approximate message sizes.

  • Routing nodes cannot see:

    • Message content. They route encrypted bytes with no ability to decrypt.

    • [1] What your application does, [2] what services you expose, or [3] what commands are being sent.

    • Anything inside the encrypted payload: application-level metadata like JSON fields, headers, or protocol details.

'Routing nodes' also includes bootstrap peers.


Information

Sender

Receiver

Routing Nodes

Message content

Yes

Yes

No

Application metadata

Yes

Yes

No

Who is communicating

Yes

Yes

Yes (public keys)

When and how often

Yes

Yes

Yes

Approximate message size

Yes

Yes

Yes

Your IP address

Yes (direct peers only)


Limitations

Yggdrasil, unlike Tor, does not use onion routing, meaning there is no traffic obfuscation. Although it protects the content of conversations, anyone who controls the routing nodes can observe the communication patterns, specifically who communicates with whom and when, but they cannot see the actual content of this communication.

Additionally, your Internet Protocol (IP) address is visible to direct connections. This visibility extends to bootstrap peers and any node you directly connect with, making your real IP address accessible to them.

In the event of a key compromise, the consequences are total. If this happens, anyone with access to your private.pem file can impersonate your node. Due to this risk, you must private key with the same vigilance as an SSH key.

Moreover, Yggdrasil does not include access control mechanisms. Any node possessing your public key can send messages to you, so it is crucial that your application independently validate message senders when necessary.

Data Flow

This section describes the flow of data during sending and receiving messages within an AXL-powered application.

Sending a Message

Your application posts data to localhost:9002/send with the destination peer's public key.

The node dials the remote peer over the gVisor TCP stack, writes a length-prefixed message, and returns: there is response from the remote destination.

Receiving a Message

When a remote peer sends your node a message, the multiplexer checks it against registered protocol streams (MCP, A2A). If nothing matches, it goes into an in-memory queue which your application then polls.

MCP / A2A (Request-Response)

For structured communication, the node:

  1. wraps your JSON-RPC body in a transport envelope

  2. sends it to the remote peer

  3. waits for a response (30-second timeout)

  4. unwraps it, then returns it.

The remote peer must have the corresponding service running.

Wire Format

All TCP messages between nodes are length-prefixed: a 4-byte big-endian uint32 length followed by a payload of XYZ number of bytes.

Max message size defaults to 16 MB which is configurable via max_message_size.

Envelope Routing

The multiplexer determines how to handle each inbound message by inspecting its content:

Envelope Pattern

Routed To

{"service": "...", "request": {...}}

MCP Router

{"a2a": true, "request": {...}}

A2A Server

Anything else

Message queue (your app reads via polling)

The first protocol stream whose discriminator matches 'wins' and any unmatched messages go to the queue.

Internals

Here you can find a breakdown of the AXL repo and some additional information on the dependencies it requires.


Project Layout


gVisor TCP Stack

AXL uses gVisor's userspace network stack instead of the OS kernel's TCP/IP.

For this, there is/are:

  • No TUN device: No virtual network interface are created.

  • No root privileges: Everything runs in userspace.

  • No system configuration: No sysctl and no routing table changes.

The stack bridges to Yggdrasil's core, providing standard TCP operations ([1] listen, [2] accept, [3] dial) over the encrypted mesh.

  • Inbound: A TCP listener on tcp_port (default 7000) accepts connections from remote nodes. Each connection delivers a length-prefixed message routed by the multiplexer.

  • Outbound: DialPeerConnection converts a 64-char hex public key to a Yggdrasil IPv6 address and dials it through the gVisor stack.

The Stream Interface

The multiplexer routes messages using a Stream interface:

  • IsAllowed inspects raw bytes and returns true if this stream handles the message.

  • Forward processes the message and returns a response.

Streams are checked in order, where the first match wins. If there is no match it is sent to the message queue.

MCP and A2A are implemented as streams. To add a new protocol, implement Stream and register it in internal/tcp/listen/listener.go.

Connection Limits

There are some default connection limits (as shown below) but these values can be adjusted. See the Configuration page for more information.

Setting

Default

Description

max_concurrent_conns

128

Max simultaneous inbound TCP connections

conn_read_timeout_secs

60

Read timeout per connection

conn_idle_timeout_secs

300

Idle timeout per connection

Message Queue

Inbound messages that don't match any stream go to an in-memory queue:

  • Unbounded: Messages accumulate until read.

  • Single-consumer: Each read dequeues one message. If you need multiple consumers, you'll need to build a fan-out layer that polls /recv and distributes to your consumers.

  • Non-persistent: The queue empties on restart.

  • First-in, First Out (FIFO): Messages are returned in the order they arrived.

Dependencies

These are the dependencies required by AXL.

Dependency

Purpose

Mesh networking core

Userspace TCP/IP stack

Structured logging

Running Tests

The repo includes Go tests for the core node (API handlers, TCP transport and protocol streams) and Python tests for the MCP router and A2A server integrations. You don't need any external services.

Last updated