IPv6 Link-Local Only: The Sovereign Mesh Without DHCP or NAT

How scoped addresses keep the RiNET stack simple, secure, and self-contained

by

When we built the RiNET stack—three Hetzner servers, a WireGuard mesh, Qwen on vLLM, BGE-M3 embeddings, Postgres, Qdrant, Neo4j, and nightly LoRA runs—we made a deliberate choice that raised eyebrows: the mesh speaks IPv6 link-local only. No DHCP, no NAT, no globally routable addresses on the internal interfaces. Just scoped addresses and a routing table that knows exactly where everything lives. This isn't a quirk; it's a design philosophy that buys us simplicity, security, and sovereignty. Here's why it works and how you can do the same.

The Problem with Traditional Addressing

Most networks default to IPv4 with DHCP, NAT, and private ranges like 10.0.0.0/8. That's fine for a home lab, but it carries baggage: DHCP servers are a single point of failure, NAT breaks end-to-end connectivity, and address management becomes a chore. For a sovereign mesh—where we control every packet—that's unnecessary complexity.

IPv6 gives us a better way. Link-local addresses (fe80::/10) are automatically configured on every interface, require no central authority, and are only valid on the local link. That's perfect for a mesh where all nodes are directly connected via WireGuard. We don't need globally routable addresses on the mesh; we need a deterministic, self-configuring network that can't be accidentally exposed to the internet.

WireGuard creates a virtual network interface (e.g., wg0) on each node. Normally you'd assign an IPv4 address like 10.0.0.1/24 to that interface. Instead, we assign only a link-local IPv6 address: fe80::1/64, fe80::2/64, and so on. Because WireGuard is a point-to-point tunnel, each peer sees the others as directly connected on the same link, so link-local addressing is perfectly valid.

Here's a minimal WireGuard config for a node in our mesh:

[Interface]
Address = fe80::1/64
PrivateKey = <node1-private-key>
ListenPort = 51820

[Peer]
PublicKey = <node2-public-key>
AllowedIPs = fe80::2/128
Endpoint = node2.example.com:51820

Note the AllowedIPs: we restrict each peer to its specific link-local address. This tightens security—each peer can only send traffic as its own address, and nothing else. No accidental routing to the wrong node.

No DHCP, No NAT: The Benefits

By dropping DHCP, we eliminate the need for a service to hand out addresses. Each node generates its own link-local address from its MAC or manually assigns a fixed one. That's one less service to run, one less potential failure point, and one less attack vector. In a sovereign stack, every service we remove is a win.

NAT is also gone. In IPv6, there's no need for NAT because we have enough addresses, but more importantly, link-local addresses are not routable beyond the link, so they can't be used to reach the internet. This creates a natural firewall: the mesh is isolated by design. If a packet somehow leaks out, it's dropped because link-local addresses aren't forwarded by routers.

Scoped Addressing and Routing

IPv6 introduces the concept of scope. Link-local addresses have a scope of link, meaning they're only valid on the local segment. This is enforced by the kernel: you can't route a packet with a link-local source address off the interface. That's a feature. It means our mesh traffic can't accidentally egress through a default route.

But we still need to route between nodes. WireGuard handles that by creating a virtual link. Each node's routing table includes a route to the mesh network (e.g., fe80::/64) via the wg0 interface. Since all peers are on that link, packets just flow.

For services that need to be reachable from outside the mesh (like the API server), we use a separate interface with a globally routable address, but that's not part of the mesh. The mesh itself stays link-local, ensuring that internal traffic—like model inference calls to Qwen or vector queries to Qdrant—never leaves the encrypted tunnel.

Practical Configuration: A Three-Node Example

Let's walk through a concrete setup for three Hetzner servers. We'll call them node1, node2, node3. Each has a WireGuard interface with a unique link-local address.

Node1's /etc/wireguard/wg0.conf:

[Interface]
Address = fe80::1/64
PrivateKey = <node1-priv>
ListenPort = 51820

[Peer]
PublicKey = <node2-pub>
AllowedIPs = fe80::2/128
Endpoint = node2.example.com:51820

