diff --git a/dhcp6relay/src/config_interface.cpp b/dhcp6relay/src/config_interface.cpp index 2834eae..2510712 100644 --- a/dhcp6relay/src/config_interface.cpp +++ b/dhcp6relay/src/config_interface.cpp @@ -1,6 +1,11 @@ #include #include #include +#include +#include +#include +#include +#include #include "config_interface.h" constexpr auto DEFAULT_TIMEOUT_MSEC = 1000; @@ -8,6 +13,13 @@ constexpr auto DEFAULT_TIMEOUT_MSEC = 1000; bool pollSwssNotifcation = true; swss::Select swssSelect; +// Config-monitor thread state: publishes desired config under g_cfg_mutex, wakes the main loop via g_notify_fd. +static std::mutex g_cfg_mutex; +static std::unordered_map g_desired_cfg; +static std::thread g_monitor_thread; +static std::atomic g_stop_monitor{false}; +static int g_notify_fd = -1; + /** * @code void initialize_swss() * @@ -21,7 +33,7 @@ void initialize_swss(std::unordered_map &vlans) std::shared_ptr configDbPtr = std::make_shared ("CONFIG_DB", 0); swss::SubscriberStateTable ipHelpersTable(configDbPtr.get(), "DHCP_RELAY"); swssSelect.addSelectable(&ipHelpersTable); - get_dhcp(vlans, &ipHelpersTable, false, configDbPtr); + get_dhcp(vlans, &ipHelpersTable, configDbPtr); } catch (const std::bad_alloc &e) { syslog(LOG_ERR, "Failed allocate memory. Exception details: %s", e.what()); @@ -53,14 +65,14 @@ void deinitialize_swss() /** - * @code void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, bool dynamic, + * @code void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, std::shared_ptr config_db) * * @brief initialize and get vlan table information from DHCP_RELAY * * @return none */ -void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, bool dynamic, +void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, std::shared_ptr config_db) { swss::Selectable *selectable; int ret = swssSelect.select(&selectable, DEFAULT_TIMEOUT_MSEC); @@ -70,12 +82,7 @@ void get_dhcp(std::unordered_map &vlans, swss::Subscr } else if (ret == swss::Select::TIMEOUT) { } if (selectable == static_cast (ipHelpersTable)) { - if (!dynamic) { - handleRelayNotification(*ipHelpersTable, vlans, config_db); - } else { - syslog(LOG_WARNING, "relay config changed, " - "need restart container to take effect"); - } + handleRelayNotification(*ipHelpersTable, vlans, config_db); } } @@ -207,3 +214,149 @@ bool check_is_lla_ready(std::string vlan) { } return false; } + +/** + * @code std::unordered_map build_desired_config(std::shared_ptr config_db) + * + * @brief read the full DHCP_RELAY table and build the desired per-vlan relay config + * + * @param config_db CONFIG_DB connector used to read DHCP_RELAY and VLAN_INTERFACE + * + * @return desired map of vlan name to relay_config (config fields only) + */ +std::unordered_map build_desired_config(std::shared_ptr config_db) +{ + std::unordered_map desired; + swss::Table dhcp_relay_table(config_db.get(), "DHCP_RELAY"); + std::vector keys; + dhcp_relay_table.getKeys(keys); + + std::deque entries; + for (const auto &key : keys) { + std::vector field_values; + dhcp_relay_table.get(key, field_values); + entries.emplace_back(key, "SET", field_values); + } + // Reuse the notification parser to keep parsing in one code path. + processRelayNotification(entries, desired, config_db); + return desired; +} + +/** + * @code static void publish_desired_config(std::shared_ptr config_db) + * + * @brief snapshot the desired config, store it under lock and wake the main loop + * + * @param config_db CONFIG_DB connector used to read the desired config + * + * @return none + */ +static void publish_desired_config(std::shared_ptr config_db) +{ + auto desired = build_desired_config(config_db); + { + std::lock_guard lock(g_cfg_mutex); + g_desired_cfg = std::move(desired); + } + if (g_notify_fd >= 0) { + char notify_byte = 1; + ssize_t written = write(g_notify_fd, ¬ify_byte, sizeof(notify_byte)); + if (written < 0) { + syslog(LOG_WARNING, "Failed to notify main loop of config change: %s", strerror(errno)); + } + } +} + +/** + * @code static void config_monitor_loop() + * + * @brief monitor thread: watch CONFIG_DB tables and publish desired config on change + * + * @return none + */ +static void config_monitor_loop() +{ + auto config_db = std::make_shared("CONFIG_DB", 0); + auto state_db = std::make_shared("STATE_DB", 0); + swss::SubscriberStateTable dhcpRelaySub(config_db.get(), "DHCP_RELAY"); + swss::SubscriberStateTable vlanIntfSub(config_db.get(), "VLAN_INTERFACE"); + swss::SubscriberStateTable vlanSub(config_db.get(), "VLAN"); + // Watch STATE_DB INTERFACE_TABLE to reconcile a vlan as soon as its interface is up. + swss::SubscriberStateTable intfStateSub(state_db.get(), "INTERFACE_TABLE"); + + swss::Select select; + select.addSelectable(&dhcpRelaySub); + select.addSelectable(&vlanIntfSub); + select.addSelectable(&vlanSub); + select.addSelectable(&intfStateSub); + + // Drain the initial cached snapshot to avoid a redundant first reprocess. + std::deque drain; + dhcpRelaySub.pops(drain); + vlanIntfSub.pops(drain); + vlanSub.pops(drain); + intfStateSub.pops(drain); + + try { + publish_desired_config(config_db); + } catch (const std::exception &e) { + syslog(LOG_WARNING, "config monitor: initial publish failed, will retry on next change: %s", e.what()); + } + + while (!g_stop_monitor.load()) { + swss::Selectable *selectable = nullptr; + int ret = select.select(&selectable, DEFAULT_TIMEOUT_MSEC); + if (ret == swss::Select::TIMEOUT) { + continue; + } + if (ret == swss::Select::ERROR) { + syslog(LOG_WARNING, "Select: returned ERROR in config monitor"); + continue; + } + + try { + std::deque entries; + dhcpRelaySub.pops(entries); + entries.clear(); + vlanIntfSub.pops(entries); + entries.clear(); + vlanSub.pops(entries); + entries.clear(); + intfStateSub.pops(entries); + entries.clear(); + + publish_desired_config(config_db); + } catch (const std::exception &e) { + // Don't let a transient CONFIG_DB failure terminate the monitor thread. + syslog(LOG_WARNING, "config monitor: reconcile failed, will retry: %s", e.what()); + } + } +} + +void start_dhcp_config_monitor(int notify_fd) +{ + // Stop any previous monitor before starting a new one. + if (g_monitor_thread.joinable()) { + stop_dhcp_config_monitor(); + } + g_notify_fd = notify_fd; + g_stop_monitor.store(false); + g_monitor_thread = std::thread(config_monitor_loop); +} + +void stop_dhcp_config_monitor() +{ + g_stop_monitor.store(true); + // Join before shutdown_relay tears down the state the thread uses. + if (g_monitor_thread.joinable()) { + g_monitor_thread.join(); + } + g_notify_fd = -1; +} + +bool fetch_desired_config(std::unordered_map &out) +{ + std::lock_guard lock(g_cfg_mutex); + out = g_desired_cfg; + return true; +} diff --git a/dhcp6relay/src/config_interface.h b/dhcp6relay/src/config_interface.h index 31d25e6..9eb6870 100644 --- a/dhcp6relay/src/config_interface.h +++ b/dhcp6relay/src/config_interface.h @@ -33,14 +33,14 @@ void initialize_swss(std::unordered_map &vlans); void deinitialize_swss(); /** - * @code void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, bool dynamic, + * @code void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, * std::shared_ptr config_db) * * @brief initialize and get vlan information from DHCP_RELAY * * @return none */ -void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, bool dynamic, +void get_dhcp(std::unordered_map &vlans, swss::SubscriberStateTable *ipHelpersTable, std::shared_ptr config_db); /** @@ -81,3 +81,45 @@ void processRelayNotification(std::deque &entries, * @return bool value indicates whether lla ready */ bool check_is_lla_ready(std::string vlan); + +/** + * @code build_desired_config(std::shared_ptr config_db); + * + * @brief read the full DHCP_RELAY table and build the desired per-vlan relay config + * + * @param config_db CONFIG_DB connector used to read DHCP_RELAY and VLAN_INTERFACE + * + * @return desired map of vlan name to relay_config (config fields only) + */ +std::unordered_map build_desired_config(std::shared_ptr config_db); + +/** + * @code start_dhcp_config_monitor(int notify_fd); + * + * @brief start the detached thread that watches CONFIG_DB and publishes desired config + * + * @param notify_fd write end of the pipe used to wake the libevent main loop + * + * @return none + */ +void start_dhcp_config_monitor(int notify_fd); + +/** + * @code stop_dhcp_config_monitor(); + * + * @brief signal the config monitor thread to stop + * + * @return none + */ +void stop_dhcp_config_monitor(); + +/** + * @code fetch_desired_config(std::unordered_map &out); + * + * @brief copy the latest desired config published by the monitor thread + * + * @param out map populated with the latest desired per-vlan relay config + * + * @return true on success + */ +bool fetch_desired_config(std::unordered_map &out); diff --git a/dhcp6relay/src/relay.cpp b/dhcp6relay/src/relay.cpp index ebb27de..95f793f 100644 --- a/dhcp6relay/src/relay.cpp +++ b/dhcp6relay/src/relay.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,35 @@ static std::string counter_table = "DHCPv6_COUNTER_TABLE|"; static uint8_t client_recv_buffer[BUFFER_SIZE]; static uint8_t server_recv_buffer[BUFFER_SIZE]; +/** + * @code bool should_log_throttled(const std::string &key); + * + * @brief rate-limit a per-packet log so it cannot flood syslog. + * Each key is logged at most once per LOG_THROTTLE_INTERVAL. + * Used for warnings that can fire per packet, e.g. client + * traffic on an unconfigured interface (expected before any + * DHCP_RELAY config now that dhcp6relay starts always). + * + * @param key identifier of the throttled log site (e.g. interface or + * vlan name) + * + * @return true if the caller should emit the log now + * + * @note Called only from the libevent main thread, so the static + * bookkeeping needs no locking. + */ +static bool should_log_throttled(const std::string &key) { + static std::unordered_map last_logged; + constexpr auto LOG_THROTTLE_INTERVAL = std::chrono::seconds(60); + auto now = std::chrono::steady_clock::now(); + auto it = last_logged.find(key); + if (it == last_logged.end() || (now - it->second) >= LOG_THROTTLE_INTERVAL) { + last_logged[key] = now; + return true; + } + return false; +} + /* DHCPv6 filter */ /* sudo tcpdump -dd "inbound and ip6 dst ff02::1:2 && udp dst port 547" */ @@ -454,6 +484,31 @@ int sock_open(const struct sock_fprog *fprog) return s; } +/** + * @code build_servers_sock(relay_config &config); + * + * @brief (re)build the cached server sockaddr list from config.servers + * + * @param config relay interface config whose servers_sock is rebuilt + * + * @return none + */ +void build_servers_sock(relay_config &config) { + config.servers_sock.clear(); + for(auto server: config.servers) { + sockaddr_in6 tmp; + if(inet_pton(AF_INET6, server.c_str(), &tmp.sin6_addr) != 1) + { + syslog(LOG_WARNING, "inet_pton: Failed to convert IPv6 address\n"); + } + tmp.sin6_family = AF_INET6; + tmp.sin6_flowinfo = 0; + tmp.sin6_port = htons(RELAY_PORT); + tmp.sin6_scope_id = 0; + config.servers_sock.push_back(tmp); + } +} + /** * @code prepare_relay_config(relay_config &interface_config, int gua_sock, int filter); * @@ -473,18 +528,7 @@ void prepare_relay_config(relay_config &interface_config, int gua_sock, int filt interface_config.gua_sock = gua_sock; interface_config.filter = filter; - for(auto server: interface_config.servers) { - sockaddr_in6 tmp; - if(inet_pton(AF_INET6, server.c_str(), &tmp.sin6_addr) != 1) - { - syslog(LOG_WARNING, "inet_pton: Failed to convert IPv6 address\n"); - } - tmp.sin6_family = AF_INET6; - tmp.sin6_flowinfo = 0; - tmp.sin6_port = htons(RELAY_PORT); - tmp.sin6_scope_id = 0; - interface_config.servers_sock.push_back(tmp); - } + build_servers_sock(interface_config); if (getifaddrs(&ifa) == -1) { syslog(LOG_WARNING, "getifaddrs: Unable to get network interfaces\n"); @@ -899,14 +943,17 @@ void client_callback(evutil_socket_t fd, short event, void *arg) { // add is_lla_ready flag check in this callback func auto vlan = vlan_map.find(intf); if (vlan == vlan_map.end()) { - if (intf.find(CLIENT_IF_PREFIX) != std::string::npos) { + if (intf.find(CLIENT_IF_PREFIX) != std::string::npos && + should_log_throttled("invalid_intf:" + intf)) { syslog(LOG_WARNING, "Invalid input interface %s\n", interfaceName); } continue; } auto config_itr = vlans->find(vlan->second); if (config_itr == vlans->end()) { - syslog(LOG_WARNING, "Config not found for vlan %s\n", vlan->second.c_str()); + if (should_log_throttled("no_config:" + vlan->second)) { + syslog(LOG_WARNING, "Config not found for vlan %s\n", vlan->second.c_str()); + } continue; } auto config = config_itr->second; @@ -1309,6 +1356,24 @@ void loop_relay(std::unordered_map &vlans) { // hence manually invoke it here to immediate execute it lla_check_callback(-1, 0, timer_args); + // Runtime config monitor: apply relay config changes without a container restart (wakes this loop via a self-pipe). + int cfg_pipe[2]; + if (pipe(cfg_pipe) == 0) { + evutil_make_socket_nonblocking(cfg_pipe[0]); + evutil_make_socket_nonblocking(cfg_pipe[1]); + auto *apply_ctx = new config_apply_ctx{&vlans, timer_args, timer_event, cfg_pipe[0]}; + auto cfg_event = event_new(base, cfg_pipe[0], EV_READ|EV_PERSIST, config_change_callback, apply_ctx); + if (cfg_event != NULL) { + event_add(cfg_event, NULL); + start_dhcp_config_monitor(cfg_pipe[1]); + syslog(LOG_INFO, "libevent: Add runtime config monitor event\n"); + } else { + syslog(LOG_ERR, "libevent: Failed to create runtime config monitor event\n"); + } + } else { + syslog(LOG_ERR, "Failed to create config monitor pipe: %s\n", strerror(errno)); + } + if(signal_init() == 0 && signal_start() == 0) { shutdown_relay(); for(std::size_t i = 0; i < sockets.size(); i++) { @@ -1323,6 +1388,7 @@ void loop_relay(std::unordered_map &vlans) { * @brief free signals and terminate threads */ void shutdown_relay() { + stop_dhcp_config_monitor(); event_del(ev_sigint); event_del(ev_sigterm); event_free(ev_sigint); @@ -1409,13 +1475,14 @@ void lla_check_callback(evutil_socket_t fd, short event, void *arg) { sockets.push_back(lla_sock); prepare_relay_config(vlan.second, gua_sock, filter); if (!dual_tor_sock) { - auto server_callback_event = event_new(base, gua_sock, EV_READ|EV_PERSIST, + vlan.second.server_event = event_new(base, gua_sock, EV_READ|EV_PERSIST, server_callback, &(vlan.second)); - if (server_callback_event == NULL) { + if (vlan.second.server_event == NULL) { syslog(LOG_ERR, "libevent: Failed to create server listen libevent\n"); + } else { + event_add(vlan.second.server_event, NULL); + syslog(LOG_INFO, "libevent: add server listen socket for %s\n", vlan.first.c_str()); } - event_add(server_callback_event, NULL); - syslog(LOG_INFO, "libevent: add server listen socket for %s\n", vlan.first.c_str()); } } else { syslog(LOG_ERR, "Failed to create dualtor loopback listen socket"); @@ -1427,3 +1494,155 @@ void lla_check_callback(evutil_socket_t fd, short event, void *arg) { event_del(timer_event); } } + +/** + * @code void teardown_vlan_relay(relay_config &config); + * + * @brief free libevent event and sockets for a vlan being removed at runtime + * + * @param config relay interface config being torn down + * + * @return none + */ +void teardown_vlan_relay(relay_config &config) { + if (config.server_event != nullptr) { + event_del(config.server_event); + event_free(config.server_event); + config.server_event = nullptr; + } + if (config.is_lla_ready) { + if (config.gua_sock > 0) { + close(config.gua_sock); + config.gua_sock = -1; + } + if (config.lla_sock > 0) { + close(config.lla_sock); + config.lla_sock = -1; + } + } + // Remove this vlan's entries from the global lookup maps to avoid stale packet associations. + for (auto it = vlan_map.begin(); it != vlan_map.end(); ) { + if (it->second == config.interface) { + it = vlan_map.erase(it); + } else { + ++it; + } + } + for (auto it = addr_vlan_map.begin(); it != addr_vlan_map.end(); ) { + if (it->second == config.interface) { + it = addr_vlan_map.erase(it); + } else { + ++it; + } + } +} + +/** + * @code bool apply_desired_config(std::unordered_map &vlans, + * std::unordered_map &desired); + * + * @brief reconcile the live vlans map with the desired config (add/remove/update) + * + * @param vlans live per-vlan relay config (mutated in place) + * @param desired desired per-vlan relay config (config fields only) + * + * @return true if at least one vlan was newly added (sockets need arming) + */ +bool apply_desired_config(std::unordered_map &vlans, + std::unordered_map &desired) { + bool added = false; + + // Remove relay configs for vlans no longer present in the desired config. + for (auto it = vlans.begin(); it != vlans.end(); ) { + if (desired.find(it->first) == desired.end()) { + syslog(LOG_INFO, "Remove relay config for %s at runtime\n", it->first.c_str()); + teardown_vlan_relay(it->second); + it = vlans.erase(it); + } else { + ++it; + } + } + + // Add new vlans and update existing ones. + for (auto &desired_entry : desired) { + const std::string &name = desired_entry.first; + relay_config &dcfg = desired_entry.second; + auto it = vlans.find(name); + if (it == vlans.end()) { + relay_config ncfg; + ncfg.interface = name; + ncfg.servers = dcfg.servers; + ncfg.is_option_79 = dcfg.is_option_79; + ncfg.is_interface_id = dcfg.is_interface_id; + ncfg.mux_key = ""; + ncfg.state_db = nullptr; + ncfg.is_lla_ready = false; + ncfg.server_event = nullptr; + vlans[name] = ncfg; + added = true; + syslog(LOG_INFO, "Add relay config for %s at runtime\n", name.c_str()); + } else { + relay_config &live = it->second; + bool changed = (live.servers != dcfg.servers) || + (live.is_option_79 != dcfg.is_option_79) || + (live.is_interface_id != dcfg.is_interface_id); + if (changed) { + live.servers = dcfg.servers; + live.is_option_79 = dcfg.is_option_79; + live.is_interface_id = dcfg.is_interface_id; + // Rebuild the cached server sockaddr list only if the relay is active; else lla_check_callback builds it later. + if (live.is_lla_ready) { + build_servers_sock(live); + } + syslog(LOG_INFO, "Update relay config for %s at runtime\n", name.c_str()); + } + } + } + + return added; +} + +/** + * @code void config_change_callback(evutil_socket_t fd, short event, void *arg); + * + * @brief libevent callback that applies runtime relay configuration changes + * + * @param fd notify pipe read end + * @param event libevent triggered event + * @param arg pointer to config_apply_ctx + * + * @return none + */ +void config_change_callback(evutil_socket_t fd, short event, void *arg) { + auto *ctx = reinterpret_cast(arg); + + // Drain the notify pipe (the monitor may have coalesced several changes into wake bytes). + char drain_buf[64]; + while (read(ctx->notify_rd, drain_buf, sizeof(drain_buf)) > 0) { + // discard + } + + std::unordered_map desired; + if (!fetch_desired_config(desired)) { + return; + } + + bool added = apply_desired_config(*ctx->vlans, desired); + + // Re-fire the link-local check so newly-ready vlans (is_lla_ready == false) get armed now, not at the next 60s tick. + bool pending_lla = false; + for (const auto &vlan : *ctx->vlans) { + if (!vlan.second.is_lla_ready) { + pending_lla = true; + break; + } + } + + if (added || pending_lla) { + struct timeval tv; + evutil_timerclear(&tv); + tv.tv_sec = 60; + event_add(ctx->timer_event, &tv); + lla_check_callback(-1, 0, ctx->timer_args); + } +} diff --git a/dhcp6relay/src/relay.h b/dhcp6relay/src/relay.h index 2b129fc..8569f57 100644 --- a/dhcp6relay/src/relay.h +++ b/dhcp6relay/src/relay.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include "dbconnector.h" @@ -77,6 +78,7 @@ struct relay_config { std::shared_ptr mux_table; std::shared_ptr config_db; bool is_lla_ready; + struct event *server_event = nullptr; }; /* DHCPv6 messages and options */ @@ -284,6 +286,67 @@ void server_callback_dualtor(evutil_socket_t fd, short event, void *arg); */ void loop_relay(std::unordered_map &vlans); +/* + * Context passed to config_change_callback so the libevent main thread can + * reconcile the live vlans map with the desired configuration published by the + * runtime configuration monitor thread. + */ +struct config_apply_ctx { + std::unordered_map *vlans; + void *timer_args; + struct event *timer_event; + int notify_rd; +}; + +/** + * @code build_servers_sock(relay_config &config); + * + * @brief (re)build the cached server sockaddr list from config.servers + * + * @param config relay interface config whose servers_sock is rebuilt + * + * @return none + */ +void build_servers_sock(relay_config &config); + +/** + * @code teardown_vlan_relay(relay_config &config); + * + * @brief free libevent event and sockets for a vlan being removed at runtime + * + * @param config relay interface config being torn down + * + * @return none + */ +void teardown_vlan_relay(relay_config &config); + +/** + * @code apply_desired_config(std::unordered_map &vlans, + * std::unordered_map &desired); + * + * @brief reconcile the live vlans map with the desired config (add/remove/update) + * + * @param vlans live per-vlan relay config (mutated in place) + * @param desired desired per-vlan relay config (config fields only) + * + * @return true if at least one vlan was newly added (sockets need arming) + */ +bool apply_desired_config(std::unordered_map &vlans, + std::unordered_map &desired); + +/** + * @code config_change_callback(evutil_socket_t fd, short event, void *arg); + * + * @brief libevent callback that applies runtime relay configuration changes + * + * @param fd notify pipe read end + * @param event libevent triggered event + * @param arg pointer to config_apply_ctx + * + * @return none + */ +void config_change_callback(evutil_socket_t fd, short event, void *arg); + /** * @code signal_init(); * diff --git a/dhcp6relay/test/mock_config_interface.cpp b/dhcp6relay/test/mock_config_interface.cpp index 104ef96..ca6557d 100644 --- a/dhcp6relay/test/mock_config_interface.cpp +++ b/dhcp6relay/test/mock_config_interface.cpp @@ -1,3 +1,6 @@ +#include +#include +#include #include "mock_config_interface.h" using namespace ::testing; @@ -25,12 +28,12 @@ TEST(configInterface, get_dhcp) { swss::SubscriberStateTable ipHelpersTable(config_db.get(), "DHCP_RELAY"); std::unordered_map vlans; - ASSERT_NO_THROW(get_dhcp(vlans, &ipHelpersTable, false, config_db)); + ASSERT_NO_THROW(get_dhcp(vlans, &ipHelpersTable, config_db)); EXPECT_EQ(vlans.size(), 0); swssSelect.addSelectable(&ipHelpersTable); - ASSERT_NO_THROW(get_dhcp(vlans, &ipHelpersTable, false, config_db)); + ASSERT_NO_THROW(get_dhcp(vlans, &ipHelpersTable, config_db)); EXPECT_EQ(vlans.size(), 1); } @@ -70,3 +73,137 @@ TEST(configInterface, stopSwssNotificationPoll) { TEST(configInterface, check_is_lla_ready) { EXPECT_FALSE(check_is_lla_ready("Vlan1000")); } + +TEST(configInterface, build_desired_config) { + std::shared_ptr config_db = std::make_shared ("CONFIG_DB", 0); + config_db->hset("DHCP_RELAY|Vlan2000", "dhcpv6_servers@", "fc02:2000::1,fc02:2000::2"); + config_db->hset("DHCP_RELAY|Vlan2000", "dhcpv6_option|rfc6939_support", "false"); + config_db->hset("DHCP_RELAY|Vlan2000", "dhcpv6_option|interface_id", "true"); + config_db->hset("VLAN_INTERFACE|Vlan2000|fc02:2000::1", "", ""); + + auto desired = build_desired_config(config_db); + ASSERT_EQ(desired.count("Vlan2000"), 1); + EXPECT_EQ(desired["Vlan2000"].servers.size(), 2); + EXPECT_FALSE(desired["Vlan2000"].is_option_79); + EXPECT_TRUE(desired["Vlan2000"].is_interface_id); +} + +TEST(configInterface, build_desired_config_skips_vlan_without_ipv6) { + std::shared_ptr config_db = std::make_shared ("CONFIG_DB", 0); + // No VLAN_INTERFACE IPv6 address for Vlan3000, so it must not be relayed. + config_db->hset("DHCP_RELAY|Vlan3000", "dhcpv6_servers@", "fc02:3000::1"); + + auto desired = build_desired_config(config_db); + EXPECT_EQ(desired.count("Vlan3000"), 0); +} + +TEST(configInterface, build_desired_config_interface_id_default_tracks_dualtor) { + // The interface-id option defaults to enabled in Dual-ToR mode and disabled + // otherwise. build_desired_config (used by the runtime config monitor) must + // honour this default for a VLAN whose DHCP_RELAY entry does not set + // dhcpv6_option|interface_id explicitly, in both Dual-ToR and non-Dual-ToR + // mode. dual_tor_sock is fixed at startup from the -u option. + std::shared_ptr config_db = std::make_shared ("CONFIG_DB", 0); + config_db->hset("DHCP_RELAY|Vlan4200", "dhcpv6_servers@", "fc02:4200::1"); + config_db->hset("VLAN_INTERFACE|Vlan4200|fc02:4200::1", "", ""); + + bool saved_dual_tor_sock = dual_tor_sock; + + // Non-Dual-ToR: interface-id default is disabled. + dual_tor_sock = false; + auto desired_non_dualtor = build_desired_config(config_db); + ASSERT_EQ(desired_non_dualtor.count("Vlan4200"), 1); + EXPECT_FALSE(desired_non_dualtor["Vlan4200"].is_interface_id); + + // Dual-ToR: interface-id default is enabled. + dual_tor_sock = true; + auto desired_dualtor = build_desired_config(config_db); + ASSERT_EQ(desired_dualtor.count("Vlan4200"), 1); + EXPECT_TRUE(desired_dualtor["Vlan4200"].is_interface_id); + + // Restore the global so the mode does not leak into other tests. + dual_tor_sock = saved_dual_tor_sock; + + config_db->del("DHCP_RELAY|Vlan4200"); + config_db->del("VLAN_INTERFACE|Vlan4200|fc02:4200::1"); +} + +TEST(configInterface, fetch_desired_config) { + std::unordered_map out; + EXPECT_TRUE(fetch_desired_config(out)); +} + +TEST(configInterface, start_stop_dhcp_config_monitor) { + std::shared_ptr config_db = std::make_shared ("CONFIG_DB", 0); + config_db->hset("DHCP_RELAY|Vlan4000", "dhcpv6_servers@", "fc02:4000::1"); + config_db->hset("VLAN_INTERFACE|Vlan4000|fc02:4000::1", "", ""); + + int pipefd[2]; + ASSERT_EQ(pipe(pipefd), 0); + + // Start the monitor thread; it reads CONFIG_DB, publishes the desired config + // and wakes the (read end of the) notify pipe. + ASSERT_NO_THROW(start_dhcp_config_monitor(pipefd[1])); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + + std::unordered_map out; + EXPECT_TRUE(fetch_desired_config(out)); + + // Stop the monitor and give the select loop time to observe the stop flag. + ASSERT_NO_THROW(stop_dhcp_config_monitor()); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + + close(pipefd[0]); + close(pipefd[1]); +} + +TEST(configInterface, config_monitor_reacts_to_state_db_interface_table) { + // The monitor also watches STATE_DB INTERFACE_TABLE so that a vlan whose + // interface becomes ready after startup is reconciled immediately. Drive a + // STATE_DB INTERFACE_TABLE change while the monitor runs and confirm the + // monitor wakes (the notify pipe receives a byte) and re-publishes the + // desired config. + std::shared_ptr config_db = std::make_shared ("CONFIG_DB", 0); + config_db->hset("DHCP_RELAY|Vlan4100", "dhcpv6_servers@", "fc02:4100::1"); + config_db->hset("VLAN_INTERFACE|Vlan4100|fc02:4100::1", "", ""); + + int pipefd[2]; + ASSERT_EQ(pipe(pipefd), 0); + evutil_make_socket_nonblocking(pipefd[0]); + + ASSERT_NO_THROW(start_dhcp_config_monitor(pipefd[1])); + // Let the monitor publish its startup snapshot and drain that wake byte. + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + char drain[64]; + while (read(pipefd[0], drain, sizeof(drain)) > 0) { /* discard startup wake */ } + + // A STATE_DB INTERFACE_TABLE change must wake the monitor's select loop. + std::shared_ptr state_db = std::make_shared ("STATE_DB", 0); + state_db->hset("INTERFACE_TABLE|Vlan4100|fc02:4100::1", "state", "ok"); + + // The monitor should re-publish after the STATE_DB change (notify byte). Poll + // for up to ~3s to stay robust to scheduling/keyspace-notification latency. + ssize_t got = -1; + for (int i = 0; i < 30 && got <= 0; ++i) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + got = read(pipefd[0], drain, sizeof(drain)); + } + EXPECT_GT(got, 0); + + // And the latest published desired config still contains the relayed vlan. + std::unordered_map out; + EXPECT_TRUE(fetch_desired_config(out)); + EXPECT_EQ(out.count("Vlan4100"), 1); + + ASSERT_NO_THROW(stop_dhcp_config_monitor()); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + + // Clean up the keys this test added so it does not perturb the shared redis + // state other tests rely on. + config_db->del("DHCP_RELAY|Vlan4100"); + config_db->del("VLAN_INTERFACE|Vlan4100|fc02:4100::1"); + state_db->del("INTERFACE_TABLE|Vlan4100|fc02:4100::1"); + + close(pipefd[0]); + close(pipefd[1]); +} diff --git a/dhcp6relay/test/mock_relay.cpp b/dhcp6relay/test/mock_relay.cpp index 2a75578..c027ac1 100644 --- a/dhcp6relay/test/mock_relay.cpp +++ b/dhcp6relay/test/mock_relay.cpp @@ -10,6 +10,7 @@ #include "gmock/gmock.h" #include "mock_relay.h" +#include "../src/config_interface.h" using namespace ::testing; @@ -1163,5 +1164,257 @@ TEST(relay, server_callback_dualtor) { ASSERT_NO_THROW(server_callback_dualtor(0, 0, &vlans_in_loop)); } +TEST(relay, build_servers_sock) { + struct relay_config config{}; + config.interface = "Vlan1000"; + config.servers.push_back("fc02:2000::1"); + config.servers.push_back("fc02:2000::2"); + + build_servers_sock(config); + EXPECT_EQ(config.servers_sock.size(), 2); + + // rebuilding is idempotent: the cached list is cleared then repopulated + build_servers_sock(config); + EXPECT_EQ(config.servers_sock.size(), 2); + + // shrinking the server list shrinks the cached sockaddr list + config.servers.pop_back(); + build_servers_sock(config); + EXPECT_EQ(config.servers_sock.size(), 1); +} + +TEST(relay, teardown_vlan_relay_config_only) { + struct relay_config config{}; + config.interface = "Vlan1000"; + config.is_lla_ready = false; + config.server_event = nullptr; + + vlan_map["Ethernet0"] = "Vlan1000"; + addr_vlan_map["fc02:1000::1"] = "Vlan1000"; + + // A config-only entry (no event, not yet active) tears down cleanly and + // clears its entries from the global lookup maps. + ASSERT_NO_THROW(teardown_vlan_relay(config)); + EXPECT_EQ(vlan_map.count("Ethernet0"), 0); + EXPECT_EQ(addr_vlan_map.count("fc02:1000::1"), 0); +} + +TEST(relay, apply_desired_config_add_update_remove) { + std::unordered_map vlans; + std::unordered_map desired; + + relay_config d1{}; + d1.interface = "Vlan1000"; + d1.servers = {"fc02:2000::1", "fc02:2000::2"}; + d1.is_option_79 = true; + d1.is_interface_id = false; + d1.is_lla_ready = false; + d1.server_event = nullptr; + desired["Vlan1000"] = d1; + + // Add: empty live map gains Vlan1000 and reports that a vlan was added. + bool added = apply_desired_config(vlans, desired); + EXPECT_TRUE(added); + ASSERT_EQ(vlans.count("Vlan1000"), 1); + EXPECT_EQ(vlans["Vlan1000"].servers.size(), 2); + EXPECT_TRUE(vlans["Vlan1000"].is_option_79); + EXPECT_FALSE(vlans["Vlan1000"].is_interface_id); + + // Update: changing servers and options is applied in place with no add. + desired["Vlan1000"].servers = {"fc02:2000::9"}; + desired["Vlan1000"].is_option_79 = false; + desired["Vlan1000"].is_interface_id = true; + added = apply_desired_config(vlans, desired); + EXPECT_FALSE(added); + EXPECT_EQ(vlans["Vlan1000"].servers.size(), 1); + EXPECT_FALSE(vlans["Vlan1000"].is_option_79); + EXPECT_TRUE(vlans["Vlan1000"].is_interface_id); + + // Remove: an empty desired map tears the vlan down. + std::unordered_map empty_desired; + added = apply_desired_config(vlans, empty_desired); + EXPECT_FALSE(added); + EXPECT_EQ(vlans.count("Vlan1000"), 0); +} + +TEST(relay, apply_desired_config_rebuilds_active_servers_sock) { + std::unordered_map vlans; + std::unordered_map desired; + + // An already-active vlan (is_lla_ready true) with a cached sockaddr list. + relay_config live{}; + live.interface = "Vlan1000"; + live.servers = {"fc02:2000::1"}; + live.is_lla_ready = true; + build_servers_sock(live); + EXPECT_EQ(live.servers_sock.size(), 1); + vlans["Vlan1000"] = live; + + // Desired adds a second server; the cached sockaddr list must be rebuilt. + relay_config d{}; + d.interface = "Vlan1000"; + d.servers = {"fc02:2000::1", "fc02:2000::2"}; + desired["Vlan1000"] = d; + + bool added = apply_desired_config(vlans, desired); + EXPECT_FALSE(added); + EXPECT_EQ(vlans["Vlan1000"].servers.size(), 2); + EXPECT_EQ(vlans["Vlan1000"].servers_sock.size(), 2); +} + +TEST(relay, teardown_vlan_relay_active) { + // An active relay (lla ready) owns a libevent event and two sockets; teardown + // must free the event, close both sockets and clear the global lookup maps. + base = event_base_new(); + ASSERT_NE(base, nullptr); + + struct relay_config config{}; + config.interface = "Vlan2000"; + config.is_lla_ready = true; + config.gua_sock = socket(AF_INET6, SOCK_DGRAM, 0); + config.lla_sock = socket(AF_INET6, SOCK_DGRAM, 0); + ASSERT_GT(config.gua_sock, 0); + ASSERT_GT(config.lla_sock, 0); + config.server_event = event_new(base, config.gua_sock, EV_READ | EV_PERSIST, server_callback, &config); + ASSERT_NE(config.server_event, nullptr); + + vlan_map["Ethernet4"] = "Vlan2000"; + addr_vlan_map["fc02:2000::1"] = "Vlan2000"; + + ASSERT_NO_THROW(teardown_vlan_relay(config)); + + EXPECT_EQ(config.server_event, nullptr); + EXPECT_EQ(config.gua_sock, -1); + EXPECT_EQ(config.lla_sock, -1); + EXPECT_EQ(vlan_map.count("Ethernet4"), 0); + EXPECT_EQ(addr_vlan_map.count("fc02:2000::1"), 0); + + event_base_free(base); + base = nullptr; +} + +TEST(relay, config_change_callback_remove) { + // No desired config has been published yet, so the callback must drain the + // notify pipe, fetch the (empty) desired config and remove the live vlan. + std::unordered_map vlans; + relay_config c{}; + c.interface = "Vlan1000"; + c.is_lla_ready = false; + c.server_event = nullptr; + vlans["Vlan1000"] = c; + + int pipefd[2]; + ASSERT_EQ(pipe(pipefd), 0); + evutil_make_socket_nonblocking(pipefd[0]); + char wake = 1; + ASSERT_EQ(write(pipefd[1], &wake, 1), 1); + + config_apply_ctx ctx{}; + ctx.vlans = &vlans; + ctx.timer_args = nullptr; + ctx.timer_event = nullptr; + ctx.notify_rd = pipefd[0]; + + ASSERT_NO_THROW(config_change_callback(pipefd[0], 0, &ctx)); + EXPECT_EQ(vlans.count("Vlan1000"), 0); + + close(pipefd[0]); + close(pipefd[1]); +} + +TEST(relay, config_change_callback_interface_ready_pending_lla) { + // A vlan that is configured but whose interface is not yet link-local ready + // must trigger the lla-check arming path even though no vlan is newly added + // (the interface-ready signal arrives via STATE_DB INTERFACE_TABLE). The + // callback must re-arm the timer and fire the lla check without throwing. + base = event_base_new(); + ASSERT_NE(base, nullptr); + + // Clear DHCP_RELAY so the published desired config contains only Vlan9100, + // making 'desired' match 'live' exactly (added==false, so pending_lla is what + // drives the arming path). + std::shared_ptr config_db = std::make_shared("CONFIG_DB", 0); + for (const auto &k : config_db->keys("DHCP_RELAY|*")) { + config_db->del(k); + } + config_db->hset("DHCP_RELAY|Vlan9100", "dhcpv6_servers@", "fc02:9100::1"); + config_db->hset("VLAN_INTERFACE|Vlan9100|fc02:9100::1", "", ""); + + // Publish {Vlan9100} into the global desired config via a brief monitor cycle. + int notify[2]; + ASSERT_EQ(pipe(notify), 0); + ASSERT_NO_THROW(start_dhcp_config_monitor(notify[1])); + std::this_thread::sleep_for(std::chrono::milliseconds(600)); + ASSERT_NO_THROW(stop_dhcp_config_monitor()); + std::this_thread::sleep_for(std::chrono::milliseconds(1200)); + close(notify[0]); + close(notify[1]); + + std::unordered_map published; + ASSERT_TRUE(fetch_desired_config(published)); + ASSERT_EQ(published.count("Vlan9100"), 1); + + // Live map already has Vlan9100 but it is not lla ready yet (interface down). + std::unordered_map vlans; + relay_config c{}; + c.interface = "Vlan9100"; + c.servers = {"fc02:9100::1"}; + c.is_lla_ready = false; + c.server_event = nullptr; + vlans["Vlan9100"] = c; + + // Build a valid timer_args tuple (same shape lla_check_callback expects) and a + // real timer event so the pending-lla arming path can run end to end. + auto state_db = std::make_shared("STATE_DB", 0); + std::shared_ptr mux_table = nullptr; + std::vector sockets; + auto timer_args = new std::tuple< + std::unordered_map &, + std::shared_ptr, + std::shared_ptr, + std::shared_ptr, + std::vector, + int, + int, + struct event * + >(vlans, config_db, state_db, mux_table, sockets, 0, 0, nullptr); + struct event *timer_event = event_new(base, -1, EV_PERSIST, lla_check_callback, timer_args); + ASSERT_NE(timer_event, nullptr); + std::get<7>(*timer_args) = timer_event; + + int pipefd[2]; + ASSERT_EQ(pipe(pipefd), 0); + evutil_make_socket_nonblocking(pipefd[0]); + char wake = 1; + ASSERT_EQ(write(pipefd[1], &wake, 1), 1); + + config_apply_ctx ctx{}; + ctx.vlans = &vlans; + ctx.timer_args = timer_args; + ctx.timer_event = timer_event; + ctx.notify_rd = pipefd[0]; + + // Vlan9100 stays configured (still in desired) and not lla ready, so the + // pending-lla branch must arm the timer (event_add is globally mocked) and run + // the lla check. + EXPECT_GLOBAL_CALL(event_add, event_add(_, _)).Times(1).WillOnce(Return(0)); + ASSERT_NO_THROW(config_change_callback(pipefd[0], 0, &ctx)); + EXPECT_EQ(vlans.count("Vlan9100"), 1); + EXPECT_FALSE(vlans["Vlan9100"].is_lla_ready); + + // Clean up the keys this test added so it does not perturb the shared redis + // state other tests rely on. + config_db->del("DHCP_RELAY|Vlan9100"); + config_db->del("VLAN_INTERFACE|Vlan9100|fc02:9100::1"); + + event_del(timer_event); + event_free(timer_event); + delete timer_args; + event_base_free(base); + base = nullptr; + close(pipefd[0]); + close(pipefd[1]); +} +