Previously, sd-dhcp-server can be run as a DHCP relay agent.
But, DHCP server and DHCP relay agent behave completely differently,
hence there is almost no code that can be shared between the two modes.
Let's split out the DHCP relay agent feature from sd-dhcp-server.
The new DHCP relay agent supports:
- multiple upstream and downstream interfaces,
- gateway address (giaddr field in DHCP message header) is configurable,
- supports more DHCP relay agent information sub-options,
- each interface has their own socket fd, and each socket is bound to
the interface, so that we can enable/disable each interface
safely without affecting other interfaces, and we can filter out any
unexpected packets from unmanaged interfaces,
networkd integration and test cases will be added later.
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.h>
+
+#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);
+}
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/ip.h>
+
+#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;
+}
--- /dev/null
+/* 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__)
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.h>
+
+#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;
+}
'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',
'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',
--- /dev/null
+/* 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);
+}
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;
'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',
--- /dev/null
+/* 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 <https://www.gnu.org/licenses/>.
+***/
+
+#include <netinet/in.h>
+#include <sys/uio.h>
+
+#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