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):