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 --- src/migration-scripts/dhcp-server/8-to-9 | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100755 src/migration-scripts/dhcp-server/8-to-9 (limited to 'src') 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 (limited to 'src') 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(-) (limited to 'src') 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(-) (limited to 'src') 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(+) (limited to 'src') 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