How to Configure IPsec Virtual Tunnel Interfaces (VTI) in Ubuntu Using systemd-networkd

In enterprise networking, establishing site-to-site Virtual Private Networks (VPNs) between remote data centres, branch offices, and cloud VPCs has traditionally relied on IPsec in policy-based mode. In policy-based IPsec, the Linux kernel relies on the Security Policy Database (SPD) and xfrm states to intercept packets matching designated subnets and encrypt them on the fly. However, policy-based IPsec lacks a routable network interface, preventing administrators from inspecting interface packet counters, assigning dynamic routing protocols (such as BGP or OSPF via FRRouting), or applying standard firewall chains.

To overcome these limitations, modern network architectures employ route-based IPsec using Virtual Tunnel Interfaces (VTI). A VTI tunnel encapsulates IPsec traffic inside a dedicated kernel virtual interface (e.g. vti0 or vti6). On Ubuntu Server, VTI tunnel interfaces can be provisioned, configured, and managed natively using systemd-networkd alongside StrongSwan or Libreswan, providing persistent, declarative route-based VPN infrastructure.

Understanding VTI Architecture and systemd-networkd Integration

A Virtual Tunnel Interface functions as a point-to-point tunnel interface bound to an underlying IPsec Security Association (SA). The connection operates using two coordinated layers:

  • IKE Daemon (e.g. StrongSwan): Manages Internet Key Exchange (IKEv2) authentication, negotiates cryptographic keys (AES-GCM, SHA-256, Diffie-Hellman groups), and installs kernel xfrm Security Associations flagged with a specific numerical mark (e.g. mark = 42).
  • systemd-networkd VTI Device (.netdev): Instantiates a virtual tunnel device of Kind=vti (for IPv4) or Kind=vti6 (for IPv6). By configuring matching Key= attributes, systemd-networkd instructs the kernel VFS to bind outgoing packets on the VTI interface directly to the matching IPsec mark.
  • Network Link Configuration (.network): Assigns point-to-point IP addresses, MTU ceilings, and static or dynamic routes to the VTI interface.

Configuring the Underlying Host Network

Before instantiating the VTI interface, ensure the local physical interface has a static IP address reachable by the remote IPsec gateway. For this scenario, assume:

  • Local underlay WAN IP: 198.51.100.10
  • Remote peer gateway IP: 203.0.113.20
  • IPsec Mark / Tunnel Key: 42
  • Local point-to-point overlay IP: 10.100.0.1/30
  • Remote point-to-point overlay IP: 10.100.0.2/30
  • Remote corporate subnet: 172.16.0.0/16

Ensure UDP ports 500 and 4500, as well as IP protocol 50 (ESP), are open in the firewall:

sudo ufw allow 500/udp comment "IKEv2 Negotiation"
sudo ufw allow 4500/udp comment "IPsec NAT-Traversal"
sudo ufw allow proto esp from 203.0.113.20 comment "IPsec ESP Ingress"

Creating the VTI Virtual NetDev Definition (.netdev)

Create the declarative virtual device definition in /etc/systemd/network/25-vti0.netdev:

sudo nano /etc/systemd/network/25-vti0.netdev

Insert the following configuration:

[NetDev]
Name=vti0
Kind=vti
Description=Route-Based IPsec VTI Tunnel to Branch Office

[Tunnel]
Local=198.51.100.10
Remote=203.0.113.20
Key=42

Configuration directives explained:

  • Kind=vti: Specifies an IPv4-over-IPv4 IPsec Virtual Tunnel Interface. (Use Kind=vti6 for IPv6 underlay endpoints).
  • Local=198.51.100.10: The local source IP address where IKE and ESP packets originate.
  • Remote=203.0.113.20: The remote peer endpoint IP address.
  • Key=42: The numerical mark value that links packets routed out of vti0 to the corresponding StrongSwan IPsec Security Association.

Configuring IP Addressing and Routes on the VTI Interface (.network)

Create the corresponding network file /etc/systemd/network/25-vti0.network to bind overlay IP addresses, specify MTU values, and route remote enterprise subnets through the tunnel:

sudo nano /etc/systemd/network/25-vti0.network

Populate the file with the following directives:

[Match]
Name=vti0

[Network]
Description=Overlay Subnet for VTI Branch Link
Address=10.100.0.1/30
Peer=10.100.0.2/30
LinkLocalAddressing=no
ConfigureWithoutCarrier=yes

[Link]
MTUBytes=1420
RequiredForOnline=no

[Route]
Destination=172.16.0.0/16
Gateway=10.100.0.2
Metric=100

Setting MTUBytes=1420 accounts for IPsec ESP and NAT-T encapsulation overhead (typically 60 to 76 bytes), preventing fragmentation across standard 1500-byte WAN links.

Aligning StrongSwan IKEv2 Configuration with VTI Mark

To ensure StrongSwan binds its Security Associations to the VTI interface mark, verify your StrongSwan connection profile in /etc/swanctl/conf.d/vti0.conf (or legacy ipsec.conf) includes the matching mark:

connections {
    branch-vpn {
        remote_addrs = 203.0.113.20
        local_addrs  = 198.51.100.10
        version = 2
        proposals = aes256gcm16-sha256-modp2048

        local {
            auth = psk
            id = vpn-local.example.com
        }
        remote {
            auth = psk
            id = vpn-remote.example.com
        }

        children {
            vti-child {
                local_ts  = 0.0.0.0/0
                remote_ts = 0.0.0.0/0
                esp_proposals = aes256gcm16-sha256
                mark_in  = 42
                mark_out = 42
                updown = /usr/lib/ipsec/_updown iptables
            }
        }
    }
}

Setting local_ts = 0.0.0.0/0 and remote_ts = 0.0.0.0/0 alongside mark_in = 42 and mark_out = 42 instructs StrongSwan to establish a wildcard SA that only matches traffic carrying mark 42, leaving routing decisions entirely to the Linux routing table via vti0.

Applying Configuration and Validating Tunnel Operation

Reload systemd-networkd and StrongSwan to instantiate the tunnel:

sudo systemctl restart systemd-networkd
sudo swanctl --load-all
sudo swanctl --initiate --child vti-child

Inspect the operational state of the VTI device using networkctl:

networkctl status vti0

Output confirms Type: vti and Operational: routable.

Check the low-level tunnel properties using ip:

ip -d link show vti0

The output confirms the tunnel keys and endpoints:

vti0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN
    link/ipip 198.51.100.10 peer 203.0.113.20 promiscuity 0
    vti remote 203.0.113.20 local 198.51.100.10 ikey 42 okey 42 numtxqueues 1 numrxqueues 1

Test connectivity by pinging the remote tunnel peer over the encrypted overlay:

ping -c 4 10.100.0.2

Verify that packets traversing the physical interface are encrypted ESP datagrams using tcpdump:

sudo tcpdump -ni any proto 50 or port 4500

Troubleshooting Common VTI Configuration Issues

If traffic routed to the VTI interface fails to decrypt or drops silently, check these common diagnostic points:

  • Mismatched Key / Mark: The Key=42 value in 25-vti0.netdev must match the mark_out and mark_in values in the IKE configuration exactly. A mismatch causes the kernel to discard packets because no corresponding xfrm state matches the mark.
  • Reverse Path Filtering (rp_filter): The Linux kernel drops decrypted packets arriving on VTI interfaces if rp_filter is set to strict mode (1). Configure loose reverse path filtering in /etc/sysctl.d/99-vti.conf:
    net.ipv4.conf.vti0.rp_filter=2
    net.ipv4.conf.all.rp_filter=2
  • Path MTU Truncation: If small ICMP packets succeed but TLS or SSH sessions hang during key exchanges, enable TCP Maximum Segment Size (MSS) clamping on vti0:
    sudo iptables -t mangle -A FORWARD -o vti0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.