Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
326b5e7
dhcp-server: T3936: Added support for DHCP Option 82
cblackburn-igl Aug 17, 2025
fcd39c2
Merge branch 'T3936-DHCP-Option-82-Support' of https://github.com/cbl…
cblackburn-igl Aug 28, 2025
9c35d29
Re-added changes to the XML configuration heirachy which were deleted…
cblackburn-igl Aug 28, 2025
6616e6a
Update interface-definitions/include/dhcp/dhcp-server-common-config.x…
cblackburn-igl Sep 15, 2025
6853438
Update interface-definitions/include/dhcp/dhcp-server-common-config.x…
cblackburn-igl Sep 15, 2025
0859bc9
Update python/vyos/kea.py
cblackburn-igl Sep 15, 2025
5ae378d
Merge branch 'vyos:current' into T3936-DHCP-Option-82-Support
cblackburn-igl Sep 20, 2025
650d7c0
Merge branch 'vyos:current' into T3936-DHCP-Option-82-Support
cblackburn-igl Sep 28, 2025
43538d7
Changes requested by dmbaturin to improve testing. This also exposed …
cblackburn-igl Sep 28, 2025
a57d01a
Update interface-definitions/include/dhcp/dhcp-server-common-config.x…
cblackburn-igl Oct 2, 2025
79d1aaf
Update python/vyos/kea.py
cblackburn-igl Oct 2, 2025
d64acc3
ssh: T7839: fix warning on deprecated algorithms during commit
c-po Sep 20, 2025
3c7762e
T7820: Add support for yescrypt encypted hashes
Sep 14, 2025
80fdc2a
T7773: VPP move crypto engines to crypto-engines template section
sever-sever Sep 22, 2025
accc592
frr_exporter: T7851: export IPv6 BGP sessions
peterablehmann Sep 20, 2025
1ff03a0
Debian: T7847: add explicit dependency on net-tools providing arp binary
c-po Sep 20, 2025
cc7efa0
T7737: add commit hashes for analogue of legacy configfs
jestabro Sep 11, 2025
495aec1
T7737: add generated protobuf files
jestabro Sep 11, 2025
4fdc891
T7737: use standardized env var for config session pid
jestabro Sep 11, 2025
b1687d4
T7737: update init data as needed for vyos-commitd environment
jestabro Sep 11, 2025
94aae7b
T7737: inject env vars provided by vyconfd session
jestabro Sep 11, 2025
aebc231
T7737: add auxiliary set/delete functions
jestabro Sep 11, 2025
d4dd0a4
T7737: add vyconf aware versions of add/delete_cli_node
jestabro Sep 11, 2025
c5f38d9
T7737: use vyconf aware add/delete_cli_node if enabled
jestabro Sep 11, 2025
c605df7
Firewall: T7475: Disable conntrack per firewall chain
l0crian1 Sep 5, 2025
4a1b7dd
firewall: T7475: Remove redundant if statement in jinja template
l0crian1 Sep 13, 2025
8e07d56
image: T7818: re-use global path definitions from vyos.defaults
c-po Sep 22, 2025
a876205
image: T7818: avoid error when skipping config migration
c-po Sep 22, 2025
a4b546b
kea: T7281: Use correct Kea unit files
sarthurdev Sep 22, 2025
395e5de
kea: T7821: Update paths for Kea v2.7.9 security changes
sarthurdev Sep 22, 2025
ba8418b
kea: T7281: Set folder permissions to expected 0750
sarthurdev Sep 23, 2025
dd13331
kea: T7281: Preserve systemd unit environment in VRF exec
sarthurdev Sep 23, 2025
d9edcdc
bgp: T7760: deprecate per bgp vrf instance system-as node
c-po Aug 28, 2025
75a4d8b
bgp: T7760: remove per vrf instance system-as node
c-po Sep 9, 2025
667d3d5
T7850: add util list_strip to remove initial or final sub-list
jestabro Sep 23, 2025
84fb601
T7850: add nosetest for util list_strip
jestabro Sep 23, 2025
582f2d6
T7850: make op_mode_config_dict edit level aware
jestabro Sep 23, 2025
c6e2ae0
T7859: VPP de-hardcode plugin path
sever-sever Sep 24, 2025
4c1bec4
T7801: VPP fails to start with logging level set to 'Error'
natali-rs1985 Sep 25, 2025
090d7ba
T7748: using mergify rule to handle conflict checks for private repo
kumvijaya Sep 24, 2025
19a2fa2
T7748: using mergify rule to handle conflict checks for private repo
kumvijaya Sep 24, 2025
67f491d
T7861: System options CPU vendor_id bug for some platforms
sever-sever Sep 24, 2025
f9489be
l2tpv3: T7721: fix show l2tpv3 interface information
awolfnet Sep 24, 2025
4133ce8
T7862: VPP: Enable support of ixgbevf driver for DPDK
natali-rs1985 Sep 25, 2025
20fb87f
tpm: T7727: Prompt to overwrite an existing backup
sarthurdev Aug 18, 2025
63ae926
tpm: T7720: Handle encrypt failure and gracefully abort
sarthurdev Aug 18, 2025
3ec6c32
tpm: T7717: Preserve group on config and archives
sarthurdev Aug 18, 2025
300a160
tpm: T7713: Restore original config mounts when decrypting
sarthurdev Aug 19, 2025
33a99ae
tpm: T7735: Only require key/recovery if unmapped
sarthurdev Aug 19, 2025
0861767
tpm: T4919: Use vyos module function for running image, single-line i…
sarthurdev Aug 19, 2025
8509fb2
tpm: T7726: Copy encrypted volume when adding system images
sarthurdev Aug 19, 2025
484d8f1
tpm: T7726: Prompt user before clearing TPM key
sarthurdev Aug 19, 2025
d56cf2c
tpm: T7726: Prompt before overwriting existing TPM key
sarthurdev Sep 9, 2025
ec0fac7
tpm: T7726: Test TPM key or prompt recovery key
sarthurdev Sep 23, 2025
3fd2b55
T7709: add utils write_file_sync and write_file_atomic
jestabro Sep 24, 2025
18962d4
T7709: use write_file_sync/atomic in vyos-save-config.py script
jestabro Sep 24, 2025
e8561cb
T7709: use standard script vyos-save-config.py for vyconf
jestabro Sep 24, 2025
24b7783
T7709: add smoketest for config save
jestabro Sep 24, 2025
2f392f5
T7855: vyos-configd redirect stdout and catch exceptions on frr render
jestabro Sep 22, 2025
3e56726
T7855: vyos-commitd redirect stdout and catch exceptions on frr render
jestabro Sep 23, 2025
64d4ee3
migrator: T7760: remove debug print statement
c-po Sep 26, 2025
9f398c0
bgp: T7760: improfe verify() logic on default VRF removal
c-po Sep 26, 2025
e88bc1c
frr: T7875: add pre-apply validation using frr-reload --test
c-po Sep 27, 2025
6b75000
Changes requested by dmbaturin to improve testing. This also exposed …
cblackburn-igl Sep 28, 2025
8715c06
Update interface-definitions/include/dhcp/dhcp-server-common-config.x…
cblackburn-igl Oct 2, 2025
31215ce
Update python/vyos/kea.py
cblackburn-igl Oct 2, 2025
2d9d211
Changed option82 to relay-agent-information as requested by maintaine…
cblackburn-igl Oct 2, 2025
67d787d
Merge branch 'T3936-DHCP-Option-82-Support' of https://github.com/cbl…
cblackburn-igl Oct 2, 2025
093340b
Merge branch 'vyos:current' into T3936-DHCP-Option-82-Support
cblackburn-igl Oct 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions data/templates/dhcp-server/kea-dhcp4.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"persist": true,
"name": "{{ lease_file }}"
},
{% if client_class is vyos_defined %}
"client-classes": {{ client_class | kea_client_class_json }},
{% endif %}
"option-def": [
{
"name": "wpad-url",
Expand Down
61 changes: 61 additions & 0 deletions interface-definitions/include/dhcp/dhcp-server-common-config.xml.i
Original file line number Diff line number Diff line change
@@ -1,4 +1,49 @@
<!-- include start from dhcp/dhcp-server-common-config.xml.i -->
<tagNode name="client-class">
<properties>
<help>Client class name</help>
<constraint>
#include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Client class name may only contain letters, numbers, dots, underscores, and hyphens</constraintErrorMessage>
</properties>
<children>
#include <include/generic-disable-node.xml.i>
<node name="relay-agent-information">
<properties>
<help>Match DHCP Option 82 (relay agent information)</help>
</properties>
<children>
<leafNode name="circuit-id">
<properties>
<help>Filters on the contents of the circuit-id sub option</help>
<valueHelp>
<format>hex</format>
<description>Values that start with 0x are interpreted as raw hex. This must only be hexadecimal characters e.g. 0x1234567890ABCDEF</description>
</valueHelp>
<valueHelp>
<format>txt</format>
<description>Any other text string is interpreted as ASCII text</description>
</valueHelp>
</properties>
</leafNode>
<leafNode name="remote-id">
<properties>
<help>Filters on the contents of the remote-id sub option.</help>
<valueHelp>
<format>hex</format>
<description>Values that start with 0x are interpreted as raw hex. This must only be hexadecimal characters e.g. 0x1234567890ABCDEF</description>
</valueHelp>
<valueHelp>
<format>txt</format>
<description>Any other text string is interpreted as ASCII text</description>
</valueHelp>
</properties>
</leafNode>
</children>
</node>
</children>
</tagNode>
#include <include/generic-disable-node.xml.i>
<node name="dynamic-dns-update">
<properties>
Expand Down Expand Up @@ -229,6 +274,14 @@
#include <include/dhcp/ping-check.xml.i>
#include <include/generic-description.xml.i>
#include <include/generic-disable-node.xml.i>
<leafNode name="client-class">
<properties>
<help>DHCP client class</help>
<completionHelp>
<path>service dhcp-server client-class</path>
</completionHelp>
</properties>
</leafNode>
<node name="dynamic-dns-update">
<properties>
<help>Dynamically update Domain Name System (RFC4702)</help>
Expand Down Expand Up @@ -280,6 +333,14 @@
</properties>
<children>
#include <include/dhcp/option-v4.xml.i>
<leafNode name="client-class">
<properties>
<help>DHCP client class</help>
<completionHelp>
<path>service dhcp-server client-class</path>
</completionHelp>
</properties>
</leafNode>
<leafNode name="start">
<properties>
<help>First IP address for DHCP lease range</help>
Expand Down
25 changes: 25 additions & 0 deletions python/vyos/kea.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ def kea_parse_subnet(subnet, config):
if 'ping_check' in config:
out['user-context']['enable-ping-check'] = True

if 'client_class' in config:
out['client-class'] = config['client_class']

if 'range' in config:
pools = []
for num, range_config in config['range'].items():
Expand All @@ -184,6 +187,9 @@ def kea_parse_subnet(subnet, config):
if 'bootfile_server' in range_config['option']:
pool['next-server'] = range_config['option']['bootfile_server']

if 'client_class' in range_config:
pool['client-class'] = range_config['client_class']

pools.append(pool)
out['pools'] = pools

Expand Down Expand Up @@ -667,3 +673,22 @@ def kea_get_server_leases(config, inet, vrf_name, pools=[], state=[], origin=Non
data.pop(idx)

return data

def _build_relay_hex_condition(sub_option_index, value):
if value.startswith("0x"):
return f"relay4[{sub_option_index}].hex == {value}"
else:
return f"relay4[{sub_option_index}].hex == 0x{value.encode().hex().lower()}"

def kea_build_client_class_test(config):
conditions = []

if "relay_agent_information" in config:
if "circuit_id" in config["relay_agent_information"]:
conditions.append(_build_relay_hex_condition(1, config["relay_agent_information"]["circuit_id"]))
if "remote_id" in config["relay_agent_information"]:
conditions.append(_build_relay_hex_condition(2, config["relay_agent_information"]["remote_id"]))

test = " and ".join(conditions)

return test
19 changes: 19 additions & 0 deletions python/vyos/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,25 @@ def kea_high_availability_json(config):

return dumps(data)

@register_filter('kea_client_class_json')
def kea_client_class_json(client_classes):
from vyos.kea import kea_build_client_class_test
from json import dumps
out = []

for name, config in client_classes.items():
if 'disable' in config:
continue

client_class = {
'name': name,
'test': kea_build_client_class_test(config)
}

out.append(client_class)

return dumps(out, indent=4)

@register_filter('kea_dynamic_dns_update_main_json')
def kea_dynamic_dns_update_main_json(config):
from vyos.kea import kea_parse_ddns_settings
Expand Down
128 changes: 107 additions & 21 deletions smoketest/scripts/cli/test_service_dhcp-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,27 +112,7 @@ def test_dhcp_single_pool_range(self):
range_1_start = inc_ip(subnet, 40)
range_1_stop = inc_ip(subnet, 50)

self.cli_set(base_path + ['listen-interface', interface])

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check'])

pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
self.cli_set(pool + ['ignore-client-id'])
self.cli_set(pool + ['ping-check'])
# we use the first subnet IP address as default gateway
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):
self.cli_commit()
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', '1', 'start', range_1_start])
self.cli_set(pool + ['range', '1', 'stop', range_1_stop])
self.setup_single_pool_range(range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name)

# commit changes
self.cli_commit()
Expand Down Expand Up @@ -209,6 +189,112 @@ def test_dhcp_single_pool_range(self):
# Check for running process
self.verify_service_running()

def setup_single_pool_range(self, range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name):
self.cli_set(base_path + ['listen-interface', interface])
self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check'])

pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]

self.cli_set(pool + ['subnet-id', '1'])
self.cli_set(pool + ['ignore-client-id'])
self.cli_set(pool + ['ping-check'])
# we use the first subnet IP address as default gateway
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):
self.cli_commit()

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', '1', 'start', range_1_start])
self.cli_set(pool + ['range', '1', 'stop', range_1_stop])

