summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjjakob <jernej.jakob@gmail.com>2020-04-11 11:45:14 +0200
committerjjakob <jernej.jakob@gmail.com>2020-04-13 14:30:20 +0200
commitbb36bdec1506c7fbf57b786c907b0c7cd5efc117 (patch)
treeb4fd8dda4eeb91fccb0a9544b30f4832cb1a8690
parent1cf1cb506e6c868f0e1159c8056ea1bba815e5a8 (diff)
downloadvyos-1x-bb36bdec1506c7fbf57b786c907b0c7cd5efc117.tar.gz
vyos-1x-bb36bdec1506c7fbf57b786c907b0c7cd5efc117.zip
openvpn: T2235: add custom server pool handling
- add config options and logic for server client-ip-pool - add function for determining default IPs for the server in different configurations - verify for pool IPs and maximum subnet prefix length - move remote netmask logic for client ifconfig-push to use new function - add topology 'net30' , set it as default (as it already was) - replace generic ip_* with IPv4* where necessary - print warning to console when server client IP is in server pool - fix server subnet help field
-rw-r--r--data/templates/openvpn/server.conf.tmpl11
-rw-r--r--interface-definitions/interfaces-openvpn.xml.in54
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py156
3 files changed, 193 insertions, 28 deletions
diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl
index 340ead269..37e9c7f2a 100644
--- a/data/templates/openvpn/server.conf.tmpl
+++ b/data/templates/openvpn/server.conf.tmpl
@@ -71,13 +71,18 @@ nobind
#
{%- if server_topology %}
-topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %}
+topology {% if server_topology == 'point-to-point' %}p2p{% else %}{{ server_topology }}{% endif %}
{%- endif %}
{%- if bridge_member %}
-server-bridge nogw
+mode server
+tls-server
{%- else %}
-server {{ server_subnet }}
+server {{ server_subnet }}{% if server_pool_start %} nopool{% endif %}
+{%- endif %}
+
+{%- if server_pool_start %}
+ifconfig-pool {{ server_pool_start }} {{ server_pool_stop }}{% if server_pool_netmask %} {{ server_pool_netmask }}{% endif %}
{%- endif %}
{%- if server_max_conn %}
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index 92bac3fab..d926876f7 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -444,6 +444,52 @@
</leafNode>
</children>
</tagNode>
+ <node name="client-ip-pool">
+ <properties>
+ <help>Pool of client IP addresses</help>
+ </properties>
+ <children>
+ <leafNode name="start">
+ <properties>
+ <help>First IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="stop">
+ <properties>
+ <help>Last IP address in the pool</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="subnet-mask">
+ <properties>
+ <help>Subnet mask pushed to dynamic clients.
+ If not set the server subnet mask will be used.
+ Only used with topology subnet or device type tap.
+ Not used with bridged interfaces.</help>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 subnet mask</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
<leafNode name="domain-name">
<properties>
<help>DNS suffix to be pushed to all clients</help>
@@ -501,7 +547,7 @@
<help>Server-mode subnet (from which client IPs are allocated)</help>
<valueHelp>
<format>ipv4net</format>
- <description>IPv4 address and prefix length</description>
+ <description>IPv4 network and prefix length</description>
</valueHelp>
<constraint>
<validator name="ipv4-prefix"/>
@@ -512,9 +558,13 @@
<properties>
<help>Topology for clients</help>
<completionHelp>
- <list>point-to-point subnet</list>
+ <list>net30 point-to-point subnet</list>
</completionHelp>
<valueHelp>
+ <format>net30</format>
+ <description>net30 topology</description>
+ </valueHelp>
+ <valueHelp>
<format>point-to-point</format>
<description>Point-to-point topology</description>
</valueHelp>
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 7bbc1c778..8975a2d79 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -18,8 +18,8 @@ import os
import re
from copy import deepcopy
-from sys import exit
-from ipaddress import ip_address,ip_network,IPv4Network
+from sys import exit,stderr
+from ipaddress import IPv4Address,IPv4Network,summarize_address_range
from netifaces import interfaces
from time import sleep
from shutil import rmtree
@@ -72,10 +72,14 @@ default_config_data = {
'server_domain': '',
'server_max_conn': '',
'server_dns_nameserver': [],
+ 'server_pool': False,
+ 'server_pool_start': '',
+ 'server_pool_stop': '',
+ 'server_pool_netmask': '',
'server_push_route': [],
'server_reject_unconfigured': False,
'server_subnet': '',
- 'server_topology': '',
+ 'server_topology': 'net30',
'shared_secret_file': '',
'tls': False,
'tls_auth': '',
@@ -113,6 +117,66 @@ def checkCertHeader(header, filename):
return False
+def getDefaultServer(network, topology, devtype):
+ """
+ Gets the default server parameters for a "server" directive.
+ Currently only IPv4 routed but may be extended to support bridged and/or IPv6 in the future.
+ Logic from openvpn's src/openvpn/helper.c.
+ Returns a dict with addresses or False if the input parameters were incorrect.
+ """
+ if not (topology and devtype):
+ return False
+
+ if not (devtype == 'tun' or devtype == 'tap'):
+ return False
+
+ if not network.prefixlen:
+ return False
+ elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30):
+ return False
+
+ server = {
+ 'local': '',
+ 'remote_netmask': '',
+ 'client_remote_netmask': '',
+ 'pool_start': '',
+ 'pool_stop': '',
+ 'pool_netmask': ''
+ }
+
+ if devtype == 'tun':
+ if topology == 'net30' or topology == 'point-to-point':
+ server['local'] = network[1]
+ server['remote_netmask'] = network[2]
+ server['client_remote_netmask'] = server['local']
+
+ # pool start is 4th host IP in subnet (.4 in a /24)
+ server['pool_start'] = network[4]
+
+ if network.prefixlen == 29:
+ server['pool_stop'] = network.broadcast_address
+ else:
+ # pool end is -4 from the broadcast address (.251 in a /24)
+ server['pool_stop'] = network[-5]
+
+ elif topology == 'subnet':
+ server['local'] = network[1]
+ server['remote_netmask'] = str(network.netmask)
+ server['client_remote_netmask'] = server['remote_netmask']
+ server['pool_start'] = network[2]
+ server['pool_stop'] = network[-3]
+ server['pool_netmask'] = server['remote_netmask']
+
+ elif devtype == 'tap':
+ server['local'] = network[1]
+ server['remote_netmask'] = str(network.netmask)
+ server['client_remote_netmask'] = server['remote_netmask']
+ server['pool_start'] = network[2]
+ server['pool_stop'] = network[-2]
+ server['pool_netmask'] = server['remote_netmask']
+
+ return server
+
def get_config():
openvpn = deepcopy(default_config_data)
conf = Config()
@@ -300,17 +364,6 @@ def get_config():
'remote_netmask': ''
}
- # note: with "topology subnet", this is "<ip> <netmask>".
- # with "topology p2p", this is "<ip> <our_ip>".
- if openvpn['server_topology'] == 'subnet':
- # we are only interested in the netmask portion of server_subnet
- data['remote_netmask'] = openvpn['server_subnet'].split(' ')[1]
- else:
- # we need the server subnet in format 192.0.2.0/255.255.255.0
- subnet = openvpn['server_subnet'].replace(' ', r'/')
- # OpenVPN always uses the subnets first available IP address
- data['remote_netmask'] = list(ip_network(subnet).hosts())[0]
-
# Option to disable client connection
if conf.exists('disable'):
data['disable'] = True
@@ -333,6 +386,19 @@ def get_config():
# re-set configuration level
conf.set_level('interfaces openvpn ' + openvpn['intf'])
+ # Server client IP pool
+ if conf.exists('server client-ip-pool'):
+ openvpn['server_pool'] = True
+
+ if conf.exists('server client-ip-pool start'):
+ openvpn['server_pool_start'] = conf.return_value('server client-ip-pool start')
+
+ if conf.exists('server client-ip-pool stop'):
+ openvpn['server_pool_stop'] = conf.return_value('server client-ip-pool stop')
+
+ if conf.exists('server client-ip-pool netmask'):
+ openvpn['server_pool_netmask'] = conf.return_value('server client-ip-pool netmask')
+
# DNS suffix to be pushed to all clients
if conf.exists('server domain-name'):
openvpn['server_domain'] = conf.return_value('server domain-name')
@@ -410,6 +476,26 @@ def get_config():
if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
openvpn['tls_dh'] = 'none'
+ # Set defaults where necessary.
+ # If any of the input parameters are missing or wrong,
+ # this will return False and no defaults will be set.
+ default_server = getDefaultServer(server_network, openvpn['server_topology'], openvpn['type'])
+ if default_server:
+ # server-bridge doesn't require a pool so don't set defaults for it
+ if not openvpn['bridge_member']:
+ openvpn['server_pool'] = True
+ if not openvpn['server_pool_start']:
+ openvpn['server_pool_start'] = default_server['pool_start']
+
+ if not openvpn['server_pool_stop']:
+ openvpn['server_pool_stop'] = default_server['pool_stop']
+
+ if not openvpn['server_pool_netmask']:
+ openvpn['server_pool_netmask'] = default_server['pool_netmask']
+
+ for client in openvpn['client']:
+ client['remote_netmask'] = default_server['client_remote_netmask']
+
return openvpn
def verify(openvpn):
@@ -504,10 +590,42 @@ def verify(openvpn):
if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
- if not openvpn['server_subnet']:
+ if openvpn['server_subnet']:
+ subnet = IPv4Network(openvpn['server_subnet'].replace(' ', '/'))
+
+ if openvpn['type'] == 'tun' and subnet.prefixlen > 29:
+ raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
+ elif openvpn['type'] == 'tap' and subnet.prefixlen > 30:
+ raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
+
+ for client in openvpn['client']:
+ if client['ip'] and not IPv4Address(client['ip']) in subnet:
+ raise ConfigError(f'Client IP "{client["ip"]}" not in server subnet "{subnet}"')
+
+ else:
if not openvpn['bridge_member']:
raise ConfigError('Must specify "server subnet" or "bridge member interface" in server mode')
+
+ if openvpn['server_pool']:
+ if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
+ raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode')
+ else:
+ v4PoolStart = IPv4Address(openvpn['server_pool_start'])
+ v4PoolStop = IPv4Address(openvpn['server_pool_stop'])
+ if v4PoolStart > v4PoolStop:
+ raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
+ if (int(v4PoolStop) - int(v4PoolStart) >= 65536):
+ raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop}], maximum is 65536 addresses.')
+
+ v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
+ for client in openvpn['client']:
+ if client['ip']:
+ for v4PoolNet in v4PoolNets:
+ if IPv4Address(client['ip']) in v4PoolNet:
+ print(f'Warning: Client "{client["name"]}" IP {client["ip"]} is in server IP pool, it is not reserved for this client.',
+ file=stderr)
+
else:
# checks for both client and site-to-site go here
if openvpn['server_reject_unconfigured']:
@@ -634,14 +752,6 @@ def verify(openvpn):
if not openvpn['auth_pass']:
raise ConfigError('Password for authentication is missing')
- #
- # Client
- #
- subnet = openvpn['server_subnet'].replace(' ', '/')
- for client in openvpn['client']:
- if client['ip'] and not ip_address(client['ip']) in ip_network(subnet):
- raise ConfigError('Client IP "{}" not in server subnet "{}'.format(client['ip'], subnet))
-
return None
def generate(openvpn):