Skip to content

Feature/tunnel wireguard for FRR#3569

Open
jbemmel wants to merge 18 commits into
ipspace:devfrom
jbemmel:feature/tunnel-wireguard
Open

Feature/tunnel wireguard for FRR#3569
jbemmel wants to merge 18 commits into
ipspace:devfrom
jbemmel:feature/tunnel-wireguard

Conversation

@jbemmel

@jbemmel jbemmel commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator
  • IPv4 or IPv6
  • default routing table or in VRF
  • WireGuard package dynamically installed
  • keys auto-generated if WireGuard tools are installed
  • MTU auto-derived and validated

Tested:

./device-module-test -d frr -p clab tunnel *wire*
./device-module-test -d frr -p libvirt tunnel *wire* (needed the default route fixes)

Notes:

  • PR generalizes Linux package installation to support both apk and apt-get; needed for FRR containers
  • The ipv6 link local address is needed for OSPFv3, made this a util method in routing
  • add_linux_packages is a candidate for general util method

jbemmel and others added 18 commits July 2, 2026 13:21
Install wireguard-tools during initial configuration on clab FRR nodes
before the management VRF is created, and add integration tests for
global and transport-VRF underlay scenarios.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add shared tunnel utilities, OSPFv3 link-local assignment, VRF routing
rules for WireGuard handshakes, and integration tests for IPv6 transport.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use tunnel-AF-specific fwmark rules so IPv6 transport handshakes work
over VRF underlays instead of peer-specific policy routing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Move WireGuard netdev creation out of the plugin's custom config (which runs
last) and into the initial FRR config, so the interface exists before FRR and
the routing modules are configured. A generic _linux_device_type interface
attribute tells the initial script which kind of netdev to create, and the
deterministic IPv6 link-local (_ipv6_link_local) needed for OSPFv3 is assigned
at the same time (kernel address auto-generation is impossible for the
ARPHRD_NONE WireGuard device).

Drop the optional cryptography fallback; key generation now relies solely on
wireguard-tools, and the integration tests use explicit static keys. Docs
updated accordingly.

Co-authored-by: Cursor <cursoragent@cursor.com>
The global WireGuard listening socket could not receive encrypted packets
arriving on a VRF underlay interface for IPv6 tunnels, so handshakes never
completed. net.ipv4.udp_l3mdev_accept governs l3mdev socket matching for both
IPv4 and IPv6 UDP (there is no net.ipv6 counterpart), but it was only set in
the IPv4 branch. Set it unconditionally and collapse the per-AF policy-rule
handling into a single command.

Co-authored-by: Cursor <cursoragent@cursor.com>
Default tunnel.allowed_ips to ::/0 for IPv6 tunnels (0.0.0.0/0 for IPv4) so
IPv6 tunnels work without setting it explicitly. Add an Allowed IPs section
explaining WireGuard cryptokey routing and its inbound/outbound semantics, and
trim the examples to the plain and VRF/IPv6 cases.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Set _default for tunnel.af, tunnel.mtu, and tunnel.persistent_keepalive in
defaults.yml instead of hard-coding them in post_transform. Only the
allowed_ips default stays in code because it depends on the resolved af.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace hardcoded default routes with a dedicated core address pool so
the static routes reference the pool rather than 0.0.0.0/0 and ::/0.

Co-authored-by: Cursor <cursoragent@cursor.com>
Schema _default values for tunnel.af, mtu, and persistent_keepalive are
materialized onto the per-node interface entries during link validation,
before link->interface propagation. They then win over an explicit
link-level override (e.g. tunnel.af: ipv6) during the interface merge,
breaking the tunnel source lookup. Resolve these defaults in
post_transform where the interface data is already merged.

Also iterate node_iflist with .items() instead of re-indexing by key.

