diff --git a/man/systemd.network.xml b/man/systemd.network.xml
index 24471adaa9..fc5658153a 100644
--- a/man/systemd.network.xml
+++ b/man/systemd.network.xml
@@ -1633,6 +1633,17 @@
+
+ SendDecline=
+
+ A boolen. When true, DHCPv4 clients receives IP address from DHCP server.
+ After new IP is received, DHCPv4 performs IPv4 Duplicate Address Detection. If duplicate use of IP is detected
+ the DHCPv4 client rejects the IP by sending a DHCPDECLINE packet DHCP clients try to obtain an IP address again.
+ See RFC 5224.
+ Defaults to unset.
+
+
+
BlackList=
diff --git a/src/libsystemd-network/sd-dhcp-client.c b/src/libsystemd-network/sd-dhcp-client.c
index 901985fc1b..4122d08d96 100644
--- a/src/libsystemd-network/sd-dhcp-client.c
+++ b/src/libsystemd-network/sd-dhcp-client.c
@@ -643,7 +643,7 @@ static int client_message_init(
assert(ret);
assert(_optlen);
assert(_optoffset);
- assert(IN_SET(type, DHCP_DISCOVER, DHCP_REQUEST, DHCP_RELEASE));
+ assert(IN_SET(type, DHCP_DISCOVER, DHCP_REQUEST, DHCP_RELEASE, DHCP_DECLINE));
optlen = DHCP_MIN_OPTIONS_SIZE;
size = sizeof(DHCPPacket) + optlen;
@@ -1966,6 +1966,48 @@ int sd_dhcp_client_send_release(sd_dhcp_client *client) {
return 0;
}
+int sd_dhcp_client_send_decline(sd_dhcp_client *client) {
+ assert_return(client, -EINVAL);
+ assert_return(client->state != DHCP_STATE_STOPPED, -ESTALE);
+ assert_return(client->lease, -EUNATCH);
+
+ _cleanup_free_ DHCPPacket *release = NULL;
+ size_t optoffset, optlen;
+ int r;
+
+ r = client_message_init(client, &release, DHCP_DECLINE, &optlen, &optoffset);
+ if (r < 0)
+ return r;
+
+ release->dhcp.ciaddr = client->lease->address;
+ memcpy(&release->dhcp.chaddr, &client->mac_addr, client->mac_addr_len);
+
+ 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,
+ DHCP_PORT_SERVER,
+ &release->dhcp,
+ sizeof(DHCPMessage) + optoffset);
+ if (r < 0)
+ return r;
+
+ log_dhcp_client(client, "DECLINE");
+
+ client_stop(client, SD_DHCP_CLIENT_EVENT_STOP);
+
+ if (client->state != DHCP_STATE_STOPPED) {
+ r = sd_dhcp_client_start(client);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
int sd_dhcp_client_stop(sd_dhcp_client *client) {
DHCP_CLIENT_DONT_DESTROY(client);
diff --git a/src/network/networkd-dhcp4.c b/src/network/networkd-dhcp4.c
index 0694ffc939..fc8f44d6aa 100644
--- a/src/network/networkd-dhcp4.c
+++ b/src/network/networkd-dhcp4.c
@@ -48,6 +48,15 @@ void dhcp4_release_old_lease(Link *link) {
link_dirty(link);
}
+static void dhcp4_check_ready(Link *link) {
+ if (link->dhcp4_messages == 0) {
+ link->dhcp4_configured = true;
+ /* New address and routes are configured now. Let's release old lease. */
+ dhcp4_release_old_lease(link);
+ link_check_ready(link);
+ }
+}
+
static int dhcp4_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
int r;
@@ -89,10 +98,8 @@ static int dhcp4_route_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *li
return 1;
}
- link->dhcp4_configured = true;
- /* New address and routes are configured now. Let's release old lease. */
- dhcp4_release_old_lease(link);
- link_check_ready(link);
+ if (!link->network->dhcp_send_decline)
+ dhcp4_check_ready(link);
}
return 1;
@@ -655,6 +662,78 @@ static int dhcp_lease_lost(Link *link) {
return 0;
}
+static void dhcp_address_on_acd(sd_ipv4acd *acd, int event, void *userdata) {
+ _cleanup_free_ char *pretty = NULL;
+ union in_addr_union address = {};
+ Link *link;
+ int r;
+
+ assert(acd);
+ assert(userdata);
+
+ link = userdata;
+
+ switch (event) {
+ case SD_IPV4ACD_EVENT_STOP:
+ log_link_debug(link, "Stopping ACD client for DHCP4...");
+ return;
+
+ case SD_IPV4ACD_EVENT_BIND:
+ if (DEBUG_LOGGING) {
+ (void) sd_dhcp_lease_get_address(link->dhcp_lease, &address.in);
+ (void) in_addr_to_string(AF_INET, &address, &pretty);
+ log_link_debug(link, "Successfully claimed DHCP4 address %s", strna(pretty));
+ }
+ dhcp4_check_ready(link);
+ break;
+
+ case SD_IPV4ACD_EVENT_CONFLICT:
+ (void) sd_dhcp_lease_get_address(link->dhcp_lease, &address.in);
+ (void) in_addr_to_string(AF_INET, &address, &pretty);
+ log_link_warning(link, "DAD conflict. Dropping DHCP4 address %s", strna(pretty));
+
+ (void) sd_dhcp_client_send_decline(link->dhcp_client);
+
+ if (link->dhcp_lease) {
+ r = dhcp_lease_lost(link);
+ if (r < 0)
+ link_enter_failed(link);
+ }
+ break;
+
+ default:
+ assert_not_reached("Invalid IPv4ACD event.");
+ }
+
+ sd_ipv4acd_stop(acd);
+
+ return;
+}
+
+static int configure_dhcpv4_duplicate_address_detection(Link *link) {
+ int r;
+
+ assert(link);
+
+ r = sd_ipv4acd_new(&link->network->dhcp_acd);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_attach_event(link->network->dhcp_acd, NULL, 0);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_ifindex(link->network->dhcp_acd, link->ifindex);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_mac(link->network->dhcp_acd, &link->mac);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
static int dhcp4_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *link) {
int r;
@@ -684,13 +763,31 @@ static int dhcp4_address_handler(sd_netlink *rtnl, sd_netlink_message *m, Link *
return 1;
}
- if (link->dhcp4_messages == 0) {
- link->dhcp4_configured = true;
- /* The new address is configured, and no route is requested.
- * Let's drop the old lease. */
- dhcp4_release_old_lease(link);
- link_check_ready(link);
- }
+ if (link->network->dhcp_send_decline) {
+ union in_addr_union addr;
+
+ (void) sd_dhcp_lease_get_address(link->dhcp_lease, &addr.in);
+
+ r = sd_ipv4acd_set_address(link->network->dhcp_acd, &addr.in);
+ if (r < 0)
+ return r;
+
+ r = sd_ipv4acd_set_callback(link->network->dhcp_acd, dhcp_address_on_acd, link);
+ if (r < 0)
+ return r;
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *pretty = NULL;
+
+ (void) in_addr_to_string(AF_INET, &addr, &pretty);
+ log_debug("Starting IPv4ACD client. Probing DHCPv4 address %s", strna(pretty));
+ }
+
+ r = sd_ipv4acd_start(link->network->dhcp_acd, true);
+ if (r < 0)
+ log_link_warning_errno(link, r, "Failed to start IPv4ACD client, ignoring: %m");
+ } else
+ dhcp4_check_ready(link);
return 1;
}
@@ -1033,6 +1130,7 @@ static int dhcp4_handler(sd_dhcp_client *client, int event, void *userdata) {
return r;
if (r != 0)
return -ENOMSG;
+
break;
default:
if (event < 0)
@@ -1334,7 +1432,14 @@ int dhcp4_configure(Link *link) {
if (r < 0)
return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to set ip service type: %m");
}
- return dhcp4_set_client_identifier(link);
+
+ if (link->network->dhcp_send_decline) {
+ r = configure_dhcpv4_duplicate_address_detection(link);
+ if (r < 0)
+ return log_link_error_errno(link, r, "DHCP4 CLIENT: Failed to configure service type: %m");
+ }
+
+ return dhcp4_set_client_identifier(link);
}
int config_parse_dhcp_max_attempts(
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
index 29a05b2f30..d44b567069 100644
--- a/src/network/networkd-network-gperf.gperf
+++ b/src/network/networkd-network-gperf.gperf
@@ -174,6 +174,7 @@ DHCPv4.UseTimezone, config_parse_bool,
DHCPv4.IAID, config_parse_iaid, 0, 0
DHCPv4.ListenPort, config_parse_uint16, 0, offsetof(Network, dhcp_client_port)
DHCPv4.SendRelease, config_parse_bool, 0, offsetof(Network, dhcp_send_release)
+DHCPv4.SendDecline, config_parse_bool, 0, offsetof(Network, dhcp_send_decline)
DHCPv4.BlackList, config_parse_dhcp_black_listed_ip_address, 0, 0
DHCPv4.IPServiceType, config_parse_ip_service_type, 0, offsetof(Network, ip_service_type)
DHCPv4.SendOption, config_parse_dhcp_send_option, 0, offsetof(Network, dhcp_client_send_options)
diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c
index e2e2999354..40394eb766 100644
--- a/src/network/networkd-network.c
+++ b/src/network/networkd-network.c
@@ -619,6 +619,9 @@ static Network *network_free(Network *network) {
set_free(network->dhcp_request_options);
free(network->mac);
+ if (network->dhcp_acd)
+ sd_ipv4acd_unref(network->dhcp_acd);
+
strv_free(network->ntp);
free(network->dns);
strv_free(network->sip);
diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h
index bd3c96d972..4f229103a7 100644
--- a/src/network/networkd-network.h
+++ b/src/network/networkd-network.h
@@ -5,6 +5,7 @@
#include "sd-bus.h"
#include "sd-device.h"
+#include "sd-ipv4acd.h"
#include "bridge.h"
#include "condition.h"
@@ -111,7 +112,9 @@ struct Network {
bool dhcp_use_hostname;
bool dhcp_route_table_set;
bool dhcp_send_release;
+ bool dhcp_send_decline;
DHCPUseDomains dhcp_use_domains;
+ sd_ipv4acd *dhcp_acd;
Set *dhcp_black_listed_ip;
Set *dhcp_request_options;
OrderedHashmap *dhcp_client_send_options;
diff --git a/src/systemd/sd-dhcp-client.h b/src/systemd/sd-dhcp-client.h
index f97e35b654..0002ea0e59 100644
--- a/src/systemd/sd-dhcp-client.h
+++ b/src/systemd/sd-dhcp-client.h
@@ -184,6 +184,7 @@ int sd_dhcp_client_set_dhcp_option(sd_dhcp_client *client, sd_dhcp_option *v);
int sd_dhcp_client_stop(sd_dhcp_client *client);
int sd_dhcp_client_start(sd_dhcp_client *client);
int sd_dhcp_client_send_release(sd_dhcp_client *client);
+int sd_dhcp_client_send_decline(sd_dhcp_client *client);
int sd_dhcp_client_send_renew(sd_dhcp_client *client);
sd_dhcp_client *sd_dhcp_client_ref(sd_dhcp_client *client);
diff --git a/test/fuzz/fuzz-network-parser/directives.network b/test/fuzz/fuzz-network-parser/directives.network
index 0c76bd5f91..b7fde0e018 100644
--- a/test/fuzz/fuzz-network-parser/directives.network
+++ b/test/fuzz/fuzz-network-parser/directives.network
@@ -98,6 +98,7 @@ SendRelease=
MaxAttempts=
IPServiceType=
SendOption=
+SendDecline=
[DHCPv6]
UseNTP=
UseDNS=
diff --git a/test/test-network/conf/dhcp-client-decline.network b/test/test-network/conf/dhcp-client-decline.network
new file mode 100644
index 0000000000..123a459f7f
--- /dev/null
+++ b/test/test-network/conf/dhcp-client-decline.network
@@ -0,0 +1,9 @@
+[Match]
+Name=veth99
+
+[Network]
+DHCP=ipv4
+IPv6AcceptRA=false
+
+[DHCPv4]
+SendDecline=yes
diff --git a/test/test-network/conf/dhcp-server-decline.network b/test/test-network/conf/dhcp-server-decline.network
new file mode 100644
index 0000000000..9009eaeb57
--- /dev/null
+++ b/test/test-network/conf/dhcp-server-decline.network
@@ -0,0 +1,14 @@
+[Match]
+Name=veth-peer
+
+[Network]
+Address=192.168.5.1/24
+Address=192.168.5.10/24
+IPv6AcceptRA=false
+DHCPServer=yes
+
+[DHCPServer]
+PoolOffset=10
+PoolSize=1
+DNS=192.168.5.1
+NTP=192.168.5.1
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index e1055fcf79..6b59efa09d 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -2632,6 +2632,7 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities):
'25-vrf.netdev',
'25-vrf.network',
'dhcp-client-anonymize.network',
+ 'dhcp-client-decline.network',
'dhcp-client-gateway-onlink-implicit.network',
'dhcp-client-ipv4-dhcp-settings.network',
'dhcp-client-ipv4-only-ipv6-disabled.network',
@@ -2656,6 +2657,7 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities):
'dhcp-client-with-ipv4ll-fallback-without-dhcp-server.network',
'dhcp-client-with-static-address.network',
'dhcp-client.network',
+ 'dhcp-server-decline.network',
'dhcp-server-veth-peer.network',
'dhcp-v4-server-veth-peer.network',
'dhcp-client-use-domains.network',
@@ -3328,6 +3330,14 @@ class NetworkdDHCPClientTests(unittest.TestCase, Utilities):
print(output)
self.assertRegex(output, 'example.com')
+ def test_dhcp_client_decline(self):
+ copy_unit_to_networkd_unit_path('25-veth.netdev', 'dhcp-server-decline.network', 'dhcp-client-decline.network')
+
+ start_networkd()
+ self.wait_online(['veth-peer:carrier'])
+ rc = call(*wait_online_cmd, '--timeout=10s', '--interface=veth99:routable', env=env)
+ self.assertTrue(rc == 1)
+
class NetworkdIPv6PrefixTests(unittest.TestCase, Utilities):
links = ['veth99']