def test_dhcp_client_class(self):
shared_net_name = 'SMOKE-1'

range_0_start = inc_ip(subnet, 10)
range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 40)
range_1_stop = inc_ip(subnet, 50)

self.setup_single_pool_range(range_0_start, range_0_stop, range_1_start, range_1_stop, shared_net_name)

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'client-class', 'test'])

# check validate() - Client class referenced that doesn't exist yet
with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_delete(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'client-class', 'test'])

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'range', '0', 'client-class', 'test'])

# check validate() - Client class referenced that doesn't exist yet
with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'client-class', 'test'])

client_class = base_path + ['client-class', 'test']

# Test that invalid hex is rejected
self.cli_set(client_class + ['relay-agent-information', 'circuit-id', '0xHELLOWORLD'])

with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_delete(client_class + ['relay-agent-information', 'circuit-id'])
self.cli_set(client_class + ['relay-agent-information', 'remote-id', '0xHELLOWORLD'])

with self.assertRaises(ConfigSessionError):
self.cli_commit()

self.cli_delete(client_class + ['relay-agent-information', 'remote-id'])

# Test string literals
self.cli_set(client_class + ['relay-agent-information', 'circuit-id', 'foo'])
self.cli_set(client_class + ['relay-agent-information', 'remote-id', 'bar'])

