summaryrefslogtreecommitdiff
path: root/src/conf_mode/dhcp_server.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/dhcp_server.py')
-rwxr-xr-xsrc/conf_mode/dhcp_server.py290
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("&quot;",'"')
@@ -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)