Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

T6183: interfaces openvpn: suppport specifying IP protocol version #3975

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions data/templates/openvpn/server.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 %}
Expand Down Expand Up @@ -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 }}

Expand Down
28 changes: 28 additions & 0 deletions interface-definitions/interfaces_openvpn.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
186 changes: 186 additions & 0 deletions smoketest/scripts/cli/test_interfaces_openvpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,67 @@ def test_openvpn_client_interfaces(self):
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'
Expand Down Expand Up @@ -453,6 +514,74 @@ def test_openvpn_server_subnet_topology(self):
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
Expand Down Expand Up @@ -627,6 +756,63 @@ def test_openvpn_site2site_interfaces_tun(self):
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.
Expand Down
37 changes: 37 additions & 0 deletions src/conf_mode/interfaces_openvpn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -257,13 +269,19 @@ 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')

#
# 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')

Expand Down Expand Up @@ -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']):
Expand Down
Loading