self.cli_commit()

self.check_client_class_in_config()

self.cli_delete(client_class + ['relay-agent-information', 'circuit-id'])
self.cli_delete(client_class + ['relay-agent-information', 'remote-id'])

# Test hex strings
self.cli_set(client_class + ['relay-agent-information', 'circuit-id', '0x666f6f'])
self.cli_set(client_class + ['relay-agent-information', 'remote-id', '0x626172'])

self.cli_commit()

self.check_client_class_in_config()

def check_client_class_in_config(self):
config = read_file(KEA4_CONF)
obj = loads(config)
self.verify_config_value(
obj, ['Dhcp4', 'client-classes', 0], 'name', 'test'
)
self.verify_config_value(
obj, ['Dhcp4', 'client-classes', 0], 'test',
'relay4[1].hex == 0x666f6f and relay4[2].hex == 0x626172'
)
self.verify_config_value(
obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0], 'client-class',
'test'
)
self.verify_config_value(
obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0],
'client-class', 'test'
)
# Check for running process
self.verify_service_running()

def test_dhcp_single_pool_options(self):
shared_net_name = 'SMOKE-0815'

Expand Down
39 changes: 39 additions & 0 deletions src/conf_mode/service_dhcp-server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import os
import re

from sys import exit
from sys import argv
Expand Down Expand Up @@ -286,6 +287,15 @@ def verify(dhcp):
f'DHCP static-route "{route}" requires router to be defined!'
)

