summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLucas Christian <lucas@lucasec.com>2024-08-11 23:06:02 -0700
committerLucas Christian <lucas@lucasec.com>2024-08-13 21:47:05 -0700
commit18ea3673a105d9d12699afb2607c81412dde441b (patch)
treecf8692ab9414d777b16819348ec6ccababc27c0c
parente229d7498cc77298c79af9811a742f3ad54edf4c (diff)
downloadvyos-1x-18ea3673a105d9d12699afb2607c81412dde441b.tar.gz
vyos-1x-18ea3673a105d9d12699afb2607c81412dde441b.zip
T6183: interfaces openvpn: suppport specifying IP protocol version
-rw-r--r--data/templates/openvpn/server.conf.j212
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in28
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_openvpn.py186
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py37
4 files changed, 260 insertions, 3 deletions
diff --git a/data/templates/openvpn/server.conf.j2 b/data/templates/openvpn/server.conf.j2
index 408103558..be811f45e 100644
--- a/data/templates/openvpn/server.conf.j2
+++ b/data/templates/openvpn/server.conf.j2
@@ -11,11 +11,11 @@ dev-type {{ device_type }}
dev {{ ifname }}
persist-key
{% if protocol is vyos_defined('tcp-active') %}
-proto tcp-client
+proto tcp{{ protocol_modifier }}-client
{% elif protocol is vyos_defined('tcp-passive') %}
-proto tcp-server
+proto tcp{{ protocol_modifier }}-server
{% else %}
-proto udp
+proto udp{{ protocol_modifier }}
{% endif %}
{% if local_host is vyos_defined %}
local {{ local_host }}
@@ -63,6 +63,9 @@ nobind
#
# OpenVPN Server mode
#
+{% if ip_version is vyos_defined('ipv6') %}
+bind ipv6only
+{% endif %}
mode server
tls-server
{% if server is vyos_defined %}
@@ -131,6 +134,9 @@ plugin "{{ plugin_dir }}/openvpn-otp.so" "otp_secrets=/config/auth/openvpn/{{ if
#
# OpenVPN site-2-site mode
#
+{% if ip_version is vyos_defined('ipv6') %}
+bind ipv6only
+{% endif %}
ping {{ keep_alive.interval }}
ping-restart {{ keep_alive.failure_count }}
diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in
index 3563caef2..3c844107e 100644
--- a/interface-definitions/interfaces_openvpn.xml.in
+++ b/interface-definitions/interfaces_openvpn.xml.in
@@ -318,6 +318,34 @@
</properties>
<defaultValue>udp</defaultValue>
</leafNode>
+ <leafNode name="ip-version">
+ <properties>
+ <help>Force OpenVPN to use a specific IP protocol version</help>
+ <completionHelp>
+ <list>auto ipv4 ipv6 dual-stack</list>
+ </completionHelp>
+ <valueHelp>
+ <format>auto</format>
+ <description>Select one IP protocol to use based on local or remote host</description>
+ </valueHelp>
+ <valueHelp>
+ <format>_ipv4</format>
+ <description>Accept connections on or initate connections to IPv4 addresses only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>_ipv6</format>
+ <description>Accept connections on or initate connections to IPv6 addresses only</description>
+ </valueHelp>
+ <valueHelp>
+ <format>dual-stack</format>
+ <description>Accept connections on both protocols simultaneously (only supported in server mode)</description>
+ </valueHelp>
+ <constraint>
+ <regex>(auto|ipv4|ipv6|dual-stack)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>auto</defaultValue>
+ </leafNode>
<leafNode name="remote-address">
<properties>
<help>IP address of remote end of tunnel</help>
diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py
index d8a091aaa..e087b8735 100755
--- a/smoketest/scripts/cli/test_interfaces_openvpn.py
+++ b/smoketest/scripts/cli/test_interfaces_openvpn.py
@@ -250,6 +250,67 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
interface = f'vtun{ii}'
self.assertNotIn(interface, interfaces())
+ def test_openvpn_client_ip_version(self):
+ # Test the client mode behavior combined with different IP protocol versions
+
+ interface = 'vtun10'
+ remote_host = '192.0.2.10'
+ remote_host_v6 = 'fd00::2:10'
+ path = base_path + [interface]
+ auth_hash = 'sha1'
+
+ # Default behavior: client uses uspecified protocol version (udp)
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes256'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'client'])
+ self.cli_set(path + ['persistent-tunnel'])
+ self.cli_set(path + ['protocol', 'udp'])
+ self.cli_set(path + ['remote-host', remote_host])
+ self.cli_set(path + ['remote-port', remote_port])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['vrf', vrf_name])
+ self.cli_set(path + ['authentication', 'username', interface+'user'])
+ self.cli_set(path + ['authentication', 'password', interface+'secretpw'])
+
+ self.cli_commit()
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev vtun10', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto udp', config)
+ self.assertIn(f'rport {remote_port}', config)
+ self.assertIn(f'remote {remote_host}', config)
+ self.assertIn(f'persist-tun', config)
+
+ # IPv4 only: client usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: client uses udp6 protocol
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_delete(path + ['remote-host', remote_host])
+ self.cli_set(path + ['remote-host', remote_host_v6])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+
+ # IPv6 dual-stack: not allowed in client mode
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
def test_openvpn_server_verify(self):
# Create one OpenVPN server interface and check required verify() stages
interface = 'vtun5000'
@@ -453,6 +514,74 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
interface = f'vtun{ii}'
self.assertNotIn(interface, interfaces())
+ def test_openvpn_server_ip_version(self):
+ # Test the server mode behavior combined with each IP protocol version
+
+ auth_hash = 'sha256'
+ port = '2000'
+
+ interface = 'vtun20'
+ subnet = '192.0.20.0/24'
+ path = base_path + [interface]
+
+ # Default behavior: client uses uspecified protocol version (udp)
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['encryption', 'data-ciphers', 'aes192'])
+ self.cli_set(path + ['hash', auth_hash])
+ self.cli_set(path + ['mode', 'server'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['server', 'subnet', subnet])
+ self.cli_set(path + ['server', 'topology', 'subnet'])
+
+ self.cli_set(path + ['replace-default-route'])
+ self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'certificate', 'ovpn_test'])
+ self.cli_set(path + ['tls', 'dh-params', 'ovpn_test'])
+
+ self.cli_commit()
+
+ start_addr = inc_ip(subnet, '2')
+ stop_addr = last_host_address(subnet)
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'persist-key', config)
+ self.assertIn(f'proto udp', config) # default protocol
+ self.assertIn(f'auth {auth_hash}', config)
+ self.assertIn(f'data-ciphers AES-192-CBC', config)
+ self.assertIn(f'topology subnet', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'push "redirect-gateway def1"', config)
+
+ # IPv4 only: server usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: server uses udp6 protocol + bind ipv6only
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertIn(f'bind ipv6only', config)
+
+ # IPv6 dual-stack: server uses udp6 protocol without bind ipv6only
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertNotIn(f'bind ipv6only', config)
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
def test_openvpn_site2site_verify(self):
# Create one OpenVPN site2site interface and check required
# verify() stages
@@ -627,6 +756,63 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase):
self.assertNotIn(interface, interfaces())
+ def test_openvpn_site2site_ip_version(self):
+ # Test the site-to-site mode behavior combined with each IP protocol version
+
+ encryption_cipher = 'aes256'
+
+ interface = 'vtun30'
+ local_address = '192.0.30.1'
+ local_address_subnet = '255.255.255.252'
+ remote_address = '172.16.30.1'
+ path = base_path + [interface]
+ port = '3030'
+
+ self.cli_set(path + ['local-address', local_address])
+ self.cli_set(path + ['device-type', 'tun'])
+ self.cli_set(path + ['mode', 'site-to-site'])
+ self.cli_set(path + ['local-port', port])
+ self.cli_set(path + ['remote-port', port])
+ self.cli_set(path + ['shared-secret-key', 'ovpn_test'])
+ self.cli_set(path + ['remote-address', remote_address])
+ self.cli_set(path + ['encryption', 'cipher', encryption_cipher])
+
+ self.cli_commit()
+
+ config_file = f'/run/openvpn/{interface}.conf'
+ config = read_file(config_file)
+
+ self.assertIn(f'dev-type tun', config)
+ self.assertIn(f'ifconfig {local_address} {remote_address}', config)
+ self.assertIn(f'proto udp', config)
+ self.assertIn(f'dev {interface}', config)
+ self.assertIn(f'secret /run/openvpn/{interface}_shared.key', config)
+ self.assertIn(f'lport {port}', config)
+ self.assertIn(f'rport {port}', config)
+
+ # IPv4 only: server usees udp4 protocol
+ self.cli_set(path + ['ip-version', 'ipv4'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp4', config)
+
+ # IPv6 only: server uses udp6 protocol + bind ipv6only
+ self.cli_set(path + ['ip-version', 'ipv6'])
+ self.cli_commit()
+
+ config = read_file(config_file)
+ self.assertIn(f'proto udp6', config)
+ self.assertIn(f'bind ipv6only', config)
+
+ # IPv6 dual-stack: not allowed in site-to-site mode
+ self.cli_set(path + ['ip-version', 'dual-stack'])
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ self.cli_delete(base_path)
+ self.cli_commit()
+
def test_openvpn_server_server_bridge(self):
# Create OpenVPN server interface using bridge.
# Validate configuration afterwards.
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index 9105ce1f8..8c1213e2b 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -123,6 +123,18 @@ def get_config(config=None):
openvpn['module_load_dco'] = {}
break
+ # Calculate the protocol modifier. This is concatenated to the protocol string to direct
+ # OpenVPN to use a specific IP protocol version. If unspecified, the kernel decides which
+ # type of socket to open. In server mode, an additional "ipv6-dual-stack" option forces
+ # binding the socket in IPv6 mode, which can also receive IPv4 traffic (when using the
+ # default "ipv6" mode, we specify "bind ipv6only" to disable kernel dual-stack behaviors).
+ if openvpn['ip_version'] == 'ipv4':
+ openvpn['protocol_modifier'] = '4'
+ elif openvpn['ip_version'] in ['ipv6', 'dual-stack']:
+ openvpn['protocol_modifier'] = '6'
+ else:
+ openvpn['protocol_modifier'] = ''
+
return openvpn
def is_ec_private_key(pki, cert_name):
@@ -257,6 +269,9 @@ def verify(openvpn):
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
+ if 'ip_version' in openvpn and openvpn['ip_version'] == 'dual-stack':
+ raise ConfigError('"ip-version dual-stack" is not supported in client mode')
+
if dict_search('tls.dh_params', openvpn):
raise ConfigError('Cannot specify "tls dh-params" in client mode')
@@ -264,6 +279,9 @@ def verify(openvpn):
# OpenVPN site-to-site - VERIFY
#
elif openvpn['mode'] == 'site-to-site':
+ if 'ip_version' in openvpn and openvpn['ip_version'] == 'dual-stack':
+ raise ConfigError('"ip-version dual-stack" is not supported in site-to-site mode')
+
if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
raise ConfigError('Must specify "local-address" or add interface to bridge')
@@ -487,6 +505,25 @@ def verify(openvpn):
# not depending on any operation mode
#
+ # verify that local_host/remote_host match with any ip_version override
+ # specified (if a dns name is specified for remote_host, no attempt is made
+ # to verify that record resolves to an address of the configured family)
+ if 'local_host' in openvpn:
+ if openvpn['ip_version'] == 'ipv4' and is_ipv6(openvpn['local_host']):
+ raise ConfigError('Cannot use an IPv6 "local-host" with "ip-version ipv4"')
+ elif openvpn['ip_version'] == 'ipv6' and is_ipv4(openvpn['local_host']):
+ raise ConfigError('Cannot use an IPv4 "local-host" with "ip-version ipv6"')
+ elif openvpn['ip_version'] == 'dual-stack':
+ raise ConfigError('Cannot use "local-host" with "ip-version dual-stack". "dual-stack" is only supported when OpenVPN binds to all available interfaces.')
+
+ if 'remote_host' in openvpn:
+ remote_hosts = dict_search('remote_host', openvpn)
+ for remote_host in remote_hosts:
+ if openvpn['ip_version'] == 'ipv4' and is_ipv6(remote_host):
+ raise ConfigError('Cannot use an IPv6 "remote-host" with "ip-version ipv4"')
+ elif openvpn['ip_version'] == 'ipv6' and is_ipv4(remote_host):
+ raise ConfigError('Cannot use an IPv4 "remote-host" with "ip-version ipv6"')
+
# verify specified IP address is present on any interface on this system
if 'local_host' in openvpn:
if not is_addr_assigned(openvpn['local_host']):