]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
test: add unit test for DHCP relay agent
authorYu Watanabe <watanabe.yu+github@gmail.com>
Sat, 2 May 2026 14:18:32 +0000 (23:18 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Thu, 21 May 2026 07:55:59 +0000 (16:55 +0900)
src/libsystemd-network/meson.build
src/libsystemd-network/test-dhcp-relay.c [new file with mode: 0644]

index 5d84b9934aadc219764026a456a3045db2448b38..7693687a132116bf986eef5243abde15c7f929f7 100644 (file)
@@ -96,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'),
         },
diff --git a/src/libsystemd-network/test-dhcp-relay.c b/src/libsystemd-network/test-dhcp-relay.c
new file mode 100644 (file)
index 0000000..dd4e765
--- /dev/null
@@ -0,0 +1,383 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.h>
+#include <sys/socket.h>
+
+#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);