# If a client class has been specified then it must exist
if 'client_class' in subnet_config:
client_class = subnet_config['client_class']
if 'client_class' not in dhcp:
raise ConfigError(f'Client class "{client_class}" set in subnet "{subnet}" but does not exist')

if client_class not in dhcp['client_class'].keys():
raise ConfigError(f'Client class "{client_class}" set in subnet "{subnet}" but does not exist')

# Check if DHCP address range is inside configured subnet declaration
if 'range' in subnet_config:
networks = []
Expand All @@ -295,6 +305,15 @@ def verify(dhcp):
f'DHCP range "{range}" start and stop address must be defined!'
)

# If a client class has been specified then it must exist
if 'client_class' in range_config:
client_class = range_config['client_class']
if 'client_class' not in dhcp:
raise ConfigError(f'Client class "{client_class}" set in range "{range}" but does not exist')

if client_class not in dhcp['client_class'].keys():
raise ConfigError(f'Client class "{client_class}" set in range "{range}" but does not exist')

# Start/Stop address must be inside network
for key in ['start', 'stop']:
if ip_address(range_config[key]) not in ip_network(subnet):
Expand Down Expand Up @@ -503,6 +522,26 @@ def verify(dhcp):
if 'reverse_domain' in ddns:
verify_ddns_domain_servers('Reverse', ddns['reverse_domain'])

if 'client_class' in dhcp:
# Check client class values are valid
for class_name, class_config in dhcp['client_class'].items():
if 'relay_agent_information' in class_config:
relay_agent_information_config = class_config['relay_agent_information']
# Compile a regex that will scan for valid inputs. Input can be
# either hex in the form 0x0123456789ABCDEF or a string that
# does *not* start with 0x. i.e. 0xHELLOWORLD is bad
pattern = re.compile(r'^(?:0x[0-9A-Fa-f]+|(?!0x).+)$')

if 'circuit_id' in relay_agent_information_config:
circuit_id = relay_agent_information_config['circuit_id']
if not pattern.match(circuit_id):
raise ConfigError(f'Invalid circuit-id "{circuit_id}" must be either text literal or hex string starting with 0x')

if 'remote_id' in relay_agent_information_config:
remote_id = relay_agent_information_config['remote_id']
if not pattern.match(remote_id):
raise ConfigError(f'Invalid remote-id "{remote_id}" must be either text literal or hex string starting with 0x')

return None


Expand Down
Loading