[Peer]
PublicKey = <node3-pub>
AllowedIPs = fe80::3/128
Endpoint = node3.example.com:51820

Node2 and Node3 follow the same pattern, each with its own address and peer entries. After starting WireGuard, you can verify connectivity with ping6:

ping6 fe80::2%wg0

The %wg0 specifies the scope zone, which is required for link-local addresses. This is a small syntax detail but critical for debugging.

Handling DNS and Service Discovery

Without DHCP, you also lose DHCP's DNS options. But in a sovereign mesh, you don't want to rely on external DNS for internal services. We run our own DNS, typically a simple resolver like Knot or Unbound, on one node. Since all nodes are on the same link, we can use link-local addresses in the DNS configuration.

For example, in /etc/resolv.conf on each node:

nameserver fe80::1%wg0

That tells the resolver to use node1's link-local address via the wg0 interface. Again, the scope zone is essential.

Service discovery can be done with mDNS (Avahi) or simply by hardcoding addresses in configuration files. In a small mesh, hardcoding is fine. We know the addresses; we set them. No need for dynamic discovery.

Security Implications

Link-local addressing is a security win. Because the addresses are not routable, any packet with a link-local source that leaves the mesh is dropped. This prevents IP spoofing from outside and reduces the attack surface. Additionally, since we assign specific addresses to each peer, we can enforce strict firewall rules based on those addresses.

For example, a simple nftables rule on node1 to allow only mesh traffic on wg0:

eyJsYW5nIjoibmZ0IiwidGV4dCI6Im5mdCBhZGQgcnVsZSBpbmV0IGZpbHRlciBpbnB1dCBpaWZuYW1lIHdnMCBpcDYgc2FkZHIgZmU4MDo6LzEwIGFjY2VwdFxubmZ0IGFkZCBydWxlIGluZXQgZmlsdGVyIGlucHV0IGlpZm5hbWUgd2cwIGRyb3AifQ==

This ensures only link-local traffic is accepted on the mesh interface. Everything else is dropped.

One common pitfall is forgetting the scope zone in commands. ping6 fe80::2 will fail; you must use ping6 fe80::2%wg0. Similarly, in applications that connect to link-local addresses, you need to specify the interface. For example, in a Python script using sockets:

import socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.bind(('fe80::1%wg0', 8080))

The %wg0 is part of the address string. This is a minor annoyance but manageable.

Another issue is that some tools don't handle link-local well. For instance, curl to a link-local address needs the --interface option:

curl --interface wg0 'http://[fe80::1%wg0]:8080/health'

This is a small price for the simplicity we gain.

Scaling Beyond Three Nodes

Link-local addressing scales fine for meshes up to a few dozen nodes. The limitation is that all nodes must be on the same link, which in our case is the WireGuard mesh. WireGuard handles that transparently. For larger meshes, you might consider using unique local addresses (ULA, fc00::/7) instead, which are routable within a site but still not globally. But for our sovereign stack, three nodes is the sweet spot.

Conclusion

Running a sovereign mesh on IPv6 link-local only is a radical simplification. It removes DHCP and NAT, reduces attack surface, and forces us to be deliberate about addressing. The RiNET stack benefits from this design: the WireGuard mesh is clean, the services are isolated, and we have full control. If you're building your own sovereign infrastructure, consider this approach. It's not for everyone, but for a small, self-contained mesh, it's elegant.

We've been running this setup for months, and it's been rock solid. No DHCP server to maintain, no NAT rules to debug, just scoped addresses and a clear routing table. That's the kind of simplicity that lets us focus on what matters: the models, the data, and the autonomy of our stack.

Note: This article reflects our experience with the RiNET stack. Your mileage may vary, but the principles are sound.

#ipv6#link-local#network-design#sovereign-infrastructure#wireguard
Share — X / Twitter · LinkedIn · HN · Email
Damir Radulić
Founder of RiNET. On the Croatian internet since 1996 (Kvarner Net). In Amsterdam now, building autonomous AI infrastructure that runs on Monday morning when nobody's watching — sovereign stacks, agent swarms, LoRA fine-tuning, civic-intelligence platforms.