From bb36bdec1506c7fbf57b786c907b0c7cd5efc117 Mon Sep 17 00:00:00 2001
From: jjakob <jernej.jakob@gmail.com>
Date: Sat, 11 Apr 2020 11:45:14 +0200
Subject: 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
---
 src/conf_mode/interfaces-openvpn.py | 156 ++++++++++++++++++++++++++++++------
 1 file changed, 133 insertions(+), 23 deletions(-)

(limited to 'src')

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):
-- 
cgit v1.2.3