When managing multiple OpenVPN client configurations through systemd (openvpn@foo
and openvpn@bar
in this case), the first established connection always claims tun0
. This creates challenges for:
- Applying VPN-specific firewall rules
- Maintaining consistent interface naming across reboots
- Handling dynamic IP assignments
--dev and --dev-node
/h2>
The most straightforward solution is to explicitly define interface names in each configuration:
# In foo.conf
dev tun-foo
dev-node /dev/net/tun-foo
# In bar.conf
dev tun-bar
dev-node /dev/net/tun-bar
This requires creating persistent device nodes:
# Create persistent device nodes
sudo mkdir -p /dev/net
sudo mknod /dev/net/tun-foo c 10 200
sudo mknod /dev/net/tun-bar c 10 201
sudo chmod 0666 /dev/net/tun-*
For systemd-controlled OpenVPN instances, we can use temporary files to track associations:
# In foo.conf
script-security 2
up "/bin/sh -c 'echo $dev > /run/openvpn/foo.iface'"
down "/bin/sh -c 'rm -f /run/openvpn/foo.iface'"
Then query the mapping:
#!/bin/bash
get_vpn_iface() {
local vpn_name=$1
cat "/run/openvpn/${vpn_name}.iface"
}
# Example usage:
FOO_IFACE=$(get_vpn_iface "foo")
iptables -A OUTPUT -o $FOO_IFACE -j ACCEPT
For more complex scenarios, consider network namespaces:
# Create namespaces
sudo ip netns add vpn-foo
sudo ip netns add vpn-bar
# Launch OpenVPN in namespaces
sudo ip netns exec vpn-foo openvpn --config foo.conf
sudo ip netns exec vpn-bar openvpn --config bar.conf
Here's how to implement rules dynamically:
#!/bin/bash
# Dynamic firewall rules based on VPN config
apply_vpn_rules() {
local vpn_name=$1
local iface=$(get_vpn_iface $vpn_name)
case $vpn_name in
foo)
iptables -A OUTPUT -o $iface -d 192.168.1.0/24 -j ACCEPT
;;
bar)
iptables -A OUTPUT -o $iface -d 10.0.0.0/8 -j ACCEPT
;;
esac
}
# Apply rules when VPN comes up
apply_vpn_rules "foo"
apply_vpn_rules "bar"
For modern systems with cgroup v2:
# Create cgroups
sudo mkdir /sys/fs/cgroup/unified/vpn.foo
sudo mkdir /sys/fs/cgroup/unified/vpn.bar
# Assign PIDs
echo $OPENVPN_PID > /sys/fs/cgroup/unified/vpn.foo/cgroup.procs
# BPF-based filtering
bpftool prog load firewall.o /sys/fs/bpf/firewall_foo
bpftool cgroup attach /sys/fs/cgroup/unified/vpn.foo egress pinned /sys/fs/bpf/firewall_foo
To verify interface-configuration mapping:
#!/bin/bash
for pid in $(pgrep openvpn); do
conf=$(ps -p $pid -o cmd= | grep -oP '(?<=--config\s)\S+')
iface=$(ip -o link | grep -B1 "tun.*$pid" | head -1 | awk -F': ' '{print $2}')
echo "PID $pid: $conf -> $iface"
done
- For simple setups: Use explicit
dev
naming - For automated environments: Implement the systemd temporary files solution
- For maximum isolation: Consider network namespaces
- For enterprise deployments: Explore cgroup/BPF filtering
When running multiple OpenVPN clients simultaneously, you'll notice the first connected VPN always grabs tun0
, while subsequent connections get assigned incrementing interface names (tun1
, tun2
, etc.). This becomes problematic when:
- Creating firewall rules tied to specific VPN connections
- Writing scripts that need consistent interface references
- Maintaining configuration files across reboots
--dev and --dev-type
Parameters /h2>
OpenVPN provides configuration options to specify exact interface names:
# In your OpenVPN config file (e.g., foo.ovpn)
dev tun-foo
dev-type tun
# In your second config (e.g., bar.ovpn)
dev tun-bar
dev-type tun
This approach ensures:
- Consistent interface naming across reboots
- Clear identification of which VPN uses which interface
- No interface number conflicts
When using systemd, modify your service files to pass these parameters:
# /etc/systemd/system/openvpn-foo.service
[Unit]
Description=OpenVPN Client Foo
After=network.target
[Service]
Type=simple
ExecStart=/usr/sbin/openvpn --config /etc/openvpn/foo.conf --dev tun-foo --dev-type tun
[Install]
WantedBy=multi-user.target
With persistent interface names, you can now create accurate iptables rules:
# Allow traffic only through VPN foo
iptables -A OUTPUT -o tun-foo -j ACCEPT
# Block traffic if VPN foo disconnects
iptables -A OUTPUT ! -o tun-foo -j DROP
Check interface assignments with:
ip link show | grep tun
For debugging, add these to your OpenVPN config:
log /var/log/openvpn-foo.log
verb 4
For advanced isolation, consider network namespaces:
ip netns add vpn-foo
ip netns exec vpn-foo openvpn --config foo.ovpn
This completely separates the VPN interfaces and routing tables.
Here's a complete working example for two VPNs:
# /etc/openvpn/foo.conf
client
dev tun-foo
dev-type tun
remote vpn-foo.example.com 1194
persist-tun
persist-key
verb 3
# /etc/openvpn/bar.conf
client
dev tun-bar
dev-type tun
remote vpn-bar.example.com 1194
persist-tun
persist-key
verb 3