Co-authored-by: Cursor <cursoragent@cursor.com>
Infer tunnel.af from the underlay source interface (IPv4 when available,
IPv6 for IPv6-only underlays) so IPv6-only links no longer need an explicit
tunnel.af. Derive the tunnel interface link-local address from the low 64
bits (interface identifier) of the overlay address via a new
routing.get_ipv6_link_local helper, guaranteeing distinct link-locals on
both tunnel endpoints. Drop the now-redundant tunnel.af/allowed_ips from
the IPv6 integration tests and update the docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the redundant tunnel.mtu schema attribute and reuse the standard
interface mtu attribute. When not set explicitly, derive the WireGuard
interface MTU from the underlay source interface MTU minus the AF-specific
encapsulation overhead (80 bytes for IPv6, 60 bytes for IPv4), so it scales
with jumbo-frame underlays. Add underlay MTU settings and a 1500-byte ping
validation to the tunnel integration tests, and update the docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
… _source_intf

Move the persistent_keepalive default back to the attribute schema and
rename the internal tunnel._source to tunnel._source_intf to match the
_source_intf convention used elsewhere in the codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use the node's default MTU (node setting, lab default, or device default)
before the hardcoded 1500 when the underlay source interface has no
explicit MTU.

Co-authored-by: Cursor <cursoragent@cursor.com>
intf.mtu and tunnel.af are always resolved during transformation, so the
template default() filters are unnecessary.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reword the add_linux_packages docstring: netlab_linux_packages is set as a
per-node host var (not device-wide) so wireguard-tools is installed only on
nodes with tunnels, starting from the device defaults since the host var
replaces the group var.

Co-authored-by: Cursor <cursoragent@cursor.com>
Declare features.tunnel.wireguard (with VRF support) so nodes using the
default none device pass the tunnel feature check.

Co-authored-by: Cursor <cursoragent@cursor.com>
'or configure tunnel.private_key and tunnel.public_key)',
module='tunnel.wireguard')

def generate_keypair() -> tuple[str, str]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to use the wg-tools callout's in the generation of keys if they dont exist?

As its rather simple to just let the python handle this with, which i think you partially had in one of your earlier commits?

import base64
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives import serialization

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, my thinking was to let WireGuard tools generate the keys for WireGuard tunnels so it's likely to work, but I'm not married to them

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benefit of it not using those tools is the plugin can be more easily used for other OSs that also might not have the wg-tools or similar package. (thinking of OpenBSD/RouterOS or similar that do have wg implementations)

Plus this appears to be the only use of calling external program from within this python code.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benefit of it not using those tools is the plugin can be more easily used for other OSs that also might not have the wg-tools or similar package.

As the wg-tools are executed on the netlab hosts, the availability of the package on lab devices is irrelevant.

@jbemmel -- I don't care how you generate the keys, apart from being nervous seeing "cryptography.hazmat' in source code 🤔

@ipspace ipspace left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you (or your AI friend) copied the utils/tunnel.py code, resulting in duplicate functionality. Please fix that.

And yes, I know you'll tell me something about AFs, but we can deal with that.

def _interface_has_af(intf: Box, af: str) -> bool:
return af in intf and isinstance(intf[af],str)

def _interface_matches_constraints(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why exactly did you reinvent the utility function? There's a reason I put it into utils, not into tunnel.gre

peer_node: str) -> typing.Optional[Box]:
'''
Find the tunnel underlay interface. When the source is not pinned to a
specific interface, prefer the underlay link connected to the tunnel peer.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually we build VPN tunnels over something, but OK, let's assume you have to provider P2P encryption. We could add another parameter to the link selection instead of reinventing the wheel.

we derive a deterministic address that the initial config script assigns when
the interface is created.
'''
if intf.get('tunnel.af') != 'ipv6' or 'ipv6' not in intf:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the wireguard tunnels are not dual-stacked (I know I could check), then you have to throw an error, not just check the AF, shrug, and return. If they are dual-stacked, then I don't understand why you're checking tunnel.af (which impacts the underlay interface selection)

if intf.get('tunnel.af') != 'ipv6' or 'ipv6' not in intf:
return

if not isinstance(intf.ipv6,str):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will this work with LLA-only wireguard interfaces?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants