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
xfrmSecurity Associations flagged with a specific numerical mark (e.g.mark = 42). - systemd-networkd VTI Device (
.netdev): Instantiates a virtual tunnel device ofKind=vti(for IPv4) orKind=vti6(for IPv6). By configuring matchingKey=attributes,systemd-networkdinstructs 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. (UseKind=vti6for 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 numericalmarkvalue that links packets routed out ofvti0to 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=42value in25-vti0.netdevmust match themark_outandmark_invalues in the IKE configuration exactly. A mismatch causes the kernel to discard packets because no correspondingxfrmstate matches the mark. - Reverse Path Filtering (rp_filter): The Linux kernel drops decrypted packets arriving on VTI interfaces if
rp_filteris 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