diff --git a/NEWS b/NEWS
index 386b803cf6..42705e21f6 100644
--- a/NEWS
+++ b/NEWS
@@ -73,6 +73,27 @@ CHANGES WITH 261 in spe:
effectively means live upgrades from versions older than v247 are not
supported anymore.
+ * systemd-networkd gained a new sd-dhcp-relay backend for DHCP relay
+ agent support. As part of this change, the following [DHCPServer]
+ settings are deprecated:
+ - BindToInterface=
+ - RelayTarget=
+ - RelayAgentCircuitId=
+ - RelayAgentRemoteId=
+ They are replaced by DHCPRelay= in [Network], along with new
+ [DHCPRelay] section settings in .network files:
+ - AgentAddress=
+ - GatewayAddress=
+ - CircuitId=
+ - VirtualSubnetSelection=
+ - ExtraOption=
+ - InterfacePriority=
+ and in networkd.conf:
+ - ServerAddress=
+ - OverrideServerIdentifier=
+ - RemoteId=
+ - ExtraOption=
+
* Required version of musl (when built with -Dlibc=musl) has been raised
from 1.2.5 to 1.2.6.
diff --git a/man/networkd.conf.xml b/man/networkd.conf.xml
index f611e244f5..8dd1181508 100644
--- a/man/networkd.conf.xml
+++ b/man/networkd.conf.xml
@@ -393,6 +393,67 @@ DUIDRawData=00:00:ab:11:f9:2a:c2:77:29:f9:5c:00
+
+ [DHCPRelay] Section Options
+ The [DHCPRelay] section contains host-wide settings for the DHCP relay agent.
+
+
+
+
+ ServerAddress=
+
+ Specifies the IPv4 address of the upstream DHCP server. This is required for the host to act
+ as a DHCP relay agent.
+
+
+
+
+
+
+ OverrideServerIdentifier=
+
+ Takes a boolean value. When enabled, the Server Identifier Override sub-option and the Relay
+ Agent Flags sub-option in the Relay Agent Information option will be appended to the DHCP message
+ forwarded to the upstream DHCP server. Defaults to false.
+
+
+
+
+
+
+ RemoteId=
+
+ Specifies the remote ID, which is typically used by DHCP servers to identify the DHCP relay
+ agent. Takes a data type and data separated with a colon
+ (type:value). The type
+ takes one of uint8, uint16, uint32,
+ ipv4address, ipv6address, or string.
+ Special characters in the data string may be escaped using
+ C-style escapes.
+ If unset, a UUID generated from the local machine ID will be used.
+
+
+
+
+
+
+ ExtraOption=
+
+ Specifies an extra sub-option in the Relay Agent Information option, which is appended to
+ DHCP messages forwarded to the upstream DHCP server. Takes a sub-option code, data type, and data
+ separated with a colon
+ (code:type:value).
+ The code is an integer in 1…254. See RemoteId= in the above for the acceptable
+ type and data. This setting can be specified multiple times. When an empty string is specified,
+ all previous assignments are cleared. Defaults to unset, and no extra sub-option will be appended.
+
+
+
+
+
+
+
+
[DHCPServer] Section Options
diff --git a/man/systemd.network.xml b/man/systemd.network.xml
index d62e4d6232..dc97e17838 100644
--- a/man/systemd.network.xml
+++ b/man/systemd.network.xml
@@ -435,6 +435,26 @@
+
+ DHCPRelay=
+
+ Takes one of upstream or downstream. When specified,
+ the host acts as a DHCP relay agent. When set to upstream, the interface behaves
+ as an upstream interface of the DHCP relay agent. When set to downstream, the
+ interface behaves as a downstream interface of the DHCP relay agent. To make the DHCP relay agent
+ work, at least one upstream interface and one downstream interface must be configured on the host.
+ This requires ServerAddress= in the [DHCPRelay] section to be configured in
+ networkd.conf. If ServerAddress= is not configured, this
+ setting is ignored. See
+ networkd.conf5
+ for more details about ServerAddress= and other host-wide settings.
+ Also, further per-interface settings can be configured in the [DHCPRelay] section described below.
+ Defaults to unset, and the interface is not used for DHCP relay forwarding.
+
+
+
+
+
DHCPServer=
@@ -444,10 +464,9 @@
Even if this is enabled, the DHCP server will not be started automatically and wait for the
persistent storage being ready to load/save leases in the storage, unless
- RelayTarget= or PersistLeases=no/runtime are specified in the
- [DHCPServer] section. It will be started after
- systemd-networkd-persistent-storage.service is started, which calls
- networkctl persistent-storage yes. See
+ PersistLeases=no/runtime is specified in the [DHCPServer] section. It will be
+ started after systemd-networkd-persistent-storage.service is started, which
+ calls networkctl persistent-storage yes. See
networkctl1
for more details.
@@ -3868,6 +3887,104 @@ Token=prefixstable:2002:da8:1::
+
+ [DHCPRelay] Section Options
+ The [DHCPRelay] section contains per-interface settings for the DHCP relay agent. The settings in
+ this section are only used when DHCPRelay= in the [Network] section is configured.
+
+
+
+
+
+ AgentAddress=
+
+ Takes an IPv4 address. The specified address is used as the source IP address of packets
+ forwarded to the upstream DHCP server. The address may also be used for the Server Identifier
+ Override sub-option and the Link Selection sub-option in the Relay Agent Information option
+ appended to DHCP messages forwarded to the upstream DHCP server. This setting applies to both
+ upstream and downstream interfaces. Defaults to unset. If unset, a statically configured IPv4
+ address in the .network file is selected automatically. When multiple static IPv4 addresses are
+ configured, it is recommended to specify this setting explicitly.
+
+
+
+
+
+
+ GatewayAddress=
+
+ Takes an IPv4 address. The specified address is set to the giaddr field of
+ the DHCP message header when the DHCP message is forwarded to the upstream DHCP server. This is
+ used only when DHCPRelay=downstream, and ignored otherwise. Defaults to unset,
+ and the same address specified with AgentAddress= will be used.
+
+
+
+
+
+
+ CircuitId=
+
+ Specifies the circuit ID of the downstream interface. This is set as the Circuit ID
+ sub-option in the Relay Agent Information option in the forwarded DHCP message. Takes a data type
+ and data separated with a colon
+ (type:value). The type
+ takes one of uint8, uint16, uint32,
+ ipv4address, ipv6address, or string.
+ Special characters in the data string may be escaped using
+ C-style escapes.
+ This is used only when DHCPRelay=downstream, and ignored otherwise. If unset,
+ the name of the interface will be used.
+
+
+
+
+
+
+ VirtualSubnetSelection=
+
+ Specifies a unique identifier used by the DHCP server to select the downstream subnet
+ independently of giaddr. This value is used as the Virtual Subnet Selection sub-option in the Relay
+ Agent Information option in the forwarded DHCP message. Takes a value in the same format as
+ CircuitId=. This is used only when DHCPRelay=downstream, and
+ ignored otherwise. Defaults to unset, and the sub-option is not set.
+
+
+
+
+
+
+ ExtraOption=
+
+ Specifies an extra sub-option in the Relay Agent Information option, which is appended to
+ DHCP messages forwarded to the upstream DHCP server. Takes a sub-option code, data type, and data
+ separated with a colon
+ (code:type:value).
+ The code is an integer in 1…254. See CircuitId= in the above for the acceptable
+ type and data. This setting can be specified multiple times. When an empty string is specified,
+ all previous assignments are cleared. This is used only when
+ DHCPRelay=downstream, and ignored otherwise. Defaults to unset, and no extra
+ sub-option will be appended.
+
+
+
+
+
+
+ InterfacePriority=
+
+ Specifies the priority of the upstream interface. Takes an integer value. When the host has
+ multiple upstream interfaces, the upstream interface with the highest (largest) priority will be
+ used for forwarding DHCP messages to the upstream DHCP server. When multiple interfaces have the
+ same priority, which interface is used is unspecified. This is used only when
+ DHCPRelay=upstream, and ignored otherwise. Defaults to zero.
+
+
+
+
+
+
+
[DHCPServer] Section Options
The [DHCPServer] section contains settings for the DHCP server, if enabled via the
@@ -4161,52 +4278,6 @@ ServerAddress=192.168.0.1/24
-
- BindToInterface=
-
- Takes a boolean value. When yes, DHCP server socket will be bound
- to its network interface and all socket communication will be restricted to this interface.
- Defaults to yes, except if RelayTarget= is used (see below),
- in which case it defaults to no.
-
-
-
-
-
- RelayTarget=
-
- Takes an IPv4 address, which must be in the format described in
- inet_pton3.
- Turns this DHCP server into a DHCP relay agent. See RFC 1542.
- The address is the address of DHCP server or another relay agent to forward DHCP messages to and from.
-
-
-
-
-
- RelayAgentCircuitId=
-
- Specifies value for Agent Circuit ID suboption of Relay Agent Information option.
- Takes a string, which must be in the format string:value,
- where value should be replaced with the value of the suboption.
- Defaults to unset (means no Agent Circuit ID suboption is generated).
- Ignored if RelayTarget= is not specified.
-
-
-
-
-
- RelayAgentRemoteId=
-
- Specifies value for Agent Remote ID suboption of Relay Agent Information option.
- Takes a string, which must be in the format string:value,
- where value should be replaced with the value of the suboption.
- Defaults to unset (means no Agent Remote ID suboption is generated).
- Ignored if RelayTarget= is not specified.
-
-
-
-
RapidCommit=
@@ -4238,9 +4309,6 @@ ServerAddress=192.168.0.1/24
networkd.conf5,
which defaults to yes, will be used.
- When RelayTarget= is specified, this setting will be ignored and no leases
- will be saved, as there will be no bound lease on the server.
-
diff --git a/src/libsystemd-network/dhcp-option.c b/src/libsystemd-network/dhcp-option.c
index c5e8b29794..7ad203f183 100644
--- a/src/libsystemd-network/dhcp-option.c
+++ b/src/libsystemd-network/dhcp-option.c
@@ -7,7 +7,6 @@
#include "alloc-util.h"
#include "dhcp-option.h"
-#include "dhcp-server-internal.h"
#include "dns-domain.h"
#include "hostname-util.h"
#include "memory-util.h"
@@ -39,8 +38,6 @@ static int option_append(uint8_t options[], size_t size, size_t *offset,
assert(size > 0);
assert(offset);
- int r;
-
if (code != SD_DHCP_OPTION_END)
/* always make sure there is space for an END option */
size--;
@@ -68,34 +65,6 @@ static int option_append(uint8_t options[], size_t size, size_t *offset,
*offset += 3 + optlen;
break;
- case SD_DHCP_OPTION_RELAY_AGENT_INFORMATION: {
- /* When called with raw data (optlen > 0), e.g. from SendOption=, append as a plain TLV.
- * The structured handling below expects optval to be an sd_dhcp_server*. */
- if (optlen > 0)
- return dhcp_option_append_tlv(options, size, offset, code, optlen, optval);
-
- sd_dhcp_server *server = (sd_dhcp_server *) optval;
- size_t current_offset = *offset + 2;
-
- if (server->agent_circuit_id) {
- r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_CIRCUIT_ID,
- strlen(server->agent_circuit_id), server->agent_circuit_id);
- if (r < 0)
- return r;
- }
- if (server->agent_remote_id) {
- r = dhcp_option_append_tlv(options, size, ¤t_offset, SD_DHCP_RELAY_AGENT_REMOTE_ID,
- strlen(server->agent_remote_id), server->agent_remote_id);
- if (r < 0)
- return r;
- }
-
- options[*offset] = code;
- options[*offset + 1] = current_offset - *offset - 2;
- assert(current_offset - *offset - 2 <= UINT8_MAX);
- *offset = current_offset;
- break;
- }
default:
return dhcp_option_append_tlv(options, size, offset, code, optlen, optval);
}
diff --git a/src/libsystemd-network/dhcp-relay-downstream.c b/src/libsystemd-network/dhcp-relay-downstream.c
new file mode 100644
index 0000000000..dd825d02a2
--- /dev/null
+++ b/src/libsystemd-network/dhcp-relay-downstream.c
@@ -0,0 +1,503 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-event.h"
+
+#include "dhcp-message.h"
+#include "dhcp-protocol.h"
+#include "dhcp-relay-internal.h"
+#include "errno-util.h"
+#include "ether-addr-util.h"
+#include "fd-util.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "iovec-util.h"
+#include "siphash24.h"
+#include "socket-util.h"
+
+int sd_dhcp_relay_downstream_set_circuit_id(sd_dhcp_relay_interface *interface, const struct iovec *iov) {
+ assert_return(interface, -EINVAL);
+ assert_return(!interface->upstream, -EINVAL);
+ assert_return(!sd_dhcp_relay_interface_is_running(interface), -EBUSY);
+
+ return iovec_done_and_memdup(&interface->circuit_id, iov);
+}
+
+int sd_dhcp_relay_downstream_set_virtual_subnet_selection(sd_dhcp_relay_interface *interface, const struct iovec *iov) {
+ assert_return(interface, -EINVAL);
+ assert_return(!interface->upstream, -EINVAL);
+ assert_return(!sd_dhcp_relay_interface_is_running(interface), -EBUSY);
+
+ return iovec_done_and_memdup(&interface->vss, iov);
+}
+
+int downstream_set_extra_options(sd_dhcp_relay_interface *interface, TLV *options) {
+ assert(interface);
+ assert(!interface->upstream);
+ assert(!sd_dhcp_relay_interface_is_running(interface));
+
+ return unref_and_replace_new_ref(interface->extra_options, options, tlv_ref, tlv_unref);
+}
+
+int sd_dhcp_relay_downstream_set_gateway_address(sd_dhcp_relay_interface *interface, const struct in_addr *address) {
+ assert_return(interface, -EINVAL);
+ assert_return(!interface->upstream, -EINVAL);
+ assert_return(!sd_dhcp_relay_interface_is_running(interface), -EBUSY);
+
+ if (address)
+ interface->gateway_address = *address;
+ else
+ interface->gateway_address = (struct in_addr) {};
+
+ return 0;
+}
+
+static void downstream_hash_func(const sd_dhcp_relay_interface *interface, struct siphash *state) {
+ int b;
+
+ assert(interface);
+ assert(!interface->upstream);
+ assert(state);
+
+ siphash24_compress_typesafe(interface->gateway_address, state);
+
+ b = iovec_is_set(&interface->circuit_id);
+ siphash24_compress_typesafe(b, state);
+ if (b)
+ siphash24_compress_iovec(&interface->circuit_id, state);
+
+ b = iovec_is_set(&interface->vss);
+ siphash24_compress_typesafe(b, state);
+ if (b)
+ siphash24_compress_iovec(&interface->vss, state);
+}
+
+static int downstream_compare_func(const sd_dhcp_relay_interface *a, const sd_dhcp_relay_interface *b) {
+ int r;
+
+ assert(a);
+ assert(!a->upstream);
+ assert(b);
+ assert(!b->upstream);
+
+ r = CMP(a->gateway_address.s_addr, b->gateway_address.s_addr);
+ if (r != 0)
+ return r;
+
+ r = iovec_memcmp(&a->circuit_id, &b->circuit_id);
+ if (r != 0)
+ return r;
+
+ return iovec_memcmp(&a->vss, &b->vss);
+}
+
+DEFINE_PRIVATE_HASH_OPS(
+ downstream_hash_ops,
+ sd_dhcp_relay_interface,
+ downstream_hash_func,
+ downstream_compare_func);
+
+int downstream_register(sd_dhcp_relay_interface *interface) {
+ assert(interface);
+ assert(interface->relay);
+ assert(!interface->upstream);
+ assert(in4_addr_is_set(&interface->address));
+ assert(in4_addr_is_set(&interface->gateway_address));
+ assert(!sd_dhcp_relay_interface_is_running(interface));
+
+ /* Do not use a Set; otherwise, we cannot deduplicate entries. */
+ return hashmap_ensure_put(&interface->relay->downstream_interfaces, &downstream_hash_ops, interface, interface);
+}
+
+void downstream_unregister(sd_dhcp_relay_interface *interface) {
+ assert(interface);
+ assert(interface->relay);
+ assert(!interface->upstream);
+
+ hashmap_remove_value(interface->relay->downstream_interfaces, interface, interface);
+}
+
+void downstream_done(sd_dhcp_relay_interface *interface) {
+ assert(interface);
+ assert(!interface->upstream);
+
+ downstream_unregister(interface);
+ iovec_done(&interface->circuit_id);
+ iovec_done(&interface->vss);
+ interface->extra_options = tlv_unref(interface->extra_options);
+}
+
+int downstream_get(sd_dhcp_relay *relay, sd_dhcp_message *message, sd_dhcp_relay_interface **ret) {
+ int r;
+
+ assert(relay);
+ assert(message);
+
+ /* RFC 3046 section 2.2:
+ * DHCP servers claiming to support the Relay Agent Information option SHALL echo the entire contents
+ * of the Relay Agent Information option in all replies.
+ *
+ * So, first try to find the suitable downstream interface by the gateway address and circuit ID in
+ * the reply message. */
+ sd_dhcp_relay_interface key = {
+ .gateway_address.s_addr = message->header.giaddr,
+ };
+
+ _cleanup_(tlv_unrefp) TLV *agent_info = NULL;
+ r = dhcp_message_get_option_sub_tlv(
+ message,
+ SD_DHCP_OPTION_RELAY_AGENT_INFORMATION,
+ TLV_DHCP4_SUBOPTION,
+ &agent_info);
+ if (r < 0 && r != -ENODATA)
+ return r;
+
+ if (agent_info) {
+ r = tlv_get(agent_info, SD_DHCP_RELAY_AGENT_CIRCUIT_ID, &key.circuit_id);
+ if (r < 0 && r != -ENODATA)
+ return r;
+
+ r = tlv_get(agent_info, SD_DHCP_RELAY_AGENT_VIRTUAL_SUBNET_SELECTION, &key.vss);
+ if (r < 0 && r != -ENODATA)
+ return r;
+ }
+
+ sd_dhcp_relay_interface *interface = hashmap_get(relay->downstream_interfaces, &key);
+ if (!interface) {
+ /* Some DHCP servers may not understand the Relay Agent Information option and may not echo
+ * it back. To support this case, we fall back to finding a suitable downstream interface
+ * using only the gateway address. Note that if the downstream network uses VRF or the Link
+ * Selection sub-option, multiple interfaces may share the same gateway address. In such
+ * cases, we cannot reliably determine the correct downstream interface, so we must drop the
+ * packet. */
+ sd_dhcp_relay_interface *i;
+ HASHMAP_FOREACH(i, relay->downstream_interfaces) {
+ if (i->gateway_address.s_addr != message->header.giaddr)
+ continue;
+
+ if (interface)
+ /* multiple interfaces have the same gateway address?? */
+ return -ENOTUNIQ;
+
+ interface = i;
+ }
+ }
+ if (!interface)
+ return -ENODEV;
+
+ assert(!interface->upstream);
+ assert(interface->io_event_source);
+
+ if (ret)
+ *ret = interface;
+ return 0;
+}
+
+static int downstream_append_relay_agent_information(
+ sd_dhcp_relay_interface *interface,
+ sd_dhcp_message *message,
+ const struct in_pktinfo *pktinfo) {
+
+ int r;
+
+ assert(interface);
+ assert(interface->relay);
+ assert(!interface->upstream);
+ assert(message);
+
+ _cleanup_(tlv_done) TLV tlv = TLV_INIT(TLV_DHCP4_SUBOPTION);
+
+ /* First, set per-interface options. */
+ if (iovec_is_set(&interface->circuit_id)) {
+ r = tlv_append_iov(&tlv, SD_DHCP_RELAY_AGENT_CIRCUIT_ID, &interface->circuit_id);
+ if (r < 0)
+ return r;
+ }
+
+ if (iovec_is_set(&interface->vss)) {
+ r = tlv_append_iov(&tlv, SD_DHCP_RELAY_AGENT_VIRTUAL_SUBNET_SELECTION, &interface->vss);
+ if (r < 0)
+ return r;
+ }
+
+ if (!in4_addr_equal(&interface->address, &interface->gateway_address)) {
+ /* RFC 3527 section 3
+ * The link-selection sub-option is used by any DHCP relay agent that desires to specify a
+ * subnet/link for a DHCP client request that it is relaying but needs the subnet/link
+ * specification to be different from the IP address the DHCP server should use when
+ * communicating with the relay agent. */
+ r = tlv_append(&tlv, SD_DHCP_RELAY_AGENT_LINK_SELECTION, sizeof(struct in_addr), &interface->address);
+ if (r < 0)
+ return r;
+ }
+
+ r = tlv_append_tlv(&tlv, interface->extra_options);
+ if (r < 0)
+ return r;
+
+ /* Then, set agent-wide options. */
+ if (iovec_is_set(&interface->relay->remote_id)) {
+ r = tlv_append_iov(&tlv, SD_DHCP_RELAY_AGENT_REMOTE_ID, &interface->relay->remote_id);
+ if (r < 0)
+ return r;
+ }
+
+ if (interface->relay->server_identifier_override) {
+ /* RFC 5107 section 1:
+ * This DHCP relay agent suboption, Server Identifier Override, allows the relay agent to
+ * tell the DHCP server what value to place into the Server Identifier option. Using this,
+ * the relay agent can force a host in RENEWING state to send DHCPREQUEST messages to the
+ * relay agent instead of directly to the server. */
+ r = tlv_append(&tlv, SD_DHCP_RELAY_AGENT_SERVER_IDENTIFIER_OVERRIDE, sizeof(struct in_addr), &interface->address);
+ if (r < 0)
+ return r;
+
+ /* RFC 5107 section 4:
+ * DHCP relay agents implementing this suboption SHOULD also implement and use the DHCPv4
+ * Relay Agent Flags Suboption in order to specify whether the DHCP relay agent received the
+ * original message as a broadcast or unicast. */
+ uint8_t flags = 0;
+ SET_FLAG(flags, DHCP_RELAY_AGENT_FLAG_UNICAST,
+ pktinfo &&
+ pktinfo->ipi_addr.s_addr != INADDR_BROADCAST &&
+ pktinfo->ipi_addr.s_addr != interface->subnet_broadcast.s_addr);
+ r = tlv_append(&tlv, SD_DHCP_RELAY_AGENT_FLAGS, sizeof(uint8_t), &flags);
+ if (r < 0)
+ return r;
+ }
+
+ r = tlv_append_tlv(&tlv, interface->relay->extra_options);
+ if (r < 0)
+ return r;
+
+ if (tlv_isempty(&tlv))
+ return 0;
+
+ return dhcp_message_append_option_sub_tlv(message, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION, &tlv);
+}
+
+int downstream_process_message(
+ sd_dhcp_relay_interface *interface,
+ const struct iovec *iov,
+ const struct in_pktinfo *pktinfo) {
+
+ int r;
+
+ assert(interface);
+ assert(interface->relay);
+ assert(!interface->upstream);
+ assert(in4_addr_is_set(&interface->address));
+ assert(in4_addr_is_set(&interface->gateway_address));
+ assert(iov);
+
+ _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *message = NULL;
+ r = dhcp_message_parse(
+ iov,
+ BOOTREQUEST,
+ /* xid= */ NULL,
+ ARPHRD_NONE,
+ /* hw_addr= */ NULL,
+ &message);
+ if (r < 0)
+ return r;
+
+ /* RFC 1542 section 4.1.1:
+ * The relay agent MUST silently discard BOOTREQUEST messages whose 'hops' field exceeds the value 16. */
+ if (message->header.hops >= 16)
+ return 0;
+ message->header.hops++;
+
+ /* RFC 3046 section 2.1.1:
+ * Relay agents configured to add a Relay Agent option which receive a client DHCP packet with a
+ * nonzero giaddr SHALL discard the packet if the giaddr spoofs a giaddr address implemented by the
+ * local agent itself. */
+ if (message->header.giaddr != INADDR_ANY) {
+ sd_dhcp_relay_interface *i;
+ HASHMAP_FOREACH(i, interface->relay->interfaces) {
+ if (i->upstream)
+ continue;
+ if (message->header.giaddr == i->address.s_addr ||
+ message->header.giaddr == i->gateway_address.s_addr)
+ return -EBADMSG;
+ }
+ }
+
+ /* RFC 1542 section 4.1.1:
+ * If the relay agent does decide to relay the request, it MUST examine the 'giaddr' ("gateway" IP
+ * address) field. If this field is zero, the relay agent MUST fill this field with the IP address of
+ * the interface on which the request was received. (snip) If the 'giaddr' field contains some
+ * non-zero value, the 'giaddr' field MUST NOT be modified.
+ *
+ * RFC 3046 section 2.1.1:
+ * the relay agent SHALL forward any received DHCP packet with a valid non-zero giaddr WITHOUT adding
+ * any relay agent options. Per RFC 2131, it shall also NOT modify the giaddr value.
+ *
+ * Therefore, we set giaddr and the Relay Agent Information option here only when the giaddr in the
+ * received message is zero. */
+ if (message->header.giaddr == INADDR_ANY) {
+ message->header.giaddr = interface->gateway_address.s_addr;
+
+ /* RFC 3046 section 2.1:
+ * Relay agents receiving a DHCP packet from an untrusted circuit with giaddr set to zero
+ * (indicating that they are the first-hop router) but with a Relay Agent Information option
+ * already present in the packet SHALL discard the packet and increment an error count. */
+ if (dhcp_message_has_option(message, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION))
+ return -EBADMSG;
+
+ r = downstream_append_relay_agent_information(interface, message, pktinfo);
+ if (r < 0)
+ return r;
+ }
+
+ log_dhcp_relay_interface(interface, "Received BOOTREQUEST (0x%"PRIx32")", be32toh(message->header.xid));
+
+ sd_dhcp_relay_interface *upstream;
+ r = upstream_get(interface->relay, &upstream);
+ if (r < 0)
+ return r;
+
+ return upstream_send_message(upstream, message);
+}
+
+static int downstream_acquire_raw_socket(sd_dhcp_relay_interface *interface) {
+ int r;
+
+ assert(interface);
+ assert(!interface->upstream);
+
+ if (interface->socket_fd >= 0)
+ /* When a socket fd is given externally, unconditionally use it. */
+ return interface->socket_fd;
+
+ if (interface->raw_socket_fd >= 0)
+ /* Already opened. */
+ return interface->raw_socket_fd;
+
+ /* This is a send-only socket, hence it is opened with protocol=0, and do not call bind().
+ * The interface binding will be done on send. */
+ _cleanup_close_ int fd = RET_NERRNO(socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, /* protocol= */ 0));
+ if (fd < 0)
+ return fd;
+
+ r = setsockopt_int(fd, SOL_SOCKET, SO_PRIORITY, tos_to_priority(interface->ip_service_type));
+ if (r < 0)
+ return r;
+
+ return interface->raw_socket_fd = TAKE_FD(fd);
+}
+
+static int downstream_send_l2_unicast(
+ sd_dhcp_relay_interface *interface,
+ sd_dhcp_message *message,
+ const struct hw_addr_data *hw_addr) {
+
+ int r;
+
+ assert(interface);
+ assert(!interface->upstream);
+ assert(message);
+ assert(message->header.yiaddr != INADDR_ANY);
+ assert(!hw_addr_is_null(hw_addr));
+
+ int fd = downstream_acquire_raw_socket(interface);
+ if (fd < 0)
+ return fd;
+
+ r = dhcp_message_send_raw(
+ message,
+ fd,
+ interface->ifindex,
+ interface->address.s_addr,
+ interface->port,
+ hw_addr,
+ message->header.yiaddr,
+ DHCP_PORT_CLIENT,
+ interface->ip_service_type);
+ if (r < 0)
+ return r;
+
+ log_dhcp_relay_interface(interface, "Forwarded BOOTREPLY (0x%"PRIx32") to %s (L2 unicast).",
+ be32toh(message->header.xid),
+ IN4_ADDR_TO_STRING(&(struct in_addr) { .s_addr = message->header.yiaddr }));
+ return 0;
+}
+
+static int downstream_send_udp(
+ sd_dhcp_relay_interface *interface,
+ sd_dhcp_message *message,
+ be32_t address) {
+
+ int r;
+
+ assert(interface);
+ assert(!interface->upstream);
+ assert(message);
+ assert(address != INADDR_ANY);
+
+ int fd = sd_event_source_get_io_fd(interface->io_event_source);
+ if (fd < 0)
+ return fd;
+
+ r = dhcp_message_send_udp(
+ message,
+ fd,
+ interface->address.s_addr,
+ address,
+ DHCP_PORT_CLIENT);
+ if (r < 0)
+ return r;
+
+ log_dhcp_relay_interface(interface, "Forwarded BOOTREPLY (0x%"PRIx32") to %s (UDP).",
+ be32toh(message->header.xid),
+ IN4_ADDR_TO_STRING(&(struct in_addr) { .s_addr = address }));
+ return 0;
+}
+
+int downstream_send_message(sd_dhcp_relay_interface *interface, sd_dhcp_message *message) {
+ int r;
+
+ assert(interface);
+ assert(!interface->upstream);
+ assert(message);
+ assert(message->header.op == BOOTREPLY);
+
+ /* See RFC 2131 Section 4.1
+ *
+ * (Note, we are a relay agent, hence conditions for giaddr in the statements are ignored.) */
+
+ uint8_t type;
+ r = dhcp_message_get_option_u8(message, SD_DHCP_OPTION_MESSAGE_TYPE, &type);
+ if (r < 0)
+ return r;
+
+ /* the server broadcasts any DHCPNAK messages to 0xffffffff. */
+ if (type == DHCP_NAK)
+ return downstream_send_udp(interface, message, INADDR_BROADCAST);
+
+ /* If (...) the ’ciaddr’ field is nonzero, then the server unicasts DHCPOFFER and DHCPACK messages
+ * to the address in ’ciaddr’. */
+ if (message->header.ciaddr != INADDR_ANY)
+ return downstream_send_udp(interface, message, message->header.ciaddr);
+
+ /* If (...) ’ciaddr’ is zero, and the broadcast bit is set, then the server broadcasts DHCPOFFER
+ * and DHCPACK messages to 0xffffffff.
+ *
+ * (Note, even the broadcast flag is unset, we may not know the client hardware address, e.g.
+ * InfiniBand. In that case, we cannot unicast in the below, so need to broadcast. Also, for other
+ * message types we do not support, also broadcast if 'yiaddr' is zero.) */
+ struct hw_addr_data hw_addr = {};
+ if (!dhcp_message_has_broadcast_flag(message) &&
+ message->header.yiaddr != INADDR_ANY) {
+ r = dhcp_message_get_hw_addr(message, &hw_addr);
+ if (r < 0)
+ return r;
+ }
+
+ if (hw_addr_is_null(&hw_addr))
+ return downstream_send_udp(interface, message, INADDR_BROADCAST);
+
+ /* If the broadcast bit is not set (...) and ’ciaddr’ is zero, then the server unicasts DHCPOFFER
+ * and DHCPACK messages to the client’s hardware address and ’yiaddr’ address. */
+ return downstream_send_l2_unicast(interface, message, &hw_addr);
+}
diff --git a/src/libsystemd-network/dhcp-relay-interface.c b/src/libsystemd-network/dhcp-relay-interface.c
new file mode 100644
index 0000000000..218f56fc04
--- /dev/null
+++ b/src/libsystemd-network/dhcp-relay-interface.c
@@ -0,0 +1,334 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "dhcp-protocol.h"
+#include "dhcp-relay-internal.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "iovec-util.h"
+#include "socket-util.h"
+#include "string-util.h"
+
+static sd_dhcp_relay_interface* dhcp_relay_interface_free(sd_dhcp_relay_interface *interface) {
+ if (!interface)
+ return NULL;
+
+ assert(interface->relay);
+
+ sd_event_source_disable_unref(interface->io_event_source);
+ safe_close(interface->socket_fd);
+ safe_close(interface->raw_socket_fd);
+
+ if (interface->upstream)
+ upstream_done(interface);
+ else
+ downstream_done(interface);
+
+ hashmap_remove_value(interface->relay->interfaces, INT_TO_PTR(interface->ifindex), interface);
+ sd_dhcp_relay_unref(interface->relay);
+
+ free(interface->ifname);
+ return mfree(interface);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_relay_interface, sd_dhcp_relay_interface, dhcp_relay_interface_free);
+
+int sd_dhcp_relay_add_interface(sd_dhcp_relay *relay, int ifindex, int is_upstream, sd_dhcp_relay_interface **ret) {
+ int r;
+
+ assert_return(relay, -EINVAL);
+ assert_return(ifindex > 0 || (ifindex == DHCP_RELAY_IFINDEX_UNBOUND && !!is_upstream), -EINVAL);
+ assert_return(ret, -EINVAL);
+
+ _cleanup_(sd_dhcp_relay_interface_unrefp) sd_dhcp_relay_interface *interface = new(sd_dhcp_relay_interface, 1);
+ if (!interface)
+ return -ENOMEM;
+
+ /* RFC 1542 section 5.4:
+ * The server SHOULD next check the 'giaddr' field. If this field is non-zero, the server SHOULD send
+ * the BOOTREPLY as an IP unicast to the IP address identified in the 'giaddr' field. The UDP
+ * destination port MUST be set to BOOTPS (67).
+ *
+ * Hence, the relay agent needs to use DHCP_PORT_SERVER (67) for both source and destination port. */
+ *interface = (sd_dhcp_relay_interface) {
+ .n_ref = 1,
+ .relay = sd_dhcp_relay_ref(relay),
+ .upstream = !!is_upstream,
+ .ifindex = ifindex,
+ .port = DHCP_PORT_SERVER,
+ .socket_fd = -EBADF,
+ .raw_socket_fd = -EBADF,
+ .ip_service_type = IPTOS_CLASS_CS6, /* Defaults to CS6 (Internetwork Control). */
+ };
+
+ r = hashmap_ensure_put(&relay->interfaces, NULL, INT_TO_PTR(interface->ifindex), interface);
+ if (r < 0)
+ return r;
+
+ *ret = TAKE_PTR(interface);
+ return 0;
+}
+
+int sd_dhcp_relay_interface_set_ifname(sd_dhcp_relay_interface *interface, const char *ifname) {
+ assert_return(interface, -EINVAL);
+
+ return free_and_strdup(&interface->ifname, ifname);
+}
+
+int sd_dhcp_relay_interface_get_ifname(sd_dhcp_relay_interface *interface, const char **ret) {
+ int r;
+
+ assert_return(interface, -EINVAL);
+
+ r = get_ifname(interface->ifindex, &interface->ifname);
+ if (r < 0)
+ return r;
+
+ if (ret)
+ *ret = interface->ifname;
+
+ return 0;
+}
+
+int sd_dhcp_relay_interface_set_address(sd_dhcp_relay_interface *interface, const struct in_addr *address, uint8_t prefixlen) {
+ assert_return(interface, -EINVAL);
+ assert_return(!sd_dhcp_relay_interface_is_running(interface), -EBUSY);
+ assert_return(prefixlen <= sizeof(struct in_addr) * 8, -EINVAL);
+
+ if (address)
+ interface->address = *address;
+ else
+ interface->address = (struct in_addr) {};
+
+ if (in4_addr_is_set(&interface->address)) {
+ interface->address_prefixlen = prefixlen;
+
+ struct in_addr netmask;
+ in4_addr_prefixlen_to_netmask(&netmask, prefixlen);
+ interface->subnet_broadcast.s_addr = (interface->address.s_addr & netmask.s_addr) | ~netmask.s_addr;
+ } else {
+ interface->address_prefixlen = 0;
+ interface->subnet_broadcast = (struct in_addr) {};
+ }
+
+ return 0;
+}
+
+int sd_dhcp_relay_interface_get_address(sd_dhcp_relay_interface *interface, struct in_addr *ret_address, uint8_t *ret_prefixlen) {
+ assert_return(interface, -EINVAL);
+
+ if (ret_address)
+ *ret_address = interface->address;
+ if (ret_prefixlen)
+ *ret_prefixlen = interface->address_prefixlen;
+
+ return in4_addr_is_set(&interface->address);
+}
+
+int sd_dhcp_relay_interface_set_port(sd_dhcp_relay_interface *interface, uint16_t port) {
+ assert_return(interface, -EINVAL);
+ assert_return(!sd_dhcp_relay_interface_is_running(interface), -EBUSY);
+
+ interface->port = port;
+ return 0;
+}
+
+int sd_dhcp_relay_interface_set_ip_service_type(sd_dhcp_relay_interface *interface, uint8_t type) {
+ assert_return(interface, -EINVAL);
+ assert_return(!sd_dhcp_relay_interface_is_running(interface), -EBUSY);
+
+ interface->ip_service_type = type;
+ return 0;
+}
+
+int sd_dhcp_relay_interface_is_running(sd_dhcp_relay_interface *interface) {
+ return interface && sd_event_source_get_enabled(interface->io_event_source, /* ret= */ NULL) > 0;
+}
+
+static int interface_open_socket(sd_dhcp_relay_interface *interface) {
+ int r;
+
+ assert(interface);
+
+ _cleanup_close_ int fd = RET_NERRNO(socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0));
+ if (fd < 0)
+ return fd;
+
+ if (interface->ifindex > 0) {
+ r = socket_bind_to_ifindex(fd, interface->ifindex);
+ if (r < 0)
+ return r;
+ }
+
+ r = setsockopt_int(fd, SOL_SOCKET, SO_REUSEADDR, true);
+ if (r < 0)
+ return r;
+
+ r = setsockopt_int(fd, SOL_SOCKET, SO_BROADCAST, true);
+ if (r < 0)
+ return r;
+
+ r = setsockopt_int(fd, SOL_SOCKET, SO_PRIORITY, tos_to_priority(interface->ip_service_type));
+ if (r < 0)
+ return r;
+
+ r = setsockopt_int(fd, IPPROTO_IP, IP_TOS, interface->ip_service_type);
+ if (r < 0)
+ return r;
+
+ r = setsockopt_int(fd, IPPROTO_IP, IP_PKTINFO, true);
+ if (r < 0)
+ return r;
+
+ union sockaddr_union sa = {
+ .in.sin_family = AF_INET,
+ .in.sin_port = htobe16(interface->port),
+ .in.sin_addr.s_addr = INADDR_ANY,
+ };
+
+ if (bind(fd, &sa.sa, sizeof(sa.in)) < 0)
+ return -errno;
+
+ return TAKE_FD(fd);
+}
+
+static int interface_receive_message(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ sd_dhcp_relay_interface *interface = ASSERT_PTR(userdata);
+ int r;
+
+ assert(fd >= 0);
+
+ ssize_t buflen = next_datagram_size_fd(fd);
+ if (ERRNO_IS_NEG_TRANSIENT(buflen) || ERRNO_IS_NEG_DISCONNECT(buflen))
+ return 0;
+ if (buflen < 0) {
+ log_dhcp_relay_interface_errno(
+ interface, buflen,
+ "Failed to determine datagram size to read, ignoring: %m");
+ return 0;
+ }
+
+ _cleanup_free_ void *buf = malloc0(buflen);
+ if (!buf)
+ return log_oom_debug();
+
+ CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct in_pktinfo))) control = {};
+ struct msghdr msg = {
+ .msg_iov = &IOVEC_MAKE(buf, buflen),
+ .msg_iovlen = 1,
+ .msg_control = &control,
+ .msg_controllen = sizeof(control),
+ };
+
+ ssize_t len = recvmsg_safe(fd, &msg, MSG_DONTWAIT);
+ if (ERRNO_IS_NEG_TRANSIENT(len) || ERRNO_IS_NEG_DISCONNECT(len))
+ return 0;
+ if (len < 0) {
+ log_dhcp_relay_interface_errno(
+ interface, len,
+ "Could not receive message, ignoring: %m");
+ return 0;
+ }
+
+ if (interface->upstream)
+ r = upstream_process_message(
+ interface,
+ &IOVEC_MAKE(buf, len),
+ CMSG_FIND_DATA(&msg, IPPROTO_IP, IP_PKTINFO, struct in_pktinfo));
+ else
+ r = downstream_process_message(
+ interface,
+ &IOVEC_MAKE(buf, len),
+ CMSG_FIND_DATA(&msg, IPPROTO_IP, IP_PKTINFO, struct in_pktinfo));
+ if (r < 0)
+ log_dhcp_relay_interface_errno(
+ interface, r,
+ "Could not process message, ignoring: %m");
+
+ return 0;
+}
+
+int sd_dhcp_relay_interface_start(sd_dhcp_relay_interface *interface) {
+ int r;
+
+ assert_return(interface, -EINVAL);
+ assert_return(interface->relay, -ESTALE);
+ assert_return(interface->relay->event, -EINVAL);
+
+ if (in4_addr_is_null(&interface->relay->server_address) ||
+ (!interface->upstream && in4_addr_is_null(&interface->address)) ||
+ (!interface->upstream && in4_addr_is_null(&interface->gateway_address)))
+ return -EADDRNOTAVAIL;
+
+ if (sd_event_source_get_enabled(interface->io_event_source, /* ret= */ NULL) > 0)
+ return 0; /* Already started. */
+
+ _cleanup_close_ int fd_close = -EBADF;
+ int fd;
+ if (interface->socket_fd >= 0)
+ /* When a socket fd is given externally, unconditionally use it and do not close the socket
+ * even if we fail to set up the event source. */
+ fd = interface->socket_fd;
+ else {
+ /* Otherwise, open a new socket. */
+ fd = fd_close = interface_open_socket(interface);
+ if (fd < 0)
+ return fd;
+ }
+
+ _cleanup_(sd_event_source_unrefp) sd_event_source *s = NULL;
+ r = sd_event_add_io(interface->relay->event, &s, fd, EPOLLIN,
+ interface_receive_message, interface);
+ if (r < 0)
+ return r;
+
+ r = sd_event_source_set_priority(s, interface->relay->event_priority);
+ if (r < 0)
+ return r;
+
+ const char *name, *description;
+ if (sd_dhcp_relay_interface_get_ifname(interface, &name) >= 0)
+ description = strjoina("dhcp-relay-interface-io-event-source-", name);
+ else
+ description = "dhcp-relay-interface-io-event-source";
+ (void) sd_event_source_set_description(s, description);
+
+ if (fd_close >= 0) {
+ r = sd_event_source_set_io_fd_own(s, true);
+ if (r < 0)
+ return r;
+ TAKE_FD(fd_close);
+ }
+
+ /* This may potentially fail, in which case the event source should be discarded. */
+ if (interface->upstream)
+ r = upstream_register(interface);
+ else
+ r = downstream_register(interface);
+ if (r < 0)
+ return r;
+
+ sd_event_source_disable_unref(interface->io_event_source);
+ interface->io_event_source = TAKE_PTR(s);
+ return 0;
+}
+
+int sd_dhcp_relay_interface_stop(sd_dhcp_relay_interface *interface) {
+ if (!interface)
+ return 0;
+
+ interface->raw_socket_fd = safe_close(interface->raw_socket_fd);
+ interface->io_event_source = sd_event_source_disable_unref(interface->io_event_source);
+
+ if (interface->upstream)
+ upstream_unregister(interface);
+ else
+ downstream_unregister(interface);
+ return 0;
+}
diff --git a/src/libsystemd-network/dhcp-relay-internal.h b/src/libsystemd-network/dhcp-relay-internal.h
new file mode 100644
index 0000000000..feb08a606c
--- /dev/null
+++ b/src/libsystemd-network/dhcp-relay-internal.h
@@ -0,0 +1,114 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-dhcp-relay.h"
+
+#include "dhcp-message.h"
+#include "ether-addr-util.h"
+#include "network-common.h"
+#include "sd-forward.h"
+#include "tlv-util.h"
+
+#define DHCP_RELAY_IFINDEX_UNBOUND (-100)
+
+struct sd_dhcp_relay {
+ unsigned n_ref;
+
+ sd_event *event;
+ int event_priority;
+
+ Hashmap *interfaces; /* All interfaces by their ifindex. */
+ Prioq *upstream_interfaces; /* Active upstream interfaces by their priorities. */
+ Hashmap *downstream_interfaces; /* Active downstream interfaces by their gateway address, circuit ID, and VSS. */
+
+ struct in_addr server_address;
+ uint16_t server_port;
+
+ /* Global Relay Agent Information option (82) */
+ struct iovec remote_id; /* Agent Remote ID Sub-option (2) */
+ bool server_identifier_override; /* Relay Agent Flags (10) and Server Identifier Override Sub-option (11) */
+ TLV *extra_options;
+};
+
+struct sd_dhcp_relay_interface {
+ unsigned n_ref;
+
+ sd_dhcp_relay *relay;
+ bool upstream;
+
+ int ifindex;
+ char *ifname;
+
+ /* The address used for:
+ * - the source IP of forwarded packets (both downstream and upstream),
+ * - the Server Identifier Override Sub-option (when sd_dhcp_relay.server_identifier_override is true),
+ * - the Link Selection Sub-option (when address != gateway_address).
+ * Typically, this is an address of the interface itself, but we can specify an address of another
+ * interface (e.g., for IP unnumbered setups). */
+ struct in_addr address;
+ uint8_t address_prefixlen;
+ /* subnet-directed broadcast address, e.g. 192.0.2.255 when address is 192.0.2.1/24. */
+ struct in_addr subnet_broadcast;
+ uint16_t port;
+
+ uint8_t ip_service_type; /* a.k.a. TOS */
+ int socket_fd; /* socket fd set externally, used by unit tests */
+ int raw_socket_fd; /* send-only raw socket fd, used on sending L2 unicast message. */
+ sd_event_source *io_event_source;
+
+ /* Mutually exclusive fields depending on the 'upstream' boolean */
+ union {
+ /* Upstream specific */
+ struct {
+ int priority;
+ unsigned priority_idx;
+ };
+
+ /* Downstream specific */
+ struct {
+ /* The address set in the giaddr field of the DHCP message header. Typically, it is
+ * the same as 'address' above, but we can specify a different address, and it does
+ * not need to be an address assigned to the interface. */
+ struct in_addr gateway_address;
+
+ /* Per-interface Relay Agent Information option (82) */
+ struct iovec circuit_id; /* Agent Circuit ID Sub-option (1) */
+ struct iovec vss; /* DHCPv4 Virtual Subnet Selection Sub-Option (151) */
+ TLV *extra_options;
+ };
+ };
+};
+
+int dhcp_relay_set_extra_options(sd_dhcp_relay *relay, TLV *options);
+
+int downstream_set_extra_options(sd_dhcp_relay_interface *interface, TLV *options);
+int downstream_register(sd_dhcp_relay_interface *interface);
+void downstream_unregister(sd_dhcp_relay_interface *interface);
+void downstream_done(sd_dhcp_relay_interface *interface);
+int downstream_get(sd_dhcp_relay *relay, sd_dhcp_message *message, sd_dhcp_relay_interface **ret);
+int downstream_process_message(
+ sd_dhcp_relay_interface *interface,
+ const struct iovec *iov,
+ const struct in_pktinfo *pktinfo);
+int downstream_send_message(sd_dhcp_relay_interface *interface, sd_dhcp_message *message);
+
+int upstream_register(sd_dhcp_relay_interface *interface);
+void upstream_unregister(sd_dhcp_relay_interface *interface);
+void upstream_done(sd_dhcp_relay_interface *interface);
+int upstream_get(sd_dhcp_relay *relay, sd_dhcp_relay_interface **ret);
+int upstream_process_message(
+ sd_dhcp_relay_interface *interface,
+ const struct iovec *iov,
+ const struct in_pktinfo *pktinfo);
+int upstream_send_message(sd_dhcp_relay_interface *interface, sd_dhcp_message *message);
+
+#define log_dhcp_relay_interface_errno(interface, error, fmt, ...) \
+ log_interface_prefix_full_errno( \
+ "DHCPv4 relay: ", \
+ sd_dhcp_relay_interface, interface, \
+ error, fmt, ##__VA_ARGS__)
+#define log_dhcp_relay_interface(interface, fmt, ...) \
+ log_interface_prefix_full_errno_zerook( \
+ "DHCPv4 relay: ", \
+ sd_dhcp_relay_interface, interface, \
+ 0, fmt, ##__VA_ARGS__)
diff --git a/src/libsystemd-network/dhcp-relay-upstream.c b/src/libsystemd-network/dhcp-relay-upstream.c
new file mode 100644
index 0000000000..f1b4189153
--- /dev/null
+++ b/src/libsystemd-network/dhcp-relay-upstream.c
@@ -0,0 +1,150 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-event.h"
+
+#include "dhcp-message.h"
+#include "dhcp-protocol.h"
+#include "dhcp-relay-internal.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "prioq.h"
+
+int sd_dhcp_relay_upstream_set_priority(sd_dhcp_relay_interface *interface, int priority) {
+ assert_return(interface, -EINVAL);
+ assert_return(interface->upstream, -EINVAL);
+ assert_return(!sd_dhcp_relay_interface_is_running(interface), -EBUSY);
+
+ interface->priority = priority;
+ return 0;
+}
+
+static int upstream_compare_func(const sd_dhcp_relay_interface *a, const sd_dhcp_relay_interface *b) {
+ assert(a);
+ assert(a->upstream);
+ assert(b);
+ assert(b->upstream);
+
+ /* Higher priority first */
+ return CMP(b->priority, a->priority);
+}
+
+int upstream_register(sd_dhcp_relay_interface *interface) {
+ assert(interface);
+ assert(interface->relay);
+ assert(interface->upstream);
+ assert(!sd_dhcp_relay_interface_is_running(interface));
+
+ interface->priority_idx = PRIOQ_IDX_NULL;
+ return prioq_ensure_put(&interface->relay->upstream_interfaces, upstream_compare_func, interface, &interface->priority_idx);
+}
+
+void upstream_unregister(sd_dhcp_relay_interface *interface) {
+ assert(interface);
+ assert(interface->relay);
+ assert(interface->upstream);
+
+ (void) prioq_remove(interface->relay->upstream_interfaces, interface, &interface->priority_idx);
+}
+
+void upstream_done(sd_dhcp_relay_interface *interface) {
+ upstream_unregister(interface);
+}
+
+int upstream_get(sd_dhcp_relay *relay, sd_dhcp_relay_interface **ret) {
+ sd_dhcp_relay_interface *interface = prioq_peek(relay->upstream_interfaces);
+ if (!interface)
+ return -ENETDOWN;
+
+ assert(interface->upstream);
+ assert(interface->io_event_source);
+
+ if (ret)
+ *ret = interface;
+ return 0;
+}
+
+int upstream_process_message(
+ sd_dhcp_relay_interface *interface,
+ const struct iovec *iov,
+ const struct in_pktinfo *pktinfo) {
+
+ int r;
+
+ assert(interface);
+ assert(interface->relay);
+ assert(interface->upstream);
+ assert(iov);
+
+ if (pktinfo && pktinfo->ipi_ifindex > 0 &&
+ interface->ifindex < 0 &&
+ hashmap_contains(interface->relay->interfaces, INT_TO_PTR(pktinfo->ipi_ifindex)))
+ return 0; /* This message is not for the catch-all interface. */
+
+ _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *message = NULL;
+ r = dhcp_message_parse(
+ iov,
+ BOOTREPLY,
+ /* xid= */ NULL,
+ ARPHRD_NONE,
+ /* hw_addr= */ NULL,
+ &message);
+ if (r < 0)
+ return r;
+
+ if (message->header.giaddr == INADDR_ANY)
+ return 0; /* Not a relay message, so it is probably not for us. */
+
+ if (pktinfo && pktinfo->ipi_addr.s_addr != message->header.giaddr)
+ return -EBADMSG;
+
+ log_dhcp_relay_interface(interface, "Received BOOTREPLY (0x%"PRIx32")", be32toh(message->header.xid));
+
+ sd_dhcp_relay_interface *downstream;
+ r = downstream_get(interface->relay, message, &downstream);
+ if (r < 0)
+ return r;
+
+ /* RFC 3046 abstract:
+ * The DHCP Server echoes the option back verbatim to the relay agent in server-to-client
+ * replies, and the relay agent strips the option before forwarding the reply to the client.
+ *
+ * RFC 3046 section 2.1:
+ * The Relay Agent Information option echoed by a server MUST be removed by either the relay
+ * agent or the trusted downstream network element which added it when forwarding a
+ * server-to-client response back to the client.
+ *
+ * Here, we do not check the contents of the option, and unconditionally remove it. */
+ dhcp_message_remove_option(message, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION);
+
+ return downstream_send_message(downstream, message);
+}
+
+int upstream_send_message(sd_dhcp_relay_interface *interface, sd_dhcp_message *message) {
+ int r;
+
+ assert(interface);
+ assert(interface->upstream);
+ assert(message);
+ assert(message->header.op == BOOTREQUEST);
+ assert(message->header.giaddr != INADDR_ANY);
+
+ int fd = sd_event_source_get_io_fd(interface->io_event_source);
+ if (fd < 0)
+ return fd;
+
+ r = dhcp_message_send_udp(
+ message,
+ fd,
+ interface->address.s_addr,
+ interface->relay->server_address.s_addr,
+ interface->relay->server_port);
+ if (r < 0)
+ return r;
+
+ log_dhcp_relay_interface(interface, "Forwarded BOOTREQUEST (0x%"PRIx32") to %s (UDP).",
+ be32toh(message->header.xid),
+ IN4_ADDR_TO_STRING(&interface->relay->server_address));
+ return 0;
+}
diff --git a/src/libsystemd-network/dhcp-server-internal.h b/src/libsystemd-network/dhcp-server-internal.h
index a8e2cbf5c7..d4caae66d4 100644
--- a/src/libsystemd-network/dhcp-server-internal.h
+++ b/src/libsystemd-network/dhcp-server-internal.h
@@ -32,14 +32,11 @@ typedef struct sd_dhcp_server {
sd_event *event;
int event_priority;
sd_event_source *receive_message;
- sd_event_source *receive_broadcast;
int fd;
int fd_raw;
- int fd_broadcast;
int ifindex;
char *ifname;
- bool bind_to_interface;
be32_t address;
be32_t netmask;
be32_t subnet;
@@ -73,11 +70,6 @@ typedef struct sd_dhcp_server {
sd_dhcp_server_callback_t callback;
void *callback_userdata;
- struct in_addr relay_target;
-
- char *agent_circuit_id;
- char *agent_remote_id;
-
int lease_dir_fd;
char *lease_file;
} sd_dhcp_server;
diff --git a/src/libsystemd-network/fuzz-dhcp-relay.c b/src/libsystemd-network/fuzz-dhcp-relay.c
new file mode 100644
index 0000000000..2c828b9548
--- /dev/null
+++ b/src/libsystemd-network/fuzz-dhcp-relay.c
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-event.h"
+
+#include "dhcp-relay-internal.h"
+#include "fd-util.h"
+#include "fuzz.h"
+#include "in-addr-util.h"
+#include "iovec-util.h"
+#include "tests.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+ union in_addr_union a;
+
+ fuzz_setup_logging();
+
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+
+ _cleanup_(sd_dhcp_relay_unrefp) sd_dhcp_relay *relay = NULL;
+ ASSERT_OK(sd_dhcp_relay_new(&relay));
+ ASSERT_OK(sd_dhcp_relay_attach_event(relay, e, SD_EVENT_PRIORITY_NORMAL));
+ ASSERT_OK(in_addr_from_string(AF_INET, "198.51.100.1", &a));
+ ASSERT_OK(sd_dhcp_relay_set_server_address(relay, &a.in));
+ ASSERT_OK(sd_dhcp_relay_set_remote_id(relay, &IOVEC_MAKE_STRING("test-remote-id")));
+ ASSERT_OK(sd_dhcp_relay_set_server_identifier_override(relay, true));
+
+ _cleanup_(sd_dhcp_relay_interface_unrefp) sd_dhcp_relay_interface *upstream = NULL;
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4242, /* is_upstream= */ true, &upstream));
+ ASSERT_OK(sd_dhcp_relay_interface_set_ifname(upstream, "test-upstream"));
+ ASSERT_OK(in_addr_from_string(AF_INET, "198.51.100.2", &a));
+ ASSERT_OK(sd_dhcp_relay_interface_set_address(upstream, &a.in, 24));
+
+ _cleanup_close_pair_ int upstream_fd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, upstream_fd));
+ upstream->socket_fd = TAKE_FD(upstream_fd[0]);
+ ASSERT_OK(sd_dhcp_relay_interface_start(upstream));
+
+ _cleanup_(sd_dhcp_relay_interface_unrefp) sd_dhcp_relay_interface *downstream = NULL;
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4343, /* is_upstream= */ false, &downstream));
+ ASSERT_OK(sd_dhcp_relay_interface_set_ifname(downstream, "test-downstream"));
+ ASSERT_OK(in_addr_from_string(AF_INET, "192.0.2.1", &a));
+ ASSERT_OK(sd_dhcp_relay_interface_set_address(downstream, &a.in, 24));
+
+ ASSERT_OK(in_addr_from_string(AF_INET, "203.0.113.1", &a));
+ ASSERT_OK(sd_dhcp_relay_downstream_set_gateway_address(downstream, &a.in));
+ ASSERT_OK(sd_dhcp_relay_downstream_set_circuit_id(downstream, &IOVEC_MAKE_STRING("test-circuit-id")));
+ ASSERT_OK(sd_dhcp_relay_downstream_set_virtual_subnet_selection(downstream, &IOVEC_MAKE_STRING("test-virtual-net")));
+
+ _cleanup_close_pair_ int downstream_fd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, downstream_fd));
+ downstream->socket_fd = TAKE_FD(downstream_fd[0]);
+ ASSERT_OK(sd_dhcp_relay_interface_start(downstream));
+
+ (void) downstream_process_message(downstream, &IOVEC_MAKE(data, size), /* pktinfo= */ NULL);
+ (void) upstream_process_message(upstream, &IOVEC_MAKE(data, size), /* pktinfo= */ NULL);
+
+ return 0;
+}
diff --git a/src/libsystemd-network/fuzz-dhcp-server-relay.c b/src/libsystemd-network/fuzz-dhcp-server-relay.c
deleted file mode 100644
index b2c881c067..0000000000
--- a/src/libsystemd-network/fuzz-dhcp-server-relay.c
+++ /dev/null
@@ -1,45 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-
-#include
-
-#include "fuzz.h"
-#include "sd-dhcp-server.c"
-
-ssize_t sendto(int __fd, const void *__buf, size_t __n, int flags, const struct sockaddr *__addr, socklen_t __addr_len) {
- return __n;
-}
-
-ssize_t sendmsg(int __fd, const struct msghdr *__message, int flags) {
- return 0;
-}
-
-int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
- _cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL;
- struct in_addr address = {.s_addr = htobe32(UINT32_C(10) << 24 | UINT32_C(1))};
- union in_addr_union relay_address;
- _cleanup_free_ uint8_t *message = NULL;
-
- if (size < sizeof(DHCPMessage))
- return 0;
-
- fuzz_setup_logging();
-
- assert_se(sd_dhcp_server_new(&server, 1) >= 0);
- assert_se(sd_dhcp_server_attach_event(server, NULL, 0) >= 0);
- assert_se(sd_dhcp_server_configure_pool(server, &address, 24, 0, 0) >= 0);
- assert_se(in_addr_from_string(AF_INET, "192.168.5.1", &relay_address) >= 0);
- assert_se(sd_dhcp_server_set_relay_target(server, &relay_address.in) >= 0);
- assert_se(sd_dhcp_server_set_bind_to_interface(server, false) >= 0);
- assert_se(sd_dhcp_server_set_relay_agent_information(server, "string:sample_circuit_id", "string:sample_remote_id") >= 0);
-
- size_t buflen = size;
- buflen += relay_agent_information_length(server->agent_circuit_id, server->agent_remote_id) + 2;
- assert_se(message = malloc(buflen));
- memcpy(message, data, size);
-
- server->fd = open("/dev/null", O_RDWR|O_CLOEXEC|O_NOCTTY);
- assert_se(server->fd >= 0);
-
- (void) dhcp_server_relay_message(server, (DHCPMessage *) message, size - sizeof(DHCPMessage), buflen);
- return 0;
-}
diff --git a/src/libsystemd-network/meson.build b/src/libsystemd-network/meson.build
index b9b8c7062e..9b36f6edf2 100644
--- a/src/libsystemd-network/meson.build
+++ b/src/libsystemd-network/meson.build
@@ -9,6 +9,9 @@ libsystemd_network_sources = files(
'dhcp-option.c',
'dhcp-packet.c',
'dhcp-protocol.c',
+ 'dhcp-relay-downstream.c',
+ 'dhcp-relay-interface.c',
+ 'dhcp-relay-upstream.c',
'dhcp-route.c',
'dhcp6-network.c',
'dhcp6-option.c',
@@ -25,6 +28,7 @@ libsystemd_network_sources = files(
'sd-dhcp-client.c',
'sd-dhcp-duid.c',
'sd-dhcp-lease.c',
+ 'sd-dhcp-relay.c',
'sd-dhcp-server-lease.c',
'sd-dhcp-server.c',
'sd-dhcp6-client.c',
@@ -92,6 +96,9 @@ executables += [
network_test_template + {
'sources' : files('test-dhcp-option.c'),
},
+ network_test_template + {
+ 'sources' : files('test-dhcp-relay.c'),
+ },
network_test_template + {
'sources' : files('test-dhcp-server.c'),
},
@@ -129,15 +136,15 @@ executables += [
network_fuzz_template + {
'sources' : files('fuzz-dhcp-client.c'),
},
+ network_fuzz_template + {
+ 'sources' : files('fuzz-dhcp-relay.c'),
+ },
network_fuzz_template + {
'sources' : files('fuzz-dhcp6-client.c'),
},
network_fuzz_template + {
'sources' : files('fuzz-dhcp-server.c'),
},
- network_fuzz_template + {
- 'sources' : files('fuzz-dhcp-server-relay.c'),
- },
network_fuzz_template + {
'sources' : files('fuzz-lldp-rx.c'),
},
diff --git a/src/libsystemd-network/sd-dhcp-relay.c b/src/libsystemd-network/sd-dhcp-relay.c
new file mode 100644
index 0000000000..1442a43fda
--- /dev/null
+++ b/src/libsystemd-network/sd-dhcp-relay.c
@@ -0,0 +1,136 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-event.h"
+
+#include "alloc-util.h"
+#include "dhcp-protocol.h"
+#include "dhcp-relay-internal.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "iovec-util.h"
+#include "prioq.h"
+
+static sd_dhcp_relay* dhcp_relay_free(sd_dhcp_relay *relay) {
+ if (!relay)
+ return NULL;
+
+ assert(hashmap_isempty(relay->interfaces));
+ hashmap_free(relay->interfaces);
+ assert(hashmap_isempty(relay->downstream_interfaces));
+ hashmap_free(relay->downstream_interfaces);
+ assert(prioq_isempty(relay->upstream_interfaces));
+ prioq_free(relay->upstream_interfaces);
+
+ sd_event_unref(relay->event);
+
+ iovec_done(&relay->remote_id);
+ tlv_unref(relay->extra_options);
+ return mfree(relay);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_relay, sd_dhcp_relay, dhcp_relay_free);
+
+int sd_dhcp_relay_new(sd_dhcp_relay **ret) {
+ assert_return(ret, -EINVAL);
+
+ sd_dhcp_relay *relay = new(sd_dhcp_relay, 1);
+ if (!relay)
+ return -ENOMEM;
+
+ *relay = (sd_dhcp_relay) {
+ .n_ref = 1,
+ .server_port = DHCP_PORT_SERVER,
+ };
+
+ *ret = TAKE_PTR(relay);
+ return 0;
+}
+
+int sd_dhcp_relay_is_running(sd_dhcp_relay *relay) {
+ if (!relay)
+ return false;
+
+ return
+ !prioq_isempty(relay->upstream_interfaces) ||
+ !hashmap_isempty(relay->downstream_interfaces);
+}
+
+int sd_dhcp_relay_attach_event(sd_dhcp_relay *relay, sd_event *event, int64_t priority) {
+ int r;
+
+ assert_return(relay, -EINVAL);
+ assert_return(!relay->event, -EBUSY);
+
+ if (event)
+ relay->event = sd_event_ref(event);
+ else {
+ r = sd_event_default(&relay->event);
+ if (r < 0)
+ return r;
+ }
+
+ relay->event_priority = priority;
+ return 0;
+}
+
+int sd_dhcp_relay_detach_event(sd_dhcp_relay *relay) {
+ assert_return(relay, -EINVAL);
+ assert_return(!sd_dhcp_relay_is_running(relay), -EBUSY);
+
+ relay->event = sd_event_unref(relay->event);
+ return 0;
+}
+
+sd_event* sd_dhcp_relay_get_event(sd_dhcp_relay *relay) {
+ assert_return(relay, NULL);
+
+ return relay->event;
+}
+
+int sd_dhcp_relay_set_server_address(sd_dhcp_relay *relay, const struct in_addr *address) {
+ assert_return(relay, -EINVAL);
+ assert_return(!sd_dhcp_relay_is_running(relay), -EBUSY);
+
+ if (address)
+ relay->server_address = *address;
+ else
+ relay->server_address = (struct in_addr) {};
+
+ return 0;
+}
+
+int sd_dhcp_relay_get_server_address(sd_dhcp_relay *relay, struct in_addr *ret) {
+ assert_return(relay, -EINVAL);
+
+ if (ret)
+ *ret = relay->server_address;
+
+ return in4_addr_is_set(&relay->server_address);
+}
+
+int sd_dhcp_relay_set_server_port(sd_dhcp_relay *relay, uint16_t port) {
+ assert_return(relay, -EINVAL);
+ assert_return(!sd_dhcp_relay_is_running(relay), -EBUSY);
+
+ relay->server_port = port;
+ return 0;
+}
+
+int sd_dhcp_relay_set_remote_id(sd_dhcp_relay *relay, const struct iovec *iov) {
+ assert_return(relay, -EINVAL);
+
+ return iovec_done_and_memdup(&relay->remote_id, iov);
+}
+
+int sd_dhcp_relay_set_server_identifier_override(sd_dhcp_relay *relay, int b) {
+ assert_return(relay, -EINVAL);
+
+ relay->server_identifier_override = !!b;
+ return 0;
+}
+
+int dhcp_relay_set_extra_options(sd_dhcp_relay *relay, TLV *options) {
+ assert(relay);
+
+ return unref_and_replace_new_ref(relay->extra_options, options, tlv_ref, tlv_unref);
+}
diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c
index fc28469a86..07f15ef6dc 100644
--- a/src/libsystemd-network/sd-dhcp-server.c
+++ b/src/libsystemd-network/sd-dhcp-server.c
@@ -113,12 +113,6 @@ int sd_dhcp_server_is_running(sd_dhcp_server *server) {
return !!server->receive_message;
}
-int sd_dhcp_server_is_in_relay_mode(sd_dhcp_server *server) {
- assert_return(server, -EINVAL);
-
- return in4_addr_is_set(&server->relay_target);
-}
-
static sd_dhcp_server* dhcp_server_free(sd_dhcp_server *server) {
assert(server);
@@ -142,9 +136,6 @@ static sd_dhcp_server* dhcp_server_free(sd_dhcp_server *server) {
tlv_unref(server->extra_options);
tlv_unref(server->vendor_options);
- free(server->agent_circuit_id);
- free(server->agent_remote_id);
-
safe_close(server->lease_dir_fd);
free(server->lease_file);
@@ -168,11 +159,9 @@ int sd_dhcp_server_new(sd_dhcp_server **ret, int ifindex) {
.n_ref = 1,
.fd_raw = -EBADF,
.fd = -EBADF,
- .fd_broadcast = -EBADF,
.address = htobe32(INADDR_ANY),
.netmask = htobe32(INADDR_ANY),
.ifindex = ifindex,
- .bind_to_interface = true,
.default_lease_time = DHCP_DEFAULT_LEASE_TIME_USEC,
.max_lease_time = DHCP_MAX_LEASE_TIME_USEC,
.rapid_commit = true,
@@ -289,11 +278,9 @@ int sd_dhcp_server_stop(sd_dhcp_server *server) {
running = sd_dhcp_server_is_running(server);
server->receive_message = sd_event_source_disable_unref(server->receive_message);
- server->receive_broadcast = sd_event_source_disable_unref(server->receive_broadcast);
server->fd_raw = safe_close(server->fd_raw);
server->fd = safe_close(server->fd);
- server->fd_broadcast = safe_close(server->fd_broadcast);
if (running)
log_dhcp_server(server, "STOPPED");
@@ -385,27 +372,21 @@ static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination,
assert(message);
assert(len >= sizeof(DHCPMessage));
- if (server->bind_to_interface) {
- msg.msg_control = &control;
- msg.msg_controllen = sizeof(control);
+ msg.msg_control = &control;
+ msg.msg_controllen = sizeof(control);
- cmsg = CMSG_FIRSTHDR(&msg);
- assert(cmsg);
+ cmsg = CMSG_FIRSTHDR(&msg);
+ assert(cmsg);
- cmsg->cmsg_level = IPPROTO_IP;
- cmsg->cmsg_type = IP_PKTINFO;
- cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
+ cmsg->cmsg_level = IPPROTO_IP;
+ cmsg->cmsg_type = IP_PKTINFO;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
- /* we attach source interface and address info to the message
- rather than binding the socket. This will be mostly useful
- when we gain support for arbitrary number of server addresses
- */
- pktinfo = CMSG_TYPED_DATA(cmsg, struct in_pktinfo);
- assert(pktinfo);
+ pktinfo = CMSG_TYPED_DATA(cmsg, struct in_pktinfo);
+ assert(pktinfo);
- pktinfo->ipi_ifindex = server->ifindex;
- pktinfo->ipi_spec_dst.s_addr = server->address;
- }
+ pktinfo->ipi_ifindex = server->ifindex;
+ pktinfo->ipi_spec_dst.s_addr = server->address;
if (sendmsg(server->fd, &msg, 0) < 0)
return -errno;
@@ -1000,83 +981,6 @@ static bool address_is_in_pool(sd_dhcp_server *server, be32_t address) {
return true;
}
-static int append_agent_information_option(sd_dhcp_server *server, DHCPMessage *message, size_t opt_length, size_t size) {
- int r;
- size_t offset;
-
- assert(server);
- assert(message);
-
- r = dhcp_option_find_option(message->options, opt_length, SD_DHCP_OPTION_END, &offset);
- if (r < 0)
- return r;
-
- r = dhcp_option_append(message, size, &offset, 0, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION, 0, server);
- if (r < 0)
- return r;
-
- r = dhcp_option_append(message, size, &offset, 0, SD_DHCP_OPTION_END, 0, NULL);
- if (r < 0)
- return r;
- return offset;
-}
-
-static int dhcp_server_relay_message(sd_dhcp_server *server, DHCPMessage *message, size_t opt_length, size_t buflen) {
- _cleanup_free_ DHCPPacket *packet = NULL;
- int r;
-
- assert(server);
- assert(message);
- assert(sd_dhcp_server_is_in_relay_mode(server));
-
- if (message->hlen == 0 || message->hlen > sizeof(message->chaddr) || memeqzero(message->chaddr, message->hlen))
- return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EBADMSG),
- "(relay agent) received message without/invalid hardware address, discarding.");
-
- if (message->op == BOOTREQUEST) {
- log_dhcp_server(server, "(relay agent) BOOTREQUEST (0x%x)", be32toh(message->xid));
- if (message->hops >= 16)
- return -ETIME;
- message->hops++;
-
- /* https://tools.ietf.org/html/rfc1542#section-4.1.1 */
- if (message->giaddr == 0)
- message->giaddr = server->address;
-
- if (server->agent_circuit_id || server->agent_remote_id) {
- r = append_agent_information_option(server, message, opt_length, buflen - sizeof(DHCPMessage));
- if (r < 0)
- return log_dhcp_server_errno(server, r, "could not append relay option: %m");
- opt_length = r;
- }
-
- return dhcp_server_send_udp(server, server->relay_target.s_addr, DHCP_PORT_SERVER, message, sizeof(DHCPMessage) + opt_length);
- } else if (message->op == BOOTREPLY) {
- log_dhcp_server(server, "(relay agent) BOOTREPLY (0x%x)", be32toh(message->xid));
- if (message->giaddr != server->address)
- return log_dhcp_server_errno(server, SYNTHETIC_ERRNO(EBADMSG),
- "(relay agent) BOOTREPLY giaddr mismatch, discarding");
-
- int message_type = dhcp_option_parse(message, sizeof(DHCPMessage) + opt_length, NULL, NULL, NULL);
- if (message_type < 0)
- return message_type;
-
- packet = malloc0(sizeof(DHCPPacket) + opt_length);
- if (!packet)
- return -ENOMEM;
- memcpy(&packet->dhcp, message, sizeof(DHCPMessage) + opt_length);
-
- r = dhcp_option_remove_option(packet->dhcp.options, opt_length, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION);
- if (r > 0)
- opt_length = r;
-
- bool l2_broadcast = requested_broadcast(message) || message_type == DHCP_NAK;
- const be32_t destination = message_type == DHCP_NAK ? INADDR_ANY : message->ciaddr;
- return dhcp_server_send(server, message->hlen, message->chaddr, destination, DHCP_PORT_CLIENT, packet, opt_length, l2_broadcast);
- }
- return -EBADMSG;
-}
-
static int server_ack_request(sd_dhcp_server *server, DHCPRequest *req, be32_t address) {
usec_t expiration;
int r;
@@ -1315,15 +1219,6 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, siz
return 0;
}
-static size_t relay_agent_information_length(const char* agent_circuit_id, const char* agent_remote_id) {
- size_t sum = 0;
- if (agent_circuit_id)
- sum += 2 + strlen(agent_circuit_id);
- if (agent_remote_id)
- sum += 2 + strlen(agent_remote_id);
- return sum;
-}
-
static int server_receive_message(sd_event_source *s, int fd,
uint32_t revents, void *userdata) {
_cleanup_free_ DHCPMessage *message = NULL;
@@ -1351,10 +1246,6 @@ static int server_receive_message(sd_event_source *s, int fd,
}
size_t buflen = datagram_size;
- if (sd_dhcp_server_is_in_relay_mode(server))
- /* Preallocate the additional size for DHCP Relay Agent Information Option if needed */
- buflen += relay_agent_information_length(server->agent_circuit_id, server->agent_remote_id) + 2;
-
message = malloc0(buflen);
if (!message)
return -ENOMEM;
@@ -1377,15 +1268,10 @@ static int server_receive_message(sd_event_source *s, int fd,
if (info && info->ipi_ifindex != server->ifindex)
return 0;
- if (sd_dhcp_server_is_in_relay_mode(server)) {
- r = dhcp_server_relay_message(server, message, len - sizeof(DHCPMessage), buflen);
- if (r < 0)
- log_dhcp_server_errno(server, r, "Couldn't relay message, ignoring: %m");
- } else {
- r = dhcp_server_handle_message(server, message, (size_t) len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg));
- if (r < 0)
- log_dhcp_server_errno(server, r, "Couldn't process incoming message, ignoring: %m");
- }
+ r = dhcp_server_handle_message(server, message, (size_t) len, TRIPLE_TIMESTAMP_FROM_CMSG(&msg));
+ if (r < 0)
+ log_dhcp_server_errno(server, r, "Couldn't process incoming message, ignoring: %m");
+
return 0;
}
@@ -1424,10 +1310,7 @@ int sd_dhcp_server_start(sd_dhcp_server *server) {
}
server->fd_raw = r;
- if (server->bind_to_interface)
- r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_ANY, DHCP_PORT_SERVER, -1);
- else
- r = dhcp_network_bind_udp_socket(0, server->address, DHCP_PORT_SERVER, -1);
+ r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_ANY, DHCP_PORT_SERVER, -1);
if (r < 0)
goto on_error;
server->fd = r;
@@ -1443,25 +1326,6 @@ int sd_dhcp_server_start(sd_dhcp_server *server) {
if (r < 0)
goto on_error;
- if (!server->bind_to_interface) {
- r = dhcp_network_bind_udp_socket(server->ifindex, INADDR_BROADCAST, DHCP_PORT_SERVER, -1);
- if (r < 0)
- goto on_error;
-
- server->fd_broadcast = r;
-
- r = sd_event_add_io(server->event, &server->receive_broadcast,
- server->fd_broadcast, EPOLLIN,
- server_receive_message, server);
- if (r < 0)
- goto on_error;
-
- r = sd_event_source_set_priority(server->receive_broadcast,
- server->event_priority);
- if (r < 0)
- goto on_error;
- }
-
r = dhcp_server_load_leases(server);
if (r < 0)
log_dhcp_server_errno(server, r, "Failed to load lease file %s, ignoring: %m", strna(server->lease_file));
@@ -1490,18 +1354,6 @@ int sd_dhcp_server_forcerenew(sd_dhcp_server *server) {
return r;
}
-int sd_dhcp_server_set_bind_to_interface(sd_dhcp_server *server, int enabled) {
- assert_return(server, -EINVAL);
- assert_return(!sd_dhcp_server_is_running(server), -EBUSY);
-
- if (!!enabled == server->bind_to_interface)
- return 0;
-
- server->bind_to_interface = enabled;
-
- return 1;
-}
-
int sd_dhcp_server_set_timezone(sd_dhcp_server *server, const char *tz) {
int r;
@@ -1649,46 +1501,6 @@ int sd_dhcp_server_set_callback(sd_dhcp_server *server, sd_dhcp_server_callback_
return 0;
}
-int sd_dhcp_server_set_relay_target(sd_dhcp_server *server, const struct in_addr *address) {
- assert_return(server, -EINVAL);
- assert_return(address, -EINVAL);
- assert_return(!sd_dhcp_server_is_running(server), -EBUSY);
-
- if (memcmp(address, &server->relay_target, sizeof(struct in_addr)) == 0)
- return 0;
-
- server->relay_target = *address;
- return 1;
-}
-
-int sd_dhcp_server_set_relay_agent_information(
- sd_dhcp_server *server,
- const char *agent_circuit_id,
- const char *agent_remote_id) {
- _cleanup_free_ char *circuit_id_dup = NULL, *remote_id_dup = NULL;
-
- assert_return(server, -EINVAL);
-
- if (relay_agent_information_length(agent_circuit_id, agent_remote_id) > UINT8_MAX)
- return -ENOBUFS;
-
- if (agent_circuit_id) {
- circuit_id_dup = strdup(agent_circuit_id);
- if (!circuit_id_dup)
- return -ENOMEM;
- }
-
- if (agent_remote_id) {
- remote_id_dup = strdup(agent_remote_id);
- if (!remote_id_dup)
- return -ENOMEM;
- }
-
- free_and_replace(server->agent_circuit_id, circuit_id_dup);
- free_and_replace(server->agent_remote_id, remote_id_dup);
- return 0;
-}
-
int sd_dhcp_server_set_lease_file(sd_dhcp_server *server, int dir_fd, const char *path) {
int r;
diff --git a/src/libsystemd-network/test-dhcp-relay.c b/src/libsystemd-network/test-dhcp-relay.c
new file mode 100644
index 0000000000..dd4e7652d9
--- /dev/null
+++ b/src/libsystemd-network/test-dhcp-relay.c
@@ -0,0 +1,383 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+#include
+
+#include "sd-event.h"
+
+#include "dhcp-protocol.h"
+#include "dhcp-relay-internal.h" /* IWYU pragma: keep */
+#include "ether-addr-util.h"
+#include "fd-util.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "iovec-util.h"
+#include "ip-util.h"
+#include "socket-util.h"
+#include "tests.h"
+
+static uint32_t xid = 12345;
+
+static const struct hw_addr_data hw_addr = {
+ .length = ETH_ALEN,
+ .ether = {{ 'A', 'B', 'C', '1', '2', '3' }},
+};
+
+static unsigned fake_server_message_count = 0;
+static unsigned fake_client_message_count = 0;
+
+TEST(sd_dhcp_relay_ref_unref) {
+ _cleanup_(sd_dhcp_relay_unrefp) sd_dhcp_relay *relay = NULL;
+ _cleanup_(sd_dhcp_relay_interface_unrefp) sd_dhcp_relay_interface *upstream = NULL, *downstream = NULL;
+
+ ASSERT_OK(sd_dhcp_relay_new(&relay));
+ ASSERT_NOT_NULL(relay);
+
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4242, /* is_upstream= */ true, &upstream));
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4343, /* is_upstream= */ false, &downstream));
+ ASSERT_PTR_EQ(hashmap_get(relay->interfaces, INT_TO_PTR(4242)), upstream);
+ ASSERT_PTR_EQ(hashmap_get(relay->interfaces, INT_TO_PTR(4343)), downstream);
+
+ /* Each interface holds a reference to the sd_dhcp_relay object, so we can safely drop our reference. */
+ relay = sd_dhcp_relay_unref(relay);
+ ASSERT_PTR_EQ(hashmap_get(upstream->relay->interfaces, INT_TO_PTR(4242)), upstream);
+ ASSERT_PTR_EQ(hashmap_get(downstream->relay->interfaces, INT_TO_PTR(4343)), downstream);
+
+ /* Still upstream interface has the reference. */
+ downstream = sd_dhcp_relay_interface_unref(downstream);
+ ASSERT_PTR_EQ(hashmap_get(upstream->relay->interfaces, INT_TO_PTR(4242)), upstream);
+ ASSERT_FALSE(hashmap_contains(upstream->relay->interfaces, INT_TO_PTR(4343)));
+
+ /* Everything should be freed with this. */
+ upstream = sd_dhcp_relay_interface_unref(upstream);
+
+ /* Let's check the inverse order. */
+ ASSERT_OK(sd_dhcp_relay_new(&relay));
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4242, /* is_upstream= */ true, &upstream));
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4343, /* is_upstream= */ false, &downstream));
+ ASSERT_PTR_EQ(hashmap_get(relay->interfaces, INT_TO_PTR(4242)), upstream);
+ ASSERT_PTR_EQ(hashmap_get(relay->interfaces, INT_TO_PTR(4343)), downstream);
+
+ downstream = sd_dhcp_relay_interface_unref(downstream);
+ ASSERT_PTR_EQ(hashmap_get(relay->interfaces, INT_TO_PTR(4242)), upstream);
+ ASSERT_FALSE(hashmap_contains(relay->interfaces, INT_TO_PTR(4343)));
+
+ upstream = sd_dhcp_relay_interface_unref(upstream);
+ ASSERT_FALSE(hashmap_contains(relay->interfaces, INT_TO_PTR(4242)));
+ ASSERT_FALSE(hashmap_contains(relay->interfaces, INT_TO_PTR(4343)));
+}
+
+static void send_message(int fd, sd_dhcp_message *m) {
+ ASSERT_OK(dhcp_message_send_udp(
+ m,
+ fd,
+ /* src_addr= */ INADDR_ANY,
+ /* dst_addr= */ INADDR_ANY,
+ /* dst_port= */ 0));
+}
+
+static int fake_server_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ sd_dhcp_relay *relay = ASSERT_PTR(userdata);
+
+ fake_server_message_count++;
+ log_debug("%s: count=%u", __func__, fake_server_message_count);
+
+ ssize_t buflen = ASSERT_OK_POSITIVE(next_datagram_size_fd(fd));
+ _cleanup_free_ void *buf = ASSERT_NOT_NULL(malloc0(buflen));
+
+ struct msghdr msg = {
+ .msg_iov = &IOVEC_MAKE(buf, buflen),
+ .msg_iovlen = 1,
+ };
+ ssize_t len = ASSERT_OK_ERRNO(recvmsg_safe(fd, &msg, MSG_DONTWAIT));
+
+ _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
+ ASSERT_OK(dhcp_message_parse(
+ &IOVEC_MAKE(buf, len),
+ BOOTREQUEST,
+ &xid,
+ ARPHRD_ETHER,
+ &hw_addr,
+ &m));
+
+ ASSERT_EQ(m->header.hops, 1u);
+
+ sd_dhcp_relay_interface *downstream;
+ ASSERT_OK(downstream_get(relay, m, &downstream));
+ ASSERT_FALSE(downstream->upstream);
+
+ ASSERT_EQ(m->header.giaddr, downstream->gateway_address.s_addr);
+
+ _cleanup_(tlv_unrefp) TLV *agent_info = NULL;
+ ASSERT_OK(dhcp_message_get_option_sub_tlv(
+ m,
+ SD_DHCP_OPTION_RELAY_AGENT_INFORMATION,
+ TLV_DHCP4_SUBOPTION,
+ &agent_info));
+
+ void *key, *value;
+ HASHMAP_FOREACH_KEY(value, key, agent_info->entries) {
+ uint32_t tag = PTR_TO_UINT32(key);
+ _cleanup_(iovec_done) struct iovec iov = {};
+ ASSERT_OK(tlv_get_alloc(agent_info, tag, &iov));
+
+ switch (tag) {
+ case SD_DHCP_RELAY_AGENT_CIRCUIT_ID:
+ ASSERT_TRUE(iovec_equal(&iov, &downstream->circuit_id));
+ break;
+ case SD_DHCP_RELAY_AGENT_REMOTE_ID:
+ ASSERT_TRUE(iovec_equal(&iov, &relay->remote_id));
+ break;
+ case SD_DHCP_RELAY_AGENT_LINK_SELECTION:
+ ASSERT_TRUE(iovec_equal(&iov, &IOVEC_MAKE(&downstream->address, sizeof(struct in_addr))));
+ break;
+ case SD_DHCP_RELAY_AGENT_FLAGS: {
+ ASSERT_TRUE(relay->server_identifier_override);
+ ASSERT_EQ(iov.iov_len, 1u);
+ uint8_t flags = *(uint8_t*) iov.iov_base;
+ /* In the unit test, we cannot detect if the message is broadcast or unicast because
+ * AF_UNIX is used; therefore, the unicast flag is not set. */
+ ASSERT_FALSE(FLAGS_SET(flags, DHCP_RELAY_AGENT_FLAG_UNICAST));
+ break;
+ }
+ case SD_DHCP_RELAY_AGENT_SERVER_IDENTIFIER_OVERRIDE:
+ ASSERT_TRUE(relay->server_identifier_override);
+ ASSERT_TRUE(iovec_equal(&iov, &IOVEC_MAKE(&downstream->address, sizeof(struct in_addr))));
+ break;
+ case SD_DHCP_RELAY_AGENT_VIRTUAL_SUBNET_SELECTION:
+ ASSERT_TRUE(iovec_equal(&iov, &downstream->vss));
+ break;
+ default:
+ assert_not_reached();
+ }
+ }
+
+ uint8_t t;
+ ASSERT_OK(dhcp_message_get_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, &t));
+
+ switch (fake_server_message_count) {
+ case 1:
+ ASSERT_EQ(t, DHCP_DISCOVER);
+ break;
+ case 2:
+ ASSERT_EQ(t, DHCP_REQUEST);
+ break;
+ case 3:
+ ASSERT_EQ(t, DHCP_RELEASE);
+
+ if (fake_client_message_count == 3)
+ ASSERT_OK(sd_event_exit(sd_event_source_get_event(s), 0));
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+static void fake_client_verify(int fd, uint8_t type, bool raw) {
+ ssize_t buflen = ASSERT_OK_POSITIVE(next_datagram_size_fd(fd));
+ _cleanup_free_ void *buf = ASSERT_NOT_NULL(malloc0(buflen));
+
+ struct msghdr msg = {
+ .msg_iov = &IOVEC_MAKE(buf, buflen),
+ .msg_iovlen = 1,
+ };
+ ssize_t len = ASSERT_OK_ERRNO(recvmsg_safe(fd, &msg, MSG_DONTWAIT));
+
+ struct iovec payload = IOVEC_MAKE(buf, len);
+ if (raw)
+ ASSERT_OK(udp_packet_verify(&payload, DHCP_PORT_CLIENT, /* checksum= */ true, &payload));
+
+ _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
+ ASSERT_OK(dhcp_message_parse(
+ &payload,
+ BOOTREPLY,
+ &xid,
+ ARPHRD_ETHER,
+ &hw_addr,
+ &m));
+
+ ASSERT_EQ(m->header.hops, 0u);
+ ASSERT_FALSE(dhcp_message_has_option(m, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION));
+
+ uint8_t t;
+ ASSERT_OK(dhcp_message_get_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, &t));
+ ASSERT_EQ(t, type);
+}
+
+static int fake_client_handler(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+ fake_client_message_count++;
+ log_debug("%s: count=%u", __func__, fake_client_message_count);
+
+ switch (fake_client_message_count) {
+ case 1:
+ fake_client_verify(fd, DHCP_OFFER, /* raw= */ true);
+ break;
+ case 2:
+ fake_client_verify(fd, DHCP_ACK, /* raw= */ false);
+ break;
+ case 3:
+ fake_client_verify(fd, DHCP_NAK, /* raw= */ false);
+
+ if (fake_server_message_count == 3)
+ ASSERT_OK(sd_event_exit(sd_event_source_get_event(s), 0));
+ break;
+ default:
+ assert_not_reached();
+ }
+
+ return 0;
+}
+
+TEST(forwarding) {
+ union in_addr_union a;
+
+ _cleanup_(sd_event_unrefp) sd_event *e = NULL;
+ ASSERT_OK(sd_event_new(&e));
+
+ _cleanup_(sd_dhcp_relay_unrefp) sd_dhcp_relay *relay = NULL;
+ ASSERT_OK(sd_dhcp_relay_new(&relay));
+ ASSERT_OK(sd_dhcp_relay_attach_event(relay, e, SD_EVENT_PRIORITY_NORMAL));
+ ASSERT_OK(in_addr_from_string(AF_INET, "198.51.100.1", &a));
+ ASSERT_OK(sd_dhcp_relay_set_server_address(relay, &a.in));
+ ASSERT_OK(sd_dhcp_relay_set_remote_id(relay, &IOVEC_MAKE_STRING("test-remote-id")));
+ ASSERT_OK(sd_dhcp_relay_set_server_identifier_override(relay, true));
+
+ /* Setting up an upstream interface. */
+ _cleanup_(sd_dhcp_relay_interface_unrefp) sd_dhcp_relay_interface *upstream = NULL;
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4242, /* is_upstream= */ true, &upstream));
+ ASSERT_OK(sd_dhcp_relay_interface_set_ifname(upstream, "test-upstream"));
+ ASSERT_OK_ZERO(sd_dhcp_relay_interface_get_address(upstream, /* ret_address= */ NULL, /* ret_prefixlen= */ NULL));
+ ASSERT_OK(in_addr_from_string(AF_INET, "198.51.100.2", &a));
+ ASSERT_OK(sd_dhcp_relay_interface_set_address(upstream, &a.in, 24));
+ ASSERT_OK_POSITIVE(sd_dhcp_relay_interface_get_address(upstream, /* ret_address= */ NULL, /* ret_prefixlen= */ NULL));
+
+ _cleanup_close_pair_ int upstream_fd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, upstream_fd));
+ upstream->socket_fd = TAKE_FD(upstream_fd[0]);
+ ASSERT_OK(sd_dhcp_relay_interface_start(upstream));
+
+ /* IO event source for the server side. */
+ _cleanup_(sd_event_source_unrefp) sd_event_source *fake_server = NULL;
+ ASSERT_OK(sd_event_add_io(e, &fake_server, upstream_fd[1], EPOLLIN, fake_server_handler, relay));
+ ASSERT_OK(sd_event_source_set_priority(fake_server, SD_EVENT_PRIORITY_IMPORTANT));
+ ASSERT_OK(sd_event_source_set_description(fake_server, "fake-server-io-event-source"));
+
+ /* Setting up a downstream interface. */
+ _cleanup_(sd_dhcp_relay_interface_unrefp) sd_dhcp_relay_interface *downstream = NULL;
+ ASSERT_OK(sd_dhcp_relay_add_interface(relay, 4343, /* is_upstream= */ false, &downstream));
+ ASSERT_OK(sd_dhcp_relay_interface_set_ifname(downstream, "test-downstream"));
+ ASSERT_OK(in_addr_from_string(AF_INET, "192.0.2.1", &a));
+ ASSERT_OK(sd_dhcp_relay_interface_set_address(downstream, &a.in, 24));
+
+ ASSERT_OK(in_addr_from_string(AF_INET, "203.0.113.1", &a));
+ ASSERT_OK(sd_dhcp_relay_downstream_set_gateway_address(downstream, &a.in));
+ ASSERT_OK(sd_dhcp_relay_downstream_set_circuit_id(downstream, &IOVEC_MAKE_STRING("test-circuit-id")));
+ ASSERT_OK(sd_dhcp_relay_downstream_set_virtual_subnet_selection(downstream, &IOVEC_MAKE_STRING("test-virtual-net")));
+
+ _cleanup_close_pair_ int downstream_fd[2] = EBADF_PAIR;
+ ASSERT_OK_ERRNO(socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0, downstream_fd));
+ downstream->socket_fd = TAKE_FD(downstream_fd[0]);
+ ASSERT_OK(sd_dhcp_relay_interface_start(downstream));
+
+ /* IO event source for the client side. */
+ _cleanup_(sd_event_source_unrefp) sd_event_source *fake_client = NULL;
+ ASSERT_OK(sd_event_add_io(e, &fake_client, downstream_fd[1], EPOLLIN, fake_client_handler, relay));
+ ASSERT_OK(sd_event_source_set_priority(fake_client, SD_EVENT_PRIORITY_NORMAL));
+ ASSERT_OK(sd_event_source_set_description(fake_client, "fake-client-io-event-source"));
+
+ _cleanup_(sd_dhcp_message_unrefp) sd_dhcp_message *m = NULL;
+ ASSERT_OK(dhcp_message_new(&m));
+ ASSERT_OK(dhcp_message_init_header(
+ m,
+ BOOTREQUEST,
+ xid,
+ ARPHRD_ETHER,
+ &hw_addr));
+
+ /* Test: downstream -> upstream */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, DHCP_DISCOVER));
+ send_message(downstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, DHCP_REQUEST));
+ send_message(downstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+
+ /* Invalid message (unexpected BOOTP operation). */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, 100));
+ m->header.op = BOOTREPLY;
+ send_message(downstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ m->header.op = BOOTREQUEST;
+
+ /* Invalid message (too large hops). */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, 101));
+ m->header.hops = 16;
+ send_message(downstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ m->header.hops = 0;
+
+ /* Invalid message (invalid giaddr). */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, 102));
+ m->header.giaddr = downstream->address.s_addr;
+ send_message(downstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ m->header.giaddr = INADDR_ANY;
+
+ /* Invalid message (unexpected relay agent information). */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, 103));
+ ASSERT_OK(dhcp_message_append_option_flag(m, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION));
+ send_message(downstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_RELAY_AGENT_INFORMATION);
+
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, DHCP_RELEASE));
+ send_message(downstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+
+ /* Test: upstream -> downstream */
+ m->header.op = BOOTREPLY;
+ m->header.giaddr = downstream->gateway_address.s_addr;
+ m->header.yiaddr = 0x12345678;
+
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, DHCP_OFFER));
+ send_message(upstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, DHCP_ACK));
+ m->header.ciaddr = 0x12345678;
+ send_message(upstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ m->header.ciaddr = 0;
+
+ /* Invalid message (unexpected BOOTP operation). */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, 200));
+ m->header.op = BOOTREQUEST;
+ send_message(upstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ m->header.op = BOOTREPLY;
+
+ /* Invalid message (NULL giaddr). */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, 201));
+ m->header.giaddr = INADDR_ANY;
+ send_message(upstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ m->header.giaddr = downstream->gateway_address.s_addr;
+
+ /* Invalid message (unexpected giaddr). */
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, 202));
+ m->header.giaddr = 1234567;
+ send_message(upstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+ m->header.giaddr = downstream->gateway_address.s_addr;
+
+ ASSERT_OK(dhcp_message_append_option_u8(m, SD_DHCP_OPTION_MESSAGE_TYPE, DHCP_NAK));
+ send_message(upstream_fd[1], m);
+ dhcp_message_remove_option(m, SD_DHCP_OPTION_MESSAGE_TYPE);
+
+ ASSERT_OK(sd_event_loop(e));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/libsystemd-network/test-dhcp-server.c b/src/libsystemd-network/test-dhcp-server.c
index 4da4fdb116..34263f044d 100644
--- a/src/libsystemd-network/test-dhcp-server.c
+++ b/src/libsystemd-network/test-dhcp-server.c
@@ -22,7 +22,7 @@ static void test_pool(struct in_addr *address, unsigned size, int ret) {
ASSERT_RETURN_IS_CRITICAL(false, ASSERT_ERROR(sd_dhcp_server_configure_pool(server, address, 8, 0, size), -ret));
}
-static int test_basic(bool bind_to_interface) {
+static int test_basic(void) {
_cleanup_(sd_dhcp_server_unrefp) sd_dhcp_server *server = NULL;
_cleanup_(sd_event_unrefp) sd_event *event = NULL;
struct in_addr address_lo = {
@@ -33,14 +33,13 @@ static int test_basic(bool bind_to_interface) {
};
int r;
- log_debug("/* %s(bind_to_interface=%s) */", __func__, yes_no(bind_to_interface));
+ log_debug("/* %s */", __func__);
ASSERT_OK(sd_event_new(&event));
/* attach to loopback interface */
ASSERT_OK(sd_dhcp_server_new(&server, 1));
ASSERT_NOT_NULL(server);
- server->bind_to_interface = bind_to_interface;
ASSERT_OK(sd_dhcp_server_attach_event(server, event, 0));
ASSERT_RETURN_EXPECTED(ASSERT_ERROR(sd_dhcp_server_attach_event(server, event, 0), EBUSY));
@@ -410,13 +409,9 @@ int main(int argc, char *argv[]) {
test_static_lease();
test_domain_name();
- r = test_basic(true);
+ r = test_basic();
if (r < 0)
- return log_tests_skipped_errno(r, "cannot start dhcp server(bound to interface)");
-
- r = test_basic(false);
- if (r < 0)
- return log_tests_skipped_errno(r, "cannot start dhcp server(non-bound to interface)");
+ return log_tests_skipped_errno(r, "cannot start dhcp server");
test_message_handler();
diff --git a/src/libsystemd/sd-common/sd-forward.h b/src/libsystemd/sd-common/sd-forward.h
index 96ab84e982..0a1d673e14 100644
--- a/src/libsystemd/sd-common/sd-forward.h
+++ b/src/libsystemd/sd-common/sd-forward.h
@@ -79,8 +79,10 @@ typedef struct sd_ipv4ll sd_ipv4ll;
typedef struct sd_dhcp_client sd_dhcp_client;
typedef struct sd_dhcp_lease sd_dhcp_lease;
typedef struct sd_dhcp_route sd_dhcp_route;
-typedef struct sd_dns_resolver sd_dns_resolver;
+typedef struct sd_dhcp_relay sd_dhcp_relay;
+typedef struct sd_dhcp_relay_interface sd_dhcp_relay_interface;
typedef struct sd_dhcp_server sd_dhcp_server;
+typedef struct sd_dns_resolver sd_dns_resolver;
typedef struct sd_ndisc sd_ndisc;
typedef struct sd_radv sd_radv;
typedef struct sd_dhcp6_client sd_dhcp6_client;
diff --git a/src/network/meson.build b/src/network/meson.build
index 9844229ebd..110af9511c 100644
--- a/src/network/meson.build
+++ b/src/network/meson.build
@@ -43,6 +43,7 @@ systemd_networkd_extract_sources = files(
'networkd-conf.c',
'networkd-dhcp-common.c',
'networkd-dhcp-prefix-delegation.c',
+ 'networkd-dhcp-relay.c',
'networkd-dhcp-server-bus.c',
'networkd-dhcp-server-static-lease.c',
'networkd-dhcp-server.c',
diff --git a/src/network/networkd-address.c b/src/network/networkd-address.c
index 3880eab47f..11337126e5 100644
--- a/src/network/networkd-address.c
+++ b/src/network/networkd-address.c
@@ -19,6 +19,7 @@
#include "networkd-address.h"
#include "networkd-address-pool.h"
#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-dhcp-relay.h"
#include "networkd-dhcp-server.h"
#include "networkd-ipv4acd.h"
#include "networkd-link.h"
@@ -245,6 +246,9 @@ static Address* address_detach_impl(Address *address) {
if (address->network->dhcp_server_address == address)
address->network->dhcp_server_address = NULL;
+ if (address->network->dhcp_relay_agent_address == address)
+ address->network->dhcp_relay_agent_address = NULL;
+
address->network = NULL;
return address;
}
@@ -925,6 +929,8 @@ static int address_drop(Address *in, bool removed_by_us) {
ipv4acd_detach(link, address);
+ (void) link_dhcp_relay_address_dropped(link, address);
+
address_detach(address);
if (!removed_by_us) {
@@ -2517,5 +2523,6 @@ int network_drop_invalid_addresses(Network *network) {
if (r < 0)
return r;
+ network_adjust_dhcp_relay(network);
return 0;
}
diff --git a/src/network/networkd-conf.c b/src/network/networkd-conf.c
index bce16284f8..ba3e2f5a43 100644
--- a/src/network/networkd-conf.c
+++ b/src/network/networkd-conf.c
@@ -22,6 +22,7 @@ int manager_parse_config_file(Manager *m) {
"DHCPv4\0"
"DHCPv6\0"
"DHCPServer\0"
+ "DHCPRelay\0"
"DHCP\0",
config_item_perf_lookup, networkd_gperf_lookup,
CONFIG_PARSE_WARN,
diff --git a/src/network/networkd-dhcp-relay.c b/src/network/networkd-dhcp-relay.c
new file mode 100644
index 0000000000..7811d9d585
--- /dev/null
+++ b/src/network/networkd-dhcp-relay.c
@@ -0,0 +1,380 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include
+
+#include "sd-event.h"
+#include "sd-id128.h"
+
+#include "conf-parser.h"
+#include "dhcp-relay-internal.h"
+#include "hashmap.h"
+#include "iovec-util.h"
+#include "networkd-address.h"
+#include "networkd-dhcp-relay.h"
+#include "networkd-link.h"
+#include "networkd-manager.h"
+#include "networkd-network.h"
+#include "networkd-queue.h"
+#include "string-table.h"
+#include "string-util.h" /* IWYU pragma: keep */
+
+#define DHCP_RELAY_APP_REMOTE_ID SD_ID128_MAKE(85,bb,eb,d2,b8,56,47,0b,b0,86,4c,f3,d3,9b,c1,b5)
+
+void network_adjust_dhcp_relay(Network *network) {
+ assert(network);
+ assert(network->manager);
+
+ if (network->dhcp_relay_interface_mode < 0)
+ return;
+
+ if (network->bond) {
+ log_warning("%s: DHCPRelay= is enabled for bond slave. Disabling DHCP relay agent.",
+ network->filename);
+ network->dhcp_relay_interface_mode = _DHCP_RELAY_INTERFACE_INVALID;
+ return;
+ }
+
+ if (network->dhcp_server) {
+ log_warning("%s: DHCPRelay= cannot be enabled when DHCPServer= is enabled. Disabling DHCP relay agent.",
+ network->filename);
+ network->dhcp_relay_interface_mode = _DHCP_RELAY_INTERFACE_INVALID;
+ return;
+ }
+
+ if (FLAGS_SET(network->dhcp, ADDRESS_FAMILY_IPV4)) {
+ log_warning("%s: DHCPRelay= cannot be enabled when DHCPv4 client is enabled. Disabling DHCP relay agent.",
+ network->filename);
+ network->dhcp_relay_interface_mode = _DHCP_RELAY_INTERFACE_INVALID;
+ return;
+ }
+
+ if (in4_addr_is_null(&network->manager->dhcp_relay_server_address)) {
+ log_warning("%s: DHCPRelay= is enabled, but [DHCPRelay] ServerAddress= in networkd.conf is not configured. Disabling DHCP relay agent.",
+ network->filename);
+ network->dhcp_relay_interface_mode = _DHCP_RELAY_INTERFACE_INVALID;
+ return;
+ }
+
+ Address *a;
+ ORDERED_HASHMAP_FOREACH(a, network->addresses_by_section) {
+ assert(!section_is_invalid(a->section));
+
+ if (a->family != AF_INET)
+ continue;
+
+ if (in4_addr_is_set(&a->in_addr_peer.in))
+ continue;
+
+ if (in4_addr_is_set(&network->dhcp_relay_agent_address_in_addr)) {
+ if (!in4_addr_equal(&a->in_addr.in, &network->dhcp_relay_agent_address_in_addr))
+ continue;
+
+ } else {
+ if (in4_addr_is_localhost(&a->in_addr.in))
+ continue;
+
+ if (in4_addr_is_link_local(&a->in_addr.in))
+ continue;
+
+ if (a->scope != RT_SCOPE_UNIVERSE)
+ continue;
+ }
+
+ network->dhcp_relay_agent_address = a;
+ break;
+ }
+
+ if (!network->dhcp_relay_agent_address) {
+ if (in4_addr_is_set(&network->dhcp_relay_agent_address_in_addr))
+ log_warning("%s: Configured AgentAddress=%s not found among static addresses. Disabling DHCP relay agent.",
+ network->filename, IN4_ADDR_TO_STRING(&network->dhcp_relay_agent_address_in_addr));
+ else
+ log_warning("%s: DHCPRelay= is enabled, but no suitable static address configured. Disabling DHCP relay agent.",
+ network->filename);
+ network->dhcp_relay_interface_mode = _DHCP_RELAY_INTERFACE_INVALID;
+ return;
+ }
+}
+
+static int manager_configure_dhcp_relay(Manager *manager) {
+ int r;
+
+ assert(manager);
+ assert(manager->event);
+
+ if (manager->dhcp_relay)
+ return 0;
+
+ if (in4_addr_is_null(&manager->dhcp_relay_server_address))
+ return -EADDRNOTAVAIL;
+
+ _cleanup_(sd_dhcp_relay_unrefp) sd_dhcp_relay *relay = NULL;
+ r = sd_dhcp_relay_new(&relay);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_relay_attach_event(relay, manager->event, SD_EVENT_PRIORITY_NORMAL);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_relay_set_server_address(relay, &manager->dhcp_relay_server_address);
+ if (r < 0)
+ return r;
+
+ if (iovec_is_set(&manager->dhcp_relay_remote_id)) {
+ r = sd_dhcp_relay_set_remote_id(relay, &manager->dhcp_relay_remote_id);
+ if (r < 0)
+ return r;
+ } else {
+ sd_id128_t id;
+ r = sd_id128_get_machine_app_specific(DHCP_RELAY_APP_REMOTE_ID, &id);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_relay_set_remote_id(relay, &IOVEC_MAKE_STRING(SD_ID128_TO_STRING(id)));
+ if (r < 0)
+ return r;
+ }
+
+ r = sd_dhcp_relay_set_server_identifier_override(relay, manager->dhcp_relay_override_server_id);
+ if (r < 0)
+ return r;
+
+ r = dhcp_relay_set_extra_options(relay, &manager->dhcp_relay_extra_options);
+ if (r < 0)
+ return r;
+
+ manager->dhcp_relay = TAKE_PTR(relay);
+ return 0;
+}
+
+static int link_configure_dhcp_relay(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(link->network);
+ assert(!link->dhcp_relay_interface);
+ assert(link->network->dhcp_relay_agent_address);
+ assert(link->network->dhcp_relay_interface_mode >= 0 && link->network->dhcp_relay_interface_mode < _DHCP_RELAY_INTERFACE_MAX);
+
+ r = manager_configure_dhcp_relay(link->manager);
+ if (r < 0)
+ return r;
+
+ bool upstream = link->network->dhcp_relay_interface_mode == DHCP_RELAY_INTERFACE_UPSTREAM;
+
+ _cleanup_(sd_dhcp_relay_interface_unrefp) sd_dhcp_relay_interface *interface = NULL;
+ r = sd_dhcp_relay_add_interface(link->manager->dhcp_relay, link->ifindex, upstream, &interface);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_relay_interface_set_ifname(interface, link->ifname);
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_relay_interface_set_address(
+ interface,
+ &link->network->dhcp_relay_agent_address->in_addr.in,
+ link->network->dhcp_relay_agent_address->prefixlen);
+ if (r < 0)
+ return r;
+
+ if (upstream) {
+ r = sd_dhcp_relay_upstream_set_priority(interface, link->network->dhcp_relay_interface_priority);
+ if (r < 0)
+ return r;
+ } else {
+ if (in4_addr_is_set(&link->network->dhcp_relay_gateway_address))
+ r = sd_dhcp_relay_downstream_set_gateway_address(interface, &link->network->dhcp_relay_gateway_address);
+ else
+ r = sd_dhcp_relay_downstream_set_gateway_address(interface, &link->network->dhcp_relay_agent_address->in_addr.in);
+ if (r < 0)
+ return r;
+
+ if (iovec_is_set(&link->network->dhcp_relay_circuit_id))
+ r = sd_dhcp_relay_downstream_set_circuit_id(interface, &link->network->dhcp_relay_circuit_id);
+ else
+ r = sd_dhcp_relay_downstream_set_circuit_id(interface, &IOVEC_MAKE_STRING(link->ifname));
+ if (r < 0)
+ return r;
+
+ r = sd_dhcp_relay_downstream_set_virtual_subnet_selection(interface, &link->network->dhcp_relay_vss);
+ if (r < 0)
+ return r;
+
+ r = downstream_set_extra_options(interface, &link->network->dhcp_relay_extra_options);
+ if (r < 0)
+ return r;
+ }
+
+ link->dhcp_relay_interface = TAKE_PTR(interface);
+
+ if (link->network->dhcp_relay_interface_mode == DHCP_RELAY_INTERFACE_COMPAT &&
+ !link->dhcp_relay_interface_compat) {
+ r = sd_dhcp_relay_add_interface(
+ link->manager->dhcp_relay,
+ DHCP_RELAY_IFINDEX_UNBOUND,
+ /* is_upstream= */ true,
+ &link->dhcp_relay_interface_compat);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static bool dhcp_relay_is_ready_to_configure(Link *link) {
+ assert(link);
+ assert(link->network);
+
+ if (!link_is_ready_to_configure(link, /* allow_unmanaged= */ false))
+ return false;
+
+ if (!link_has_carrier(link))
+ return false;
+
+ if (!link->static_addresses_configured)
+ return false;
+
+ Address *a;
+ if (address_get(link, link->network->dhcp_relay_agent_address, &a) < 0)
+ return false;
+
+ if (!address_is_ready(a))
+ return false;
+
+ return true;
+}
+
+static int dhcp_relay_process_request(Request *req, Link *link, void *userdata) {
+ int r;
+
+ assert(link);
+
+ if (!dhcp_relay_is_ready_to_configure(link))
+ return 0;
+
+ r = link_configure_dhcp_relay(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to configure DHCP relay agent: %m");
+
+ log_link_debug(link, "DHCP relay agent is configured.");
+
+ r = link_start_dhcp_relay(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to start DHCP relay agent: %m");
+
+ return 1;
+}
+
+int link_request_dhcp_relay(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ if (link->manager->state != MANAGER_RUNNING)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (link->network->dhcp_relay_interface_mode < 0)
+ return 0;
+
+ if (link->dhcp_relay_interface)
+ return 0;
+
+ r = link_queue_request(link, REQUEST_TYPE_DHCP_RELAY, dhcp_relay_process_request, NULL);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Failed to request configuring of the DHCP relay agent: %m");
+
+ log_link_debug(link, "Requested configuring of the DHCP relay agent.");
+ return 0;
+}
+
+int link_start_dhcp_relay(Link *link) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+
+ if (!link->dhcp_relay_interface)
+ return 0; /* Not configured yet. */
+
+ if (!link_has_carrier(link))
+ return 0;
+
+ r = sd_dhcp_relay_interface_start(link->dhcp_relay_interface);
+ if (r < 0)
+ return r;
+
+ if (link->dhcp_relay_interface_compat) {
+ r = sd_dhcp_relay_interface_start(link->dhcp_relay_interface_compat);
+ if (r < 0)
+ return r;
+ }
+
+ log_link_debug(link, "Relaying DHCPv4 messages.");
+ return 0;
+}
+
+int link_dhcp_relay_address_dropped(Link *link, const Address *address) {
+ int r;
+
+ assert(link);
+ assert(link->manager);
+ assert(address);
+
+ /* This is called when an address is removed from the interface. */
+
+ if (link->manager->state != MANAGER_RUNNING)
+ return 0;
+
+ if (!link->network)
+ return 0;
+
+ if (!link->dhcp_relay_interface)
+ return 0;
+
+ if (address->family != AF_INET)
+ return 0;
+
+ struct in_addr a;
+ uint8_t prefixlen;
+ r = sd_dhcp_relay_interface_get_address(link->dhcp_relay_interface, &a, &prefixlen);
+ if (r <= 0)
+ return r;
+
+ if (!in4_addr_equal(&address->in_addr.in, &a))
+ return 0;
+
+ if (address->prefixlen != prefixlen)
+ return 0;
+
+ r = sd_dhcp_relay_interface_stop(link->dhcp_relay_interface);
+ if (r < 0)
+ return r;
+
+ link->dhcp_relay_interface = sd_dhcp_relay_interface_unref(link->dhcp_relay_interface);
+
+ if (!IN_SET(link->state, LINK_STATE_CONFIGURING, LINK_STATE_CONFIGURED))
+ return 0;
+
+ /* The address may be reconfigured later. Let's reconfigure DHCP relay interface when the address comes back. */
+ return link_request_dhcp_relay(link);
+}
+
+static const char * const dhcp_relay_interface_mode_table[_DHCP_RELAY_INTERFACE_MAX] = {
+ [DHCP_RELAY_INTERFACE_UPSTREAM] = "upstream",
+ [DHCP_RELAY_INTERFACE_DOWNSTREAM] = "downstream",
+};
+
+DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(dhcp_relay_interface_mode, DHCPRelayInterfaceMode);
+
+DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT(
+ config_parse_dhcp_relay_interface_mode,
+ dhcp_relay_interface_mode,
+ DHCPRelayInterfaceMode,
+ _DHCP_RELAY_INTERFACE_INVALID);
diff --git a/src/network/networkd-dhcp-relay.h b/src/network/networkd-dhcp-relay.h
new file mode 100644
index 0000000000..2a676931bd
--- /dev/null
+++ b/src/network/networkd-dhcp-relay.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "networkd-forward.h"
+
+typedef enum DHCPRelayInterfaceMode {
+ DHCP_RELAY_INTERFACE_UPSTREAM,
+ DHCP_RELAY_INTERFACE_DOWNSTREAM,
+ DHCP_RELAY_INTERFACE_COMPAT,
+ _DHCP_RELAY_INTERFACE_MAX,
+ _DHCP_RELAY_INTERFACE_INVALID = -EINVAL,
+} DHCPRelayInterfaceMode;
+
+void network_adjust_dhcp_relay(Network *network);
+
+int link_request_dhcp_relay(Link *link);
+int link_start_dhcp_relay(Link *link);
+int link_dhcp_relay_address_dropped(Link *link, const Address *address);
+
+CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_relay_interface_mode);
diff --git a/src/network/networkd-dhcp-server-bus.c b/src/network/networkd-dhcp-server-bus.c
index e5ee6d3f6d..58e0a26f22 100644
--- a/src/network/networkd-dhcp-server-bus.c
+++ b/src/network/networkd-dhcp-server-bus.c
@@ -30,9 +30,6 @@ static int property_get_leases(
if (!s)
return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has no DHCP server.", l->ifname);
- if (sd_dhcp_server_is_in_relay_mode(s))
- return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Link %s has DHCP relay agent active.", l->ifname);
-
r = sd_bus_message_open_container(reply, 'a', "(uayayayayt)");
if (r < 0)
return r;
@@ -82,15 +79,12 @@ static int property_get_pool_size(
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
+
Link *l = ASSERT_PTR(userdata);
- sd_dhcp_server *s;
- uint32_t v;
assert(reply);
- s = l->dhcp_server;
- v = s && !sd_dhcp_server_is_in_relay_mode(s) ? s->pool_size : UINT32_MAX;
-
+ uint32_t v = l->dhcp_server ? l->dhcp_server->pool_size : UINT32_MAX;
return sd_bus_message_append_basic(reply, 'u', &v);
}
@@ -102,15 +96,12 @@ static int property_get_pool_offset(
sd_bus_message *reply,
void *userdata,
sd_bus_error *error) {
+
Link *l = ASSERT_PTR(userdata);
- sd_dhcp_server *s;
- uint32_t v;
assert(reply);
- s = l->dhcp_server;
- v = s && !sd_dhcp_server_is_in_relay_mode(s) ? s->pool_offset : UINT32_MAX;
-
+ uint32_t v = l->dhcp_server ? l->dhcp_server->pool_offset : UINT32_MAX;
return sd_bus_message_append_basic(reply, 'u', &v);
}
diff --git a/src/network/networkd-dhcp-server.c b/src/network/networkd-dhcp-server.c
index 9903c6b9d3..2ee5a70abc 100644
--- a/src/network/networkd-dhcp-server.c
+++ b/src/network/networkd-dhcp-server.c
@@ -16,6 +16,7 @@
#include "fileio.h"
#include "hashmap.h"
#include "hostname-setup.h"
+#include "iovec-util.h"
#include "network-common.h"
#include "networkd-address.h"
#include "networkd-dhcp-server.h"
@@ -66,6 +67,31 @@ int network_adjust_dhcp_server(Network *network, Set **addresses) {
return 0;
}
+ if (in4_addr_is_set(&network->dhcp_relay_target_address)) {
+ if (!in4_addr_is_set(&network->manager->dhcp_relay_server_address)) {
+ /* [DHCPServer] RelayTarget= in .network file is replaced with
+ * [DHCPRelay] ServerAddress= in networkd.conf. */
+ network->manager->dhcp_relay_server_address = network->dhcp_relay_target_address;
+
+ /* [DHCPServer] RelayAgentRemoteId= in .network file is replaced with
+ * [DHCPRelay] RemoteId= in networkd.conf. */
+ if (!iovec_is_set(&network->manager->dhcp_relay_remote_id)) {
+ iovec_done(&network->manager->dhcp_relay_remote_id);
+ network->manager->dhcp_relay_remote_id = TAKE_STRUCT(network->dhcp_relay_remote_id);
+ }
+ }
+
+ /* Copy [DHCPServer] ServerAddress= to [DHCPRelay] AgentAddress= if unspecified. */
+ if (!in4_addr_is_set(&network->dhcp_relay_agent_address_in_addr))
+ network->dhcp_relay_agent_address_in_addr = network->dhcp_server_address_in_addr;
+
+ /* Assume this interface acts as a downstream interface of the DHCP relay agent. Also,
+ * configure a catch-all upstream socket. */
+ network->dhcp_relay_interface_mode = DHCP_RELAY_INTERFACE_COMPAT;
+ network->dhcp_server = false;
+ return 0;
+ }
+
assert(network->dhcp_server_address_prefixlen <= 32);
if (network->dhcp_server_address_prefixlen == 0) {
@@ -160,9 +186,6 @@ static DHCPServerPersistLeases link_get_dhcp_server_persist_leases(Link *link) {
assert(link->manager);
assert(link->network);
- if (in4_addr_is_set(&link->network->dhcp_server_relay_target))
- return DHCP_SERVER_PERSIST_LEASES_NO; /* On relay mode. Nothing saved in the persistent storage. */
-
if (link->network->dhcp_server_persist_leases >= 0)
return link->network->dhcp_server_persist_leases;
@@ -571,7 +594,6 @@ static int dhcp4_server_configure(Link *link) {
DHCPStaticLease *static_lease;
Link *uplink = NULL;
Address *address;
- bool bind_to_interface;
int r;
assert(link);
@@ -681,19 +703,6 @@ static int dhcp4_server_configure(Link *link) {
return log_link_error_errno(link, r, "Failed to set router address for DHCP server: %m");
}
- r = sd_dhcp_server_set_relay_target(link->dhcp_server, &link->network->dhcp_server_relay_target);
- if (r < 0)
- return log_link_error_errno(link, r, "Failed to set relay target for DHCP server: %m");
-
- bind_to_interface = sd_dhcp_server_is_in_relay_mode(link->dhcp_server) ? false : link->network->dhcp_server_bind_to_interface;
- r = sd_dhcp_server_set_bind_to_interface(link->dhcp_server, bind_to_interface);
- if (r < 0)
- return log_link_error_errno(link, r, "Failed to set interface binding for DHCP server: %m");
-
- r = sd_dhcp_server_set_relay_agent_information(link->dhcp_server, link->network->dhcp_server_relay_agent_circuit_id, link->network->dhcp_server_relay_agent_remote_id);
- if (r < 0)
- return log_link_error_errno(link, r, "Failed to set agent circuit/remote id for DHCP server: %m");
-
if (link->network->dhcp_server_emit_timezone) {
_cleanup_free_ char *buffer = NULL;
const char *tz = NULL;
@@ -815,38 +824,6 @@ int link_request_dhcp_server(Link *link) {
return 0;
}
-int config_parse_dhcp_server_relay_agent_suboption(
- const char *unit,
- const char *filename,
- unsigned line,
- const char *section,
- unsigned section_line,
- const char *lvalue,
- int ltype,
- const char *rvalue,
- void *data,
- void *userdata) {
-
- char **suboption_value = data;
-
- assert(filename);
- assert(lvalue);
- assert(rvalue);
-
- if (isempty(rvalue)) {
- *suboption_value = mfree(*suboption_value);
- return 0;
- }
-
- const char *p = startswith(rvalue, "string:");
- if (!p) {
- log_syntax(unit, LOG_WARNING, filename, line, 0,
- "Failed to parse %s=%s'. Invalid format, ignoring.", lvalue, rvalue);
- return 0;
- }
- return free_and_strdup(suboption_value, empty_to_null(p));
-}
-
int config_parse_dhcp_server_emit(
const char *unit,
const char *filename,
diff --git a/src/network/networkd-dhcp-server.h b/src/network/networkd-dhcp-server.h
index e46ad3a857..f60ca2d397 100644
--- a/src/network/networkd-dhcp-server.h
+++ b/src/network/networkd-dhcp-server.h
@@ -18,7 +18,6 @@ int link_request_dhcp_server(Link *link);
int link_start_dhcp4_server(Link *link);
void manager_toggle_dhcp4_server_state(Manager *manager, bool start);
-CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_relay_agent_suboption);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_emit);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_address);
CONFIG_PARSER_PROTOTYPE(config_parse_dhcp_server_ipv6_only_preferred);
diff --git a/src/network/networkd-gperf.gperf b/src/network/networkd-gperf.gperf
index 3684a696ff..242da72c27 100644
--- a/src/network/networkd-gperf.gperf
+++ b/src/network/networkd-gperf.gperf
@@ -45,6 +45,10 @@ DHCPv6.UseDomains, config_parse_use_domains,
DHCPv6.DUIDType, config_parse_duid_type, 0, offsetof(Manager, dhcp6_duid)
DHCPv6.DUIDRawData, config_parse_duid_rawdata, 0, offsetof(Manager, dhcp6_duid)
DHCPServer.PersistLeases, config_parse_dhcp_server_persist_leases, 0, offsetof(Manager, dhcp_server_persist_leases)
+DHCPRelay.ServerAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Manager, dhcp_relay_server_address)
+DHCPRelay.OverrideServerIdentifier, config_parse_bool, 0, offsetof(Manager, dhcp_relay_override_server_id)
+DHCPRelay.RemoteId, config_parse_dhcp_option, /* check_length= */ true, offsetof(Manager, dhcp_relay_remote_id)
+DHCPRelay.ExtraOption, config_parse_dhcp_option_tlv, 0, offsetof(Manager, dhcp_relay_extra_options)
/* Deprecated */
DHCP.DUIDType, config_parse_manager_duid_type, 0, 0
DHCP.DUIDRawData, config_parse_manager_duid_rawdata, 0, 0
diff --git a/src/network/networkd-link-bus.c b/src/network/networkd-link-bus.c
index a375d0c18b..53ac080a47 100644
--- a/src/network/networkd-link-bus.c
+++ b/src/network/networkd-link-bus.c
@@ -855,8 +855,7 @@ int link_object_find(sd_bus *bus, const char *path, const char *interface, void
if (r < 0)
return 0;
- if (streq(interface, "org.freedesktop.network1.DHCPServer") &&
- (!link->dhcp_server || sd_dhcp_server_is_in_relay_mode(link->dhcp_server)))
+ if (streq(interface, "org.freedesktop.network1.DHCPServer") && !link->dhcp_server)
return 0;
if (streq(interface, "org.freedesktop.network1.DHCPv4Client") && !link->dhcp_client)
diff --git a/src/network/networkd-link.c b/src/network/networkd-link.c
index 16fb529797..c57657597f 100644
--- a/src/network/networkd-link.c
+++ b/src/network/networkd-link.c
@@ -10,6 +10,7 @@
#include "sd-bus.h"
#include "sd-dhcp-client.h"
+#include "sd-dhcp-relay.h"
#include "sd-dhcp-server.h"
#include "sd-dhcp6-client.h"
#include "sd-dhcp6-lease.h"
@@ -41,6 +42,7 @@
#include "networkd-bridge-mdb.h"
#include "networkd-bridge-vlan.h"
#include "networkd-dhcp-prefix-delegation.h"
+#include "networkd-dhcp-relay.h"
#include "networkd-dhcp-server.h"
#include "networkd-dhcp4.h"
#include "networkd-dhcp6.h"
@@ -240,6 +242,8 @@ static void link_free_engines(Link *link) {
if (!link)
return;
+ link->dhcp_relay_interface = sd_dhcp_relay_interface_unref(link->dhcp_relay_interface);
+ link->dhcp_relay_interface_compat = sd_dhcp_relay_interface_unref(link->dhcp_relay_interface_compat);
link->dhcp_server = sd_dhcp_server_unref(link->dhcp_server);
link->dhcp_client = sd_dhcp_client_unref(link->dhcp_client);
@@ -424,6 +428,14 @@ int link_stop_engines(Link *link, bool may_keep_dynamic) {
ndisc_flush(link);
}
+ r = sd_dhcp_relay_interface_stop(link->dhcp_relay_interface);
+ if (r < 0)
+ RET_GATHER(ret, log_link_warning_errno(link, r, "Could not stop DHCP relay agent: %m"));
+
+ r = sd_dhcp_relay_interface_stop(link->dhcp_relay_interface_compat);
+ if (r < 0)
+ RET_GATHER(ret, log_link_warning_errno(link, r, "Could not stop DHCP relay agent (compat): %m"));
+
r = sd_dhcp_server_stop(link->dhcp_server);
if (r < 0)
RET_GATHER(ret, log_link_warning_errno(link, r, "Could not stop DHCPv4 server: %m"));
@@ -739,6 +751,10 @@ static int link_acquire_dynamic_ipv4_conf(Link *link) {
log_link_debug(link, "Acquiring IPv4 link-local address.");
}
+ r = link_start_dhcp_relay(link);
+ if (r < 0)
+ return log_link_warning_errno(link, r, "Could not start DHCP relay agent: %m");
+
r = link_start_dhcp4_server(link);
if (r < 0)
return log_link_warning_errno(link, r, "Could not start DHCP server: %m");
@@ -1196,6 +1212,8 @@ static int link_drop_dynamic_config(Link *link, Network *network) {
RET_GATHER(r, link_drop_dhcp4_config(link, network));
RET_GATHER(r, link_drop_dhcp6_config(link, network));
RET_GATHER(r, link_drop_dhcp_pd_config(link, network));
+ link->dhcp_relay_interface = sd_dhcp_relay_interface_unref(link->dhcp_relay_interface);
+ link->dhcp_relay_interface_compat = sd_dhcp_relay_interface_unref(link->dhcp_relay_interface_compat);
link->dhcp_server = sd_dhcp_server_unref(link->dhcp_server);
link->lldp_rx = sd_lldp_rx_unref(link->lldp_rx); /* TODO: keep the received neighbors. */
link->lldp_tx = sd_lldp_tx_unref(link->lldp_tx);
@@ -1314,6 +1332,10 @@ static int link_configure(Link *link) {
if (r < 0)
return r;
+ r = link_request_dhcp_relay(link);
+ if (r < 0)
+ return r;
+
r = link_request_dhcp_server(link);
if (r < 0)
return r;
@@ -2645,6 +2667,12 @@ static int link_update_name(Link *link, sd_netlink_message *message) {
return log_link_debug_errno(link, r, "Failed to update interface name in NDisc: %m");
}
+ if (link->dhcp_relay_interface) {
+ r = sd_dhcp_relay_interface_set_ifname(link->dhcp_relay_interface, link->ifname);
+ if (r < 0)
+ return log_link_debug_errno(link, r, "Failed to update interface name in DHCP relay interface: %m");
+ }
+
if (link->dhcp_server) {
r = sd_dhcp_server_set_ifname(link->dhcp_server, link->ifname);
if (r < 0)
diff --git a/src/network/networkd-link.h b/src/network/networkd-link.h
index b51736ae45..4c69ce7d02 100644
--- a/src/network/networkd-link.h
+++ b/src/network/networkd-link.h
@@ -143,6 +143,8 @@ typedef struct Link {
bool bridge_vlan_set:1;
bool bearer_configured:1;
+ sd_dhcp_relay_interface *dhcp_relay_interface;
+ sd_dhcp_relay_interface *dhcp_relay_interface_compat;
sd_dhcp_server *dhcp_server;
sd_ndisc *ndisc;
diff --git a/src/network/networkd-manager.c b/src/network/networkd-manager.c
index f43709da35..63ba0c166f 100644
--- a/src/network/networkd-manager.c
+++ b/src/network/networkd-manager.c
@@ -5,6 +5,7 @@
#include
#include "sd-bus.h"
+#include "sd-dhcp-relay.h"
#include "sd-event.h"
#include "sd-netlink.h"
#include "sd-resolve.h"
@@ -25,6 +26,7 @@
#include "errno-util.h"
#include "fd-util.h"
#include "initrd-util.h"
+#include "iovec-util.h"
#include "mount-util.h"
#include "netlink-internal.h"
#include "netlink-util.h"
@@ -701,6 +703,7 @@ int manager_new(Manager **ret, bool test_mode) {
.dhcp_duid.type = DUID_TYPE_EN,
.dhcp6_duid.type = DUID_TYPE_EN,
.duid_product_uuid.type = DUID_TYPE_UUID,
+ .dhcp_relay_extra_options = TLV_INIT(TLV_DHCP4_SUBOPTION),
.dhcp_server_persist_leases = DHCP_SERVER_PERSIST_LEASES_YES,
.serialization_fd = -EBADF,
.ip_forwarding = { -1, -1, },
@@ -760,6 +763,10 @@ Manager* manager_free(Manager *m) {
sd_netlink_unref(m->nfnl);
sd_resolve_unref(m->resolve);
+ iovec_done(&m->dhcp_relay_remote_id);
+ tlv_done(&m->dhcp_relay_extra_options);
+ sd_dhcp_relay_unref(m->dhcp_relay);
+
m->routes = set_free(m->routes);
m->nexthops_by_id = hashmap_free(m->nexthops_by_id);
diff --git a/src/network/networkd-manager.h b/src/network/networkd-manager.h
index bacf3df444..2e1b643090 100644
--- a/src/network/networkd-manager.h
+++ b/src/network/networkd-manager.h
@@ -3,6 +3,7 @@
#include "networkd-forward.h"
#include "networkd-network.h"
+#include "tlv-util.h"
typedef enum ManagerState {
MANAGER_RUNNING,
@@ -74,6 +75,13 @@ typedef struct Manager {
bool has_product_uuid;
bool product_uuid_requested;
+ /* DHCP relay agent */
+ sd_dhcp_relay *dhcp_relay;
+ struct in_addr dhcp_relay_server_address;
+ bool dhcp_relay_override_server_id;
+ struct iovec dhcp_relay_remote_id;
+ TLV dhcp_relay_extra_options;
+
char* dynamic_hostname;
char* dynamic_timezone;
diff --git a/src/network/networkd-network-gperf.gperf b/src/network/networkd-network-gperf.gperf
index 9e6eb7c799..81b9784b49 100644
--- a/src/network/networkd-network-gperf.gperf
+++ b/src/network/networkd-network-gperf.gperf
@@ -132,6 +132,7 @@ Network.VLAN, config_parse_stacked_netdev,
Network.VXLAN, config_parse_stacked_netdev, NETDEV_KIND_VXLAN, offsetof(Network, stacked_netdev_names)
Network.Xfrm, config_parse_stacked_netdev, NETDEV_KIND_XFRM, offsetof(Network, stacked_netdev_names)
Network.DHCP, config_parse_dhcp, 0, offsetof(Network, dhcp)
+Network.DHCPRelay, config_parse_dhcp_relay_interface_mode, 0, offsetof(Network, dhcp_relay_interface_mode)
Network.DHCPServer, config_parse_bool, 0, offsetof(Network, dhcp_server)
Network.LinkLocalAddressing, config_parse_link_local_address_family, 0, offsetof(Network, link_local)
Network.IPv6LinkLocalAddressGenerationMode, config_parse_ipv6_link_local_address_gen_mode, 0, offsetof(Network, ipv6ll_address_gen_mode)
@@ -360,11 +361,17 @@ IPv6AcceptRA.RouteDenyList, config_parse_in_addr_prefixes,
IPv6AcceptRA.Token, config_parse_address_generation_type, 0, offsetof(Network, ndisc_tokens)
IPv6AcceptRA.NetLabel, config_parse_string, CONFIG_PARSE_STRING_SAFE, offsetof(Network, ndisc_netlabel)
IPv6AcceptRA.NFTSet, config_parse_nft_set, NFT_SET_PARSE_NETWORK, offsetof(Network, ndisc_nft_set_context)
+DHCPRelay.AgentAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_relay_agent_address_in_addr)
+DHCPRelay.GatewayAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_relay_gateway_address)
+DHCPRelay.CircuitId, config_parse_dhcp_option, /* check_length= */ true, offsetof(Network, dhcp_relay_circuit_id)
+DHCPRelay.VirtualSubnetSelection, config_parse_dhcp_option, /* check_length= */ true, offsetof(Network, dhcp_relay_vss)
+DHCPRelay.ExtraOption, config_parse_dhcp_option_tlv, 0, offsetof(Network, dhcp_relay_extra_options)
+DHCPRelay.InterfacePriority, config_parse_int, 0, offsetof(Network, dhcp_relay_interface_priority)
DHCPServer.ServerAddress, config_parse_dhcp_server_address, 0, 0
DHCPServer.UplinkInterface, config_parse_uplink, 0, 0
-DHCPServer.RelayTarget, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_relay_target)
-DHCPServer.RelayAgentCircuitId, config_parse_dhcp_server_relay_agent_suboption, 0, offsetof(Network, dhcp_server_relay_agent_circuit_id)
-DHCPServer.RelayAgentRemoteId, config_parse_dhcp_server_relay_agent_suboption, 0, offsetof(Network, dhcp_server_relay_agent_remote_id)
+DHCPServer.RelayTarget, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_relay_target_address) /* deprecated */
+DHCPServer.RelayAgentCircuitId, config_parse_dhcp_option, /* check_length= */ true, offsetof(Network, dhcp_relay_circuit_id) /* deprecated */
+DHCPServer.RelayAgentRemoteId, config_parse_dhcp_option, /* check_length= */ true, offsetof(Network, dhcp_relay_remote_id) /* deprecated */
DHCPServer.MaxLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_max_lease_time_usec)
DHCPServer.DefaultLeaseTimeSec, config_parse_sec, 0, offsetof(Network, dhcp_server_default_lease_time_usec)
DHCPServer.IPv6OnlyPreferredSec, config_parse_dhcp_server_ipv6_only_preferred, 0, offsetof(Network, dhcp_server_ipv6_only_preferred_usec)
@@ -390,7 +397,7 @@ DHCPServer.PoolOffset, config_parse_uint32,
DHCPServer.PoolSize, config_parse_uint32, 0, offsetof(Network, dhcp_server_pool_size)
DHCPServer.SendOption, config_parse_dhcp_option_tlv, 0, offsetof(Network, dhcp_server_extra_options)
DHCPServer.SendVendorOption, config_parse_dhcp_option_tlv, 0, offsetof(Network, dhcp_server_vendor_options)
-DHCPServer.BindToInterface, config_parse_bool, 0, offsetof(Network, dhcp_server_bind_to_interface)
+DHCPServer.BindToInterface, config_parse_warn_compat, DISABLED_LEGACY, 0
DHCPServer.BootServerAddress, config_parse_in_addr_non_null, AF_INET, offsetof(Network, dhcp_server_boot_server_address)
DHCPServer.BootServerName, config_parse_dns_name, 0, offsetof(Network, dhcp_server_boot_server_name)
DHCPServer.BootFilename, config_parse_string, CONFIG_PARSE_STRING_SAFE_AND_ASCII, offsetof(Network, dhcp_server_boot_filename)
diff --git a/src/network/networkd-network.c b/src/network/networkd-network.c
index dfe524d150..d63eeaa478 100644
--- a/src/network/networkd-network.c
+++ b/src/network/networkd-network.c
@@ -7,6 +7,7 @@
#include "conf-files.h"
#include "conf-parser.h"
#include "in-addr-util.h"
+#include "iovec-util.h"
#include "net-condition.h"
#include "netdev/macvlan.h"
#include "netif-sriov.h"
@@ -433,7 +434,9 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi
.dhcp_pd_subnet_id = -1,
.dhcp_pd_route_metric = DHCP6PD_ROUTE_METRIC,
- .dhcp_server_bind_to_interface = true,
+ .dhcp_relay_interface_mode = _DHCP_RELAY_INTERFACE_INVALID,
+ .dhcp_relay_extra_options = TLV_INIT(TLV_DHCP4_SUBOPTION),
+
.dhcp_server_emit[SD_DHCP_LEASE_DNS].emit = true,
.dhcp_server_emit[SD_DHCP_LEASE_NTP].emit = true,
.dhcp_server_emit[SD_DHCP_LEASE_SIP].emit = true,
@@ -547,6 +550,7 @@ int network_load_one(Manager *manager, OrderedHashmap **networks, const char *fi
"DHCPv6\0"
"DHCPv6PrefixDelegation\0" /* compat */
"DHCPPrefixDelegation\0"
+ "DHCPRelay\0"
"DHCPServer\0"
"DHCPServerStaticLease\0"
"IPv6AcceptRA\0"
@@ -767,9 +771,13 @@ static Network *network_free(Network *network) {
ordered_set_free(network->route_domains);
set_free(network->dnssec_negative_trust_anchors);
+ /* DHCP relay agent */
+ iovec_done(&network->dhcp_relay_remote_id);
+ iovec_done(&network->dhcp_relay_circuit_id);
+ iovec_done(&network->dhcp_relay_vss);
+ tlv_done(&network->dhcp_relay_extra_options);
+
/* DHCP server */
- free(network->dhcp_server_relay_agent_circuit_id);
- free(network->dhcp_server_relay_agent_remote_id);
free(network->dhcp_server_boot_server_name);
free(network->dhcp_server_boot_filename);
free(network->dhcp_server_timezone);
diff --git a/src/network/networkd-network.h b/src/network/networkd-network.h
index 625b3b9a08..893b3022bb 100644
--- a/src/network/networkd-network.h
+++ b/src/network/networkd-network.h
@@ -12,6 +12,7 @@
#include "network-util.h"
#include "networkd-bridge-vlan.h"
#include "networkd-dhcp-common.h"
+#include "networkd-dhcp-relay.h"
#include "networkd-dhcp-server.h"
#include "networkd-dhcp4.h"
#include "networkd-dhcp6.h"
@@ -208,17 +209,25 @@ typedef struct Network {
uint32_t dhcp6_route_table;
bool dhcp6_route_table_set;
+ /* DHCP Relay Agent Support */
+ DHCPRelayInterfaceMode dhcp_relay_interface_mode;
+ Address *dhcp_relay_agent_address;
+ struct in_addr dhcp_relay_target_address; /* for deprecated DHCPServer.RelayTarget= */
+ struct in_addr dhcp_relay_agent_address_in_addr;
+ struct in_addr dhcp_relay_gateway_address;
+ struct iovec dhcp_relay_remote_id; /* for deprecated DHCPServer.RelayAgentRemoteId= */
+ struct iovec dhcp_relay_circuit_id;
+ struct iovec dhcp_relay_vss;
+ TLV dhcp_relay_extra_options;
+ int dhcp_relay_interface_priority;
+
/* DHCP Server Support */
bool dhcp_server;
- bool dhcp_server_bind_to_interface;
unsigned char dhcp_server_address_prefixlen;
struct in_addr dhcp_server_address_in_addr;
const Address *dhcp_server_address;
int dhcp_server_uplink_index;
char *dhcp_server_uplink_name;
- struct in_addr dhcp_server_relay_target;
- char *dhcp_server_relay_agent_circuit_id;
- char *dhcp_server_relay_agent_remote_id;
NetworkDHCPServerEmitAddress dhcp_server_emit[_SD_DHCP_LEASE_SERVER_TYPE_MAX];
bool dhcp_server_emit_router;
struct in_addr dhcp_server_router;
diff --git a/src/network/networkd-queue.c b/src/network/networkd-queue.c
index 0ab372b4b2..ea846327b0 100644
--- a/src/network/networkd-queue.c
+++ b/src/network/networkd-queue.c
@@ -365,6 +365,7 @@ static const char *const request_type_table[_REQUEST_TYPE_MAX] = {
[REQUEST_TYPE_ADDRESS_LABEL] = "address label",
[REQUEST_TYPE_BRIDGE_FDB] = "bridge FDB",
[REQUEST_TYPE_BRIDGE_MDB] = "bridge MDB",
+ [REQUEST_TYPE_DHCP_RELAY] = "DHCP relay agent",
[REQUEST_TYPE_DHCP_SERVER] = "DHCP server",
[REQUEST_TYPE_DHCP4_CLIENT] = "DHCPv4 client",
[REQUEST_TYPE_DHCP6_CLIENT] = "DHCPv6 client",
diff --git a/src/network/networkd-queue.h b/src/network/networkd-queue.h
index d656d7aa7a..70c8b2ca9e 100644
--- a/src/network/networkd-queue.h
+++ b/src/network/networkd-queue.h
@@ -13,6 +13,7 @@ typedef enum RequestType {
REQUEST_TYPE_ADDRESS_LABEL,
REQUEST_TYPE_BRIDGE_FDB,
REQUEST_TYPE_BRIDGE_MDB,
+ REQUEST_TYPE_DHCP_RELAY,
REQUEST_TYPE_DHCP_SERVER,
REQUEST_TYPE_DHCP4_CLIENT,
REQUEST_TYPE_DHCP6_CLIENT,
diff --git a/src/systemd/meson.build b/src/systemd/meson.build
index c35f811245..cef6e3c93f 100644
--- a/src/systemd/meson.build
+++ b/src/systemd/meson.build
@@ -29,6 +29,7 @@ _not_installed_headers = [
'sd-dhcp-duid.h',
'sd-dhcp-lease.h',
'sd-dhcp-protocol.h',
+ 'sd-dhcp-relay.h',
'sd-dhcp-server-lease.h',
'sd-dhcp-server.h',
'sd-dhcp6-client.h',
diff --git a/src/systemd/sd-dhcp-relay.h b/src/systemd/sd-dhcp-relay.h
new file mode 100644
index 0000000000..47c42a21dd
--- /dev/null
+++ b/src/systemd/sd-dhcp-relay.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#ifndef foosddhcprelayhfoo
+#define foosddhcprelayhfoo
+
+/***
+ systemd is free software; you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published by
+ the Free Software Foundation; either version 2.1 of the License, or
+ (at your option) any later version.
+
+ systemd 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with systemd; If not, see .
+***/
+
+#include
+#include
+
+#include "_sd-common.h"
+
+_SD_BEGIN_DECLARATIONS;
+
+typedef struct sd_event sd_event;
+typedef struct sd_dhcp_relay sd_dhcp_relay;
+typedef struct sd_dhcp_relay_interface sd_dhcp_relay_interface;
+
+_SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_relay);
+_SD_DECLARE_TRIVIAL_REF_UNREF_FUNC(sd_dhcp_relay_interface);
+
+int sd_dhcp_relay_new(sd_dhcp_relay **ret);
+
+int sd_dhcp_relay_is_running(sd_dhcp_relay *relay);
+
+int sd_dhcp_relay_attach_event(sd_dhcp_relay *relay, sd_event *event, int64_t priority);
+int sd_dhcp_relay_detach_event(sd_dhcp_relay *relay);
+sd_event *sd_dhcp_relay_get_event(sd_dhcp_relay *relay);
+
+int sd_dhcp_relay_set_server_address(sd_dhcp_relay *relay, const struct in_addr *address);
+int sd_dhcp_relay_get_server_address(sd_dhcp_relay *relay, struct in_addr *ret);
+int sd_dhcp_relay_set_server_port(sd_dhcp_relay *relay, uint16_t port);
+int sd_dhcp_relay_set_remote_id(sd_dhcp_relay *relay, const struct iovec *iov);
+int sd_dhcp_relay_set_server_identifier_override(sd_dhcp_relay *relay, int b);
+
+int sd_dhcp_relay_add_interface(sd_dhcp_relay *relay, int ifindex, int is_upstream, sd_dhcp_relay_interface **ret);
+
+int sd_dhcp_relay_interface_set_ifname(sd_dhcp_relay_interface *interface, const char *ifname);
+int sd_dhcp_relay_interface_get_ifname(sd_dhcp_relay_interface *interface, const char **ret);
+int sd_dhcp_relay_interface_set_address(sd_dhcp_relay_interface *interface, const struct in_addr *address, uint8_t prefixlen);
+int sd_dhcp_relay_interface_get_address(sd_dhcp_relay_interface *interface, struct in_addr *ret_address, uint8_t *ret_prefixlen);
+int sd_dhcp_relay_interface_set_port(sd_dhcp_relay_interface *interface, uint16_t port);
+int sd_dhcp_relay_interface_set_ip_service_type(sd_dhcp_relay_interface *interface, uint8_t type);
+int sd_dhcp_relay_interface_is_running(sd_dhcp_relay_interface *interface);
+int sd_dhcp_relay_interface_start(sd_dhcp_relay_interface *interface);
+int sd_dhcp_relay_interface_stop(sd_dhcp_relay_interface *interface);
+
+int sd_dhcp_relay_downstream_set_gateway_address(sd_dhcp_relay_interface *interface, const struct in_addr *address);
+int sd_dhcp_relay_downstream_set_circuit_id(sd_dhcp_relay_interface *interface, const struct iovec *iov);
+int sd_dhcp_relay_downstream_set_virtual_subnet_selection(sd_dhcp_relay_interface *interface, const struct iovec *iov);
+
+int sd_dhcp_relay_upstream_set_priority(sd_dhcp_relay_interface *interface, int priority);
+
+_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_dhcp_relay, sd_dhcp_relay_unref);
+_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_dhcp_relay_interface, sd_dhcp_relay_interface_unref);
+
+_SD_END_DECLARATIONS;
+
+#endif
diff --git a/src/systemd/sd-dhcp-server.h b/src/systemd/sd-dhcp-server.h
index 1fb7d2e9ee..8c3dbab9b4 100644
--- a/src/systemd/sd-dhcp-server.h
+++ b/src/systemd/sd-dhcp-server.h
@@ -57,7 +57,6 @@ int sd_dhcp_server_configure_pool(sd_dhcp_server *server, const struct in_addr *
int sd_dhcp_server_set_boot_server_address(sd_dhcp_server *server, const struct in_addr *address);
int sd_dhcp_server_set_boot_server_name(sd_dhcp_server *server, const char *name);
int sd_dhcp_server_set_boot_filename(sd_dhcp_server *server, const char *filename);
-int sd_dhcp_server_set_bind_to_interface(sd_dhcp_server *server, int enabled);
int sd_dhcp_server_set_timezone(sd_dhcp_server *server, const char *tz);
int sd_dhcp_server_set_domain_name(sd_dhcp_server *server, const char *domain_name);
int sd_dhcp_server_set_router(sd_dhcp_server *server, const struct in_addr *router);
@@ -90,10 +89,6 @@ int sd_dhcp_server_set_rapid_commit(sd_dhcp_server *server, int enabled);
int sd_dhcp_server_forcerenew(sd_dhcp_server *server);
-int sd_dhcp_server_is_in_relay_mode(sd_dhcp_server *server);
-int sd_dhcp_server_set_relay_target(sd_dhcp_server *server, const struct in_addr* address);
-int sd_dhcp_server_set_relay_agent_information(sd_dhcp_server *server, const char* circuit_id, const char* remote_id);
-
int sd_dhcp_server_get_lease_address_by_name(sd_dhcp_server *server, const char *name, struct in_addr *ret);
_SD_DEFINE_POINTER_CLEANUP_FUNC(sd_dhcp_server, sd_dhcp_server_unref);
diff --git a/test/fuzz/fuzz-dhcp-relay/discover b/test/fuzz/fuzz-dhcp-relay/discover
new file mode 100644
index 0000000000..4ffce3951f
Binary files /dev/null and b/test/fuzz/fuzz-dhcp-relay/discover differ
diff --git a/test/fuzz/fuzz-dhcp-relay/offer b/test/fuzz/fuzz-dhcp-relay/offer
new file mode 100644
index 0000000000..fe11025f82
Binary files /dev/null and b/test/fuzz/fuzz-dhcp-relay/offer differ
diff --git a/test/fuzz/fuzz-dhcp-server-relay/sample1 b/test/fuzz/fuzz-dhcp-relay/sample1
similarity index 100%
rename from test/fuzz/fuzz-dhcp-server-relay/sample1
rename to test/fuzz/fuzz-dhcp-relay/sample1
diff --git a/test/fuzz/fuzz-dhcp-server-relay/sample2 b/test/fuzz/fuzz-dhcp-relay/sample2
similarity index 100%
rename from test/fuzz/fuzz-dhcp-server-relay/sample2
rename to test/fuzz/fuzz-dhcp-relay/sample2
diff --git a/test/fuzz/fuzz-dhcp-server-relay/too-large-packet b/test/fuzz/fuzz-dhcp-relay/too-large-packet
similarity index 100%
rename from test/fuzz/fuzz-dhcp-server-relay/too-large-packet
rename to test/fuzz/fuzz-dhcp-relay/too-large-packet
diff --git a/test/test-network/conf/25-agent-client-peer.network b/test/test-network/conf/25-agent-client-peer.network
index 4d7d758d29..eaa3dfa572 100644
--- a/test/test-network/conf/25-agent-client-peer.network
+++ b/test/test-network/conf/25-agent-client-peer.network
@@ -3,13 +3,12 @@
Name=client-peer
[Network]
-Address=192.168.6.2/24
+Address=198.51.100.10/24
DHCPServer=yes
-IPv4Forwarding=yes
IPv6AcceptRA=no
[DHCPServer]
-RelayTarget=192.168.5.1
+RelayTarget=192.0.2.1
BindToInterface=no
RelayAgentCircuitId=string:sample_circuit_id
RelayAgentRemoteId=string:sample_remote_id
diff --git a/test/test-network/conf/25-agent-client.network b/test/test-network/conf/25-agent-client.network
index 219d40a9b7..731f8fe5b6 100644
--- a/test/test-network/conf/25-agent-client.network
+++ b/test/test-network/conf/25-agent-client.network
@@ -4,5 +4,4 @@ Name=client
[Network]
DHCP=yes
-IPv4Forwarding=yes
IPv6AcceptRA=no
diff --git a/test/test-network/conf/25-agent-server-peer.network b/test/test-network/conf/25-agent-server-peer.network
index 5e005c79ec..ef1e80fcad 100644
--- a/test/test-network/conf/25-agent-server-peer.network
+++ b/test/test-network/conf/25-agent-server-peer.network
@@ -3,6 +3,5 @@
Name=server-peer
[Network]
-Address=192.168.5.2/24
-IPv4Forwarding=yes
+Address=192.0.2.2/24
IPv6AcceptRA=no
diff --git a/test/test-network/conf/25-agent-server.network b/test/test-network/conf/25-agent-server.network
deleted file mode 100644
index 0108039e6f..0000000000
--- a/test/test-network/conf/25-agent-server.network
+++ /dev/null
@@ -1,16 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-[Match]
-Name=server
-
-[Network]
-Address=192.168.5.1/24
-IPv4Forwarding=yes
-DHCPServer=yes
-IPv6AcceptRA=no
-
-[DHCPServer]
-BindToInterface=no
-PoolOffset=150
-PoolSize=1
-DNS=192.168.5.1
-NTP=192.168.5.1
diff --git a/test/test-network/conf/25-agent-veth-server.netdev b/test/test-network/conf/25-agent-veth-server.netdev
deleted file mode 100644
index 1427024d47..0000000000
--- a/test/test-network/conf/25-agent-veth-server.netdev
+++ /dev/null
@@ -1,9 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-[NetDev]
-Name=server
-Kind=veth
-MACAddress=12:34:56:78:9b:bc
-
-[Peer]
-Name=server-peer
-MACAddress=12:34:56:78:9b:bd
diff --git a/test/test-network/conf/25-dhcp-relay-downstream.network b/test/test-network/conf/25-dhcp-relay-downstream.network
new file mode 100644
index 0000000000..49ff765e95
--- /dev/null
+++ b/test/test-network/conf/25-dhcp-relay-downstream.network
@@ -0,0 +1,14 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=relay-down
+
+[Network]
+Address=198.51.100.10/24
+IPv6AcceptRA=no
+DHCPRelay=downstream
+
+[DHCPRelay]
+AgentAddress=198.51.100.10
+GatewayAddress=198.51.100.10
+CircuitId=string:test-dhcp-relay-circuit-id
+VirtualSubnetSelection=string:subnet-hoge
diff --git a/test/test-network/conf/25-dhcp-relay-upstream.network b/test/test-network/conf/25-dhcp-relay-upstream.network
new file mode 100644
index 0000000000..d84f6a71fe
--- /dev/null
+++ b/test/test-network/conf/25-dhcp-relay-upstream.network
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[Match]
+Name=relay-up
+
+[Network]
+Address=192.0.2.10/24
+IPv6AcceptRA=no
+DHCPRelay=upstream
+
+[DHCPRelay]
+AgentAddress=192.0.2.10
+InterfacePriority=100
diff --git a/test/test-network/conf/25-dhcp-relay.conf b/test/test-network/conf/25-dhcp-relay.conf
new file mode 100644
index 0000000000..5d6bcb2c4a
--- /dev/null
+++ b/test/test-network/conf/25-dhcp-relay.conf
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+[DHCPRelay]
+ServerAddress=192.0.2.1
+OverrideServerIdentifier=yes
+RemoteId=string:test-dhcp-relay-remote-id
diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py
index 8775c49ec8..cc55679351 100755
--- a/test/test-network/systemd-networkd-tests.py
+++ b/test/test-network/systemd-networkd-tests.py
@@ -1222,6 +1222,7 @@ 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-relay')
call_quiet('ip netns del ns-server')
# 4. remove links
@@ -8384,11 +8385,22 @@ class NetworkdDHCPServerRelayAgentTests(unittest.TestCase, Utilities):
tear_down_common()
def test_relay_agent(self):
+ check_output('ip netns add ns-server')
+ check_output('ip link add server type veth peer server-peer')
+ 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='198.51.100.100,198.51.100.109',
+ ipv4_router='198.51.100.10',
+ )
+
copy_network_unit(
'25-agent-veth-client.netdev',
- '25-agent-veth-server.netdev',
'25-agent-client.network',
- '25-agent-server.network',
'25-agent-client-peer.network',
'25-agent-server-peer.network',
)
@@ -8398,7 +8410,7 @@ class NetworkdDHCPServerRelayAgentTests(unittest.TestCase, Utilities):
output = networkctl_status('client')
print(output)
- self.assertRegex(output, r'Address: 192.168.5.150 \(DHCPv4 via 192.168.5.1\)')
+ self.assertRegex(output, r'Address: 198\.51\.100\.10[0-9] \(DHCPv4 via 192\.0\.2\.1\)')
def test_relay_agent_on_bridge(self):
copy_network_unit(
@@ -8412,7 +8424,116 @@ class NetworkdDHCPServerRelayAgentTests(unittest.TestCase, Utilities):
self.wait_online('bridge-relay:routable', 'client-peer:enslaved')
# For issue #30763.
- self.check_networkd_log('bridge-relay: DHCPv4 server: STARTED')
+ self.check_networkd_log('bridge-relay: Relaying DHCPv4 messages')
+
+ def test_sd_dhcp_relay(self):
+ check_output('ip netns add ns-relay')
+ check_output('ip netns add ns-server')
+
+ check_output('ip link add relay-down type veth peer client')
+ check_output('ip link set relay-down netns ns-relay')
+ check_output('ip netns exec ns-relay ip link set relay-down up')
+
+ check_output('ip link add relay-up type veth peer server')
+ check_output('ip link set relay-up netns ns-relay')
+ check_output('ip netns exec ns-relay ip link set relay-up up')
+
+ 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')
+
+ with tempfile.TemporaryDirectory() as tmp:
+ conf_dir = pathlib.Path(tmp) / 'run/systemd/networkd.conf.d'
+ unit_dir = pathlib.Path(tmp) / 'run/systemd/network'
+ netif_dir = pathlib.Path(tmp) / 'run/systemd/netif'
+ mkdir_p(conf_dir)
+ mkdir_p(unit_dir)
+ mkdir_p(netif_dir)
+
+ # Do not use shutil.chown(), as it fails when running with sanitizers.
+ check_output(f'chown systemd-network:systemd-network {netif_dir}')
+
+ cp(pathlib.Path(networkd_ci_temp_dir) / '25-dhcp-relay.conf', conf_dir)
+ cp(pathlib.Path(networkd_ci_temp_dir) / '25-dhcp-relay-downstream.network', unit_dir)
+ cp(pathlib.Path(networkd_ci_temp_dir) / '25-dhcp-relay-upstream.network', unit_dir)
+
+ cmd = [
+ 'systemd-run',
+ '--unit=networkd-test-dhcp-relay.service',
+ '--service-type=notify-reload',
+ '-p', 'User=systemd-network',
+ '-p', 'FileDescriptorStoreMax=512',
+ '-p', 'AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_BPF CAP_SYS_ADMIN',
+ '-p', 'CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW CAP_BPF CAP_SYS_ADMIN',
+ '-p', f'BindPaths={conf_dir}:/run/systemd/networkd.conf.d',
+ '-p', f'BindPaths={unit_dir}:/run/systemd/network',
+ '-p', f'BindPaths={netif_dir}:/run/systemd/netif',
+ '-p', 'TemporaryFileSystem=/run/dbus',
+ '-p', 'TemporaryFileSystem=/run/systemd/report',
+ '-p', 'TemporaryFileSystem=/run/systemd/resolve.hook',
+ '-p', 'TemporaryFileSystem=/var/lib/systemd/network',
+ '-p', 'TemporaryFileSystem=/etc/systemd/network',
+ '-p', 'TemporaryFileSystem=/usr/lib/systemd/network',
+ '-p', 'ReadOnlyPaths=/sys',
+ '-p', 'NetworkNamespacePath=/run/netns/ns-relay',
+ ] # fmt: skip
+ if enable_debug:
+ cmd += ['-p', 'Environment=SYSTEMD_LOG_LEVEL=debug']
+ if asan_options:
+ cmd += ['-p', f'Environment=ASAN_OPTIONS={asan_options}']
+ if lsan_options:
+ cmd += ['-p', f'Environment=LSAN_OPTIONS={lsan_options}']
+ if ubsan_options:
+ cmd += ['-p', f'Environment=UBSAN_OPTIONS={ubsan_options}']
+ if asan_options or lsan_options or ubsan_options:
+ cmd += ['-p', 'TimeoutStopFailureMode=abort']
+
+ cmd += [networkd_bin]
+
+ try:
+ check_output(*cmd)
+
+ start_dnsmasq(
+ namespace='ns-server',
+ interface='server',
+ ipv4_range='198.51.100.100,198.51.100.109',
+ ipv4_router='198.51.100.10',
+ )
+
+ 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'198\.51\.100\.10[0-9]/24')
+
+ print('## ip -4 route show dev client')
+ output = check_output('ip -4 route show dev client')
+ print(output)
+ # fmt: off
+ self.assertRegex(output, r'default via 198\.51\.100\.10 proto dhcp src 198\.51\.100\.10[0-9]')
+ self.assertRegex(output, r'198\.51\.100\.0/24 proto kernel scope link src 198\.51\.100\.10[0-9]')
+ self.assertRegex(output, r'198\.51\.100\.10 proto dhcp scope link src 198\.51\.100\.10[0-9]')
+ # fmt: on
+
+ print('## dnsmasq log')
+ output = read_dnsmasq_log_file()
+ print(output)
+ # The option 82 is logged as agent-info since dnsmasq-2.92, otherwise logged as agent-id.
+ self.assertRegex(output, r'option: 82 (agent-id|agent-info)')
+
+ print('## journal of networkd-test-dhcp-relay.service')
+ check_output('journalctl --sync')
+ output = check_output('journalctl --no-hostname --output=short-monotonic --unit=networkd-test-dhcp-relay.service -I') # fmt: skip
+ self.assertIn('relay-down: DHCPv4 relay: Received BOOTREQUEST', output)
+ self.assertIn('relay-up: DHCPv4 relay: Forwarded BOOTREQUEST', output)
+ self.assertIn('relay-up: DHCPv4 relay: Received BOOTREPLY', output)
+ self.assertIn('relay-down: DHCPv4 relay: Forwarded BOOTREPLY', output)
+ finally:
+ call_quiet('systemctl stop networkd-test-dhcp-relay.service')
+ call_quiet('systemctl reset-failed networkd-test-dhcp-relay.service')
class NetworkdDHCPClientTests(unittest.TestCase, Utilities):