diff options
author | jjakob <jernej.jakob@gmail.com> | 2020-04-11 11:45:14 +0200 |
---|---|---|
committer | jjakob <jernej.jakob@gmail.com> | 2020-04-13 14:30:20 +0200 |
commit | bb36bdec1506c7fbf57b786c907b0c7cd5efc117 (patch) | |
tree | b4fd8dda4eeb91fccb0a9544b30f4832cb1a8690 | |
parent | 1cf1cb506e6c868f0e1159c8056ea1bba815e5a8 (diff) | |
download | vyos-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.tmpl | 11 | ||||
-rw-r--r-- | interface-definitions/interfaces-openvpn.xml.in | 54 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 156 |
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): |