From 6eaa5ba32231290971823d0ddf73bc712c384bcc Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Mon, 9 Mar 2026 15:18:24 +0900 Subject: [PATCH 1/2] sd-dhcp-client: several fixlets for sending RELEASE or DECLINE - Extract common logic to client_send_release_or_decline(). - Do not send DECLINE message on BOOTP protocol. - Drop redundant assignment of chaddr, as it is already set by client_message_init() -> dhcp_message_init(). - Do not assign acquired address in ciaddr field of DECLINE message, but use Requested IP Address option. - Broadcast DECLINE message, rather than unicast. - Set server identifier in both cases. Fixes #39299. --- src/libsystemd-network/sd-dhcp-client.c | 128 ++++++++++++++---------- 1 file changed, 73 insertions(+), 55 deletions(-) diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c index 8c373146f6..4b1cdd0b86 100644 --- a/src/libsystemd-network/sd-dhcp-client.c +++ b/src/libsystemd-network/sd-dhcp-client.c @@ -876,7 +876,7 @@ static int client_message_init( MAY contain the Parameter Request List option. */ /* NOTE: in case that there would be an option to do not send * any PRL at all, the size should be checked before sending */ - if (!set_isempty(client->req_opts) && type != DHCP_RELEASE) { + if (!set_isempty(client->req_opts) && IN_SET(type, DHCP_DISCOVER, DHCP_REQUEST)) { _cleanup_free_ uint8_t *opts = NULL; size_t n_opts, i = 0; void *val; @@ -2356,70 +2356,93 @@ int sd_dhcp_client_start(sd_dhcp_client *client) { return r; } -static int client_send_release(sd_dhcp_client *client) { - _cleanup_free_ DHCPPacket *release = NULL; - size_t optoffset, optlen; +static int client_send_release_or_decline(sd_dhcp_client *client, uint8_t type) { int r; - assert(client); + assert(IN_SET(type, DHCP_RELEASE, DHCP_DECLINE)); - if (!client->send_release) - return 0; /* disabled */ + if (!sd_dhcp_client_is_running(client) || !client->lease || client->bootp) + return 0; /* there is nothing to release or decline */ - if (!client->lease || client->bootp) - return 0; /* there is nothing to be released */ + const char *name = type == DHCP_RELEASE ? "RELEASE" : "DECLINE"; - r = client_message_init(client, DHCP_RELEASE, &release, &optlen, &optoffset); + _cleanup_free_ DHCPPacket *packet = NULL; + size_t optoffset, optlen; + r = client_message_init(client, type, &packet, &optlen, &optoffset); if (r < 0) - return r; + return log_dhcp_client_errno(client, r, "Failed to initialize DHCP %s message: %m", name); - /* Fill up release IP and MAC */ - release->dhcp.ciaddr = client->lease->address; - memcpy(&release->dhcp.chaddr, client->hw_addr.bytes, client->hw_addr.length); + /* See RFC 2131, Table 5 */ + switch (type) { + case DHCP_RELEASE: + /* On release, the acquired address must be set in ciaddr. */ + packet->dhcp.ciaddr = client->lease->address; + break; - r = dhcp_option_append(&release->dhcp, optlen, &optoffset, 0, - SD_DHCP_OPTION_END, 0, NULL); + case DHCP_DECLINE: + /* On decline, the acquired address must be set in Requested IP Address option. */ + r = dhcp_option_append(&packet->dhcp, optlen, &optoffset, /* overload= */ 0, + SD_DHCP_OPTION_REQUESTED_IP_ADDRESS, + 4, &client->lease->address); + if (r < 0) + return log_dhcp_client_errno( + client, r, + "Failed to append Requested IP Address option to DHCP %s message: %m", + name); + break; + + default: + assert_not_reached(); + } + + /* In both cases, the server identifier must be set. */ + r = dhcp_option_append(&packet->dhcp, optlen, &optoffset, /* overload= */ 0, + SD_DHCP_OPTION_SERVER_IDENTIFIER, + 4, &client->lease->server_address); if (r < 0) - return r; + return log_dhcp_client_errno( + client, r, + "Failed to append Server Identifier option to DHCP %s message: %m", + name); - r = dhcp_network_send_udp_socket(client->fd, - client->lease->server_address, - client->server_port, - &release->dhcp, - sizeof(DHCPMessage) + optoffset); + r = dhcp_option_append(&packet->dhcp, optlen, &optoffset, /* overload= */ 0, + SD_DHCP_OPTION_END, /* optlen= */ 0, /* optval= */ NULL); if (r < 0) - return r; + return log_dhcp_client_errno( + client, r, + "Failed to finalize DHCP %s message: %m", + name); - log_dhcp_client(client, "RELEASE"); - return 0; + switch (type) { + case DHCP_RELEASE: + r = dhcp_network_send_udp_socket( + client->fd, + client->lease->server_address, + client->server_port, + &packet->dhcp, + sizeof(DHCPMessage) + optoffset); + break; + case DHCP_DECLINE: + r = dhcp_client_send_raw(client, packet, sizeof(DHCPPacket) + optoffset); + break; + default: + assert_not_reached(); + } + if (r < 0) + return log_dhcp_client_errno( + client, r, + "Failed to send DHCP %s message: %m", + name); + + log_dhcp_client(client, "%s", name); + return 1; /* sent */ } int sd_dhcp_client_send_decline(sd_dhcp_client *client) { - _cleanup_free_ DHCPPacket *release = NULL; - size_t optoffset, optlen; int r; - if (!sd_dhcp_client_is_running(client) || !client->lease) - return 0; /* do nothing */ - - r = client_message_init(client, DHCP_DECLINE, &release, &optlen, &optoffset); - if (r < 0) - return r; - - release->dhcp.ciaddr = client->lease->address; - memcpy(&release->dhcp.chaddr, client->hw_addr.bytes, client->hw_addr.length); - - r = dhcp_option_append(&release->dhcp, optlen, &optoffset, 0, - SD_DHCP_OPTION_END, 0, NULL); - if (r < 0) - return r; - - r = dhcp_network_send_udp_socket(client->fd, - client->lease->server_address, - client->server_port, - &release->dhcp, - sizeof(DHCPMessage) + optoffset); - if (r < 0) + r = client_send_release_or_decline(client, DHCP_DECLINE); + if (r <= 0) return r; log_dhcp_client(client, "DECLINE"); @@ -2434,20 +2457,15 @@ int sd_dhcp_client_send_decline(sd_dhcp_client *client) { } int sd_dhcp_client_stop(sd_dhcp_client *client) { - int r; - if (!client) return 0; DHCP_CLIENT_DONT_DESTROY(client); - r = client_send_release(client); - if (r < 0) - log_dhcp_client_errno(client, r, - "Failed to send DHCP release message, ignoring: %m"); + if (client->send_release) + (void) client_send_release_or_decline(client, DHCP_RELEASE); client_stop(client, SD_DHCP_CLIENT_EVENT_STOP); - return 0; } From 5188882fbfe910c3e61f8f1a6ead4bc1b4830e7b Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Tue, 10 Mar 2026 05:07:44 +0900 Subject: [PATCH 2/2] test-network: add test case for sending DHCPv4 RELEASE message --- .../conf/25-dhcp-client-simple.network | 7 ++ test/test-network/systemd-networkd-tests.py | 68 ++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 test/test-network/conf/25-dhcp-client-simple.network diff --git a/test/test-network/conf/25-dhcp-client-simple.network b/test/test-network/conf/25-dhcp-client-simple.network new file mode 100644 index 0000000000..e98bd26200 --- /dev/null +++ b/test/test-network/conf/25-dhcp-client-simple.network @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +[Match] +Name=client + +[Network] +DHCP=ipv4 +IPv6AcceptRA=no diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 6f2fe1b9eb..1f3a99a040 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -869,13 +869,18 @@ def dnr_v6_instance_data(adn, addrs=None, prio=1, alpns=("dot",), dohpath=None): return data -def start_dnsmasq(*additional_options, interface='veth-peer', ra_mode=None, ipv4_range='192.168.5.10,192.168.5.200', ipv4_router='192.168.5.1', ipv6_range='2600::10,2600::20'): +def start_dnsmasq(*additional_options, namespace=None, interface='veth-peer', ra_mode=None, ipv4_range='192.168.5.10,192.168.5.200', ipv4_router='192.168.5.1', ipv6_range='2600::10,2600::20'): if ra_mode: ra_mode = f',{ra_mode}' else: ra_mode = '' - command = ( + if namespace: + ns_command = ('ip', 'netns', 'exec', namespace) + else: + ns_command = () + + command = ns_command + ( 'dnsmasq', f'--log-facility={dnsmasq_log_file}', '--log-queries=extra', @@ -1104,6 +1109,8 @@ def tear_down_common(): # 3. remove network namespace call_quiet('ip netns del ns99') + call_quiet('ip netns del ns-bridge') + call_quiet('ip netns del ns-server') # 4. remove links flush_l2tp_tunnels() @@ -7988,6 +7995,63 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities): self.teardown_nftset('addr4', 'network4', 'ifindex') + def test_dhcp_client_send_release(self): + check_output('ip netns add ns-bridge') + check_output('ip netns exec ns-bridge ip link add bridge99 type bridge') + check_output('ip netns exec ns-bridge ip link set bridge99 address 12:34:56:78:90:ab') + check_output('ip netns exec ns-bridge ip link set bridge99 up') + + check_output('ip link add client type veth peer clientp') + check_output('ip link set clientp netns ns-bridge') + check_output('ip netns exec ns-bridge ip link set clientp master bridge99') + check_output('ip netns exec ns-bridge ip link set clientp up') + + check_output('ip link add server type veth peer serverp') + check_output('ip link set serverp netns ns-bridge') + check_output('ip netns exec ns-bridge ip link set serverp master bridge99') + check_output('ip netns exec ns-bridge ip link set serverp up') + + check_output('ip netns add ns-server') + check_output('ip link set server netns ns-server') + check_output('ip netns exec ns-server ip link set server up') + check_output('ip netns exec ns-server ip address add 192.0.2.1/24 dev server') + + start_dnsmasq( + namespace='ns-server', + interface='server', + ipv4_range='192.0.2.100,192.0.2.109', + ipv4_router='192.0.2.1', + ) + + copy_network_unit('25-dhcp-client-simple.network') + start_networkd() + self.wait_online('client:routable') + + print('## ip -4 address show dev client scope global') + output = check_output('ip -4 address show dev client scope global') + print(output) + self.assertRegex(output, r'192.0.2.10[0-9]/24') + + print('## ip -4 route show dev client') + output = check_output('ip -4 route show dev client') + print(output) + self.assertRegex(output, r'default via 192.0.2.1 proto dhcp src 192.0.2.10[0-9]') + self.assertRegex(output, r'192.0.2.0/24 proto kernel scope link src 192.0.2.10[0-9]') + self.assertRegex(output, r'192.0.2.1 proto dhcp scope link src 192.0.2.10[0-9]') + + networkctl('down', 'client') + + print('## dnsmasq log') + for _ in range(20): + time.sleep(0.5) + output = read_dnsmasq_log_file() + if 'DHCPRELEASE' in output: + print(output) + break + else: + print(output) + self.fail('Timed out waiting for DHCPRELEASE in dnsmasq log') + def test_dhcp_client_ipv4_dbus_status(self): copy_network_unit('25-veth.netdev', '25-dhcp-server-veth-peer.network', '25-dhcp-client-ipv4-only.network') start_networkd()