diff options
Diffstat (limited to 'src/conf_mode/dhcp_server.py')
-rwxr-xr-x | src/conf_mode/dhcp_server.py | 290 |
1 files changed, 45 insertions, 245 deletions
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py index bf86e484b..553247b88 100755 --- a/src/conf_mode/dhcp_server.py +++ b/src/conf_mode/dhcp_server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2019 VyOS maintainers and contributors +# Copyright (C) 2018-2020 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 @@ -14,232 +14,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os -import jinja2 -import socket -import struct - -import vyos.validate from ipaddress import ip_address, ip_network +from jinja2 import FileSystemLoader, Environment +from socket import inet_ntoa +from struct import pack +from sys import exit + from vyos.config import Config +from vyos.defaults import directories as vyos_data_dir +from vyos.validate import is_subnet_connected from vyos import ConfigError +from vyos.util import call + config_file = r'/etc/dhcp/dhcpd.conf' lease_file = r'/config/dhcpd.leases' pid_file = r'/var/run/dhcpd.pid' daemon_config_file = r'/etc/default/isc-dhcpv4-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 %} -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 %} -{%- if host_decl_name %} -use-host-decl-names on; -{%- 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 wpad -%} -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.disabled -%} -{%- 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 configration(s) -{% for network in shared_network %} -{%- if not network.disabled -%} -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.subnet_parameters %} - # The following {{ subnet.subnet_parameters | length }} line(s) were added as subnet-parameters in the CLI and have not been validated - {%- for param in subnet.subnet_parameters %} - {{ param }} - {%- endfor -%} - {%- 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.disabled -%} - host {% if host_decl_name -%} {{ host.name }} {%- else -%} {{ network.name }}_{{ host.name }} {%- endif %} { - {%- if host.ip_address %} - fixed-address {{ host.ip_address }}; - {%- endif %} - 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 %} - {%- if subnet.failover_name %} - pool { - failover peer "{{ subnet.failover_name }}"; - deny dynamic bootp clients; - {%- for range in subnet.range %} - range {{ range.start }} {{ range.stop }}; - {%- endfor %} - } - {%- else %} - {%- for range in subnet.range %} - range {{ range.start }} {{ range.stop }}; - {%- endfor %} - {%- endif %} - } - {%- endfor %} - on commit { - set shared-networkname = "{{ network.name }}"; - {% if hostfile_update -%} - 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); - {%- endif %} - } -} -{%- endif %} -{% endfor %} -""" - -daemon_tmpl = """ -### Autogenerated by dhcp_server.py ### - -# sourced by /etc/init.d/isc-dhcpv4-server - -DHCPD_CONF={{ config_file }} -DHCPD_PID={{ pid_file }} -OPTIONS="-4 -lf {{ lease_file }}" -INTERFACES="" -""" - default_config_data = { 'lease_file': lease_file, 'disabled': False, @@ -315,6 +109,26 @@ def dhcp_slice_range(exclude_list, range_list): return output +def dhcp_static_route(static_subnet, static_router): + # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server + # Option format is: + # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> + # where bytes with the value 0 are omitted. + net = ip_network(static_subnet) + # add netmask + string = str(net.prefixlen) + ',' + # add network bytes + if net.prefixlen: + width = net.prefixlen // 8 + if net.prefixlen % 8: + width += 1 + string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ',' + + # add router bytes + string += ','.join(static_router.split('.')) + + return string + def get_config(): dhcp = default_config_data conf = Config() @@ -395,6 +209,7 @@ def get_config(): 'bootfile_server': '', 'client_prefix_length': '', 'default_router': '', + 'rfc3442_default_router': '', 'dns_server': [], 'domain_name': '', 'domain_search': [], @@ -438,11 +253,12 @@ def get_config(): 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))) + subnet['client_prefix_length'] = inet_ntoa(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') + subnet['rfc3442_default_router'] = dhcp_static_route("0.0.0.0/0", subnet['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. @@ -586,27 +402,7 @@ def get_config(): 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: - # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> - # where bytes with the value 0 are omitted. - net = 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: - string += b - if b is not bytes[-1]: - string += ',' - - subnet['static_route'] = string + subnet['static_route'] = dhcp_static_route(subnet['static_subnet'], subnet['static_router']) # HACKS AND TRICKS # @@ -776,7 +572,7 @@ def verify(dhcp): # 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): + if is_subnet_connected(subnet['network'], primary=True): listen_ok = True # Subnets must be non overlapping @@ -808,9 +604,13 @@ def generate(dhcp): print('Warning: DHCP server will be deactivated because it is disabled') return None - tmpl = jinja2.Template(config_tmpl) - config_text = tmpl.render(dhcp) + # Prepare Jinja2 template loader from files + tmpl_path = os.path.join(vyos_data_dir['data'], 'templates', 'dhcp-server') + fs_loader = FileSystemLoader(tmpl_path) + env = Environment(loader=fs_loader) + tmpl = env.get_template('dhcpd.conf.tmpl') + config_text = tmpl.render(dhcp) # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters # we can pass to ISC DHCPd config_text = config_text.replace(""",'"') @@ -818,7 +618,7 @@ def generate(dhcp): with open(config_file, 'w') as f: f.write(config_text) - tmpl = jinja2.Template(daemon_tmpl) + tmpl = env.get_template('daemon.tmpl') config_text = tmpl.render(dhcp) with open(daemon_config_file, 'w') as f: f.write(config_text) @@ -828,7 +628,7 @@ def generate(dhcp): def apply(dhcp): if (dhcp is None) or dhcp['disabled']: # DHCP server is removed in the commit - os.system('sudo systemctl stop isc-dhcpv4-server.service') + call('sudo systemctl stop isc-dhcpv4-server.service') if os.path.exists(config_file): os.unlink(config_file) if os.path.exists(daemon_config_file): @@ -838,7 +638,7 @@ def apply(dhcp): if not os.path.exists(lease_file): os.mknod(lease_file) - os.system('sudo systemctl restart isc-dhcpv4-server.service') + call('sudo systemctl restart isc-dhcpv4-server.service') return None @@ -850,4 +650,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) |