diff --git a/dhcp4relay/src/dhcp4relay.cpp b/dhcp4relay/src/dhcp4relay.cpp index bdd0979..4f8796d 100644 --- a/dhcp4relay/src/dhcp4relay.cpp +++ b/dhcp4relay/src/dhcp4relay.cpp @@ -65,6 +65,9 @@ const struct sock_fprog ether_relay_fprog = { /* interface to vlan mapping */ std::unordered_map vlan_map; +/* Vxlan kernel netdev names mirrored from CONFIG_DB VXLAN_TUNNEL_MAP. */ +std::unordered_set vxlan_netdev_set; + /* VRF sock map is created to avoid multiple sockets for same VRF We can expect multiple servers on same VRF, we no need to open VRF sockets for each VRF instead we can make use of existing VRF socket opened. @@ -99,6 +102,15 @@ std::shared_ptr config_db = std::make_shared state_db = std::make_shared("STATE_DB", 0); +/** + * @brief Return true iff @p interface_name is a known VxLAN tunnel kernel netdev. + * + * vxlan_netdev_set is populated by DHCPMgr from CONFIG_DB VXLAN_TUNNEL_MAP. + */ +bool is_vxlan_interface(const std::string &interface_name) { + return vxlan_netdev_set.count(interface_name) > 0; +} + /** * @code sock_open(const struct sock_fprog *fprog); * @@ -982,7 +994,7 @@ void pkt_in_callback(evutil_socket_t fd, short event, void *arg) { auto itr = std::find(interface_list.begin(), interface_list.end(), intf); /* To avoid duplicate packets, we are only processing packets from interface in PORT_TABLE and packets from VXLAN interface and docker0 interfaces */ - if ((itr == interface_list.end()) && (intf.rfind("VXLAN", 0) != 0) && (intf.rfind("docker0", 0) != 0)) { + if ((itr == interface_list.end()) && !is_vxlan_interface(intf) && (intf.rfind("docker0", 0) != 0)) { continue; } @@ -1466,6 +1478,18 @@ static void apply_config_event(const event_config &received_event, } delete port_msg; } + } else if (received_event.type == DHCPv4_RELAY_VXLAN_TUNNEL_UPDATE) { + vxlan_tunnel_config *msg = static_cast(received_event.msg); + if (msg) { + if (msg->is_add) { + vxlan_netdev_set.insert(msg->netdev_name); + SWSS_LOG_INFO("[DHCPV4_RELAY] Added VXLAN netdev %s to cache", msg->netdev_name.c_str()); + } else { + vxlan_netdev_set.erase(msg->netdev_name); + SWSS_LOG_INFO("[DHCPV4_RELAY] Removed VXLAN netdev %s from cache", msg->netdev_name.c_str()); + } + delete msg; + } } } diff --git a/dhcp4relay/src/dhcp4relay.h b/dhcp4relay/src/dhcp4relay.h index 1654ed6..9a8df01 100644 --- a/dhcp4relay/src/dhcp4relay.h +++ b/dhcp4relay/src/dhcp4relay.h @@ -13,6 +13,7 @@ #include #include +#include #include #include "dbconnector.h" @@ -144,7 +145,8 @@ typedef enum { * initial DHCPV4_RELAY snapshot. Reusable for Redis-reconnect * resync, atomic config replace, etc. */ - DHCPv4_RELAY_SYNC_BARRIER + DHCPv4_RELAY_SYNC_BARRIER, + DHCPv4_RELAY_VXLAN_TUNNEL_UPDATE } event_type; struct event_config { @@ -169,6 +171,11 @@ struct port_config { bool is_add; }; +/* Add/remove a VxLAN kernel netdev in vxlan_netdev_set. */ +struct vxlan_tunnel_config { + std::string netdev_name; + bool is_add; +}; struct metadata_config { std::string host_mac_addr; std::string hostname = "sonic"; diff --git a/dhcp4relay/src/dhcp4relay_mgr.cpp b/dhcp4relay/src/dhcp4relay_mgr.cpp index 7fabec0..4cd5ce5 100644 --- a/dhcp4relay/src/dhcp4relay_mgr.cpp +++ b/dhcp4relay/src/dhcp4relay_mgr.cpp @@ -1,6 +1,7 @@ #include "dhcp4relay_mgr.h" #include +#include #include #include constexpr auto DEFAULT_TIMEOUT_MSEC = 1000; @@ -67,6 +68,7 @@ void DHCPMgr::handle_swss_notification() { swss::SubscriberStateTable config_db_port_table(config_db_ptr.get(), "PORT"); swss::SubscriberStateTable config_db_dpu_table(config_db_ptr.get(), "DPUS"); swss::SubscriberStateTable state_db_interface_table(state_db_ptr.get(), "INTERFACE_TABLE"); + swss::SubscriberStateTable config_db_vxlan_tunnel_map_table(config_db_ptr.get(), "VXLAN_TUNNEL_MAP"); std::deque entries; swss::Select swss_select; @@ -83,6 +85,15 @@ void DHCPMgr::handle_swss_notification() { swss_select.addSelectable(&config_db_port_table); swss_select.addSelectable(&config_db_dpu_table); swss_select.addSelectable(&state_db_interface_table); + swss_select.addSelectable(&config_db_vxlan_tunnel_map_table); + + { + std::deque initial_entries; + config_db_vxlan_tunnel_map_table.pops(initial_entries); + if (!initial_entries.empty()) { + process_vxlan_tunnel_map_notification(initial_entries); + } + } /* * Push the initial DHCPV4_RELAY snapshot down config_pipe, then @@ -174,6 +185,9 @@ void DHCPMgr::handle_swss_notification() { } else if (selectable == static_cast(&config_db_dpu_table)) { config_db_dpu_table.pops(entries); process_port_notification(entries); + } else if (selectable == static_cast(&config_db_vxlan_tunnel_map_table)) { + config_db_vxlan_tunnel_map_table.pops(entries); + process_vxlan_tunnel_map_notification(entries); } } } @@ -898,6 +912,87 @@ void DHCPMgr::process_port_notification(std::deque } } +/** + * @brief Translate VXLAN_TUNNEL_MAP CONFIG_DB events into VXLAN_NETDEV_UPDATE + * events on config_pipe. + * + * The kernel netdev name is "-", derived from + * the row key ("|") and the row's "vlan" field ("Vlan"). + * + * On DEL, SubscriberStateTable delivers no field values (the row is already + * gone from CONFIG_DB), so vxlan_tunnel_map_netdev_cache_ holds the netdev + * name from the prior SET to allow eviction. + */ +void DHCPMgr::process_vxlan_tunnel_map_notification(std::deque &entries) { + static const std::string kVlanPrefix = "Vlan"; + + for (auto &entry : entries) { + const std::string &key = kfvKey(entry); + const std::string &operation = kfvOp(entry); + bool is_add = (operation == "SET"); + + std::string netdev_name; + if (is_add) { + auto bar = key.find('|'); + if (bar == std::string::npos || bar == 0 || bar == key.size() - 1) { + SWSS_LOG_INFO("[DHCPV4_RELAY] Skipping malformed VXLAN_TUNNEL_MAP key '%s'", + key.c_str()); + continue; + } + std::string tunnel_name = key.substr(0, bar); + + std::string vlan_field; + for (const auto &fv : kfvFieldsValues(entry)) { + if (fvField(fv) == "vlan") { + vlan_field = fvValue(fv); + break; + } + } + if (vlan_field.size() <= kVlanPrefix.size() || + vlan_field.compare(0, kVlanPrefix.size(), kVlanPrefix) != 0) { + SWSS_LOG_INFO("[DHCPV4_RELAY] VXLAN_TUNNEL_MAP '%s' has unexpected vlan '%s'", + key.c_str(), vlan_field.c_str()); + continue; + } + std::string vlan_id = vlan_field.substr(kVlanPrefix.size()); + if (!std::all_of(vlan_id.begin(), vlan_id.end(), ::isdigit)) { + SWSS_LOG_INFO("[DHCPV4_RELAY] VXLAN_TUNNEL_MAP '%s' non-numeric vlan id '%s'", + key.c_str(), vlan_id.c_str()); + continue; + } + netdev_name = tunnel_name + "-" + vlan_id; + vxlan_tunnel_map_netdev_cache_[key] = netdev_name; + } else { + auto it = vxlan_tunnel_map_netdev_cache_.find(key); + if (it == vxlan_tunnel_map_netdev_cache_.end()) { + continue; + } + netdev_name = it->second; + vxlan_tunnel_map_netdev_cache_.erase(it); + } + + vxlan_tunnel_config *msg = nullptr; + try { + msg = new vxlan_tunnel_config(); + } catch (const std::bad_alloc &e) { + SWSS_LOG_ERROR("[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + msg->netdev_name = netdev_name; + msg->is_add = is_add; + + event_config event; + event.type = DHCPv4_RELAY_VXLAN_TUNNEL_UPDATE; + event.msg = static_cast(msg); + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + SWSS_LOG_ERROR("[DHCPV4_RELAY] Failed to send VXLAN netdev update for %s", + netdev_name.c_str()); + delete msg; + } + } +} + /** * @code void DHCPMgr::stop_db_updates(); * diff --git a/dhcp4relay/src/dhcp4relay_mgr.h b/dhcp4relay/src/dhcp4relay_mgr.h index b5fb755..a07dee7 100644 --- a/dhcp4relay/src/dhcp4relay_mgr.h +++ b/dhcp4relay/src/dhcp4relay_mgr.h @@ -18,6 +18,10 @@ class DHCPMgr { private: std::atomic stop_thread; + /* CONFIG_DB key -> kernel netdev name. Populated on SET, consulted on + * DEL because SubscriberStateTable hands DEL events with no fields. */ + std::unordered_map vxlan_tunnel_map_netdev_cache_; + public: DHCPMgr() : stop_thread(false) {} ~DHCPMgr(); @@ -38,4 +42,5 @@ class DHCPMgr { void process_dhcp_server_ipv4_notification(std::deque &entries); void process_vlan_notification(std::deque &entries); void process_port_notification(std::deque &entries); + void process_vxlan_tunnel_map_notification(std::deque &entries); }; diff --git a/dhcp4relay/test/mock_relay.cpp b/dhcp4relay/test/mock_relay.cpp index 02a44ab..e726d8e 100644 --- a/dhcp4relay/test/mock_relay.cpp +++ b/dhcp4relay/test/mock_relay.cpp @@ -1071,3 +1071,188 @@ TEST(DHCPRelayTest, from_client_relay_of_relay_discard) { EXPECT_GLOBAL_CALL(send_udp, send_udp(_, _, _, _, _, _, _)).Times(0); from_client(&dhcpLayer, config); } + +/* is_vxlan_interface() does an exact-match lookup against vxlan_netdev_set. + * The set is populated by DHCPMgr from CONFIG_DB VXLAN_TUNNEL_MAP. */ +class IsVxlanInterfaceTest : public ::testing::Test { + protected: + void SetUp() override { vxlan_netdev_set.clear(); } + void TearDown() override { vxlan_netdev_set.clear(); } +}; + +TEST_F(IsVxlanInterfaceTest, exact_netdev_match_returns_true) { + vxlan_netdev_set.insert("VXLAN-101"); + EXPECT_TRUE(is_vxlan_interface("VXLAN-101")); +} + +TEST_F(IsVxlanInterfaceTest, ethernet_returns_false) { + vxlan_netdev_set.insert("VXLAN-101"); + EXPECT_FALSE(is_vxlan_interface("Ethernet0")); + EXPECT_FALSE(is_vxlan_interface("Ethernet8")); + EXPECT_FALSE(is_vxlan_interface("docker0")); +} + +TEST_F(IsVxlanInterfaceTest, hyphenated_tunnel_exact_match) { + vxlan_netdev_set.insert("Vtep-1-100-100"); + EXPECT_TRUE(is_vxlan_interface("Vtep-1-100-100")); + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100")); +} + +TEST_F(IsVxlanInterfaceTest, prefix_sharing_no_false_positive) { + vxlan_netdev_set.insert("VXLAN-100"); + vxlan_netdev_set.insert("Vtep-1-100-100"); + EXPECT_FALSE(is_vxlan_interface("VXLAN-Mgmt")); + EXPECT_FALSE(is_vxlan_interface("VXLAN-100-eth0")); + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100-eth0")); +} + +TEST_F(IsVxlanInterfaceTest, empty_set_returns_false) { + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100-100")); + EXPECT_FALSE(is_vxlan_interface("VXLAN")); + EXPECT_FALSE(is_vxlan_interface("")); +} + +TEST_F(IsVxlanInterfaceTest, handle_vxlan_netdev_events_set_and_del) { + int pipe_fds[2]; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Invoke(RealWrite)); + ASSERT_NE(pipe(pipe_fds), -1); + std::unordered_map vlans; + + vxlan_tunnel_config *add_msg = new vxlan_tunnel_config(); + add_msg->netdev_name = "Vtep-1-100-100"; + add_msg->is_add = true; + + event_config event; + event.type = DHCPv4_RELAY_VXLAN_TUNNEL_UPDATE; + event.msg = static_cast(add_msg); + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_TRUE(is_vxlan_interface("Vtep-1-100-100")); + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100-eth0")); + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100")); + + vxlan_tunnel_config *del_msg = new vxlan_tunnel_config(); + del_msg->netdev_name = "Vtep-1-100-100"; + del_msg->is_add = false; + + event.msg = static_cast(del_msg); + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100-100")); + + close(pipe_fds[0]); + close(pipe_fds[1]); +} + +TEST_F(IsVxlanInterfaceTest, process_vxlan_tunnel_map_notification_set_and_del) { + DHCPMgr mgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Invoke(RealWrite)); + + int pipe_fds[2]; + ASSERT_NE(pipe(pipe_fds), -1); + int saved_pipe = config_pipe[1]; + config_pipe[1] = pipe_fds[1]; + + std::deque entries; + entries.emplace_back("Vtep-1-100|map_2727_Vlan100", "SET", + std::vector{ + {"vlan", "Vlan100"}, {"vni", "2727"}}); + mgr.process_vxlan_tunnel_map_notification(entries); + + std::unordered_map vlans; + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_TRUE(is_vxlan_interface("Vtep-1-100-100")); + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100-eth0")); + + std::deque del_entries; + del_entries.emplace_back("Vtep-1-100|map_2727_Vlan100", "DEL", + std::vector{}); + mgr.process_vxlan_tunnel_map_notification(del_entries); + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100-100")); + + config_pipe[1] = saved_pipe; + close(pipe_fds[0]); + close(pipe_fds[1]); +} + +TEST_F(IsVxlanInterfaceTest, process_vxlan_tunnel_map_notification_unknown_del_dropped) { + DHCPMgr mgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)).Times(0); + + std::deque entries; + entries.emplace_back("Vtep-1-100|never_seen", "DEL", + std::vector{}); + mgr.process_vxlan_tunnel_map_notification(entries); + + EXPECT_FALSE(is_vxlan_interface("Vtep-1-100-anything")); +} + +TEST_F(IsVxlanInterfaceTest, process_vxlan_tunnel_map_notification_skips_malformed) { + DHCPMgr mgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)).Times(0); + + std::deque entries; + entries.emplace_back("vtep1|map_no_vlan", "SET", + std::vector{{"vni", "100"}}); + entries.emplace_back("vtep1|map_bad_vlan", "SET", + std::vector{{"vlan", "VlanXYZ"}}); + entries.emplace_back("vtep1|map_no_prefix", "SET", + std::vector{{"vlan", "100"}}); + entries.emplace_back("not_a_compound_key", "SET", + std::vector{{"vlan", "Vlan100"}}); + + mgr.process_vxlan_tunnel_map_notification(entries); + + EXPECT_TRUE(vxlan_netdev_set.empty()); +} + +/* Primer wiring: VXLAN_TUNNEL_MAP rows that exist in CONFIG_DB before + * dhcp4relay starts must reach vxlan_netdev_set through the same + * SubscriberStateTable::pops() -> process_vxlan_tunnel_map_notification + * path used at runtime. */ +TEST_F(IsVxlanInterfaceTest, primer_snapshot_populates_set_via_processor) { + swss::Table tunnel_map_table(config_db.get(), "VXLAN_TUNNEL_MAP"); + tunnel_map_table.set("Vtep-1-100|map_2727_Vlan100", + {{"vlan", "Vlan100"}, {"vni", "2727"}}); + tunnel_map_table.set("vtep1|map_100_Vlan200", + {{"vlan", "Vlan200"}, {"vni", "100"}}); + + swss::SubscriberStateTable sst(config_db.get(), "VXLAN_TUNNEL_MAP"); + std::deque initial; + sst.pops(initial); + ASSERT_EQ(initial.size(), 2u); + + DHCPMgr mgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Invoke(RealWrite)); + + int pipe_fds[2]; + ASSERT_NE(pipe(pipe_fds), -1); + int saved_pipe = config_pipe[1]; + config_pipe[1] = pipe_fds[1]; + + mgr.process_vxlan_tunnel_map_notification(initial); + + std::unordered_map vlans; + config_event_callback(pipe_fds[0], 0, &vlans); + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_TRUE(is_vxlan_interface("Vtep-1-100-100")); + EXPECT_TRUE(is_vxlan_interface("vtep1-200")); + EXPECT_FALSE(is_vxlan_interface("Ethernet0")); + + config_pipe[1] = saved_pipe; + close(pipe_fds[0]); + close(pipe_fds[1]); + testing_db::reset(); +} diff --git a/dhcp4relay/test/mock_relay.h b/dhcp4relay/test/mock_relay.h index d492abe..24756c4 100644 --- a/dhcp4relay/test/mock_relay.h +++ b/dhcp4relay/test/mock_relay.h @@ -22,3 +22,5 @@ extern bool feature_dhcp_server_enabled; extern std::unordered_map vlans_copy; extern std::string global_dhcp_server_ip; extern std::shared_ptr config_db; +extern std::unordered_set vxlan_netdev_set; +bool is_vxlan_interface(const std::string &interface_name);