From 77c1b3457439889846380c5fd5da30cd11e253d9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 16 Aug 2018 21:04:28 +0200 Subject: T778: T782: dhcp-server: XML and Python rewrite This commit changes in addtion the DHCP server config syntax as defined in "T782: Cleanup dhcp-server configuration". Replace boolean parameter from the folowing nodes and make it valueless. This requires a migration script which is tracked with this task * set service dhcp-server shared-network-name subnet 172.31.0.0/24 ip-forwarding enable (true|false) * set service dhcp-server shared-network-name authoritative (true|false) * set service dhcp-server disabled (true|false) * set service dhcp-server dynamic-dns-update enable (true|fals) * set service dhcp-server hostfile-update (enable|disable) Replace the nested start/stop ip address from "subnet 172.31.0.0/24 start 172.31.0.101 stop 172.31.0.149" to "subnet 172.31.0.0/24 range start" and "subnet 172.31.0.0/24 range stop" where foo can be any character or number. In addition the vyatta-cfg-dhcp-server package used it's own init/config file for service startup. This has been migrated to the vanilla Debian files. Copy 'on-dhcp-event.sh' from vyatta-cfg-shcp-server package commit 4749e648bca6. --- src/conf_mode/dhcp_server.py | 773 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 773 insertions(+) create mode 100755 src/conf_mode/dhcp_server.py (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py new file mode 100755 index 000000000..6dc8bf2bb --- /dev/null +++ b/src/conf_mode/dhcp_server.py @@ -0,0 +1,773 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# + +import sys +import os +import ipaddress +import jinja2 +import socket +import struct +import netifaces + +from vyos.config import Config +from vyos import ConfigError + +config_file = r'/etc/dhcp/dhcpd.conf' +daemon_config_file = r'/etc/default/isc-dhcp-server' + +# Please be careful if you edit the template. +config_tmpl = """ +### Autogenerated by dhcp_server.py ### + +log-facility local7; + +{% if hostfile_update %} +on commit { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".", leased-address); + set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "commit", ClientName, ClientIp, ClientMac, ClientDomain); +} + +on release { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} + +on expiry { + set ClientName = pick-first-value(host-decl-name, option fqdn.hostname, option host-name); + set ClientIp = binary-to-ascii(10, 8, ".",leased-address); + set ClientMac = binary-to-ascii(16, 8, ":",substring(hardware, 1, 6)); + set ClientDomain = pick-first-value(config-option domain-name, "..YYZ!"); + execute("/usr/libexec/vyos/system/on-dhcp-event.sh", "release", ClientName, ClientIp, ClientMac, ClientDomain); +} +{% endif %} +ddns-update-style {% if ddns_enable -%} interim {%- else -%} none {%- endif %}; +{% if static_route -%} +option rfc3442-static-route code 121 = array of integer 8; +option windows-static-route code 249 = array of integer 8; +{%- endif %} +{% if static_route -%} +option wpad-url code 252 = text; +{% endif %} + +{%- if global_parameters %} +# The following {{ global_parameters | length }} line(s) were added as global-parameters in the CLI and have not been validated +{%- for param in global_parameters %} +{{ param }} +{%- endfor -%} +{%- endif %} + +# Failover configuration +{% for network in shared_network %} +{%- if not network.disable -%} +{%- for subnet in network.subnet %} +{%- if subnet.failover_name -%} +failover peer "{{ subnet.failover_name }}" { +{%- if subnet.failover_status == 'primary' %} + primary; + mclt 1800; + split 128; +{%- elif subnet.failover_status == 'secondary' %} + secondary; +{%- endif %} + address {{ subnet.failover_local_addr }}; + port 520; + peer address {{ subnet.failover_peer_addr }}; + peer port 520; + max-response-delay 30; + max-unacked-updates 10; + load balance max seconds 3; +} +{% endif -%} +{% endfor -%} +{% endif -%} +{% endfor %} + +# Shared network configrations +{% for network in shared_network %} +{%- if not network.disable -%} +shared-network {{ network.name }} { + {% if network.authoritative %}authoritative;{% endif %} + {%- if network.network_parameters %} + # The following {{ network.network_parameters | length }} line(s) were added as shared-network-parameters in the CLI and have not been validated + {%- for param in network.network_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + {%- for subnet in network.subnet %} + subnet {{ subnet.address }} netmask {{ subnet.netmask }} { + {%- if subnet.dns_server %} + option domain-name-servers {{ subnet.dns_server | join(', ') }}; + {%- endif %} + {%- if subnet.domain_search %} + option domain-search {{ subnet.domain_search | join(', ') }}; + {%- endif %} + {%- if subnet.ntp_server %} + option ntp-servers {{ subnet.ntp_server | join(', ') }}; + {%- endif %} + {%- if subnet.pop_server %} + option pop-server {{ subnet.pop_server | join(', ') }}; + {%- endif %} + {%- if subnet.smtp_server %} + option smtp-server {{ subnet.smtp_server | join(', ') }}; + {%- endif %} + {%- if subnet.time_server %} + option time-servers {{ subnet.time_server | join(', ') }}; + {%- endif %} + {%- if subnet.wins_server %} + option netbios-name-servers {{ subnet.wins_server | join(', ') }}; + {%- endif %} + {%- if subnet.static_route %} + option rfc3442-static-route {{ subnet.static_route }}; + option windows-static-route {{ subnet.static_route }}; + {%- endif %} + {%- if subnet.ip_forwarding %} + option ip-forwarding true; + {%- endif -%} + {%- if subnet.default_router %} + option routers {{ subnet.default_router }}; + {%- endif -%} + {%- if subnet.server_identifier %} + option dhcp-server-identifier {{ subnet.server_identifier }}; + {%- endif -%} + {%- if subnet.domain_name %} + option domain-name "{{ subnet.domain_name }}"; + {%- endif -%} + {%- if subnet.tftp_server %} + option tftp-server-name "{{ subnet.tftp_server }}"; + {%- endif -%} + {%- if subnet.bootfile_name %} + option bootfile-name "{{ subnet.bootfile_name }}"; + filename "{{ subnet.bootfile_name }}"; + {%- endif -%} + {%- if subnet.bootfile_server %} + next-server {{ subnet.bootfile_server }}; + {%- endif -%} + {%- if subnet.time_offset %} + option time-offset {{ subnet.time_offset }}; + {%- endif -%} + {%- if subnet.wpad_url %} + option wpad-url "{{ subnet.wpad_url }}"; + {%- endif -%} + {%- if subnet.client_prefix_length %} + option subnet-mask {{ subnet.client_prefix_length }}; + {%- endif -%} + {% if subnet.lease %} + default-lease-time {{ subnet.lease }}; + max-lease-time {{ subnet.lease }}; + {%- endif -%} + {%- for host in subnet.static_mapping %} + {% if not host.disable -%} + host {{ network.name }}_{{ host.name }} { + fixed-address {{ host.ip_address }}; + hardware ethernet {{ host.mac_address }}; + {%- if host.static_parameters %} + # The following {{ host.static_parameters | length }} line(s) were added as static-mapping-parameters in the CLI and have not been validated + {%- for param in host.static_parameters %} + {{ param }} + {%- endfor -%} + {%- endif %} + } + {%- endif %} + {%- endfor %} + {%- for range in subnet.range %} + range {{ range.start }} {{ range.stop }}; + {%- endfor %} + } + {%- endfor %} + on commit { set shared-networkname = "{{ network.name }}"; } +} +{%- endif %} +{% endfor %} +""" + +daemon_tmpl = """ +### Autogenerated by dhcp_server.py ### + +# sourced by /etc/init.d/isc-dhcp-server + +# Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf). +DHCPD_CONF=/etc/dhcp/dhcpd.conf + +# Path to dhcpd's PID file (default: /var/run/dhcpd.pid). +DHCPD_PID=/var/run/dhcpd.pid + +# Additional options to start dhcpd with. +# Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead +OPTIONS="-lf /config/dhcpd.leases" + +# On what interfaces should the DHCP server (dhcpd) serve DHCP requests? +# Separate multiple interfaces with spaces, e.g. "eth0 eth1". +INTERFACES="" +""" + +default_config_data = { + 'disable': False, + 'ddns_enable': False, + 'global_parameters': [], + 'hostfile_update': False, + 'static_route': False, + 'wpad': False, + 'shared_network': [], +} + +def get_config(): + dhcp = default_config_data + conf = Config() + if not conf.exists('service dhcp-server'): + return None + else: + conf.set_level('service dhcp-server') + + # check for global disable of DHCP service + if conf.exists('disable'): + dhcp['disable'] = True + + # check for global dynamic DNS upste + if conf.exists('dynamic-dns-update'): + dhcp['ddns_enable'] = True + + # HACKS AND TRICKS + # + # check for global 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters from any user + if conf.exists('global-parameters'): + dhcp['global_parameters'] = conf.return_values('global-parameters') + + # check for global DHCP server updating /etc/host per lease + if conf.exists('hostfile-update'): + dhcp['hostfile_update'] = True + + # check for multiple, shared networks served with DHCP addresses + if conf.exists('shared-network-name'): + for network in conf.list_nodes('shared-network-name'): + conf.set_level('service dhcp-server shared-network-name {0}'.format(network)) + config = { + 'name': network, + 'authoritative': False, + 'description': '', + 'disable': False, + 'network_parameters': [], + 'subnet': [] + } + # check if DHCP server should be authoritative on this network + if conf.exists('authoritative'): + config['authoritative'] = True + + # A description for this given network + if conf.exists('description'): + config['description'] = conf.return_value('description') + + # If disabled, the shared-network configuration becomes inactive in + # the running DHCP server instance + if conf.exists('disable'): + config['disable'] = True + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('shared-network-parameters'): + config['network_parameters'] = conf.return_values('shared-network-parameters') + + # check for multiple subnet configurations in a shared network + # config segment + if conf.exists('subnet'): + for net in conf.list_nodes('subnet'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + subnet = { + 'network': net, + 'address': str(ipaddress.ip_network(net).network_address), + 'netmask': str(ipaddress.ip_network(net).netmask), + 'bootfile_name': '', + 'bootfile_server': '', + 'client_prefix_length': '', + 'default_router': '', + 'dns_server': [], + 'domain_name': '', + 'domain_search': [], + 'exclude': [], + 'failover_local_addr': '', + 'failover_name': '', + 'failover_peer_addr': '', + 'failover_status': '', + 'ip_forwarding': False, + 'lease': '86400', + 'ntp_server': [], + 'pop_server': [], + 'server_identifier': '', + 'smtp_server': [], + 'range': [], + 'static_mapping': [], + 'static_subnet': '', + 'static_router': '', + 'static_route': '', + 'subnet_parameters': [], + 'tftp_server': '', + 'time_offset': '', + 'time_server': [], + 'wins_server': [], + 'wpad_url': '' + } + + # Used to identify a bootstrap file + if conf.exists('bootfile-name'): + subnet['bootfile_name'] = conf.return_value('bootfile-name') + + # Specify host address of the server from which the initial boot file + # (specified above) is to be loaded. Should be a numeric IP address or + # domain name. + if conf.exists('bootfile-server'): + subnet['bootfile_server'] = conf.return_value('bootfile-server') + + # The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet + # mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet + # mask from the subnet declaration for the network on which an address is being assigned. + if conf.exists('client-prefix-length'): + # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python + host_bits = 32 - int(conf.return_value('client-prefix-length')) + subnet['client_prefix_length'] = socket.inet_ntoa(struct.pack('!I', (1 << 32) - (1 << host_bits))) + + # Default router IP address on the client's subnet + if conf.exists('default-router'): + subnet['default_router'] = conf.return_value('default-router') + + # Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to + # the client. Servers should be listed in order of preference. + if conf.exists('dns-server'): + subnet['dns_server'] = conf.return_values('dns-server') + + # Option specifies the domain name that client should use when resolving hostnames + # via the Domain Name System. + if conf.exists('domain-name'): + subnet['domain_name'] = conf.return_value('domain-name') + + # The domain-search option specifies a 'search list' of Domain Names to be used + # by the client to locate not-fully-qualified domain names. + if conf.exists('domain-search'): + for domain in conf.return_values('domain-search'): + subnet['domain_search'].append('"' + domain + '"') + + # IP address (local) for failover peer to connect + if conf.exists('failover local-address'): + subnet['failover_local_addr'] = conf.return_value('failover local-address') + + # DHCP failover peer name + if conf.exists('failover name'): + subnet['failover_name'] = conf.return_value('failover name') + + # IP address (remote) of failover peer + if conf.exists('failover peer-address'): + subnet['failover_peer_addr'] = conf.return_value('failover peer-address') + + # DHCP failover peer status (primary|secondary) + if conf.exists('failover status'): + subnet['failover_status'] = conf.return_value('failover status') + + # Option specifies whether the client should configure its IP layer for packet + # forwarding + if conf.exists('ip-forwarding'): + subnet['ip_forwarding'] = True + + # Time should be the length in seconds that will be assigned to a lease if the + # client requesting the lease does not ask for a specific expiration time + if conf.exists('lease'): + subnet['lease'] = conf.return_value('lease') + + # Specifies a list of IP addresses indicating NTP (RFC 5905) servers available + # to the client. + if conf.exists('ntp-server'): + subnet['ntp_server'] = conf.return_values('ntp-server') + + # POP3 server option specifies a list of POP3 servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('pop-server'): + subnet['pop_server'] = conf.return_values('pop-server') + + # DHCP servers include this option in the DHCPOFFER in order to allow the client + # to distinguish between lease offers. DHCP clients use the contents of the + # 'server identifier' field as the destination address for any DHCP messages + # unicast to the DHCP server + if conf.exists('server-identifier'): + subnet['server_identifier'] = conf.return_value('server-identifier') + + # SMTP server option specifies a list of SMTP servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('smtp-server'): + subnet['smtp_server'] = conf.return_values('smtp-server') + + # For any subnet on which addresses will be assigned dynamically, there must be at + # least one range statement. The range statement gives the lowest and highest IP + # addresses in a range. All IP addresses in the range should be in the subnet in + # which the range statement is declared. + if conf.exists('range'): + for range in conf.list_nodes('range'): + range = { + 'start': conf.return_value('range {0} start'.format(range)), + 'stop': conf.return_value('range {0} stop'.format(range)) + } + subnet['range'].append(range) + + # IP address that needs to be excluded from DHCP lease range + if conf.exists('exclude'): + # We have no need to store the exclude addresses. Exclude addresses + # are recalculated into several ranges + exclude = [] + subnet['exclude'] = conf.return_values('exclude') + for addr in subnet['exclude']: + exclude.append(ipaddress.ip_address(addr)) + + # sort excluded IP addresses ascending + exclude = sorted(exclude) + + # calculate multipe ranges based on the excluded IP addresses + output = [] + for range in subnet['range']: + range_start = range['start'] + range_stop = range['stop'] + + for i in exclude: + # Excluded IP address must be in out specified range + if (i >= ipaddress.ip_address(range_start)) and (i <= ipaddress.ip_address(range_stop)): + # Build up new IP address range ending one IP address before + # our exclude address + range = { + 'start': str(range_start), + 'stop': str(i - 1) + } + # Our next IP address range will start one address after + # our exclude address + range_start = i + 1 + output.append(range) + + # Take care of last IP address range spanning from the last exclude + # address (+1) to the end of the initial configured range + if i is exclude[-1]: + last = { + 'start': str(i + 1), + 'stop': str(range_stop) + } + output.append(last) + else: + # IP address not inside search range, take range is it is + output.append(range) + + # We successfully build up a new list containing several IP address + # ranges, replace IP address range in our dictionary + subnet['range'] = output + + # Static DHCP leases + if conf.exists('static-mapping'): + for mapping in conf.list_nodes('static-mapping'): + conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) + mapping = { + 'name': mapping, + 'disable': False, + 'ip_address': '', + 'mac_address': '', + 'static_parameters': [] + } + + # This static lease is disabled + if conf.exists('disable'): + mapping['disable'] = True + + # IP address used for this DHCP client + if conf.exists('ip-address'): + mapping['ip_address'] = conf.return_value('ip-address') + + # MAC address of requesting DHCP client + if conf.exists('mac-address'): + mapping['mac_address'] = conf.return_value('mac-address') + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('static-mapping-parameters'): + mapping['static_parameters'] = conf.return_values('static-mapping-parameters') + + # append static-mapping configuration to subnet list + subnet['static_mapping'].append(mapping) + + # Reset config level to matching hirachy + conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net)) + + # This option specifies a list of static routes that the client should install in its routing + # cache. If multiple routes to the same destination are specified, they are listed in descending + # order of priority. + if conf.exists('static-route destination-subnet'): + subnet['static_subnet'] = conf.return_value('static-route destination-subnet') + # Required for global config section + dhcp['static_route'] = True + + if conf.exists('static-route router'): + subnet['static_router'] = conf.return_value('static-route router') + + if subnet['static_router'] and subnet['static_subnet']: + # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server + # Option format is: + # , , , , , , + # where bytes with the value 0 are omitted. + net = ipaddress.ip_network(subnet['static_subnet']) + # add netmask + string = str(net.prefixlen) + ',' + # add network bytes + bytes = str(net.network_address).split('.') + for b in bytes: + if b != '0': + string += b + ',' + + # add router bytes + bytes = subnet['static_router'].split('.') + for b in bytes: + if b != '0': + string += b + if b is not bytes[-1]: + string += ',' + + subnet['static_route'] = string + + # HACKS AND TRICKS + # + # check for 'raw' ISC DHCP parameters configured by users + # actually this is a bad idea in general to pass raw parameters + # from any user + # + # deprecate this and issue a warning like we do for DNS forwarding? + if conf.exists('subnet-parameters'): + config['subnet_parameters'] = conf.return_values('subnet-parameters') + + # This option is used to identify a TFTP server and, if supported by the client, should have + # the same effect as the server-name declaration. BOOTP clients are unlikely to support this + # option. Some DHCP clients will support it, and others actually require it. + if conf.exists('tftp-server-name'): + subnet['tftp_server'] = conf.return_value('tftp-server-name') + + # The time-offset option specifies the offset of the client’s subnet in seconds from + # Coordinated Universal Time (UTC). + if conf.exists('time-offset'): + subnet['time_offset'] = conf.return_value('time-offset') + + # The time-server option specifies a list of RFC 868 time servers available to the client. + # Servers should be listed in order of preference. + if conf.exists('time-server'): + subnet['time_server'] = conf.return_values('time-server') + + # The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers + # listed in order of preference. NetBIOS Name Service is currently more commonly referred to + # as WINS. WINS servers can be specified using the netbios-name-servers option. + if conf.exists('wins-server'): + subnet['wins_server'] = conf.return_values('wins-server') + + # URL for Web Proxy Autodiscovery Protocol + if conf.exists('wpad-url'): + subnet['wpad_url'] = conf.return_value('wpad-url') + # Required for global config section + dhcp['wpad'] = True + + # append subnet configuration to shared network subnet list + config['subnet'].append(subnet) + + # append shared network configuration to config dictionary + dhcp['shared_network'].append(config) + + return dhcp + +def verify(dhcp): + if dhcp is None: + return None + + # If DHCP is enabled we need one share-network + if len(dhcp['shared_network']) == 0: + raise ConfigError('No DHCP shared networks configured. At least one DHCP shared network must be configured.') + + # A shared-network requires a subnet definition + for network in dhcp['shared_network']: + if len(network['subnet']) == 0: + raise ConfigError('No DHCP lease subnets configured for "{0}". At least one DHCP lease subnet must be configured for each shared network.'.format(network['name'])) + + # Inspect our subnet configuration + failover_names = [] + listen_ok = False + subnets = [] + for network in dhcp['shared_network']: + for subnet in network['subnet']: + # Subnet static route declaration requires destination and router + if subnet['static_subnet'] or subnet['static_router']: + if not (subnet['static_subnet'] and subnet['static_router']): + raise ConfigError('Please specify the missing DHCP static-route parameter: destination-subnet | router') + + # Failover requires all 4 parameters set + if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']: + if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']): + raise ConfigError('Please set one or more of the missing DHCP failover parameters: local-address | peer-address | name | status') + + # Failover names must be uniquie + if subnet['failover_name'] in failover_names: + raise ConfigError('Failover names should be unique: "{0}" has already been configured.'.format(subnet['failover_name'])) + else: + failover_names.append(subnet['failover_name']) + + # Failover requires start/stop ranges for pool + if (len(subnet['range']) == 0): + raise ConfigError('Atleast one start-stop range must be configured for $subnet to set up DHCP failover.') + + # Check if DHCP address range is inside configured subnet declaration + range_start = [] + range_stop = [] + for range in subnet['range']: + # DHCP stop IP required after start IP + if range['start'] and not range['stop']: + raise ConfigError('DHCP range stop IP not defined for range start IP "{0}".'.format(range['start'])) + + # Start address must be inside network + if not ipaddress.ip_address(range['start']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP range start IP "{0}" is not in subnet "{1}" specified in network "{2}."'.format(range['start'], subnet['network'], network['name'])) + + # Stop address must be inside network + if not ipaddress.ip_address(range['stop']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('DHCP range stop IP "{0}" is not in subnet "{1}" specified in network "{2}."'.format(range['stop'], subnet['network'], network['name'])) + + # Stop address must be greater or equal to start address + if not ipaddress.ip_address(range['stop']) >= ipaddress.ip_address(range['start']): + raise ConfigError('DHCP range stop IP "{0}" should be an address greater or equal to the start address "{1}".'.format(range['stop'], range['start'])) + + # Range start address must be unique + if range['start'] in range_start: + raise ConfigError('Conflicting DHCP lease ranges: Pool start address "{0}" defined multipe times'.format(range['start'])) + else: + range_start.append(range['start']) + + # Range stop address must be unique + if range['stop'] in range_stop: + raise ConfigError('Conflicting DHCP lease ranges: Pool stop address "{0}" defined multipe times'.format(range['stop'])) + else: + range_stop.append(range['stop']) + + # Exclude addresses must be in bound + for exclude in subnet['exclude']: + if not ipaddress.ip_address(exclude) in ipaddress.ip_network(subnet['network']): + raise ConfigError('Exclude IP "{0}" is outside of the DHCP lease network "{1}" under shared network "{2}".'.format(exclude, subnet['network'], network['name'])) + + # At least one DHCP address range or static-mapping required + active_mapping = False + if (len(subnet['range']) == 0): + for mapping in subnet['static_mapping']: + # we need at least one active mapping + if (not active_mapping) and (not mapping['disable']): + active_mapping = True + else: + active_mapping = True + + if not active_mapping: + raise ConfigError('No DHCP address range or active static-mapping set for subnet "{0}".'.format(subnet['network'])) + + # Static IP address mappings require both an IP address and MAC address + for mapping in subnet['static_mapping']: + # Static IP address must be configured + if not mapping['ip_address']: + raise ConfigError('No static lease IP address specified for static mapping "{0}" under shared network name "{1}".'.format(mapping['name'], network['name'])) + + # Static IP address must be in bound + if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): + raise ConfigError('Static DHCP lease IP "{0}" under static mapping "{1}" in shared network "{2}" is outside DHCP lease network "{3}".'.format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'], )) + + # Static mapping requires MAC address + if not mapping['mac_address']: + raise ConfigError('No static lease MAC address specified for static mapping "{0}" under shared network name "{1}".'.format(mapping['name'], network['name'])) + + # + # There must be one subnet connected to a real interface + # + for interface in netifaces.interfaces(): + # Retrieve IP address of network interface + ip = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr'] + if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet['network']): + listen_ok = True + + # + # Subnets must be non overlapping + # + if subnet['network'] in subnets: + raise ConfigError('Subnets must be unique! "{0}" defined multiple times.'.format(subnet)) + else: + subnets.append(subnet['network']) + + # + # Check for overlapping subnets + # + net = ipaddress.ip_network(subnet['network']) + for n in subnets: + net2 = ipaddress.ip_network(n) + if (net.compare_networks(net2) != 0): + if net.overlaps(net2): + raise ConfigError('Conflicting subnet ranges: "{0}" overlaps "{1}"'.format(net, net2)) + + if not listen_ok: + raise ConfigError('None of the DHCP lease subnets are inside any configured subnet on broadcast interfaces. At least one lease subnet must be set such that DHCP server listens on a one broadcast interface') + + return None + +def generate(dhcp): + if dhcp is None: + return None + + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(dhcp) + with open(config_file, 'w') as f: + f.write(config_text) + + tmpl = jinja2.Template(daemon_tmpl) + config_text = tmpl.render(dhcp) + with open(daemon_config_file, 'w') as f: + f.write(config_text) + + return None + +def apply(dhcp): + if dhcp is not None: + os.system('sudo systemctl restart isc-dhcp-server.service') + else: + # DHCP server is removed in the commit + os.system('sudo systemctl stop isc-dhcp-server.service') + os.unlink(config_file) + os.unlink(daemon_config_file) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 0e8bf76a10e5386fbf63ff4eec3c644ad7fe38fc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 28 Aug 2018 09:27:30 +0200 Subject: dhcp_server.py: rename dictionary key 'disable' to 'disabled' --- src/conf_mode/dhcp_server.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 6dc8bf2bb..e1bc672f8 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -79,7 +79,7 @@ option wpad-url code 252 = text; # Failover configuration {% for network in shared_network %} -{%- if not network.disable -%} +{%- if not network.disabled -%} {%- for subnet in network.subnet %} {%- if subnet.failover_name -%} failover peer "{{ subnet.failover_name }}" { @@ -105,7 +105,7 @@ failover peer "{{ subnet.failover_name }}" { # Shared network configrations {% for network in shared_network %} -{%- if not network.disable -%} +{%- if not network.disabled -%} shared-network {{ network.name }} { {% if network.authoritative %}authoritative;{% endif %} {%- if network.network_parameters %} @@ -177,7 +177,7 @@ shared-network {{ network.name }} { max-lease-time {{ subnet.lease }}; {%- endif -%} {%- for host in subnet.static_mapping %} - {% if not host.disable -%} + {% if not host.disabled -%} host {{ network.name }}_{{ host.name }} { fixed-address {{ host.ip_address }}; hardware ethernet {{ host.mac_address }}; @@ -222,7 +222,7 @@ INTERFACES="" """ default_config_data = { - 'disable': False, + 'disabled': False, 'ddns_enable': False, 'global_parameters': [], 'hostfile_update': False, @@ -241,7 +241,7 @@ def get_config(): # check for global disable of DHCP service if conf.exists('disable'): - dhcp['disable'] = True + dhcp['disabled'] = True # check for global dynamic DNS upste if conf.exists('dynamic-dns-update'): @@ -266,7 +266,7 @@ def get_config(): 'name': network, 'authoritative': False, 'description': '', - 'disable': False, + 'disabled': False, 'network_parameters': [], 'subnet': [] } @@ -281,7 +281,7 @@ def get_config(): # If disabled, the shared-network configuration becomes inactive in # the running DHCP server instance if conf.exists('disable'): - config['disable'] = True + config['disabled'] = True # HACKS AND TRICKS # @@ -485,7 +485,7 @@ def get_config(): conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping)) mapping = { 'name': mapping, - 'disable': False, + 'disabled': False, 'ip_address': '', 'mac_address': '', 'static_parameters': [] @@ -493,7 +493,7 @@ def get_config(): # This static lease is disabled if conf.exists('disable'): - mapping['disable'] = True + mapping['disabled'] = True # IP address used for this DHCP client if conf.exists('ip-address'): -- cgit v1.2.3 From 46fa3ec53f9c301b3c58af06ba532838d42a0dc6 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 28 Aug 2018 09:28:07 +0200 Subject: dhcp_server.py: issue warning and don't generate config if service is disabled --- src/conf_mode/dhcp_server.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index e1bc672f8..cc887e28e 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -601,7 +601,7 @@ def get_config(): return dhcp def verify(dhcp): - if dhcp is None: + if (dhcp is None) or (dhcp['disabled'] is True): return None # If DHCP is enabled we need one share-network @@ -736,7 +736,8 @@ def verify(dhcp): return None def generate(dhcp): - if dhcp is None: + if (dhcp is None) or (dhcp['disabled'] is True): + print('Warning: DHCP server will be deactivated because it is disabled') return None tmpl = jinja2.Template(config_tmpl) @@ -752,13 +753,13 @@ def generate(dhcp): return None def apply(dhcp): - if dhcp is not None: - os.system('sudo systemctl restart isc-dhcp-server.service') - else: + if (dhcp is None) or (dhcp['disabled'] is True): # DHCP server is removed in the commit os.system('sudo systemctl stop isc-dhcp-server.service') os.unlink(config_file) os.unlink(daemon_config_file) + else: + os.system('sudo systemctl restart isc-dhcp-server.service') return None -- cgit v1.2.3 From fd1eabe72862ec364643a61cb94b21c330a385f5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 28 Aug 2018 21:58:46 +0200 Subject: T810: bugfix broadcast-relay address validator, add 'disable' functionality Whole broadcast relay service can be temporary disabled via set service broadcast-relay disable Individual instances of the broadcast relay service can be disabled set service broadcast-relay id disable --- interface-definitions/bcast-relay.xml | 20 +++- src/conf_mode/bcast_relay.py | 166 +++++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 57 deletions(-) (limited to 'src/conf_mode') diff --git a/interface-definitions/bcast-relay.xml b/interface-definitions/bcast-relay.xml index 0437192fa..fdba554db 100644 --- a/interface-definitions/bcast-relay.xml +++ b/interface-definitions/bcast-relay.xml @@ -3,12 +3,18 @@ - + - UDP Broadcast Relay parameters + UDP broadcast relay service - + + + Globally disable broadcast relay service + + + + Unique ID for each UDP port to forward @@ -21,6 +27,12 @@ + + + Disable broadcast relay service instance + + + Set source IP of forwarded packets, otherwise original senders address is used @@ -29,7 +41,7 @@ Optional source address for forwarded packets - + diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 95f6215b5..8cc948610 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -20,55 +20,104 @@ import sys import os import fnmatch import subprocess +import jinja2 from vyos.config import Config from vyos import ConfigError config_file = r'/etc/default/udp-broadcast-relay' +config_tmpl = """ +### Autogenerated by bcast_relay.py ### + +# UDP broadcast relay configuration for instance {{ id }} +{%- if description %} +# Comment: {{ description }} +{% endif -%} +DAEMON_ARGS="{% if address %}-s {{ address }} {% endif %}{{ id }} {{ port }} {{ interfaces | join(' ') }}" +""" + +default_config_data = { + 'disabled': False, + 'instances': [] +} + def get_config(): + relay = default_config_data conf = Config() - conf.set_level("service broadcast-relay id") - relay_id = conf.list_nodes("") - relays = [] - - for id in relay_id: - interface_list = [] - address = conf.return_value("{0} address".format(id)) - description = conf.return_value("{0} description".format(id)) - port = conf.return_value("{0} port".format(id)) - - # split the interface name listing and form a list - if conf.exists("{0} interface".format(id)): - intfs_names = [] - intfs_names = conf.return_values("{0} interface".format(id)) - - for name in intfs_names: - interface_list.append(name) - - relay = { - "id": id, - "address": address, - "description": description, - "interfaces" : interface_list, - "port": port + if not conf.exists('service broadcast-relay'): + return None + else: + conf.set_level('service broadcast-relay') + + # Service can be disabled by user + if conf.exists('disable'): + relay['disabled'] = True + return relay + + # Parse configuration of each individual instance + if conf.exists('id'): + for id in conf.list_nodes('id'): + conf.set_level('service broadcast-relay id {0}'.format(id)) + config = { + 'id': id, + 'disabled': False, + 'address': '', + 'description': '', + 'interfaces': [], + 'port': '' } - relays.append(relay) - return relays + # Check if individual broadcast relay service is disabled + if conf.exists('disable'): + config['disabled'] = True + + # Source IP of forwarded packets, if empty original senders address is used + if conf.exists('address'): + config['address'] = conf.return_value('address') + + # A description for each individual broadcast relay service + if conf.exists('description'): + config['description'] = conf.return_value('description') + + # UDP port to listen on for broadcast frames + if conf.exists('port'): + config['port'] = conf.return_value('port') + + # Network interfaces to listen on for broadcast frames to be relayed + if conf.exists('interface'): + config['interfaces'] = conf.return_values('interface') + + relay['instances'].append(config) -def verify(relays): - for relay in relays: - if not relay["port"]: - raise ConfigError("UDP broadcast relay 'id {0}' requires a port number".format(relay["id"])) + return relay - if len(relay["interfaces"]) < 2: - raise ConfigError("UDP broadcast relay 'id {0}' requires at least 2 interfaces".format(relay["id"])) +def verify(relay): + if relay is None: + return None + + if relay['disabled']: + return None + + for r in relay['instances']: + # we don't have to check this instance when it's disabled + if r['disabled']: + continue + + # we certainly require a UDP port to listen to + if not r['port']: + raise ConfigError('UDP broadcast relay "{0}" requires a port number'.format(r['id'])) + + # Relaying data without two interface is kinda senseless ... + if len(r['interfaces']) < 2: + raise ConfigError('UDP broadcast relay "id {0}" requires at least 2 interfaces'.format(r['id'])) return None -def generate(relays): - config_header = '### Autogenerated by bcast_relay.py ###\n' + +def generate(relay): + if relay is None: + return None config_dir = os.path.dirname(config_file) config_filename = os.path.basename(config_file) @@ -82,32 +131,43 @@ def generate(relays): # sort our list active_configs.sort() + # delete old configuration files for id in active_configs[:]: - os.unlink(config_file + id) - - for relay in relays: - file = config_file + str(relay["id"]) - interfaces = ' '.join(str(intf) for intf in relay["interfaces"]) - config_args = 'DAEMON_ARGS="{0} {1}"\n'.format(relay["port"], interfaces) - - f = open(file, 'w') - f.write(config_header) - if relay["description"]: - f.write('# ' + relay["description"] + '\n') - f.write(config_args) - f.close() + if os.path.exists(config_file + id): + os.unlink(config_file + id) + + # If the service is disabled, we can bail out here + if relay['disabled']: + print('Warning: UDP broadcast relay service will be deactivated because it is disabled') + return None + + for r in relay['instances']: + # Skip writing instance config when it's disabled + if r['disabled']: + continue + + # configuration filename contains instance id + file = config_file + str(r['id']) + tmpl = jinja2.Template(config_tmpl) + config_text = tmpl.render(r) + with open(file, 'w') as f: + f.write(config_text) return None -def apply(relays): +def apply(relay): # first stop all running services - cmd = "sudo systemctl stop udp-broadcast-relay@{1..99}" - os.system(cmd) + os.system('sudo systemctl stop udp-broadcast-relay@{1..99}') + + if (relay is None) or relay['disabled']: + return None # start only required service instances - for relay in relays: - cmd = "sudo systemctl start udp-broadcast-relay@{0}".format(relay["id"]) - os.system(cmd) + for r in relay['instances']: + # Don't start individual instance when it's disabled + if r['disabled']: + continue + os.system('sudo systemctl start udp-broadcast-relay@{0}'.format(r['id'])) return None -- cgit v1.2.3 From 326e3cc41737ccaeb302db7b34e4adfcec261ccf Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 28 Aug 2018 22:31:15 +0200 Subject: T778: harden dhcp_server.py for non existing files --- src/conf_mode/dhcp_server.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index cc887e28e..e03a04a4d 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -736,7 +736,10 @@ def verify(dhcp): return None def generate(dhcp): - if (dhcp is None) or (dhcp['disabled'] is True): + if dhcp is None: + return None + + if dhcp['disabled'] is True: print('Warning: DHCP server will be deactivated because it is disabled') return None @@ -753,11 +756,13 @@ def generate(dhcp): return None def apply(dhcp): - if (dhcp is None) or (dhcp['disabled'] is True): + if (dhcp is None) or dhcp['disabled']: # DHCP server is removed in the commit os.system('sudo systemctl stop isc-dhcp-server.service') - os.unlink(config_file) - os.unlink(daemon_config_file) + if os.path.exists(config_file): + os.unlink(config_file) + if os.path.exists(daemon_config_file): + os.unlink(daemon_config_file) else: os.system('sudo systemctl restart isc-dhcp-server.service') -- cgit v1.2.3 From c2351f4dc49c96b70c060edfe7657da5ac423f5d Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 21:03:16 +0200 Subject: dhcp_server.py: improve handling of 'dhcpd.leases' file If there was yet no lease file present, dhcpd refused to start. Lease file is created if required. Ususally this is handeled by the isc-dhcp-server init script but we use our own path (for persistance) of that file. --- src/conf_mode/dhcp_server.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index e03a04a4d..3a095b618 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -28,12 +28,16 @@ from vyos.config import Config from vyos import ConfigError config_file = r'/etc/dhcp/dhcpd.conf' +lease_file = r'/config/dhcpd.leases' daemon_config_file = r'/etc/default/isc-dhcp-server' # Please be careful if you edit the template. config_tmpl = """ ### Autogenerated by dhcp_server.py ### +# For options please consult the following website: +# https://www.isc.org/wp-content/uploads/2017/08/dhcp43options.html + log-facility local7; {% if hostfile_update %} @@ -103,7 +107,7 @@ failover peer "{{ subnet.failover_name }}" { {% endif -%} {% endfor %} -# Shared network configrations +# Shared network configration(s) {% for network in shared_network %} {%- if not network.disabled -%} shared-network {{ network.name }} { @@ -206,22 +210,14 @@ daemon_tmpl = """ # sourced by /etc/init.d/isc-dhcp-server -# Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf). DHCPD_CONF=/etc/dhcp/dhcpd.conf - -# Path to dhcpd's PID file (default: /var/run/dhcpd.pid). DHCPD_PID=/var/run/dhcpd.pid - -# Additional options to start dhcpd with. -# Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead -OPTIONS="-lf /config/dhcpd.leases" - -# On what interfaces should the DHCP server (dhcpd) serve DHCP requests? -# Separate multiple interfaces with spaces, e.g. "eth0 eth1". +OPTIONS="-4 -lf {{ lease_file }}" INTERFACES="" """ default_config_data = { + 'lease_file': lease_file, 'disabled': False, 'ddns_enable': False, 'global_parameters': [], @@ -764,6 +760,10 @@ def apply(dhcp): if os.path.exists(daemon_config_file): os.unlink(daemon_config_file) else: + # If our file holding DHCP leases does yet not exist - create it + if not os.path.exists(lease_file): + os.mknod(lease_file) + os.system('sudo systemctl restart isc-dhcp-server.service') return None -- cgit v1.2.3 From 37aeb227c346ee52d4fbed91c79cb59774ceb5ae Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 21:26:09 +0200 Subject: dhcp_server.py: fix KeyError in verify() --- src/conf_mode/dhcp_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 3a095b618..8ca72164a 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -677,7 +677,7 @@ def verify(dhcp): if (len(subnet['range']) == 0): for mapping in subnet['static_mapping']: # we need at least one active mapping - if (not active_mapping) and (not mapping['disable']): + if (not active_mapping) and (not mapping['disabled']): active_mapping = True else: active_mapping = True -- cgit v1.2.3 From f5311c1732009f927ea1b47966e4a296d62b2ce8 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 22:28:29 +0200 Subject: dhcp_server.py: check if AF_INET address is configure before using it in verify() --- src/conf_mode/dhcp_server.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 8ca72164a..556545e6d 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -703,10 +703,13 @@ def verify(dhcp): # There must be one subnet connected to a real interface # for interface in netifaces.interfaces(): - # Retrieve IP address of network interface - ip = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr'] - if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet['network']): - listen_ok = True + # Test if IPv4 addresses are configured at all + if netifaces.AF_INET in netifaces.ifaddresses(interface).keys(): + # An interface can have multiple addresses, but ISC DHCP only supports the first :( + ip = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr'] + if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet['network']): + # Bingo! At least one subnet connected to an interface on this machine + listen_ok = True # # Subnets must be non overlapping -- cgit v1.2.3 From eb5a1be8845e75c115910d96b1bc468c872ce705 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 22:33:40 +0200 Subject: dhcp_server.py: beautify error messages generated in verify() --- src/conf_mode/dhcp_server.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 556545e6d..0f92985a9 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -602,12 +602,14 @@ def verify(dhcp): # If DHCP is enabled we need one share-network if len(dhcp['shared_network']) == 0: - raise ConfigError('No DHCP shared networks configured. At least one DHCP shared network must be configured.') + raise ConfigError('No DHCP shared networks configured.\n' + 'At least one DHCP shared network must be configured.') # A shared-network requires a subnet definition for network in dhcp['shared_network']: if len(network['subnet']) == 0: - raise ConfigError('No DHCP lease subnets configured for "{0}". At least one DHCP lease subnet must be configured for each shared network.'.format(network['name'])) + raise ConfigError('No DHCP lease subnets configured for "{0}". At least one\n' + 'lease subnet must be configured for each shared network.'.format(network['name'])) # Inspect our subnet configuration failover_names = [] @@ -633,7 +635,7 @@ def verify(dhcp): # Failover requires start/stop ranges for pool if (len(subnet['range']) == 0): - raise ConfigError('Atleast one start-stop range must be configured for $subnet to set up DHCP failover.') + raise ConfigError('Atleast one start-stop range must be configured for "{0}" to set up DHCP failover.'.format(subnet['network'])) # Check if DHCP address range is inside configured subnet declaration range_start = [] @@ -730,7 +732,9 @@ def verify(dhcp): raise ConfigError('Conflicting subnet ranges: "{0}" overlaps "{1}"'.format(net, net2)) if not listen_ok: - raise ConfigError('None of the DHCP lease subnets are inside any configured subnet on broadcast interfaces. At least one lease subnet must be set such that DHCP server listens on a one broadcast interface') + raise ConfigError('None of the DHCP lease subnets are inside any configured subnet on\n' \ + 'broadcast interfaces. At least one lease subnet must be set such that\n' \ + 'DHCP server listens on a one broadcast interface') return None -- cgit v1.2.3 From 9f79403a1d2d1d3f0069b7525e7d70421ac4929f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 23:30:22 +0200 Subject: tftp_server.py: switch to new IP address validators Commit a30dac7c2 ("vyos package: add IP address validators") added system wide Python validators for IP addresses. Remove duplicated code and switch to a single source. --- src/conf_mode/tftp_server.py | 35 ++++++++++------------------------- 1 file changed, 10 insertions(+), 25 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index 2b4732190..b6cf5c09e 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -22,8 +22,7 @@ import stat import pwd import jinja2 -import ipaddress -import netifaces +import vyos.validate from vyos.config import Config from vyos import ConfigError @@ -47,6 +46,7 @@ TFTP_ADDRESS="{% for a in listen_ipv6 -%}[{{ a }}]:{{ port }}{{- " --address " i {%- endif %} TFTP_OPTIONS="--secure {% if allow_upload %}--create --umask 000{% endif %}" + """ default_config_data = { @@ -57,20 +57,6 @@ default_config_data = { 'listen_ipv6': [] } -# Verify if an IP address is assigned to any interface, IPv4 and IPv6 -def addrok(ipaddr, ipversion): - # For every available interface on this system - for interface in netifaces.interfaces(): - # If it has any IPv4 or IPv6 address (depending on ipversion) configured - if ipversion in netifaces.ifaddresses(interface).keys(): - # For every configured IP address - for addr in netifaces.ifaddresses(interface)[ipversion]: - # Check if it matches to the address requested - if addr['addr'] == ipaddr: - return True - - return False - def get_config(): tftpd = default_config_data conf = Config() @@ -90,10 +76,9 @@ def get_config(): if conf.exists('listen-address'): for addr in conf.return_values('listen-address'): - if (ipaddress.ip_address(addr).version == 4): + if vyos.validate.is_ipv4(addr): tftpd['listen_ipv4'].append(addr) - - if (ipaddress.ip_address(addr).version == 6): + else: tftpd['listen_ipv6'].append(addr) return tftpd @@ -110,13 +95,13 @@ def verify(tftpd): if not (tftpd['listen_ipv4'] or tftpd['listen_ipv6']): raise ConfigError('TFTP server listen address must be configured!') - for address in tftpd['listen_ipv4']: - if not addrok(address, netifaces.AF_INET): - raise ConfigError('TFTP server listen address "{0}" not configured on this system.'.format(address)) + for addr in tftpd['listen_ipv4']: + if not vyos.validate.is_addr_assigned(addr): + raise ConfigError('TFTP server IPv4 listen address "{0}" not configured!'.format(addr)) - for address in tftpd['listen_ipv6']: - if not addrok(address, netifaces.AF_INET6): - raise ConfigError('TFTP server listen address "{0}" not configured on this system.'.format(address)) + for addr in tftpd['listen_ipv6']: + if not vyos.validate.is_addr_assigned(addr): + raise ConfigError('TFTP server IPv6 listen address "{0}" not configured!'.format(addr)) return None -- cgit v1.2.3 From cda75e4029d672a47c86a0c12001d15600a5d29f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 23:30:53 +0200 Subject: T733: snmp.py: switch to new IP address validators Commit a30dac7c2 ("vyos package: add IP address validators") added system wide Python validators for IP addresses. Remove duplicated code and switch to single source. --- src/conf_mode/snmp.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 3b47ffc98..27dac209f 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -24,12 +24,12 @@ import pwd import time import jinja2 -import ipaddress import random import binascii import re import vyos.version +import vyos.validate from vyos.config import Config from vyos import ConfigError @@ -65,7 +65,6 @@ access_config_tmpl = """ {% endfor %} {% endif -%} rwuser {{ vyos_user }} - """ # SNMPS template - be careful if you edit the template. @@ -142,8 +141,10 @@ agentaddress unix:/run/snmpd.socket{% if listen_on %}{% for li in listen_on %},{ {% if communities -%} {% for c in communities %} {% if c.network -%} -{% for network in c.network %} +{% for network in c.network_v4 %} {{ c.authorization }}community {{ c.name }} {{ network }} +{% endfor %} +{% for network in c.network_v6 %} {{ c.authorization }}community6 {{ c.name }} {{ network }} {% endfor %} {% else %} @@ -271,14 +272,19 @@ def get_config(): community = { 'name': name, 'authorization': 'ro', - 'network': [] + 'network_v4': [], + 'network_v6': [] } if conf.exists('community {0} authorization'.format(name)): community['authorization'] = conf.return_value('community {0} authorization'.format(name)) if conf.exists('community {0} network'.format(name)): - community['network'] = conf.return_values('community {0} network'.format(name)) + for addr in conf.return_values('community {0} network'.format(name)): + if vyos.validate.is_ipv4(addr): + community['network_v4'] = addr + else: + community['network_v6'] = addr snmp['communities'].append(community) @@ -295,14 +301,12 @@ def get_config(): if conf.exists('listen-address {0} port'.format(addr)): port = conf.return_value('listen-address {0} port'.format(addr)) - if ipaddress.ip_address(addr).version == 4: + if vyos.validate.is_ipv4(addr): # udp:127.0.0.1:161 listen = 'udp:' + addr + ':' + port - elif ipaddress.ip_address(addr).version == 6: + else: # udp6:[::1]:161 listen = 'udp6:' + '[' + addr + ']' + ':' + port - else: - raise ConfigError('Invalid IP address version') snmp['listen_on'].append(listen) -- cgit v1.2.3 From e30cb007618502fdb25f626ef95b486aa0fd6553 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 23:31:01 +0200 Subject: snmp.py: only write 'oldEngineID' to config if v3 is enabled --- src/conf_mode/snmp.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/conf_mode') diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 27dac209f..b98741913 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -84,7 +84,9 @@ usmUser 1 3 {{ u.engineID }} "{{ u.name }}" "{{ u.name }}" NULL {{ u.authOID }} {% endif %} createUser {{ vyos_user }} MD5 "{{ vyos_user_pass }}" DES +{% if v3_engineid %} oldEngineID {{ v3_engineid }} +{%- endif -%} """ # SNMPS template - be careful if you edit the template. -- cgit v1.2.3 From 0264acd21c3c67c998dca3a15b1d4363da12147a Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Thu, 30 Aug 2018 21:28:06 +0200 Subject: T813: fix the check for duplicate VRIDs on the same interface (patch by Watcher7). --- src/conf_mode/vrrp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/vrrp.py b/src/conf_mode/vrrp.py index d21e3ef40..28633f1b4 100755 --- a/src/conf_mode/vrrp.py +++ b/src/conf_mode/vrrp.py @@ -273,7 +273,7 @@ def verify(data): count = len(_groups) - 1 index = 0 while (index < count): - if _groups[index]["vrid"] == _groups[index + 1]["vrid"]: + if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]): raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format( _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"])) else: -- cgit v1.2.3 From c16a8fcb9dca029a233ca9365ad7791b1df495f1 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 30 Aug 2018 22:04:55 +0200 Subject: dhcp_server.py: rework verify() error messages/error checking Commit 067a6b1524 ("vyos: package: extend validator by is_subnet_connected()") added a mechanism to probe if a given IPv4/IPv6 address is connected to any interface on the subnet - or is part of this subnet. We now use this call instead of producing more and more biler-plate code! --- src/conf_mode/dhcp_server.py | 90 +++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 39 deletions(-) (limited to 'src/conf_mode') diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index 0f92985a9..1458ed1d0 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -22,7 +22,8 @@ import ipaddress import jinja2 import socket import struct -import netifaces + +import vyos.validate from vyos.config import Config from vyos import ConfigError @@ -602,13 +603,13 @@ def verify(dhcp): # If DHCP is enabled we need one share-network if len(dhcp['shared_network']) == 0: - raise ConfigError('No DHCP shared networks configured.\n' + raise ConfigError('No DHCP shared networks configured.\n' \ 'At least one DHCP shared network must be configured.') # A shared-network requires a subnet definition for network in dhcp['shared_network']: if len(network['subnet']) == 0: - raise ConfigError('No DHCP lease subnets configured for "{0}". At least one\n' + raise ConfigError('No DHCP lease subnets configured for {0}. At least one\n' \ 'lease subnet must be configured for each shared network.'.format(network['name'])) # Inspect our subnet configuration @@ -620,59 +621,71 @@ def verify(dhcp): # Subnet static route declaration requires destination and router if subnet['static_subnet'] or subnet['static_router']: if not (subnet['static_subnet'] and subnet['static_router']): - raise ConfigError('Please specify the missing DHCP static-route parameter: destination-subnet | router') + raise ConfigError('Please specify missing DHCP static-route parameter(s):\n' \ + 'destination-subnet | router') # Failover requires all 4 parameters set if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']: if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']): - raise ConfigError('Please set one or more of the missing DHCP failover parameters: local-address | peer-address | name | status') + raise ConfigError('Please specify missing DHCP failover parameter(s):\n' \ + 'local-address | peer-address | name | status') # Failover names must be uniquie if subnet['failover_name'] in failover_names: - raise ConfigError('Failover names should be unique: "{0}" has already been configured.'.format(subnet['failover_name'])) + raise ConfigError('Failover names must be unique:\n' \ + '{0} has already been configured!'.format(subnet['failover_name'])) else: failover_names.append(subnet['failover_name']) # Failover requires start/stop ranges for pool if (len(subnet['range']) == 0): - raise ConfigError('Atleast one start-stop range must be configured for "{0}" to set up DHCP failover.'.format(subnet['network'])) + raise ConfigError('At least one start-stop range must be configured for {0}\n' \ + 'to set up DHCP failover!'.format(subnet['network'])) # Check if DHCP address range is inside configured subnet declaration range_start = [] range_stop = [] for range in subnet['range']: + start = range['start'] + stop = range['stop'] # DHCP stop IP required after start IP - if range['start'] and not range['stop']: - raise ConfigError('DHCP range stop IP not defined for range start IP "{0}".'.format(range['start'])) + if start and not stop: + raise ConfigError('Stop IP address in DHCP range for start {0} is not defined!'.format(start)) # Start address must be inside network - if not ipaddress.ip_address(range['start']) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCP range start IP "{0}" is not in subnet "{1}" specified in network "{2}."'.format(range['start'], subnet['network'], network['name'])) + if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']): + raise ConfigError('Start IP address {0} of DHCP range is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(start, subnet['network'], network['name'])) # Stop address must be inside network - if not ipaddress.ip_address(range['stop']) in ipaddress.ip_network(subnet['network']): - raise ConfigError('DHCP range stop IP "{0}" is not in subnet "{1}" specified in network "{2}."'.format(range['stop'], subnet['network'], network['name'])) + if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']): + raise ConfigError('Stop IP address {0} of DHCP range is not in subnet {1}\n' \ + 'specified for shared network {2}!'.format(stop, subnet['network'], network['name'])) # Stop address must be greater or equal to start address - if not ipaddress.ip_address(range['stop']) >= ipaddress.ip_address(range['start']): - raise ConfigError('DHCP range stop IP "{0}" should be an address greater or equal to the start address "{1}".'.format(range['stop'], range['start'])) + if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start): + raise ConfigError('Stop IP address {0} of DHCP range should be greater or equal\n' \ + 'to the start IP address {1} of this range!'.format(stop, start)) # Range start address must be unique - if range['start'] in range_start: - raise ConfigError('Conflicting DHCP lease ranges: Pool start address "{0}" defined multipe times'.format(range['start'])) + if start in range_start: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool start IP address {0} defined multipe times!'.format(range['start'])) else: - range_start.append(range['start']) + range_start.append(start) # Range stop address must be unique - if range['stop'] in range_stop: - raise ConfigError('Conflicting DHCP lease ranges: Pool stop address "{0}" defined multipe times'.format(range['stop'])) + if stop in range_stop: + raise ConfigError('Conflicting DHCP lease range:\n' \ + 'Pool stop IP address {0} defined multipe times!'.format(range['stop'])) else: - range_stop.append(range['stop']) + range_stop.append(stop) # Exclude addresses must be in bound for exclude in subnet['exclude']: if not ipaddress.ip_address(exclude) in ipaddress.ip_network(subnet['network']): - raise ConfigError('Exclude IP "{0}" is outside of the DHCP lease network "{1}" under shared network "{2}".'.format(exclude, subnet['network'], network['name'])) + raise ConfigError('Exclude IP address {0} is outside of the DHCP lease network {1}\n' \ + 'under shared network {2}!'.format(exclude, subnet['network'], network['name'])) # At least one DHCP address range or static-mapping required active_mapping = False @@ -685,39 +698,38 @@ def verify(dhcp): active_mapping = True if not active_mapping: - raise ConfigError('No DHCP address range or active static-mapping set for subnet "{0}".'.format(subnet['network'])) + raise ConfigError('No DHCP address range or active static-mapping set\n' \ + 'for subnet {0}!'.format(subnet['network'])) # Static IP address mappings require both an IP address and MAC address for mapping in subnet['static_mapping']: # Static IP address must be configured if not mapping['ip_address']: - raise ConfigError('No static lease IP address specified for static mapping "{0}" under shared network name "{1}".'.format(mapping['name'], network['name'])) + raise ConfigError('No static lease IP address specified for static mapping {0}\n' \ + 'under shared network name {1}!'.format(mapping['name'], network['name'])) # Static IP address must be in bound if not ipaddress.ip_address(mapping['ip_address']) in ipaddress.ip_network(subnet['network']): - raise ConfigError('Static DHCP lease IP "{0}" under static mapping "{1}" in shared network "{2}" is outside DHCP lease network "{3}".'.format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'], )) + raise ConfigError('Static DHCP lease IP address {0} under static mapping {1}\n' \ + 'in shared network {2} is outside DHCP lease network {3}!' \ + .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network'])) # Static mapping requires MAC address if not mapping['mac_address']: - raise ConfigError('No static lease MAC address specified for static mapping "{0}" under shared network name "{1}".'.format(mapping['name'], network['name'])) + raise ConfigError('No static lease MAC address specified for static mapping\n' \ + '{0} under shared network name {1}!'.format(mapping['name'], network['name'])) - # - # There must be one subnet connected to a real interface - # - for interface in netifaces.interfaces(): - # Test if IPv4 addresses are configured at all - if netifaces.AF_INET in netifaces.ifaddresses(interface).keys(): - # An interface can have multiple addresses, but ISC DHCP only supports the first :( - ip = netifaces.ifaddresses(interface)[netifaces.AF_INET][0]['addr'] - if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet['network']): - # Bingo! At least one subnet connected to an interface on this machine - listen_ok = True + # There must be one subnet connected to a listen interface. + # This only counts if the network itself is not disabled! + if not network['disabled']: + if vyos.validate.is_subnet_connected(subnet['network'], primary=True): + listen_ok = True # # Subnets must be non overlapping # if subnet['network'] in subnets: - raise ConfigError('Subnets must be unique! "{0}" defined multiple times.'.format(subnet)) + raise ConfigError('Subnets must be unique! Subnet {0} defined multiple times!'.format(subnet)) else: subnets.append(subnet['network']) @@ -729,7 +741,7 @@ def verify(dhcp): net2 = ipaddress.ip_network(n) if (net.compare_networks(net2) != 0): if net.overlaps(net2): - raise ConfigError('Conflicting subnet ranges: "{0}" overlaps "{1}"'.format(net, net2)) + raise ConfigError('Conflicting subnet ranges: {0} overlaps with {1}'.format(net, net2)) if not listen_ok: raise ConfigError('None of the DHCP lease subnets are inside any configured subnet on\n' \ -- cgit v1.2.3