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
---
smoketest/scripts/cli/test_service_dhcp-server.py | 132 ++++++++++++++++------
1 file changed, 97 insertions(+), 35 deletions(-)
(limited to 'smoketest')
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):
--
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
(limited to 'smoketest')
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 41913f4d1d63ddd39d9125b0140b8a33449c2cfb Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
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(+)
(limited to 'smoketest')
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