diff --git a/src/libsystemd-network/dhcp-relay-downstream.c b/src/libsystemd-network/dhcp-relay-downstream.c new file mode 100644 index 00000000000..dd825d02a21 --- /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 00000000000..218f56fc04c --- /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 00000000000..feb08a606cf --- /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 00000000000..f1b4189153b --- /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/meson.build b/src/libsystemd-network/meson.build index b9b8c7062ef..5d84b9934aa 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', diff --git a/src/libsystemd-network/sd-dhcp-relay.c b/src/libsystemd-network/sd-dhcp-relay.c new file mode 100644 index 00000000000..1442a43fda7 --- /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/sd-common/sd-forward.h b/src/libsystemd/sd-common/sd-forward.h index 8abe655209d..ba800bbd016 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/systemd/meson.build b/src/systemd/meson.build index ad455d73b21..ca5444310ff 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 00000000000..47c42a21ddb --- /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