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. --- interface-definitions/dhcp-server.xml | 456 ++++++++++++++++++ src/conf_mode/dhcp_server.py | 773 +++++++++++++++++++++++++++++++ src/migration-scripts/dhcp-server/4-to-5 | 121 +++++ src/system/on-dhcp-event.sh | 98 ++++ 4 files changed, 1448 insertions(+) create mode 100644 interface-definitions/dhcp-server.xml create mode 100755 src/conf_mode/dhcp_server.py create mode 100755 src/migration-scripts/dhcp-server/4-to-5 create mode 100755 src/system/on-dhcp-event.sh diff --git a/interface-definitions/dhcp-server.xml b/interface-definitions/dhcp-server.xml new file mode 100644 index 000000000..0fcb79454 --- /dev/null +++ b/interface-definitions/dhcp-server.xml @@ -0,0 +1,456 @@ + + + + + + + + Dynamic Host Configuration Protocol (DHCP) for DHCP server + 911 + + + + + Option to disable DHCP server + + + + + + DHCP server to dynamically update the Domain Name System (DNS) + + + + + + Additional global parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors. + + + + + + Enable DHCP server updating /etc/hosts (per client lease) + + + + + + DHCP shared network name [REQUIRED] + + ^[-_a-zA-Z0-9.]+$ + + Invalid DHCP pool name + + + + + Option to make DHCP server authoritative for this physical network + + + + + + Shared-network-name description + + + + + Option to disable DHCP configuration for shared-network + + + + + + Additional shared-network parameters for DHCP server. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors. + + + + + + DHCP subnet for shared network + + ipv4net + IPv4 address and prefix length + + + + + + + + + Bootstrap file name + + + + + Server (IP address or domain name) from which the initial + boot file is to be loaded + + + + + Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used. + + 0-32 + DHCP client prefix length must be 0 to 32 + + + + + DHCP client prefix length must be 0 to 32 + + + + + IP address of default router + + ipv4 + Default router IPv4 address + + + + + + + + + DNS server IPv4 address + + ipv4 + DNS server IPv4 address + + + + + + + + + + Client domain name + + + + + Client domain search + + + + + + IP address that needs to be excluded from DHCP lease range + + ipv4 + IPv4 address to exclude from lease range + + + + + + + + + + DHCP failover parameters + + + + + IP address for failover peer to connect [REQUIRED] + + ipv4 + IPv4 address to exclude from lease range + + + + + + + + + DHCP failover peer name [REQUIRED] + + ^[-_a-zA-Z0-9.]+$ + + Invalid failover peer name + + + + + IP address of failover peer [REQUIRED] + + ipv4 + IPv4 address to exclude from lease range + + + + + + + + + DHCP failover peer status (primary|secondary) [REQUIRED] + + primary secondary + + + (primary|secondary) + + Invalid DHCP failover peer status + + + + + + + Enable IP forwarding on client + + + + + + Lease timeout in seconds (default: 86400) + + 0-2592000 + DHCP lease time must be 0 to 2592000 (30 days) + + + + + DHCP lease time must be 0 to 2592000 + + + + + IP address of NTP server + + ipv4 + NTP server IPv4 address + + + + + + + + + + IP address of POP3 server + + ipv4 + POP3 server IPv4 address + + + + + + + + + + Address for DHCP server identifier + + ipv4 + DHCP server identifier IPv4 address + + + + + + + + + IP address of SMTP server + + ipv4 + SMTP server IPv4 address + + + + + + + + + + DHCP lease range + + ^[-_a-zA-Z0-9.]+$ + + Invalid DHCP lease range name + + + + + First IP address for DHCP lease range + + ipv4 + IPv4 start address of pool + + + + + + + + + Last IP address for DHCP lease range + + ipv4 + IPv4 end address of pool + + + + + + + + + + + Static mapping for specified address type + + ^[-_a-zA-Z0-9.]+$ + + Invalid static-mapping name + + + + + Option to disable static-mapping + + + + + + Static mapping for specified IP address [REQUIRED] + + ipv4 + IPv4 address used in static mapping + + + + + + + + + Static mapping for specified MAC address [REQUIRED] + + h:h:h:h:h:h + MAC address used in static mapping [REQUIRED] + + + + + + Additional static-mapping parameters for DHCP server. + You must use the syntax of dhcpd.conf in this text-field. + Using this without proper knowledge may result in a crashed + DHCP server. Check system log to look for errors. + + + + + + + + Classless static route + + + + + Destination subnet [REQUIRED] + + ipv4net + IPv4 address and prefix length + + + + + + + + + IP address of router to be used to reach the destination subnet [REQUIRED] + + ipv4 + IPv4 address of router + + + + + + + + + + + Additional subnet parameters for DHCP server. You must + use the syntax of dhcpd.conf in this text-field. Using this + without proper knowledge may result in a crashed DHCP server. + Check system log to look for errors. + + + + + + TFTP server name + + + + + Offset of the client's subnet in seconds from Coordinated Universal Time (UTC) + + ^-?[0-9]+$ + + Invalid time offset valuee + + + + + IP address of time server + + ipv4 + Time server IPv4 address + + + + + + + + + + IP address for Windows Internet Name Service (WINS) server + + ipv4 + WINS server IPv4 address + + + + + + + + + + Web Proxy Autodiscovery (WPAD) URL + + + + + + + + + + + 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) diff --git a/src/migration-scripts/dhcp-server/4-to-5 b/src/migration-scripts/dhcp-server/4-to-5 new file mode 100755 index 000000000..8b973d608 --- /dev/null +++ b/src/migration-scripts/dhcp-server/4-to-5 @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 + +# Removes boolean operator from: +# - "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)" + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +if not config.exists(['service', 'dhcp-server']): + # Nothing to do + sys.exit(0) +else: + base = ['service', 'dhcp-server'] + # Make node "set service dhcp-server dynamic-dns-update enable (true|false)" valueless + if config.exists(base + ['dynamic-dns-update']): + bool_val = config.return_value(base + ['dynamic-dns-update', 'enable']) + + # Delete the node with the old syntax + config.delete(base + ['dynamic-dns-update']) + if str(bool_val) == 'true': + # Enable dynamic-dns-update with new syntax + config.set(base + ['dynamic-dns-update'], value=None) + + # Make node "set service dhcp-server disabled (true|false)" valueless + if config.exists(base + ['disabled']): + bool_val = config.return_value(base + ['disabled']) + + # Delete the node with the old syntax + config.delete(base + ['disabled']) + if str(bool_val) == 'true': + # Now disable DHCP server with the new syntax + config.set(base + ['disable'], value=None) + + # Make node "set service dhcp-server hostfile-update (enable|disable) valueless + if config.exists(base + ['hostfile-update']): + bool_val = config.return_value(base + ['hostfile-update']) + + # Delete the node with the old syntax incl. all subnodes + config.delete(base + ['hostfile-update']) + if str(bool_val) == 'enable': + # Enable hostfile update with new syntax + config.set(base + ['hostfile-update'], value=None) + + # Run this for every instance if 'shared-network-name' + for network in config.list_nodes(base + ['shared-network-name']): + base_network = base + ['shared-network-name', network] + # format as tag node to avoid loading problems + config.set_tag(base + ['shared-network-name']) + + # Run this for every specified 'subnet' + for subnet in config.list_nodes(base_network + ['subnet']): + base_subnet = base_network + ['subnet', subnet] + # format as tag node to avoid loading problems + config.set_tag(base_network + ['subnet']) + + # Make node "set service dhcp-server shared-network-name subnet 172.31.0.0/24 ip-forwarding enable" valueless + if config.exists(base_subnet + ['ip-forwarding', 'enable']): + bool_val = config.return_value(base_subnet + ['ip-forwarding', 'enable']) + # Delete the node with the old syntax + config.delete(base_subnet + ['ip-forwarding']) + if str(bool_val) == 'true': + # Recreate node with new syntax + config.set(base_subnet + ['ip-forwarding'], value=None) + + # Rename node "set service dhcp-server shared-network-name subnet 172.31.0.0/24 start <172.16.0.4> stop <172.16.0.9> + if config.exists(base_subnet + ['start']): + # This is the new "range" id for DHCP lease ranges + r_id = 0 + for range in config.list_nodes(base_subnet + ['start']): + range_start = range + range_stop = config.return_value(base_subnet + ['start', range_start, 'stop']) + + # Delete the node with the old syntax + config.delete(base_subnet + ['start', range_start]) + + # Create the node for the new syntax + # Note: range is a tag node, counter is its child, not a value + config.set(base_subnet + ['range', r_id]) + config.set(base_subnet + ['range', r_id, 'start'], value=range_start) + config.set(base_subnet + ['range', r_id, 'stop'], value=range_stop) + + # format as tag node to avoid loading problems + config.set_tag(base_subnet + ['range']) + + # increment range id for possible next range definition + r_id += 1 + + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'subnet', subnet, 'start']) + + + # Make node "set service dhcp-server shared-network-name authoritative" valueless + if config.exists(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']): + bool_val = config.return_value(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + # Delete the node with the old syntax + config.delete(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + if str(bool_val) == 'true': + # Recreate node with new syntax + config.set(['service', 'dhcp-server', 'shared-network-name', network, 'authoritative']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + + sys.exit(1) diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh new file mode 100755 index 000000000..d671bffd6 --- /dev/null +++ b/src/system/on-dhcp-event.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# This script came from ubnt.com forum user "bradd" in the following post +# http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311 +# It has been modified by Ubiquiti to update the /etc/host file +# instead of adding to the CLI. +# Thanks to forum user "itsmarcos" for bug fix & improvements +# Thanks to forum user "ruudboon" for multiple domain fix +# Thanks to forum user "chibby85" for expire patch and static-mapping + +if [ $# -lt 5 ]; then + echo Invalid args + logger -s -t on-dhcp-event "Invalid args \"$@\"" + exit 1 +fi + +action=$1 +client_name=$2 +client_ip=$3 +client_mac=$4 +domain=$5 +file=/etc/hosts +changes=0 + +if [ "$domain" == "..YYZ!" ]; then + client_fqdn_name=$client_name + client_search_expr=$client_name +else + client_fqdn_name=$client_name.$domain + client_search_expr="$client_name\\.$domain" +fi + +case "$action" in + commit) # add mapping for new lease + echo "- new lease event, setting static mapping for host "\ + "$client_fqdn_name (MAC=$client_mac, IP=$client_ip)" + # + # grep fails miserably with \t in the search expression. + # In the following line one is used after $client_search_expr + # followed by a single space + grep -q " $client_search_expr #on-dhcp-event " $file + if [ $? == 0 ]; then + echo pattern found, removing + wc1=`cat $file | wc -l` + sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file + wc2=`cat $file | wc -l` + if [ "$wc1" -eq "$wc2" ]; then + echo No change + fi + else + echo pattern NOT found + fi + + # check if hostname already exists (e.g. a static host mapping) + # if so don't overwrite + grep -q " $client_search_expr " $file + if [ $? == 0 ]; then + echo host $client_fqdn_name already exists, exiting + exit 1 + fi + + line="$client_ip\t $client_fqdn_name\t #on-dhcp-event $client_mac" + sudo sh -c "echo -e '$line' >> $file" + ((changes++)) + echo Entry was added + ;; + + release) # delete mapping for released address + echo "- lease release event, deleting static mapping for host $client_fqdn_name" + wc1=`cat $file | wc -l` + sudo sed -i "/ $client_search_expr\t #on-dhcp-event /d" $file + wc2=`cat $file | wc -l` + if [ "$wc1" -eq "$wc2" ]; then + echo No change + else + echo Entry was removed + ((changes++)) + fi + ;; + + *) + logger -s -t on-dhcp-event "Invalid command \"$1\"" + exit 1; + ;; +esac + +if [ $changes -gt 0 ]; then + echo Success + pid=`pgrep pdns_recursor` + if [ -n "$pid" ]; then + sudo rec_control reload-zones + fi +else + echo No changes made +fi +exit 0 + + -- 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(-) 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(-) 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(-) 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(-) 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 a0406e87389ad9ff9a045383d50d2bce49179382 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Tue, 28 Aug 2018 23:03:10 +0200 Subject: bcast-relay.xml: move priority from tagNode to base node --- interface-definitions/bcast-relay.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface-definitions/bcast-relay.xml b/interface-definitions/bcast-relay.xml index fdba554db..96ce16639 100644 --- a/interface-definitions/bcast-relay.xml +++ b/interface-definitions/bcast-relay.xml @@ -6,6 +6,7 @@ UDP broadcast relay service + 990 @@ -21,7 +22,6 @@ 1-99 Numerical ID # - 990 -- 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(-) 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(-) 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(-) 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(-) 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 a30dac7c24549f9ee32a484b74af049607ffd42b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 29 Aug 2018 22:40:10 +0200 Subject: vyos package: add IP address validators * is_addr_assigned(addr) - Test if address is assigned to ANY interface on the system * is_ipv4(addr) - Test if it is an IPv4 address, both network and host * is_ipv6(addr) - Test if it is an IPv6 address, both network and host --- python/vyos/validate.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 python/vyos/validate.py diff --git a/python/vyos/validate.py b/python/vyos/validate.py new file mode 100644 index 000000000..549812371 --- /dev/null +++ b/python/vyos/validate.py @@ -0,0 +1,63 @@ +# Copyright 2018 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import netifaces +import ipaddress + +def is_ipv4(addr): + """ + Check addr if it is an IPv4 address/network. + + Return True/False + """ + if ipaddress.ip_network(addr).version == 4: + return True + else: + return False + +def is_ipv6(addr): + """ + Check addr if it is an IPv6 address/network. + + Return True/False + """ + if ipaddress.ip_network(addr).version == 6: + return True + else: + return False + +def is_addr_assigned(addr): + """ + Verify if the given IPv4/IPv6 address is assigned to any interface on this system + + Return True/False + """ + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET + if is_ipv6(addr): + addr_type = netifaces.AF_INET6 + + for interface in netifaces.interfaces(): + # check if the requested address type is configured at all + if addr_type in netifaces.ifaddresses(interface).keys(): + # Check every IP address on this interface for a match + for ip in netifaces.ifaddresses(interface)[addr_type]: + # Check if it matches to the address requested + if ip['addr'] == addr: + return True + + return False + -- 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(-) 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(-) 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(+) 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(-) 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 b8baf2191062b35ce78f87fdd7e9b922f57d7d70 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 30 Aug 2018 22:03:28 +0200 Subject: vyos: package: extend validator by is_subnet_connected() Verify given IPv4/IPv6 subnet is connected to any interface on this system. Required by e.g. DHCP server that we have for IPv4 and IPv6. --- python/vyos/validate.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 549812371..b681edc5e 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -40,7 +40,8 @@ def is_ipv6(addr): def is_addr_assigned(addr): """ - Verify if the given IPv4/IPv6 address is assigned to any interface on this system + Verify if the given IPv4/IPv6 address is assigned to any interface on this + system. Return True/False """ @@ -61,3 +62,38 @@ def is_addr_assigned(addr): return False +def is_subnet_connected(subnet, primary=False): + """ + Verify is the given IPv4/IPv6 subnet is connected to any interface on this + system. + + primary check if the subnet is reachable via the primary IP address of this + interface. E.g. ISC DHCP can only listen on primary addresses. + + Return True/False + """ + + # determine IP version (AF_INET or AF_INET6) depending on passed address + addr_type = netifaces.AF_INET + if is_ipv6(subnet): + addr_type = netifaces.AF_INET6 + + for interface in netifaces.interfaces(): + # check if the requested address type is configured at all + if addr_type not in netifaces.ifaddresses(interface).keys(): + return False + + # An interface can have multiple addresses, but some software components + # only support the primary address :( + if primary: + ip = netifaces.ifaddresses(interface)[addr_type][0]['addr'] + if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet): + return True + else: + # Check every assigned IP address if it is connected to the subnet + # in question + for ip in netifaces.ifaddresses(interface)[addr_type]: + if ipaddress.ip_address(ip['addr']) in ipaddress.ip_network(subnet): + return True + + return False -- 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(-) 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