From daffee2cbf001dab13799f5b2b69330162491214 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 7 Jan 2024 09:24:10 +0100
Subject: dhcp: T3316: Move options to separate node and extend scopes
---
interface-definitions/include/dhcp/option-v4.xml.i | 257 ++++++++++++++++++++
.../include/version/dhcp-server-version.xml.i | 2 +-
interface-definitions/service_dhcp-server.xml.in | 258 +--------------------
python/vyos/kea.py | 56 +++--
python/vyos/template.py | 15 +-
smoketest/scripts/cli/test_service_dhcp-server.py | 132 ++++++++---
src/migration-scripts/dhcp-server/8-to-9 | 69 ++++++
7 files changed, 480 insertions(+), 309 deletions(-)
create mode 100644 interface-definitions/include/dhcp/option-v4.xml.i
create mode 100755 src/migration-scripts/dhcp-server/8-to-9
diff --git a/interface-definitions/include/dhcp/option-v4.xml.i b/interface-definitions/include/dhcp/option-v4.xml.i
new file mode 100644
index 000000000..bd6fc6043
--- /dev/null
+++ b/interface-definitions/include/dhcp/option-v4.xml.i
@@ -0,0 +1,257 @@
+
+
+
+ DHCP option
+
+
+ #include
+ #include
+ #include
+ #include
+ #include
+
+
+ Bootstrap file name
+
+ [[:ascii:]]{1,253}
+
+
+
+
+
+ Server from which the initial boot file is to be loaded
+
+ ipv4
+ Bootfile server IPv4 address
+
+
+ hostname
+ Bootfile server FQDN
+
+
+
+
+
+
+
+
+
+ Bootstrap file size
+
+ u32:1-16
+ Bootstrap file size in 512 byte blocks
+
+
+
+
+
+
+
+
+ Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.
+
+ u32: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
+
+
+
+
+
+
+
+
+ Enable IP forwarding on client
+
+
+
+
+
+ Disable IPv4 on IPv6 only hosts (RFC 8925)
+
+ u32
+ Seconds
+
+
+
+
+ Seconds must be between 0 and 4294967295 (49 days)
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+ Classless static route destination subnet
+
+ ipv4net
+ IPv4 address and prefix length
+
+
+
+
+
+
+
+
+ IP address of router to be used to reach the destination subnet
+
+ ipv4
+ IPv4 address of router
+
+
+
+
+
+
+
+
+
+
+ TFTP server name
+
+ ipv4
+ TFTP server IPv4 address
+
+
+ hostname
+ TFTP server FQDN
+
+
+
+
+
+
+
+
+
+ Client subnet offset in seconds from Coordinated Universal Time (UTC)
+
+ [-]N
+ Time offset (number, may be negative)
+
+
+ -?[0-9]+
+
+ Invalid time offset value
+
+
+
+
+ IP address of time server
+
+ ipv4
+ Time server IPv4 address
+
+
+
+
+
+
+
+
+
+ Time zone to send to clients. Uses RFC4833 options 100 and 101
+
+
+
+
+
+
+
+
+
+
+ Vendor Specific Options
+
+
+
+
+ Ubiquiti specific parameters
+
+
+
+
+ Address of UniFi controller
+
+ ipv4
+ IP address of UniFi controller
+
+
+
+
+
+
+
+
+
+
+
+
+ IP address for Windows Internet Name Service (WINS) server
+
+ ipv4
+ WINS server IPv4 address
+
+
+
+
+
+
+
+
+
+ Web Proxy Autodiscovery (WPAD) URL
+
+
+
+
+
diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i
index cc84ea8b9..d83172e72 100644
--- a/interface-definitions/include/version/dhcp-server-version.xml.i
+++ b/interface-definitions/include/version/dhcp-server-version.xml.i
@@ -1,3 +1,3 @@
-
+
diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in
index 8e13f9372..a5cee62d1 100644
--- a/interface-definitions/service_dhcp-server.xml.in
+++ b/interface-definitions/service_dhcp-server.xml.in
@@ -89,12 +89,9 @@
- #include
- #include
- #include
+ #include
#include
#include
- #include
DHCP subnet for shared network
@@ -108,73 +105,9 @@
Invalid IPv4 subnet definition
-
-
- Bootstrap file name
-
- [[:ascii:]]{1,253}
-
-
-
-
-
- Server from which the initial boot file is to be loaded
-
- ipv4
- Bootfile server IPv4 address
-
-
- hostname
- Bootfile server FQDN
-
-
-
-
-
-
-
-
-
- Bootstrap file size
-
- u32:1-16
- Bootstrap file size in 512 byte blocks
-
-
-
-
-
-
- #include
-
-
- Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.
-
- u32: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
-
-
-
-
-
-
- #include
- #include
+ #include
#include
- #include
+ #include
IP address to exclude from DHCP lease range
@@ -188,12 +121,6 @@
-
-
- Enable IP forwarding on client
-
-
-
Lease timeout in seconds
@@ -208,45 +135,6 @@
86400
- #include
-
-
- 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
@@ -256,6 +144,7 @@
Invalid range name, may only be alphanumeric, dot and hyphen
+ #include
First IP address for DHCP lease range
@@ -291,6 +180,8 @@
Invalid static mapping hostname
+ #include
+ #include
#include
@@ -308,143 +199,6 @@
#include
-
-
- Classless static route destination subnet
-
- ipv4net
- IPv4 address and prefix length
-
-
-
-
-
-
-
-
- IP address of router to be used to reach the destination subnet
-
- ipv4
- IPv4 address of router
-
-
-
-
-
-
-
-
-
-
- Disable IPv4 on IPv6 only hosts (RFC 8925)
-
- u32
- Seconds
-
-
-
-
- Seconds must be between 0 and 4294967295 (49 days)
-
-
-
-
- TFTP server name
-
- ipv4
- TFTP server IPv4 address
-
-
- hostname
- TFTP server FQDN
-
-
-
-
-
-
-
-
-
- Client subnet offset in seconds from Coordinated Universal Time (UTC)
-
- [-]N
- Time offset (number, may be negative)
-
-
- -?[0-9]+
-
- Invalid time offset value
-
-
-
-
- IP address of time server
-
- ipv4
- Time server IPv4 address
-
-
-
-
-
-
-
-
-
- Time zone to send to clients. Uses RFC4833 options 100 and 101
-
-
-
-
-
-
-
-
-
-
- Vendor Specific Options
-
-
-
-
- Ubiquiti specific parameters
-
-
-
-
- Address of UniFi controller
-
- ipv4
- IP address of UniFi controller
-
-
-
-
-
-
-
-
-
-
-
-
- IP address for Windows Internet Name Service (WINS) server
-
- ipv4
- WINS server IPv4 address
-
-
-
-
-
-
-
-
-
- Web Proxy Autodiscovery (WPAD) URL
-
-
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 819fe16a9..2ca73044b 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -92,17 +92,28 @@ def kea_parse_options(config):
options.append({'name': 'pcode', 'data': tz_string})
options.append({'name': 'tcode', 'data': config['time_zone']})
+ unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
+ if unifi_controller:
+ options.append({
+ 'name': 'unifi-controller',
+ 'data': unifi_controller,
+ 'space': 'ubnt'
+ })
+
return options
def kea_parse_subnet(subnet, config):
out = {'subnet': subnet}
- options = kea_parse_options(config)
+ options = []
+
+ if 'option' in config:
+ out['option-data'] = kea_parse_options(config['option'])
- if 'bootfile_name' in config:
- out['boot-file-name'] = config['bootfile_name']
+ if 'bootfile_name' in config['option']:
+ out['boot-file-name'] = config['option']['bootfile_name']
- if 'bootfile_server' in config:
- out['next-server'] = config['bootfile_server']
+ if 'bootfile_server' in config['option']:
+ out['next-server'] = config['option']['bootfile_server']
if 'lease' in config:
out['valid-lifetime'] = int(config['lease'])
@@ -112,7 +123,20 @@ def kea_parse_subnet(subnet, config):
pools = []
for num, range_config in config['range'].items():
start, stop = range_config['start'], range_config['stop']
- pools.append({'pool': f'{start} - {stop}'})
+ pool = {
+ 'pool': f'{start} - {stop}'
+ }
+
+ if 'option' in range_config:
+ pool['option-data'] = kea_parse_options(range_config['option'])
+
+ if 'bootfile_name' in range_config['option']:
+ pool['boot-file-name'] = range_config['option']['bootfile_name']
+
+ if 'bootfile_server' in range_config['option']:
+ pool['next-server'] = range_config['option']['bootfile_server']
+
+ pools.append(pool)
out['pools'] = pools
if 'static_mapping' in config:
@@ -134,19 +158,17 @@ def kea_parse_subnet(subnet, config):
if 'ip_address' in host_config:
reservation['ip-address'] = host_config['ip_address']
- reservations.append(reservation)
- out['reservations'] = reservations
+ if 'option' in host_config:
+ reservation['option-data'] = kea_parse_options(host_config['option'])
- unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller')
- if unifi_controller:
- options.append({
- 'name': 'unifi-controller',
- 'data': unifi_controller,
- 'space': 'ubnt'
- })
+ if 'bootfile_name' in host_config['option']:
+ reservation['boot-file-name'] = host_config['option']['bootfile_name']
- if options:
- out['option-data'] = options
+ if 'bootfile_server' in host_config['option']:
+ reservation['next-server'] = host_config['option']['bootfile_server']
+
+ reservations.append(reservation)
+ out['reservations'] = reservations
return out
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 29ea0889b..c0c09f690 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -842,15 +842,22 @@ def kea_shared_network_json(shared_networks):
'authoritative': ('authoritative' in config),
'subnet4': []
}
- options = kea_parse_options(config)
+
+ if 'option' in config:
+ network['option-data'] = kea_parse_options(config['option'])
+
+ if 'bootfile_name' in config['option']:
+ network['boot-file-name'] = config['option']['bootfile_name']
+
+ if 'bootfile_server' in config['option']:
+ network['next-server'] = config['option']['bootfile_server']
if 'subnet' in config:
for subnet, subnet_config in config['subnet'].items():
+ if 'disable' in subnet_config:
+ continue
network['subnet4'].append(kea_parse_subnet(subnet, subnet_config))
- if options:
- network['option-data'] = options
-
out.append(network)
return dumps(out, indent=4)
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index bf0c09965..99ac406cd 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -97,10 +97,10 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
- self.cli_set(pool + ['default-router', router])
- self.cli_set(pool + ['name-server', dns_1])
- self.cli_set(pool + ['name-server', dns_2])
- self.cli_set(pool + ['domain-name', domain_name])
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
@@ -165,29 +165,26 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
- self.cli_set(pool + ['default-router', router])
- self.cli_set(pool + ['name-server', dns_1])
- self.cli_set(pool + ['name-server', dns_2])
- self.cli_set(pool + ['domain-name', domain_name])
- self.cli_set(pool + ['ip-forwarding'])
- self.cli_set(pool + ['smtp-server', smtp_server])
- self.cli_set(pool + ['pop-server', smtp_server])
- self.cli_set(pool + ['time-server', time_server])
- self.cli_set(pool + ['tftp-server-name', tftp_server])
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
+ self.cli_set(pool + ['option', 'ip-forwarding'])
+ self.cli_set(pool + ['option', 'smtp-server', smtp_server])
+ self.cli_set(pool + ['option', 'pop-server', smtp_server])
+ self.cli_set(pool + ['option', 'time-server', time_server])
+ self.cli_set(pool + ['option', 'tftp-server-name', tftp_server])
for search in search_domains:
- self.cli_set(pool + ['domain-search', search])
- self.cli_set(pool + ['bootfile-name', bootfile_name])
- self.cli_set(pool + ['bootfile-server', bootfile_server])
- self.cli_set(pool + ['wpad-url', wpad])
- self.cli_set(pool + ['server-identifier', server_identifier])
+ self.cli_set(pool + ['option', 'domain-search', search])
+ self.cli_set(pool + ['option', 'bootfile-name', bootfile_name])
+ self.cli_set(pool + ['option', 'bootfile-server', bootfile_server])
+ self.cli_set(pool + ['option', 'wpad-url', wpad])
+ self.cli_set(pool + ['option', 'server-identifier', server_identifier])
- self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
- self.cli_set(pool + ['ipv6-only-preferred', ipv6_only_preferred])
- self.cli_set(pool + ['time-zone', 'Europe/London'])
+ self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
+ self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred])
+ self.cli_set(pool + ['option', 'time-zone', 'Europe/London'])
- # check validate() - No DHCP address range or active static-mapping set
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
@@ -281,16 +278,81 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
+ def test_dhcp_single_pool_options_scoped(self):
+ shared_net_name = 'SMOKE-2'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+
+ range_router = inc_ip(subnet, 5)
+ range_dns_1 = inc_ip(subnet, 6)
+ range_dns_2 = inc_ip(subnet, 7)
+
+ shared_network = base_path + ['shared-network-name', shared_net_name]
+ pool = shared_network + ['subnet', subnet]
+ # we use the first subnet IP address as default gateway
+ self.cli_set(shared_network + ['option', 'default-router', router])
+ self.cli_set(shared_network + ['option', 'name-server', dns_1])
+ self.cli_set(shared_network + ['option', 'name-server', dns_2])
+ self.cli_set(shared_network + ['option', 'domain-name', domain_name])
+
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+ self.cli_set(pool + ['range', '0', 'option', 'default-router', range_router])
+ self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_1])
+ self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_2])
+
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ obj = loads(config)
+
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400)
+
+ # Verify shared-network options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'option-data'],
+ {'name': 'routers', 'data': router})
+
+ # Verify range options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'})
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'],
+ {'name': 'routers', 'data': range_router})
+
+ # Verify pool
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}')
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
+
def test_dhcp_single_pool_static_mapping(self):
shared_net_name = 'SMOKE-2'
domain_name = 'private'
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
- self.cli_set(pool + ['default-router', router])
- self.cli_set(pool + ['name-server', dns_1])
- self.cli_set(pool + ['name-server', dns_2])
- self.cli_set(pool + ['domain-name', domain_name])
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'name-server', dns_2])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
@@ -365,9 +427,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
- self.cli_set(pool + ['default-router', router])
- self.cli_set(pool + ['name-server', dns_1])
- self.cli_set(pool + ['domain-name', domain_name])
+ self.cli_set(pool + ['option', 'default-router', router])
+ self.cli_set(pool + ['option', 'name-server', dns_1])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
self.cli_set(pool + ['lease', lease_time])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
@@ -448,7 +510,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
range_0_stop = inc_ip(subnet, 20)
pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet]
- self.cli_set(pool + ['default-router', router])
+ self.cli_set(pool + ['option', 'default-router', router])
self.cli_set(pool + ['exclude', router])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
@@ -490,7 +552,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
range_0_start_excl = inc_ip(exclude_addr, 1)
pool = base_path + ['shared-network-name', 'EXCLUDE-TEST-2', 'subnet', subnet]
- self.cli_set(pool + ['default-router', router])
+ self.cli_set(pool + ['option', 'default-router', router])
self.cli_set(pool + ['exclude', exclude_addr])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
@@ -535,7 +597,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
range_0_stop = '10.0.250.255'
pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet]
- self.cli_set(pool + ['default-router', relay_router])
+ self.cli_set(pool + ['option', 'default-router', relay_router])
self.cli_set(pool + ['range', '0', 'start', range_0_start])
self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
@@ -572,7 +634,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
- self.cli_set(pool + ['default-router', router])
+ self.cli_set(pool + ['option', 'default-router', router])
# check validate() - No DHCP address range or active static-mapping set
with self.assertRaises(ConfigSessionError):
diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9
new file mode 100755
index 000000000..908420c18
--- /dev/null
+++ b/src/migration-scripts/dhcp-server/8-to-9
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 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 .
+
+# T3316:
+# - Migrate dhcp options under new option node
+
+import sys
+import re
+from vyos.configtree import ConfigTree
+
+if len(sys.argv) < 2:
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['service', 'dhcp-server', 'shared-network-name']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ sys.exit(0)
+
+option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal',
+ 'client-prefix-length', 'default-router', 'domain-name', 'domain-search',
+ 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server',
+ 'pop-server', 'server-identifier', 'smtp-server', 'static-route',
+ 'tftp-server-name', 'time-offset', 'time-server', 'time-zone',
+ 'vendor-option', 'wins-server', 'wpad-url']
+
+for network in config.list_nodes(base):
+ for option in option_nodes:
+ if config.exists(base + [network, option]):
+ config.set(base + [network, 'option'])
+ config.copy(base + [network, option], base + [network, 'option', option])
+ config.delete(base + [network, option])
+
+ if config.exists(base + [network, 'subnet']):
+ for subnet in config.list_nodes(base + [network, 'subnet']):
+ base_subnet = base + [network, 'subnet', subnet]
+
+ for option in option_nodes:
+ if config.exists(base + [network, 'subnet', subnet, option]):
+ config.set(base + [network, 'subnet', subnet, 'option'])
+ config.copy(base + [network, 'subnet', subnet, option], base + [network, 'subnet', subnet, 'option', option])
+ config.delete(base + [network, 'subnet', subnet, option])
+
+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))
+ exit(1)
--
cgit v1.2.3
From 74ddb29c6c9ce31450234e77fd39c73d0d51c3c5 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 7 Jan 2024 21:41:23 +0100
Subject: dhcp: T3316: Fix `listen-address` handling and add `listen-interface`
as supported by Kea
---
data/templates/dhcp-server/kea-dhcp4.conf.j2 | 8 ++++++++
.../include/listen-interface-multi-broadcast.xml.i | 18 ++++++++++++++++++
interface-definitions/service_dhcp-server.xml.in | 1 +
python/vyos/template.py | 17 +++++++++++++++++
python/vyos/utils/network.py | 4 ++--
smoketest/scripts/cli/test_service_dhcp-server.py | 9 +++++++--
src/conf_mode/service_dhcp-server.py | 9 ++++++++-
7 files changed, 61 insertions(+), 5 deletions(-)
create mode 100644 interface-definitions/include/listen-interface-multi-broadcast.xml.i
diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2
index 6ab13ab27..629fa952a 100644
--- a/data/templates/dhcp-server/kea-dhcp4.conf.j2
+++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2
@@ -1,8 +1,16 @@
{
"Dhcp4": {
"interfaces-config": {
+{% if listen_address is vyos_defined %}
+ "interfaces": {{ listen_address | kea_address_json }},
+ "dhcp-socket-type": "udp",
+{% elif listen_interface is vyos_defined %}
+ "interfaces": {{ listen_interface | tojson }},
+ "dhcp-socket-type": "raw",
+{% else %}
"interfaces": [ "*" ],
"dhcp-socket-type": "raw",
+{% endif %}
"service-sockets-max-retries": 5,
"service-sockets-retry-wait-time": 5000
},
diff --git a/interface-definitions/include/listen-interface-multi-broadcast.xml.i b/interface-definitions/include/listen-interface-multi-broadcast.xml.i
new file mode 100644
index 000000000..b3d5a3ecc
--- /dev/null
+++ b/interface-definitions/include/listen-interface-multi-broadcast.xml.i
@@ -0,0 +1,18 @@
+
+
+
+ Interface for DHCP Relay Agent to listen for requests
+
+
+
+
+ txt
+ Interface name
+
+
+ #include
+
+
+
+
+
diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in
index a5cee62d1..27485b6d4 100644
--- a/interface-definitions/service_dhcp-server.xml.in
+++ b/interface-definitions/service_dhcp-server.xml.in
@@ -74,6 +74,7 @@
#include
+ #include
Name of DHCP shared network
diff --git a/python/vyos/template.py b/python/vyos/template.py
index c0c09f690..1368f1f61 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -786,6 +786,23 @@ def range_to_regex(num_range):
regex = range_to_regex(num_range)
return f'({regex})'
+@register_filter('kea_address_json')
+def kea_address_json(addresses):
+ from json import dumps
+ from vyos.utils.network import is_addr_assigned
+
+ out = []
+
+ for address in addresses:
+ ifname = is_addr_assigned(address, return_ifname=True)
+
+ if not ifname:
+ continue
+
+ out.append(f'{ifname}/{address}')
+
+ return dumps(out)
+
@register_filter('kea_failover_json')
def kea_failover_json(config):
from json import dumps
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 997ee6309..b782e0bd8 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -308,7 +308,7 @@ def is_ipv6_link_local(addr):
return False
-def is_addr_assigned(ip_address, vrf=None) -> bool:
+def is_addr_assigned(ip_address, vrf=None, return_ifname=False) -> bool | str:
""" Verify if the given IPv4/IPv6 address is assigned to any interface """
from netifaces import interfaces
from vyos.utils.network import get_interface_config
@@ -323,7 +323,7 @@ def is_addr_assigned(ip_address, vrf=None) -> bool:
continue
if is_intf_addr_assigned(interface, ip_address):
- return True
+ return interface if return_ifname else True
return False
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index 99ac406cd..ef6191fb1 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -32,6 +32,7 @@ CTRL_PROCESS_NAME = 'kea-ctrl-agent'
KEA4_CONF = '/run/kea/kea-dhcp4.conf'
KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket'
base_path = ['service', 'dhcp-server']
+interface = 'dum8765'
subnet = '192.0.2.0/25'
router = inc_ip(subnet, 1)
dns_1 = inc_ip(subnet, 2)
@@ -46,11 +47,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
cls.cli_delete(cls, base_path)
cidr_mask = subnet.split('/')[-1]
- cls.cli_set(cls, ['interfaces', 'dummy', 'dum8765', 'address', f'{router}/{cidr_mask}'])
+ cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'])
@classmethod
def tearDownClass(cls):
- cls.cli_delete(cls, ['interfaces', 'dummy', 'dum8765'])
+ cls.cli_delete(cls, ['interfaces', 'dummy', interface])
super(TestServiceDHCPServer, cls).tearDownClass()
def tearDown(self):
@@ -95,6 +96,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
range_1_start = inc_ip(subnet, 40)
range_1_stop = inc_ip(subnet, 50)
+ self.cli_set(base_path + ['listen-interface', interface])
+
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['option', 'default-router', router])
@@ -116,6 +119,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
+ self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface])
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400)
@@ -607,6 +611,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
+ self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}'])
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY')
self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet)
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 7ebc560ba..329e18993 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -31,6 +31,7 @@ from vyos.utils.file import chmod_775
from vyos.utils.file import makedir
from vyos.utils.file import write_file
from vyos.utils.process import call
+from vyos.utils.network import interface_exists
from vyos.utils.network import is_subnet_connected
from vyos.utils.network import is_addr_assigned
from vyos import ConfigError
@@ -294,12 +295,18 @@ def verify(dhcp):
else:
raise ConfigError(f'listen-address "{address}" not configured on any interface')
-
if not listen_ok:
raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n'
'broadcast interface configured, nor was there an explicit listen-address\n'
'configured for serving DHCP relay packets!')
+ if 'listen_address' in dhcp and 'listen_interface' in dhcp:
+ raise ConfigError(f'Cannot define listen-address and listen-interface at the same time')
+
+ for interface in (dict_search('listen_interface', dhcp) or []):
+ if not interface_exists(interface):
+ raise ConfigError(f'listen-interface "{interface}" does not exist')
+
return None
def generate(dhcp):
--
cgit v1.2.3
From 0cd74e0795eaa9d71c9546cb6a4766661ffb6026 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Tue, 9 Jan 2024 22:43:35 +0100
Subject: dhcp: T5912: Fix hostfile not written for new leases
---
src/system/on-dhcp-event.sh | 33 ++++++++++++++++++++++-----------
1 file changed, 22 insertions(+), 11 deletions(-)
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index 03574bdc3..3534fe601 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -15,28 +15,39 @@ if [ $# -lt 1 ]; then
fi
action=$1
-client_name=$LEASE4_HOSTNAME
-client_ip=$LEASE4_ADDRESS
-client_mac=$LEASE4_HWADDR
hostsd_client="/usr/bin/vyos-hostsd-client"
case "$action" in
- lease4_renew|lease4_recover) # add mapping for new/recovered lease address
- if [ -z "$client_name" ]; then
- logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
- client_name=$(echo "host-$client_mac" | tr : -)
- fi
-
- $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+ lease4_renew|lease4_recover)
exit 0
;;
lease4_release|lease4_expire|lease4_decline) # delete mapping for released/declined address
+ client_ip=$LEASE4_ADDRESS
$hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply
exit 0
;;
- leases4_committed) # nothing to do
+ leases4_committed) # process committed leases (added/renewed/recovered)
+ for ((i = 0; i < $LEASES4_SIZE; i++)); do
+ client_ip_var="LEASES4_AT${i}_ADDRESS"
+ client_mac_var="LEASES4_AT${i}_HWADDR"
+ client_name_var="LEASES4_AT${i}_HOSTNAME"
+ client_subnet_id_var="LEASES4_AT${i}_SUBNET_ID"
+
+ client_ip=${!client_ip_var}
+ client_mac=${!client_mac_var}
+ client_name=${!client_name_var}
+ client_subnet_id=${!client_subnet_id_var}
+
+ if [ -z "$client_name" ]; then
+ logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead"
+ client_name=$(echo "host-$client_mac" | tr : -)
+ fi
+
+ $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply
+ done
+
exit 0
;;
--
cgit v1.2.3
From 39bf15289ca10ff5b61eb4070292ffb13f53e94e Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Tue, 9 Jan 2024 22:44:16 +0100
Subject: dhcp: T3316: Workaround to append domain suffix to hostfile entries
---
python/vyos/kea.py | 4 ++--
src/system/on-dhcp-event.sh | 32 ++++++++++++++++++++++++++++++++
2 files changed, 34 insertions(+), 2 deletions(-)
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 2ca73044b..3d8cf3637 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -25,7 +25,7 @@ from vyos.template import netmask_from_cidr
from vyos.utils.dict import dict_search_args
from vyos.utils.file import file_permissions
from vyos.utils.file import read_file
-from vyos.utils.process import cmd
+from vyos.utils.process import run
kea4_options = {
'name_server': 'domain-name-servers',
@@ -315,7 +315,7 @@ def _ctrl_socket_command(path, command, args=None):
return None
if file_permissions(path) != '0775':
- cmd(f'sudo chmod 775 {path}')
+ run(f'sudo chmod 775 {path}')
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(path)
diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh
index 3534fe601..e1a9f1884 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -17,6 +17,32 @@ fi
action=$1
hostsd_client="/usr/bin/vyos-hostsd-client"
+get_subnet_domain_name () {
+ python3 <
Date: Tue, 9 Jan 2024 22:48:55 +0100
Subject: dhcp: T5787: Prevent duplicate IP addresses on static mappings
---
smoketest/scripts/cli/test_service_dhcp-server.py | 7 +++++++
src/conf_mode/service_dhcp-server.py | 6 ++++++
2 files changed, 13 insertions(+)
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index ef6191fb1..6f24d40ec 100755
--- a/smoketest/scripts/cli/test_service_dhcp-server.py
+++ b/smoketest/scripts/cli/test_service_dhcp-server.py
@@ -375,6 +375,13 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.cli_delete(pool + ['static-mapping', 'client1', 'duid'])
+ # cannot have mappings with duplicate IP addresses
+ with self.assertRaises(ConfigSessionError):
+ self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:00:01'])
+ self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)])
+ self.cli_commit()
+ self.cli_delete(pool + ['static-mapping', 'dupe1'])
+
# commit changes
self.cli_commit()
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 329e18993..ceaba019e 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -223,6 +223,7 @@ def verify(dhcp):
if 'static_mapping' in subnet_config:
# Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
+ used_ips = []
for mapping, mapping_config in subnet_config['static_mapping'].items():
if 'ip_address' in mapping_config:
if ip_address(mapping_config['ip_address']) not in ip_network(subnet):
@@ -234,6 +235,11 @@ def verify(dhcp):
raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for '
f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!')
+ if mapping_config['ip_address'] in used_ips:
+ raise ConfigError(f'Configured IP address for static mapping "{mapping}" exists on another static mapping')
+
+ used_ips.append(mapping_config['ip_address'])
+
# There must be one subnet connected to a listen interface.
# This only counts if the network itself is not disabled!
if 'disable' not in network_config:
--
cgit v1.2.3