diff --git a/.azure-pipelines/build.yml b/.azure-pipelines/build.yml index dd5205e..aff289b 100644 --- a/.azure-pipelines/build.yml +++ b/.azure-pipelines/build.yml @@ -44,7 +44,10 @@ jobs: libnl-route-3-dev \ libnl-genl-3-dev \ libnl-nf-3-dev \ - redis-server + redis-server \ + stgit \ + cmake \ + libpcap-dev sudo sed -ri 's/^# unixsocket/unixsocket/' /etc/redis/redis.conf sudo sed -ri 's/^unixsocketperm .../unixsocketperm 777/' /etc/redis/redis.conf sudo sed -ri 's/redis-server.sock/redis.sock/' /etc/redis/redis.conf @@ -103,13 +106,24 @@ jobs: dpkg-buildpackage -us -uc -b -j$(nproc) cp ../*.deb $(Build.ArtifactStagingDirectory) workingDirectory: dhcp6relay - displayName: "Compile sonic dhcp-relay" + displayName: "Compile sonic dhcp6relay" + - script: | + rm ../*.deb || true + # Configure git identity for patch application + git config --global user.email "build@sonic.net" + git config --global user.name "SONiC Build" + dpkg-buildpackage -us -uc -b -j$(nproc) + cp ../*.deb $(Build.ArtifactStagingDirectory) + workingDirectory: dhcp4relay + displayName: "Compile sonic dhcp4relay" - publish: $(Build.ArtifactStagingDirectory) artifact: sonic-dhcp-relay.${{ parameters.arch }} displayName: "Archive dhcp-relay debian packages" - task: PublishTestResults@2 inputs: - testResultsFiles: build-test/dhcp6relay-test-test-result.xml + testResultsFiles: | + build-test/dhcp6relay-test-test-result.xml + build-test/dhcp4relay-test-test-result.xml - ${{ if and(eq(parameters.arch, 'amd64'), parameters.codeCoverage) }}: - task: PublishCodeCoverageResults@1 inputs: @@ -117,3 +131,11 @@ jobs: pathToSources: $(Build.SourcesDirectory) reportDirectory: $(Build.SourcesDirectory)/build-test codeCoverageTool: 'Cobertura' + displayName: "Publish dhcp6relay code coverage" + - task: PublishCodeCoverageResults@1 + inputs: + summaryFileLocation: build-test/dhcp4relay-test-code-coverage.xml + pathToSources: $(Build.SourcesDirectory) + reportDirectory: $(Build.SourcesDirectory)/build-test + codeCoverageTool: 'Cobertura' + displayName: "Publish dhcp4relay code coverage" diff --git a/.gitignore b/.gitignore index 4573bfe..467902d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ dhcp6relay/debian/* !dhcp6relay/debian/compat !dhcp6relay/debian/control !dhcp6relay/debian/rules +dhcp4relay/debian/* +!dhcp4relay/debian/changelog +!dhcp4relay/debian/compat +!dhcp4relay/debian/control +!dhcp4relay/debian/rules diff --git a/dhcp4relay/Makefile b/dhcp4relay/Makefile new file mode 100644 index 0000000..eda86cc --- /dev/null +++ b/dhcp4relay/Makefile @@ -0,0 +1,106 @@ +.ONESHELL: +SHELL = /bin/bash + +RM := rm -rf +WORKING_DIR := $(abspath .) +BUILD_DIR := build +BUILD_TEST_DIR := build-test +DHCP4RELAY_TARGET := $(BUILD_DIR)/dhcp4relay +DHCP4RELAY_TEST_TARGET := $(BUILD_TEST_DIR)/dhcp4relay-test +CP := cp +MKDIR := mkdir +MV := mv +FIND := find +GCOVR := gcovr + +LD_PCAPPLUSPLUS_LIB := -lPcap++ -lPacket++ -lCommon++ + +#pcap plus plus zip file pcappp_v24.09.zip +PCAPPPVAR := 24.09 +PCAPPPZIP_FILE := pcappp_v${PCAPPPVAR}.zip +PCAPPLUSPLUS_DIR := $(WORKING_DIR)/PcapPlusPlus-$(PCAPPPVAR) +PCAPPP_DONE = $(WORKING_DIR)/pcappp.stamp +INCLUDE_DIR = $(PCAPPLUSPLUS_DIR)/include +LIB_DIR = $(PCAPPLUSPLUS_DIR)/lib + +override LDLIBS += -levent -lhiredis -lswsscommon -pthread -lboost_thread -lboost_system $(LD_PCAPPLUSPLUS_LIB) -lpcap +override CPPFLAGS += -Wall -std=c++17 -fPIE -I/usr/include/swss -I$(INCLUDE_DIR) +override CPPFLAGS += -MMD -MP -MF"$(@:%.o=%.d)" -MT"$(@)" +override LDFLAGS += -L$(LIB_DIR) -Wl,-rpath=$(abspath $(LIB_DIR)) +CPPFLAGS_TEST := --coverage -fprofile-arcs -ftest-coverage -fprofile-generate -fsanitize=address -DUNIT_TEST +LDLIBS_TEST := --coverage -lgtest -lgmock -pthread -lstdc++fs -fsanitize=address +PWD := $(shell pwd) + +.PHONY: $(PCAPPP_DONE) +$(PCAPPP_DONE): + + # Remove stale files + rm -rf $(PCAPPLUSPLUS_DIR) + + unzip ${PCAPPPZIP_FILE} + pushd $(PCAPPLUSPLUS_DIR) + + # Create a git repository here for stg to apply patches + git init + git add -f * + git commit -qm "initial commit" + + # Apply patches + stg init + stg import -s ../patch/series + popd + + cd $(PCAPPLUSPLUS_DIR) && cmake -S . -B build && cmake --build build + cd build && make && sudo cmake --install . + + touch $@ + +all: $(DHCP4RELAY_TARGET) $(DHCP4RELAY_TEST_TARGET) + +-include src/subdir.mk +-include test/subdir.mk + +# Use different build directories based on whether it's a regular build or a +# test build. This is because in the test build, code coverage is enabled, +# which means the object files that get built will be different +OBJS = $(SRCS:%.cpp=$(BUILD_DIR)/%.o) +TEST_OBJS = $(TEST_SRCS:%.cpp=$(BUILD_TEST_DIR)/%.o) + +ifneq ($(MAKECMDGOALS),clean) +-include $(OBJS:%.o=%.d) +-include $(TEST_OBJS:%.o=%.d) +endif + +$(BUILD_DIR)/%.o: %.cpp + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c -o $@ $< + +$(DHCP4RELAY_TARGET): $(PCAPPP_DONE) $(OBJS) + $(CXX) $(LDFLAGS) $^ $(LDLIBS) -o $@ + +$(BUILD_TEST_DIR)/%.o: %.cpp + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(CPPFLAGS_TEST) $(LDLIBS) -c -o $@ $< + +$(DHCP4RELAY_TEST_TARGET): $(TEST_OBJS) + $(CXX) $(LDFLAGS) $^ $(LDLIBS) $(LDLIBS_TEST) -o $@ + +test: $(DHCP4RELAY_TEST_TARGET) + sudo ASAN_OPTIONS=detect_leaks=0 ./$(DHCP4RELAY_TEST_TARGET) --gtest_output=xml:$(DHCP4RELAY_TEST_TARGET)-test-result.xml || true + $(GCOVR) -r ./ --html --html-details -o $(DHCP4RELAY_TEST_TARGET)-code-coverage.html + $(GCOVR) -r ./ --xml-pretty -o $(DHCP4RELAY_TEST_TARGET)-code-coverage.xml + +install: $(DHCP4RELAY_TARGET) + install -D $(DHCP4RELAY_TARGET) $(DESTDIR)/usr/sbin/$(notdir $(DHCP4RELAY_TARGET)) + +uninstall: + $(RM) $(DESTDIR)/usr/sbin/$(notdir $(DHCP4RELAY_TARGET)) + +clean: + -$(RM) $(BUILD_DIR) $(BUILD_TEST_DIR) *.html *.xml + $(FIND) . -name *.gcda -exec rm -f {} \; + $(FIND) . -name *.gcno -exec rm -f {} \; + $(FIND) . -name *.gcov -exec rm -f {} \; + -@echo ' ' + +.PHONY: all clean test install uninstall diff --git a/dhcp4relay/debian/changelog b/dhcp4relay/debian/changelog new file mode 100644 index 0000000..77d02c6 --- /dev/null +++ b/dhcp4relay/debian/changelog @@ -0,0 +1,5 @@ +sonic-dhcp4relay (1.0.0-0) UNRELEASED; urgency=medium + + * Initial release. + + -- Ashutosh Agrawal Thu, 16 Jan 2025 09:11:40 -0700 diff --git a/dhcp4relay/debian/compat b/dhcp4relay/debian/compat new file mode 100644 index 0000000..48082f7 --- /dev/null +++ b/dhcp4relay/debian/compat @@ -0,0 +1 @@ +12 diff --git a/dhcp4relay/debian/control b/dhcp4relay/debian/control new file mode 100644 index 0000000..8623275 --- /dev/null +++ b/dhcp4relay/debian/control @@ -0,0 +1,14 @@ +Source: sonic-dhcp4relay +Section: devel +Priority: optional +Maintainer: Ashutosh Agrawal +Build-Depends: debhelper (>= 12.0.0), libevent-dev, libboost-thread-dev, libboost-system-dev, libswsscommon-dev +Standards-Version: 3.9.3 +Homepage: https://github.com/Azure/sonic-buildimage +XS-Go-Import-Path: github.com/Azure/sonic-buildimage + +Package: sonic-dhcp4relay +Architecture: any +Built-Using: ${misc:Built-Using} +Depends: ${shlibs:Depends} +Description: SONiC DHCPv4 Relay diff --git a/dhcp4relay/debian/rules b/dhcp4relay/debian/rules new file mode 100755 index 0000000..ac2cd63 --- /dev/null +++ b/dhcp4relay/debian/rules @@ -0,0 +1,6 @@ +#!/usr/bin/make -f + +export DEB_BUILD_MAINT_OPTIONS=hardening=+all + +%: + dh $@ --parallel diff --git a/dhcp4relay/patch/0001-dhcpv4-relay-accept-random-src-port.patch b/dhcp4relay/patch/0001-dhcpv4-relay-accept-random-src-port.patch new file mode 100644 index 0000000..f0729b6 --- /dev/null +++ b/dhcp4relay/patch/0001-dhcpv4-relay-accept-random-src-port.patch @@ -0,0 +1,15 @@ +diff --git a/PcapPlusPlus-24.09/Packet++/header/DhcpLayer.h b/PcapPlusPlus-24.09/Packet++/header/DhcpLayer.h +index 435becb..282d739 100644 +--- a/PcapPlusPlus-24.09/Packet++/header/DhcpLayer.h ++++ b/PcapPlusPlus-24.09/Packet++/header/DhcpLayer.h +@@ -881,8 +881,8 @@ namespace pcpp + + bool DhcpLayer::isDhcpPorts(uint16_t portSrc, uint16_t portDst) + { +- return ((portSrc == 68 && portDst == 67) || (portSrc == 67 && portDst == 68) || +- (portSrc == 67 && portDst == 67)); ++ // src port can be any ephemeral port so removing the check ++ return ((portDst == 67) || (portDst == 68)); + } + + } // namespace pcpp diff --git a/dhcp4relay/patch/series b/dhcp4relay/patch/series new file mode 100644 index 0000000..ec2293e --- /dev/null +++ b/dhcp4relay/patch/series @@ -0,0 +1 @@ +0001-dhcpv4-relay-accept-random-src-port.patch diff --git a/dhcp4relay/pcappp_v24.09.zip b/dhcp4relay/pcappp_v24.09.zip new file mode 100644 index 0000000..cdbbd6c Binary files /dev/null and b/dhcp4relay/pcappp_v24.09.zip differ diff --git a/dhcp4relay/src/dhcp4_sender.cpp b/dhcp4relay/src/dhcp4_sender.cpp new file mode 100644 index 0000000..3286835 --- /dev/null +++ b/dhcp4relay/src/dhcp4_sender.cpp @@ -0,0 +1,78 @@ +#include "dhcp4_sender.h" + +#include +#include +#include + +#include + +/** + * @code bool send_udp(int sock, uint8_t *buffer, struct sockaddr_in target, uint32_t len, const char* src_ip, bool use_src_ip); + * + * @brief send udp packet and return true if successful + * + * @param *buffer message buffer + * @param sockaddr_in target target socket + * @param len length of message + * @param src_ip source IP address as string (optional) + * @param use_src_ip if true, use src_ip as source address + * + * @return boolean True if packet successfully sent + */ +#ifndef UNIT_TEST +bool send_udp(int sock, uint8_t *buffer, struct sockaddr_in target, uint32_t len, in_addr src_ip, bool use_src_ip) { + /* Pad additional bytes if length is lesser than 300 + * to make DHCP packet length to minimum of 300 bytes */ + if (len < BOOTP_MIN_LEN) { + auto pad_len = BOOTP_MIN_LEN - len; + memset(buffer+len, 0, pad_len); + len = BOOTP_MIN_LEN; + } + + if (use_src_ip && src_ip.s_addr != 0) { + // Enable IP_PKTINFO on the socket + int on = 1; + setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &on, sizeof(on)); + + struct msghdr msg = {}; + struct iovec iov = {}; + char cmsgbuf[CMSG_SPACE(sizeof(struct in_pktinfo))]; + + iov.iov_base = buffer; + iov.iov_len = len; + + msg.msg_name = ⌖ + msg.msg_namelen = sizeof(target); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsgbuf; + msg.msg_controllen = sizeof(cmsgbuf); + + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = IPPROTO_IP; + cmsg->cmsg_type = IP_PKTINFO; + cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); + + struct in_pktinfo *pktinfo = (struct in_pktinfo *)CMSG_DATA(cmsg); + memset(pktinfo, 0, sizeof(struct in_pktinfo)); + pktinfo->ipi_spec_dst = src_ip; + + msg.msg_controllen = cmsg->cmsg_len; + + if (sendmsg(sock, &msg, 0) == -1) { + char server_addr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(target.sin_addr), server_addr, INET_ADDRSTRLEN); + syslog(LOG_ERR, "sendmsg: Failed to send to target address: %s, error: %s\n", server_addr, strerror(errno)); + return false; + } + } else { + if (sendto(sock, buffer, len, 0, (const struct sockaddr *)&target, sizeof(target)) == -1) { + char server_addr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(target.sin_addr), server_addr, INET_ADDRSTRLEN); + syslog(LOG_ERR, "sendto: Failed to send to target address: %s, error: %s\n", server_addr, strerror(errno)); + return false; + } + } + return true; +} +#endif diff --git a/dhcp4relay/src/dhcp4_sender.h b/dhcp4relay/src/dhcp4_sender.h new file mode 100644 index 0000000..a072459 --- /dev/null +++ b/dhcp4relay/src/dhcp4_sender.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +#include + +#define BOOTP_MIN_LEN 300 +/** + * @code bool send_udp(int sock, uint8_t *buffer, struct sockaddr_in target, uint32_t len, const char* src_ip, bool use_src_ip); + * + * @brief send udp packet and return true if successful + * + * @param *buffer message buffer + * @param sockaddr_in target target socket + * @param len length of message + * @param src_ip source IP address as string (optional) + * @param use_src_ip if true, use src_ip as source address + * + * @return boolean True if packet successfully sent + */ +bool send_udp(int sock, uint8_t *buffer, struct sockaddr_in target, uint32_t len, in_addr src_ip, bool use_src_ip); diff --git a/dhcp4relay/src/dhcp4relay.cpp b/dhcp4relay/src/dhcp4relay.cpp new file mode 100644 index 0000000..ef584ba --- /dev/null +++ b/dhcp4relay/src/dhcp4relay.cpp @@ -0,0 +1,1463 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "configdb.h" +#include "dhcp4_sender.h" +#include "dhcp4relay_mgr.h" +#include "dhcp4relay_stats.h" +#include "sonicv2connector.h" + +struct event_base *base; +struct event *ev_sigint; +struct event *ev_sigterm; +extern bool feature_dhcp_server_enabled; +extern std::string global_dhcp_server_ip; +extern metadata_config m_config; + +static uint8_t client_recv_buffer[BUFFER_SIZE]; +int config_pipe[2]; + +/* DHCPv4 filter */ +static struct sock_filter ether_relay_filter[] = { + /* Make sure this is an IP packet... */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 12), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, ETHERTYPE_IP, 0, 8), + + /* Make sure it's a UDP packet... */ + BPF_STMT(BPF_LD + BPF_B + BPF_ABS, 23), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, IPPROTO_UDP, 0, 6), + + /* Make sure this isn't a fragment... */ + BPF_STMT(BPF_LD + BPF_H + BPF_ABS, 20), + BPF_JUMP(BPF_JMP + BPF_JSET + BPF_K, 0x1fff, 4, 0), + + /* Get the IP header length... */ + BPF_STMT(BPF_LDX + BPF_B + BPF_MSH, 14), + + /* Make sure it's to the right port... */ + BPF_STMT(BPF_LD + BPF_H + BPF_IND, 16), + BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, 67, 0, 1), /* patch */ + + /* If we passed all the tests, ask for the whole packet. */ + BPF_STMT(BPF_RET + BPF_K, (u_int)-1), + + /* Otherwise, drop it. */ + BPF_STMT(BPF_RET + BPF_K, 0), +}; + +const struct sock_fprog ether_relay_fprog = { + lengthof(ether_relay_filter), + ether_relay_filter}; + +/* interface to vlan mapping */ +std::unordered_map vlan_map; + +/* 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. + to map that i will maintain a map with VRF against socket of vrf. + when ever there is a configuration to open a new socket for the VRF + check in this MAP if exists update relay_config or else update created + socket against VRF. +*/ +/* VRF sock map */ +std::unordered_map vrf_sock_map; + +/* This map will have client vlan to client VRF mapping */ +std::unordered_map vlan_vrf_map; + +/* This map will have interface name to interface alias map */ +std::unordered_map phy_interface_alias_map; + +/* DHCP Relay Counter Table Instance */ +DHCPCounter_table dhcp_cntr_table; + +/* DHCP Relay config manager Instance */ +DHCPMgr dhcp_mgr; + +/* Interfaces list in config DB */ +std::vector interface_list; + +#ifdef UNIT_TEST +using namespace swss; +#endif + +std::shared_ptr config_db = std::make_shared("CONFIG_DB", 0); + +std::shared_ptr state_db = std::make_shared("STATE_DB", 0); + +/** + * @code sock_open(const struct sock_fprog *fprog); + * + * @brief prepare socket to receive all DHCP packet + * + * @param fprog bpf filter "udp and port 67" + * + * @return socket descriptor + */ +int sock_open(const struct sock_fprog *fprog) { + int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); + if (s == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] socket: Failed to create socket with error %s\n", strerror(errno)); + return -1; + } + + evutil_make_listen_socket_reuseable(s); + evutil_make_socket_nonblocking(s); + + struct sockaddr_ll sll = { + .sll_family = AF_PACKET, + .sll_protocol = htons(ETH_P_ALL), + .sll_ifindex = 0 // any interface + }; + + if (bind(s, (struct sockaddr *)&sll, sizeof sll) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] bind: Failed to bind to specified interface, error: %s\n", strerror(errno)); + (void)close(s); + return -1; + } + if (fprog && setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER, fprog, sizeof *fprog) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] setsockopt: Failed to attach filter, error: %s\n", strerror(errno)); + (void)close(s); + return -1; + } + + int optval = 0; + socklen_t optlen = sizeof(optval); + if (getsockopt(s, SOL_SOCKET, SO_RCVBUF, &optval, &optlen) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] getsockopt: Failed to get recv buffer size, error: %s\n", strerror(errno)); + (void)close(s); + return -1; + } + + int optval_new = RAWSOCKET_RECV_SIZE; + if (setsockopt(s, SOL_SOCKET, SO_RCVBUF, &optval_new, sizeof(optval_new)) == -1) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] setsockopt: Failed to set recv buffer size to %d, use default value\n", optval_new); + } else { + syslog(LOG_INFO, "[DHCPV4_RELAY] setsockopt: change raw socket recv buffer size from %d to %d\n", optval, optval_new); + } + + optval = 1; + if (setsockopt(s, SOL_PACKET, PACKET_AUXDATA, &optval, sizeof(optval)) == -1) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] setsockopt: Failed to set packet AUXDATA \n"); + (void)close(s); + return -1; + } + + int ignore_outgoing = 1; + if (setsockopt(s, SOL_PACKET, PACKET_IGNORE_OUTGOING, &ignore_outgoing, sizeof(ignore_outgoing)) == -1) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] setsockopt: Failed to ignore outgoing \n"); + } else { + syslog(LOG_INFO, "[DHCPV4_RELAY] setsockopt: outgoing packet is ignored\n"); + } + + return s; +} + +void prepare_relay_server_config(relay_config &interface_config) { + for (auto server : interface_config.servers) { + sockaddr_in tmp; + if (inet_pton(AF_INET, server.c_str(), &tmp.sin_addr) != 1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] inet_pton: Failed to convert IPv4 address\n"); + return; + } + tmp.sin_family = AF_INET; + tmp.sin_port = htons(RELAY_PORT); + auto it = std::find_if( + interface_config.servers_sock.begin(), + interface_config.servers_sock.end(), + [&tmp](const sockaddr_in &entry) { + return entry.sin_addr.s_addr == tmp.sin_addr.s_addr; + } + ); + /*If server_sock is already present then don't add it.*/ + if (it == interface_config.servers_sock.end()) { + interface_config.servers_sock.push_back(tmp); + } + } + if (interface_config.servers_sock.size() != interface_config.servers.size()) { + for (size_t index = 0; index < interface_config.servers_sock.size();) { + char ip_str[INET_ADDRSTRLEN] = {0}; + inet_ntop(AF_INET, &interface_config.servers_sock[index].sin_addr, ip_str, INET_ADDRSTRLEN); + + auto found = std::find(interface_config.servers.begin(), + interface_config.servers.end(), + std::string(ip_str)); + + if (found == interface_config.servers.end()) { + interface_config.servers_sock.erase(interface_config.servers_sock.begin() + index); + } else { + ++index; + } + } + } +} + +/** + * @code prepare_relay_interface_config(relay_config &interface_config); + * + * @brief prepare for specified relay interface config: server and link address + * + * @param interface_config pointer to relay config to be prepared + * + * @return none + */ +void prepare_relay_interface_config(relay_config &interface_config) { + struct ifaddrs *ifa, *ifa_tmp; + sockaddr_in intf_addr; + sockaddr_in net_mask; + sockaddr_in src_intf_sel; + bool intf_name_set = false; + bool source_intf_sel_opt = false; + + if (getifaddrs(&ifa) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] getifaddrs: Unable to get network interfaces, error: %s\n", strerror(errno)); + return; + } + + if (m_config.is_dualTor) { + /* If DualTor is enabled, we set source interface to "Loopback0" + and link_selection option will be enabled during encoding if is_dualTor is enabled */ + interface_config.source_interface = "Loopback0"; + syslog(LOG_INFO, + "[DHCPV4_INFO][DualTor] %s: link_selection_opt is enabled and source interface is set to %s\n", + interface_config.vlan.c_str(), interface_config.source_interface.c_str()); + } + + if (interface_config.source_interface.length() > 0) { + syslog(LOG_INFO, "[DHCPV4_INFO] source interface addr is set to %s\n", + interface_config.source_interface.c_str()); + } else { + /* This flag is used to make sure source interface address is copied to config cache + if source interface selection is not set with interface we set this flag true + and we wont copy any source interface selection related information + */ + source_intf_sel_opt = true; + } + + ifa_tmp = ifa; + while (ifa_tmp) { + if (ifa_tmp->ifa_addr && ifa_tmp->ifa_addr->sa_family == AF_INET) { + struct sockaddr_in *in = (struct sockaddr_in *)ifa_tmp->ifa_addr; + struct sockaddr_in *mask = (struct sockaddr_in *)ifa_tmp->ifa_netmask; + if (strcmp(ifa_tmp->ifa_name, interface_config.vlan.c_str()) == 0) { + char ip_str[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(in->sin_addr), ip_str, INET_ADDRSTRLEN); + std::string value; + std::shared_ptr vlan_intf_tbl = std::make_shared(config_db.get(), CFG_VLAN_INTF_TABLE_NAME); + vlan_intf_tbl->hget((interface_config.vlan + "|" + ip_str), "secondary", value); + if ((value.size() == 0) || (value != "true")) { + intf_addr = *in; + net_mask = *mask; + intf_name_set = true; + } + } else if ((!source_intf_sel_opt) && + (strcmp(ifa_tmp->ifa_name, interface_config.source_interface.c_str()) == 0)) { + src_intf_sel = *in; + source_intf_sel_opt = true; + } + } + if (intf_name_set && source_intf_sel_opt) { + break; + } + ifa_tmp = ifa_tmp->ifa_next; + } + freeifaddrs(ifa); + + interface_config.link_address = intf_addr; + interface_config.link_address_netmask = net_mask; + interface_config.src_intf_sel_addr = src_intf_sel; +} + +int prepare_vrf_sockets(relay_config &config) { + /* Open a socket per server VRF, check socket is already available for that VRF */ + int vrf_sock = -1; + auto itr = vrf_sock_map.find(config.vrf.c_str()); + if (itr == vrf_sock_map.end()) { + /* Vrf sock is not available in map need to create */ + vrf_sock = socket(AF_INET, SOCK_DGRAM, 0); + if (vrf_sock == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] socket: Failed to create client_addr socket vrf %s err = %s\n", + config.vrf.c_str(), strerror(errno)); + return -1; + } + + evutil_make_listen_socket_reuseable(vrf_sock); + evutil_make_socket_nonblocking(vrf_sock); + + if (config.vrf != "default") { + if (setsockopt(vrf_sock, SOL_SOCKET, SO_BINDTODEVICE, + config.vrf.c_str(), strlen(config.vrf.c_str())) < 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] setsockopt: Failed to bind socket to %s VRF err = %s\n", + config.vrf.c_str(), strerror(errno)); + close(vrf_sock); + return -1; + } + } + + /* Bind socket with UDP port to DHCP src number */ + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(RELAY_PORT); + bind(vrf_sock, (struct sockaddr*)&addr, sizeof(addr)); + + /* Update the map */ + vrf_sock_map[config.vrf] = {vrf_sock, 1}; + } + + if (vrf_sock > 0) { + config.vrf_sock = vrf_sock; + } else { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to obtain vrf socket(%s) error:%s \n", config.vrf.c_str(), strerror(errno)); + return -1; + } + return 0; +} + +/** + * @code prepare_vlan_sockets(int &client_sock, relay_config &config); + * + * @brief prepare vlan L3 socket for sending + * + * @param client_sock socket binded to ip address of vlan interface on which server is configured. + * This socket will be used to send DHCP packet to server and client. + * + * @return int + */ +int prepare_vlan_sockets(relay_config &config) { +#ifdef UNIT_TEST + config.client_sock = 1; +#else + struct ifaddrs *ifa, *ifa_tmp; + sockaddr_in client_addr = {0}; + int client_sock = 0; + if ((client_sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] socket: Failed to create client_addr socket on interface %s, error: %s\n", + config.vlan.c_str(), strerror(errno)); + return -1; + } + + evutil_make_listen_socket_reuseable(client_sock); + evutil_make_socket_nonblocking(client_sock); + + /* Bind client socket to vlan interface */ + if (setsockopt(client_sock, SOL_SOCKET, SO_BINDTODEVICE, config.vlan.c_str(), + strlen(config.vlan.c_str())) < 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] failed to bind client_sock to vlan %s, error: %s\n", + config.vlan.c_str(), strerror(errno)); + close(client_sock); + return -1; + } + + int retry = 0; + bool bind_client_addr = false; + do { + if (getifaddrs(&ifa) == -1) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] getifaddrs: Unable to get network interfaces with %s\n", strerror(errno)); + } else { + ifa_tmp = ifa; + while (ifa_tmp) { + if (ifa_tmp->ifa_addr && (ifa_tmp->ifa_addr->sa_family == AF_INET)) { + if (strcmp(ifa_tmp->ifa_name, config.vlan.c_str()) == 0) { + struct sockaddr_in *in = (struct sockaddr_in *)ifa_tmp->ifa_addr; + bind_client_addr = true; + client_addr = *in; + client_addr.sin_family = AF_INET; + client_addr.sin_port = htons(RELAY_PORT); + } + } + ifa_tmp = ifa_tmp->ifa_next; + } + freeifaddrs(ifa); + } + + if (bind_client_addr) { + break; + } + + syslog(LOG_WARNING, "[DHCPV4_RELAY] Retry #%d to bind to sockets on interface %s\n", ++retry, config.vlan.c_str()); + sleep(5); + } while (retry < 6); + + if ((!bind_client_addr) || (bind(client_sock, (sockaddr *)&client_addr, sizeof(client_addr)) == -1)) { + syslog(LOG_ERR, "[DHCPV4_RELAY] bind: Failed to bind socket to global ipv4 address on interface %s after %d retries with %s\n", + config.vlan.c_str(), retry, strerror(errno)); + /* TODO: Need to close vrf socket if ref count is zero in all failure case */ + close(client_sock); + return -1; + } + + int broadcast_enable = 1; + if (setsockopt(client_sock, SOL_SOCKET, SO_BROADCAST, &broadcast_enable, sizeof(broadcast_enable)) < 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] setsockopt: Failed to set socket to receive broadcast address, error: %s\n", + strerror(errno)); + close(client_sock); + return -1; + } + + int optval = 1; + if (setsockopt(client_sock, IPPROTO_IP, IP_PKTINFO, &optval, sizeof(optval)) < 0) { + syslog(LOG_ERR, + "[DHCPV4_RELAY] setsockopt: Failed to set socket option to " + "get IP PKT information, error: %s\n", + strerror(errno)); + close(client_sock); + return -1; + } + + config.client_sock = client_sock; +#endif + return 0; +} + +uint8_t encode_tlv(uint8_t *buf, uint8_t t, uint8_t l, uint8_t *v) { + *buf = t; + *(buf + DHCP_SUB_OPT_TLV_LENGTH_OFFSET) = l; + memcpy((buf + DHCP_SUB_OPT_TLV_HEADER_LEN), v, l); + + return (l + DHCP_SUB_OPT_TLV_HEADER_LEN); +} + +void encode_relay_option(pcpp::DhcpLayer *dhcp_pkt, relay_config *config) { + uint8_t buf[256] = {0}; + uint8_t buf_offset = 0; + + auto vrf = vlan_vrf_map[config->vlan.c_str()]; + + /* Get interface alias */ + std::string intf_alias; + if (phy_interface_alias_map.find(config->phy_interface) != phy_interface_alias_map.end()) { + intf_alias = phy_interface_alias_map[config->phy_interface]; + } + + /* Encode circuit ID sub-option */ + /* | 1 | 4 | hostname:interface_alias:vlan | */ + std::string circuit_id; + if (feature_dhcp_server_enabled) { + circuit_id = m_config.hostname + ":" + intf_alias; + } else { + circuit_id = m_config.hostname + ":" + intf_alias + ":" + config->vlan; + } + auto offset = encode_tlv(buf, OPTION82_SUBOPT_CIRCUIT_ID, circuit_id.length(), + (uint8_t *)circuit_id.c_str()); + buf_offset += offset; + + /* Encode remote ID sub-option */ + /* | 2 | 6 | my_mac| */ + offset = encode_tlv((buf + buf_offset), OPTION82_SUBOPT_REMOTE_ID, + MAC_ADDR_STR_LEN, (uint8_t *)(m_config.host_mac_addr.c_str())); + buf_offset += offset; + + /* TODO: this sub-option should be set if source interface selection is enabled */ + /* | 5 | 4 | ipv4 | */ + if (m_config.is_dualTor || config->link_selection_opt == "enable") { + uint32_t link_sel_ip = ((config->link_address.sin_addr.s_addr) & + (config->link_address_netmask.sin_addr.s_addr)); + offset = encode_tlv((buf + buf_offset), OPTION82_SUBOPT_LINK_SELECTION, sizeof(uint32_t), + (uint8_t *)&link_sel_ip); + buf_offset += offset; + } + + /* | 11 | 4 | ipv4 | */ + if (config->server_id_override_opt == "enable") { + offset = encode_tlv((buf + buf_offset), OPTION82_SUBOPT_SERVER_OVERRIDE, sizeof(uint32_t), + (uint8_t *)(&(config->link_address.sin_addr.s_addr))); + buf_offset += offset; + } + + /* Encode VSS sub-option 151 if client is not default VRF */ + /* | 151 | vrf_len | 0 | vrf_name | */ + uint8_t vss_buf[32] = {0}; + /* Enable VSS only if client and server are in two different VRF's */ + if ((config->vrf_selection_opt == "enable") && (vrf != "default") && + (config->vrf != vrf)) { + uint8_t zero_encode = 0; + memcpy(vss_buf, &zero_encode, sizeof(uint8_t)); + memcpy((vss_buf + 1), (uint8_t *)vrf.c_str(), (uint8_t)vrf.length()); + + offset = encode_tlv((buf + buf_offset), OPTION82_SUBOPT_VIRTUAL_SUBNET, + (uint8_t)(vrf.length() + 1), vss_buf); + buf_offset += offset; + } + + /* We shouldn't append relay information if packet size is exceeding MTU size */ + if ((dhcp_pkt->getHeaderLen() + buf_offset) > MAX_DHCP_PKT_SIZE) { + syslog(LOG_ERR, + "[DHCPV4_RELAY] %ld packet size is exceeding allowed size %d" + " from interface %s", + (dhcp_pkt->getHeaderLen() + buf_offset), + MAX_DHCP_PKT_SIZE, config->vlan.c_str()); + return; + } + + dhcp_pkt->addOption(pcpp::DhcpOptionBuilder(pcpp::DHCPOPT_DHCP_AGENT_OPTIONS, + buf, buf_offset)); + return; +} + +/** + * @code void from_client(pcpp::DhcpLayer* dhcp_pkt, relay_config *config) + * + * @brief construct relay-forward message + * + * @param dhcp_pkt DHCP layered packet information. + * @param config pointer to the relay interface config + * + * @return none + */ +void from_client(pcpp::DhcpLayer *dhcp_pkt, relay_config &config) { + /* Update giaddr */ + if (!(dhcp_pkt->getDhcpHeader()->gatewayIpAddress)) { + if (config.source_interface.length() > 0) { + /* find the IP of the interface and update to giaddr */ + dhcp_pkt->getDhcpHeader()->gatewayIpAddress = + config.src_intf_sel_addr.sin_addr.s_addr; + } else { + dhcp_pkt->getDhcpHeader()->gatewayIpAddress = + config.link_address.sin_addr.s_addr; + } + if ((dhcp_pkt->getDhcpHeader()->magicNumber) && + (dhcp_pkt->getDhcpHeader()->magicNumber) == DHCP_MAGIC_NUMBER) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] encode DHCP relay option"); + encode_relay_option(dhcp_pkt, &config); + } + } else { + /* If the relay packet is from another relay, we should act based on + configuration of agent_relay_mode. + append - Forward the packet with appending relay agent. + replace - Delete existing option 82 and add my relay option. + discard - Discard the incoming packet. + */ + if (config.agent_relay_mode == "append") { + encode_relay_option(dhcp_pkt, &config); + } else if (config.agent_relay_mode == "replace") { + dhcp_pkt->removeOption(pcpp::DHCPOPT_DHCP_AGENT_OPTIONS); + encode_relay_option(dhcp_pkt, &config); + } else { + /* By default it will discard packet from relay agent */ + dhcp_cntr_table.increment_counter(config.vlan, "TX", DHCPv4_MESSAGE_TYPE_DROP); + syslog(LOG_INFO, "[DHCPV4_RELAY] agent relay mode is discard, dropping the packet %s", + config.vlan.c_str()); + return; + } + } + + /* Drop the packet if the hop count exceeds the configured maximum. */ + if (dhcp_pkt->getDhcpHeader()->hops >= config.max_hop_count) { + syslog(LOG_NOTICE, "[DHCPV4_RELAY] Dropping packet: hop count %d exceeds max allowed %d\n", + dhcp_pkt->getDhcpHeader()->hops, config.max_hop_count); + // increment drop counter + dhcp_cntr_table.increment_counter(config.vlan, "TX", DHCPv4_MESSAGE_TYPE_DROP); + return; + } + + /* Increase the hop count */ + dhcp_pkt->getDhcpHeader()->hops = dhcp_pkt->getDhcpHeader()->hops + 1; + int sock = config.vrf_sock; + uint32_t index = 0; + + // Backward compatibility for deployment_id 8. If deployment_id is 8, use client interface IP as source IP + bool use_intf_ip_as_src_ip = false; + in_addr src_ip = {0}; + if (m_config.deployment_id == 8) { + use_intf_ip_as_src_ip = true; + src_ip.s_addr = config.link_address.sin_addr.s_addr; + } + + for (auto server : config.servers_sock) { + if (send_udp(sock, (uint8_t *)dhcp_pkt->getDhcpHeader(), server, dhcp_pkt->getHeaderLen(), src_ip, use_intf_ip_as_src_ip)) { + syslog(LOG_INFO, "[DHCPV4_RELAY] DHCP packet is sent to configured server: %s, interface: %s", + config.servers[index].c_str(), config.vlan.c_str()); + dhcp_cntr_table.increment_counter(config.vlan, "TX", (int)dhcp_pkt->getMessageType()); + } else { + syslog(LOG_NOTICE, "[DHCPV4_RELAY] DHCP packet sending FAILED for configured server: %s, interface: %s", + config.servers[index].c_str(), config.vlan.c_str()); + // increment drop counter + dhcp_cntr_table.increment_counter(config.vlan, "TX", DHCPv4_MESSAGE_TYPE_DROP); + } + index++; + } +} + +uint8_t *decode_tlv(const uint8_t *buf, uint8_t t, uint8_t &l, uint32_t options_total_size) { + uint8_t *temp = (uint8_t *)buf; + uint32_t offset = 0; + uint8_t len = 0; + + while (temp && ((offset + DHCP_SUB_OPT_TLV_HEADER_LEN) <= options_total_size)) { + len = *(temp + DHCP_SUB_OPT_TLV_LENGTH_OFFSET); + if ((offset + DHCP_SUB_OPT_TLV_LENGTH_OFFSET + len) > options_total_size) { + /* Malformed packet */ + syslog(LOG_ERR, "[DHCPV4_INFO] Failed to decode relay agent sub-option %d" + " exceeded total option len %d offset %d sub-option len %d\n", + t, options_total_size, offset, len); + l = 0; + return NULL; + } + if (t == *temp) { + syslog(LOG_INFO, "[DHCPV4_INFO] Decoding relay agent sub-option %d of len %d\n", t, len); + l = len; + return (temp + DHCP_SUB_OPT_TLV_HEADER_LEN); + } + offset += (len + DHCP_SUB_OPT_TLV_HEADER_LEN); + temp += (len + DHCP_SUB_OPT_TLV_HEADER_LEN); + } + l = 0; + return NULL; +} + +/** + * @code void to_client(pcpp::DhcpLayer* dhcp_pkt, std::unordered_map *vlans, std::string src_ip); + * + * @brief API will send DHCP relay message to client. + * + * @param dhcp_pkt DHCP layer class, which will have information of DHCP packet. + * @param vlans Client information including socket to send DHCP packet to client. + * + * @return none + */ +void to_client(pcpp::DhcpLayer *dhcp_pkt, std::unordered_map *vlans, + std::string src_ip) { + struct ifaddrs *ifa, *ifa_tmp; + struct sockaddr_in target_addr = {0}; + uint32_t giaddr = dhcp_pkt->getDhcpHeader()->gatewayIpAddress; + uint32_t broadcast_addr = DHCP_BROADCAST_IPADDR; + std::unordered_map::iterator config_itr = vlans->end(); + + if (getifaddrs(&ifa) == -1) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] getifaddrs: Unable to get network interfaces, error: %s\n", strerror(errno)); + exit(1); + } + + /* Return if giaddr is empty */ + if (giaddr == 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Message received with empty giaddr from server %s\n", + src_ip.c_str()); + return; + } + + auto agent_option = dhcp_pkt->getOptionData(pcpp::DHCPOPT_DHCP_AGENT_OPTIONS); + auto options_ptr = agent_option.getValue(); + auto agent_option_size = agent_option.getDataSize(); + + /* If option 82 is available fetch Vlan information from circuit ID */ + if (options_ptr != NULL) { + uint8_t circuit_id_len = 0; + auto circuit_id_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_CIRCUIT_ID, + circuit_id_len, agent_option_size); + if (circuit_id_ptr == NULL) { + syslog(LOG_ERR, + "[DHCPV4_RELAY] Circuit id sub-option is missing in relay" + " agent option from server %s", + src_ip.c_str()); + return; + } + + std::string circuit_id((const char *)circuit_id_ptr, circuit_id_len); + + std::string vlan_interface; + auto vlan_intf_pos = circuit_id.rfind(':'); + if (vlan_intf_pos != std::string::npos) { + vlan_interface = circuit_id.substr(vlan_intf_pos + 1); + } + + if (vlan_interface.length() > 0) { + config_itr = vlans->find(vlan_interface); + if (config_itr == vlans->end()) { + syslog(LOG_INFO, + "[DHCPV4_RELAY] Vlan config not found for the circuit" + "id encoded interface %s\n", + vlan_interface.c_str()); + } + } + } + + /* If we couldnt able to find vlan config using circuit ID + Walk through all the interfaces and match for giaddrs. */ + if (config_itr == vlans->end()) { + std::string intf_name; + ifa_tmp = ifa; + while (ifa_tmp) { + if (ifa_tmp->ifa_addr && ifa_tmp->ifa_addr->sa_family == AF_INET) { + struct sockaddr_in *in = (struct sockaddr_in *)ifa_tmp->ifa_addr; + if (in->sin_addr.s_addr == giaddr) { + intf_name = ifa_tmp->ifa_name; + break; + } + } + ifa_tmp = ifa_tmp->ifa_next; + } + freeifaddrs(ifa); + + if (intf_name.length() == 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to find interface attached to address %u\n", giaddr); + return; + } + + // TODO: Add check if interface is prefix with vlan or else try to get vlan attached to ethernet + // find vlan attach using vlan map. Relay config is mapped to vlan. + + /* Expecting interface is SVI interface of vlan */ + config_itr = vlans->find(intf_name); + if (config_itr == vlans->end()) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Config not found for vlan %s\n", intf_name.c_str()); + return; + } + } else { + freeifaddrs(ifa); + } + auto config = config_itr->second; + + dhcp_cntr_table.increment_counter(config.vlan, "RX", (int)dhcp_pkt->getMessageType()); + /* TODO: Also check it is matching remote ID*/ + + memcpy(&target_addr.sin_addr, &broadcast_addr, sizeof(struct in_addr)); + target_addr.sin_family = AF_INET; + target_addr.sin_port = htons(CLIENT_PORT); + + in_addr ip_zero = {0}; + /* TODO: Send unicast message to client if BOOTP flag from client is set to unicast */ + + dhcp_pkt->removeOption(pcpp::DHCPOPT_DHCP_AGENT_OPTIONS); + if (send_udp(config.client_sock, (uint8_t *)dhcp_pkt->getDhcpHeader(), target_addr, dhcp_pkt->getHeaderLen(), ip_zero, false)) { + syslog(LOG_INFO, "[DHCPV4_RELAY] dhcp relay message is broadcast to client %s from server %s", + config.vlan.c_str(), src_ip.c_str()); + dhcp_cntr_table.increment_counter(config.vlan, "TX", (int)dhcp_pkt->getMessageType()); + } +} + +/** + * @code update_interface_vlan_mapping(std::string interface, std::string vlan, bool is_add); + * + * @brief update interface to vlan mapping and DHCP counter table + * + * @param interface interface name string + * @param vlan vlan name string + * @param is_add add or delete entry + * + * @return none + */ +void update_interface_vlan_mapping(std::string interface, std::string vlan, bool is_add) { + if (is_add) { + vlan_map[interface] = vlan; + dhcp_cntr_table.initialize_interface(vlan); + syslog(LOG_INFO, "[DHCPV4_RELAY] Add <%s, %s> into interface vlan map\n", interface.c_str(), vlan.c_str()); + } else { + vlan_map.erase(interface); + dhcp_cntr_table.remove_interface(vlan); + syslog(LOG_INFO, "[DHCPV4_RELAY] Remove <%s, %s> from interface vlan map\n", interface.c_str(), vlan.c_str()); + } +} + +/** + * @code update_vlan_mapping(std::string vlan, bool is_add); + * + * @brief build vlan member interface to vlan mapping table + * + * @param vlan vlan name string + * @param add add or delete entry + * + * @return none + */ +/** + * @brief Updates the VLAN mapping for a given VLAN. + * + * This function retrieves all VLAN members for the specified VLAN from the configuration database + * and updates the global VLAN map with the interface and VLAN information. + * + * @param vlan The VLAN identifier as a string. + * @param is_add Determines if its ADD or DELETE operation. + */ +void update_vlan_mapping(std::string vlan, bool is_add) { +#ifdef UNIT_TEST + std::vector keys; + swss::Table vlan_member_table(config_db.get(), "VLAN_MEMBER"); + vlan_member_table.getKeys(keys); +#else + auto match_pattern = std::string("VLAN_MEMBER|") + vlan + std::string("|*"); + auto keys = config_db->keys(match_pattern); +#endif + for (auto &itr : keys) { + auto found = itr.find_last_of('|'); + auto interface = itr.substr(found + 1); + update_interface_vlan_mapping(interface, vlan, is_add); + } + + /* get VRF attached to the vlan from VLAN_INTERFACE table */ + if (is_add) { + std::string value; + std::shared_ptr vlan_intf_tbl = std::make_shared(config_db.get(), CFG_VLAN_INTF_TABLE_NAME); + vlan_intf_tbl->hget(vlan, "vrf_name", value); + if (value.size() <= 0) { + /* use default instance as vrf */ + vlan_vrf_map[vlan] = "default"; + } else { + vlan_vrf_map[vlan] = value; + } + } else { + vlan_vrf_map.erase(vlan); + } +} + +uint16_t ipv4_checksum_cal(const uint8_t* ipv4_header, size_t header_len) { + uint8_t header[header_len] = {0}; + + memcpy(header, ipv4_header, header_len); + pcpp::iphdr *header_ptr = (pcpp::iphdr *)header; + + header_ptr->headerChecksum = 0; + uint32_t sum = 0; + for (size_t i = 0; i + 1 < header_len; i += 2) { + sum += (header[i] << 8) | header[i + 1]; + } + if (header_len & 1) { + sum += header[header_len - 1] << 8; + } + while (sum >> 16) { + sum = (sum & 0xFFFF) + (sum >> 16); + } + return ((uint16_t)~sum); +} + +/** + * @code pkt_in_callback(evutil_socket_t fd, short event, void *arg); + * + * @brief callback for libevent that is called everytime data is received at the filter socket + * this is expected to receive both server and client sent DHCP packets + * + * @param fd filter socket + * @param event libevent triggered event + * @param arg callback argument provided by user + * + * @return none + */ +void pkt_in_callback(evutil_socket_t fd, short event, void *arg) { + auto vlans = reinterpret_cast *>(arg); + timeval time; + struct cmsghdr *cmsg = NULL; + struct tpacket_auxdata *aux = NULL; + struct sockaddr_ll *sll; + struct sockaddr_ll addr = {0}; + struct msghdr msg = {0}; + struct iovec iov = {0}; + int pkts_num = 0; + int vlan_id = 0; + char interface_name[IF_NAMESIZE]; + char control[1024] = {0}; + + iov.iov_base = client_recv_buffer; + iov.iov_len = BUFFER_SIZE; + msg.msg_name = &addr; + msg.msg_namelen = sizeof(addr); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + + while (pkts_num++ < BATCH_SIZE) { + auto buffer_sz = recvmsg(fd, &msg, 0); + if (buffer_sz <= 0) { + if (errno != EAGAIN) { + syslog(LOG_ERR, "[DHCPV4_RELAY] recv: Failed to receive data at filter socket: %s\n", strerror(errno)); + } + return; + } + + /* Find ingress VLAN */ + sll = (struct sockaddr_ll *)msg.msg_name; + cmsg = (struct cmsghdr *)msg.msg_control; + vlan_id = 0; + if (cmsg != NULL) { + if ((cmsg->cmsg_level == (int)SOL_PACKET) && (cmsg->cmsg_type == (int)PACKET_AUXDATA)) { + aux = (struct tpacket_auxdata *)(cmsg->__cmsg_data); + if (aux != NULL) { + vlan_id = (aux->tp_vlan_tci & VLAN_MASK); + } + } + } + + if (if_indextoname(sll->sll_ifindex, interface_name) == NULL) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Invalid input interface index %d\n", sll->sll_ifindex); + continue; + } + std::string intf(interface_name); + 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 */ + if ((itr == interface_list.end()) && (intf.rfind("VXLAN", 0) != 0)) { + continue; + } + + std::string vlan_str; + if (vlan_id == 0) { + /* vlan_id can be 0 when we receive packet from the server */ + auto vlan = vlan_map.find(intf); + if (vlan == vlan_map.end()) { + if (intf.find(CLIENT_IF_PREFIX) != std::string::npos) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Invalid input interface %s\n", interface_name); + } + } else { + vlan_str = vlan->second; + } + } else { + vlan_str = "Vlan" + std::to_string(vlan_id); + } + + gettimeofday(&time, nullptr); + + // Construct raw socket. + pcpp::RawPacket raw_packet(static_cast(client_recv_buffer), buffer_sz, time, false); + + pcpp::Packet raw_pkt(&raw_packet); + + /* Extract packets in each layers */ + pcpp::EthLayer *eth_layer = raw_pkt.getLayerOfType(); + if (eth_layer == nullptr) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Invalid Ethernet packet from interface %s\n", intf.c_str()); + if (vlan_id != 0 && !vlan_str.empty()) { + dhcp_cntr_table.increment_counter(vlan_str, "RX", DHCPv4_MESSAGE_TYPE_MALFORMED); + } + continue; + } + + pcpp::IPv4Layer *ip_layer = raw_pkt.getLayerOfType(); + if (ip_layer == nullptr) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Invalid IP packet from interface %s\n", intf.c_str()); + if (vlan_id != 0 && !vlan_str.empty()) { + dhcp_cntr_table.increment_counter(vlan_str, "RX", DHCPv4_MESSAGE_TYPE_MALFORMED); + } + continue; + } + + /* Validate IP checksum is correct */ + pcpp::iphdr* ip_hdr = ip_layer->getIPv4Header(); + auto ipv4_checksum = ipv4_checksum_cal((const uint8_t*)ip_hdr, ip_layer->getHeaderLen()); + if (ip_hdr->headerChecksum != htons(ipv4_checksum)) { + if (vlan_id != 0 && !vlan_str.empty()) { + dhcp_cntr_table.increment_counter(vlan_str, "RX", DHCPv4_MESSAGE_TYPE_MALFORMED); + } + syslog(LOG_WARNING, "[DHCPV4_RELAY] Checksum failed for IP packet from interface %s\n", intf.c_str()); + continue; + } + + auto src_ip = ip_layer->getSrcIPv4Address().toString(); + + pcpp::UdpLayer *udp_layer = raw_pkt.getLayerOfType(); + if (udp_layer == nullptr) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Invalid UDP packet from interface %s\n", intf.c_str()); + if (vlan_id != 0 && !vlan_str.empty()) { + dhcp_cntr_table.increment_counter(vlan_str, "RX", DHCPv4_MESSAGE_TYPE_MALFORMED); + } + continue; + } + + /* Validate UDP checksum is correct */ + auto udp_checksum = udp_layer->calculateChecksum(false); + if (htobe16(udp_checksum) != udp_layer->getUdpHeader()->headerChecksum) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] UDP checksum validation is failing " + " packet is from interface %s\n", intf.c_str()); + if (vlan_id != 0 && !vlan_str.empty()) { + dhcp_cntr_table.increment_counter(vlan_str, "RX", DHCPv4_MESSAGE_TYPE_MALFORMED); + } + continue; + } + + pcpp::DhcpLayer *dhcp_pkt = raw_pkt.getLayerOfType(); + if (dhcp_pkt == nullptr) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Invalid DHCP packet from interface %s\n", intf.c_str()); + if (vlan_id != 0 && !vlan_str.empty()) { + dhcp_cntr_table.increment_counter(vlan_str, "RX", DHCPv4_MESSAGE_TYPE_MALFORMED); + } + continue; + } + + if (dhcp_pkt->getDhcpHeader()->opCode == BOOTPREQUEST) { + if (vlan_str.empty()) { + continue; + } + + auto config_itr = vlans->find(vlan_str); + if (config_itr == vlans->end()) { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Config not found for vlan %s\n", intf.c_str()); + continue; + } + auto config = config_itr->second; + config_itr->second.phy_interface = intf; + + dhcp_cntr_table.increment_counter(config.vlan, "RX", (int)dhcp_pkt->getMessageType()); + from_client(dhcp_pkt, config_itr->second); + } else if (dhcp_pkt->getDhcpHeader()->opCode == BOOTPREPLY) { + to_client(dhcp_pkt, vlans, src_ip); + } else { + if (vlan_id != 0 && !vlan_str.empty()) { + dhcp_cntr_table.increment_counter(vlan_str, "RX", DHCPv4_MESSAGE_TYPE_UNKNOWN); + } + continue; + } + } +} + +/** + * @code signal_init(); + * + * @brief initialize DHCPv6 Relay libevent signals + */ +int signal_init() { + int rv = -1; + do { + ev_sigint = evsignal_new(base, SIGINT, signal_callback, base); + if (ev_sigint == NULL) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Could not create SIGINT libevent signal\n"); + break; + } + + ev_sigterm = evsignal_new(base, SIGTERM, signal_callback, base); + if (ev_sigterm == NULL) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Could not create SIGTERM libevent signal\n"); + break; + } + rv = 0; + } while (0); + return rv; +} + +/** + * @code signal_start(); + * + * @brief start DHCPv6 Relay libevent base and add signals + */ +int signal_start() { + int rv = -1; + do { + if (evsignal_add(ev_sigint, NULL) != 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Could not add SIGINT libevent signal\n"); + break; + } + + if (evsignal_add(ev_sigterm, NULL) != 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Could not add SIGTERM libevent signal\n"); + break; + } + + if (event_base_dispatch(base) != 0) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Could not start libevent dispatching loop\n"); + } + + rv = 0; + } while (0); + + return rv; +} + +/** + * @code signal_callback(fd, event, arg); + * + * @brief signal handler for dhcp4relay. Initiate shutdown when signal is caught + * + * @param fd libevent socket + * @param event event triggered + * @param arg pointer to libevent base + * + * @return none + */ +void signal_callback(evutil_socket_t fd, short event, void *arg) { + syslog(LOG_ALERT, "[DHCPV4_RELAY] Received signal: '%s'\n", strsignal(fd)); + if ((fd == SIGTERM) || (fd == SIGINT)) { + dhcp4relay_stop(); + } +} + +/** + * @code dhcp4relay_stop(); + * + * @brief stop DHCPv6 Relay libevent loop upon signal + */ +void dhcp4relay_stop() { + event_base_loopexit(base, NULL); +} + +int handle_server_sock(relay_config &vlan_config, std::string new_vrf) +{ + /* Decrement the ref count for old vrf value in the vrf_sock_map and after decrement + * if the value is zero then close the client socket and delete the entry in the map. */ + if (vrf_sock_map.find(vlan_config.vrf) != vrf_sock_map.end()) { + vrf_sock_map[vlan_config.vrf].ref_count--; + if (vrf_sock_map[vlan_config.vrf].ref_count == 0) { + close(vlan_config.vrf_sock); + vrf_sock_map.erase((vlan_config.vrf)); + } + } + + /*Updating the new vrf value to vlans structure. */ + vlan_config.vrf = new_vrf; + /* For the new server vrf update case, if the entry exists in the vrf_sock_map then + * increment the ref count only else create a socket and update the ref count. */ + if (vrf_sock_map.find(new_vrf) != vrf_sock_map.end()) { + vlan_config.vrf_sock = vrf_sock_map[new_vrf.c_str()].sock; + vrf_sock_map[new_vrf].ref_count++; + } else { + if (prepare_vrf_sockets(vlan_config) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to create vrf listen socket"); + return -1; + } + } + return 0; +} + +/** + * @code void delete_all_relay_configs(std::unordered_map *vlans); + * + * @brief Delete all the existing vlan entries in case of dhcp_server is enabled/disbaled. + * + * @param vlans Client information including socket to send DHCP packet to client. + * + * @return none + */ +void delete_all_relay_configs(std::unordered_map *vlans) { + for (auto vlan = vlans->begin(); vlan != vlans->end(); ) { + if (vlan->second.client_sock > 0) { + close(vlan->second.client_sock); + } + if (vlan->second.vrf_sock > 0) { + vrf_sock_map[vlan->second.vrf].ref_count--; + if (vrf_sock_map[vlan->second.vrf].ref_count == 0) { + close(vlan->second.vrf_sock); + vrf_sock_map.erase(vlan->second.vrf); + } + } + update_vlan_mapping(vlan->first, false); + vlan = vlans->erase(vlan); + } +} + +void config_event_callback(evutil_socket_t fd, short event, void *arg) { + std::unordered_map *vlans = static_cast *>(arg); + event_config received_event; + ssize_t bytes_read = read(fd, &received_event, sizeof(received_event)); + + if (bytes_read == sizeof(received_event)) { + //Do not update the relay configs if dhcp_server is enabled + if (((received_event.type == DHCPv4_RELAY_CONFIG_UPDATE) && !feature_dhcp_server_enabled) || + (received_event.type == DHCPv4_SERVER_RELAY_CONFIG_UPDATE)) { + relay_config *relay_msg = static_cast(received_event.msg); + if (relay_msg) { + syslog(LOG_INFO, "[DHCPV4_RELAY] Processing config update: VLAN %s", relay_msg->vlan.c_str()); + + if (relay_msg->is_add) { + if (vlans->find(relay_msg->vlan) == vlans->end()) { + /*If entry not exist then creating the entry with empty structure.*/ + (*vlans)[relay_msg->vlan] = relay_config{}; + (*vlans)[relay_msg->vlan].vlan = relay_msg->vlan; + update_vlan_mapping(relay_msg->vlan, true); + if (prepare_vlan_sockets((*vlans)[relay_msg->vlan]) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to create Vlan listen socket"); + return; + } + /* Intially filling the vlan interface IP address. */ + if (relay_msg->source_interface.empty()) { + prepare_relay_interface_config((*vlans)[relay_msg->vlan]); + } + } + + if ((*vlans)[relay_msg->vlan].servers != relay_msg->servers) { + (*vlans)[relay_msg->vlan].servers = relay_msg->servers; + prepare_relay_server_config((*vlans)[relay_msg->vlan]); + } + + /* Compare the existing vrf value and the new DB updated vrf value for vrf modification case. */ + if ((*vlans)[relay_msg->vlan].vrf != relay_msg->vrf) { + if (handle_server_sock((*vlans)[relay_msg->vlan], relay_msg->vrf) < 0) { + return; + } + } + if ((*vlans)[relay_msg->vlan].source_interface != relay_msg->source_interface) { + (*vlans)[relay_msg->vlan].source_interface = relay_msg->source_interface; + prepare_relay_interface_config((*vlans)[relay_msg->vlan]); + } + + (*vlans)[relay_msg->vlan].link_selection_opt = relay_msg->link_selection_opt; + (*vlans)[relay_msg->vlan].server_id_override_opt = relay_msg->server_id_override_opt; + (*vlans)[relay_msg->vlan].vrf_selection_opt = relay_msg->vrf_selection_opt; + (*vlans)[relay_msg->vlan].agent_relay_mode = relay_msg->agent_relay_mode; + } else { + if (vlans->find(relay_msg->vlan) != vlans->end()) { + /* In case of vlan deletion, close all the sockets.*/ + if ((*vlans)[relay_msg->vlan].client_sock > 0) { + close((*vlans)[relay_msg->vlan].client_sock); + } + if ((*vlans)[relay_msg->vlan].vrf_sock > 0) { + vrf_sock_map[(*vlans)[relay_msg->vlan].vrf].ref_count--; + if (vrf_sock_map[(*vlans)[relay_msg->vlan].vrf].ref_count == 0) { + close((*vlans)[relay_msg->vlan].vrf_sock); + vrf_sock_map.erase((*vlans)[relay_msg->vlan].vrf); + } + } + vlans->erase(relay_msg->vlan); + syslog(LOG_INFO, "[DHCPV4_RELAY] Deleted VLAN %s from configuration", relay_msg->vlan.c_str()); + update_vlan_mapping(relay_msg->vlan, false); + } else { + syslog(LOG_WARNING, "[DHCPV4_RELAY] Attempted to delete non-existent VLAN %s", relay_msg->vlan.c_str()); + } + } + delete relay_msg; + } + } else if (received_event.type == DHCPv4_RELAY_INTERFACE_UPDATE) { + relay_config *relay_msg = static_cast(received_event.msg); + if (relay_msg) { + syslog(LOG_INFO, "[DHCPV4_RELAY] Updating source interface for VLAN %s", relay_msg->vlan.c_str()); + if (relay_msg->is_add) { + (*vlans)[relay_msg->vlan].src_intf_sel_addr = relay_msg->src_intf_sel_addr; + } else { + memset(&(*vlans)[relay_msg->vlan].src_intf_sel_addr, 0, sizeof(sockaddr_in)); + } + delete relay_msg; + } + } else if (received_event.type == DHCPv4_RELAY_VLAN_MEMBER_UPDATE) { + vlan_member_config *msg = static_cast(received_event.msg); + if (msg) { + syslog(LOG_INFO, "[DHCPV4_RELAY] Updating vlan member for VLAN %s", msg->vlan.c_str()); + if (vlans->find(msg->vlan) == vlans->end()) { + delete msg; + return; + } + + if ((*vlans)[msg->vlan].client_sock > 0) { + close((*vlans)[msg->vlan].client_sock); + } + + update_interface_vlan_mapping(msg->interface, msg->vlan, msg->is_add); + if (prepare_vlan_sockets((*vlans)[msg->vlan]) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to create Vlan listen socket"); + return; + } + delete msg; + } + } else if (received_event.type == DHCPv4_RELAY_VLAN_INTERFACE_UPDATE) { + vlan_interface_config *msg = static_cast(received_event.msg); + if (msg) { + syslog(LOG_INFO, "[DHCPV4_RELAY] Updating vlan interface event for VLAN %s", msg->vlan.c_str()); + if (vlans->find(msg->vlan) == vlans->end()) { + delete msg; + return; + } + + if (!msg->vrf.empty()) { + vlan_vrf_map[msg->vlan] = msg->vrf; + } else { + if ((*vlans)[msg->vlan].client_sock > 0) { + close((*vlans)[msg->vlan].client_sock); + } + if (prepare_vlan_sockets((*vlans)[msg->vlan]) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to create Vlan listen socket"); + return; + } + prepare_relay_interface_config((*vlans)[msg->vlan]); + } + + std::string value; + std::shared_ptr dhcp_relay_tbl = std::make_shared(config_db.get(), + "DHCPV4_RELAY"); + dhcp_relay_tbl->hget((msg->vlan), "server_vrf", value); + if ((msg->vrf.empty()) || (value.length() != 0)) { + return; + } + + if ((*vlans)[msg->vlan].vrf != msg->vrf) { + if (handle_server_sock((*vlans)[msg->vlan], msg->vrf) < 0) { + return; + } + } + delete msg; + } + } else if ((received_event.type == DHCPv4_SERVER_FEATURE_UPDATE) || + (received_event.type == DHCPv4_SERVER_IP_DELETE)) { + syslog(LOG_INFO, "[DHCPV4_RELAY] dhcp_server feature table update or server ip delete event received"); + delete_all_relay_configs(vlans); + } else if (received_event.type == DHCPv4_SERVER_IP_UPDATE) { + syslog(LOG_INFO, "[DHCPV4_RELAY] dhcp_server IP update in state DB event received"); + for (auto it = vlans->begin(); it != vlans->end(); ++it) { + relay_config &config = it->second; + config.servers.clear(); + config.servers_sock.clear(); + config.servers.push_back(global_dhcp_server_ip); + prepare_relay_server_config(config); + } + } else if (received_event.type == DHCPv4_RELAY_DUAL_TOR_UPDATE) { + relay_config *relay_msg = static_cast(received_event.msg); + if (relay_msg) { + if (relay_msg->is_add) { + syslog(LOG_INFO, + "[DHCPV4_RELAY][DualTor] Adding link-selection and source-interface as Loopback0 for existing vlans"); + } else { + syslog(LOG_INFO, + "[DHCPV4_RELAY][DualTor] Deleting/Restoring link-selection and source-interface configs for existing vlans"); + } + + for (auto& vlan : *vlans) { + if (relay_msg->is_add) { + prepare_relay_interface_config(vlan.second); + } else { + std::shared_ptr v4_relay_intf_tbl = std::make_shared(config_db.get(), CFG_DHCPV4_RELAY_TABLE_NAME); + std::string value; + + // Check for the presence of specific keys + v4_relay_intf_tbl->hget(vlan.second.vlan, "link_selection", value); + // Check if "link_selection" is present + if (value.length() > 0) { + // Fetch the value of "link_selection" from the database + vlan.second.link_selection_opt = value; + } else { + // link_selection key not found in DB, clear the value + vlan.second.link_selection_opt.clear(); + } + + v4_relay_intf_tbl->hget(vlan.second.vlan, "source_interface", value); + // Check if "source_interface" is present + if (value.length() > 0) { + // Fetch the value of "source_interface" from the database + vlan.second.source_interface = value; + } else { + // source_interface key not found in DB, clear the value + vlan.second.source_interface.clear(); + } + prepare_relay_interface_config(vlan.second); + } + } + delete relay_msg; + } + } else if (received_event.type == DHCPv4_RELAY_PORT_UPDATE) { + port_config *port_msg = static_cast(received_event.msg); + if (port_msg) { + syslog(LOG_INFO, "[DHCPV4_RELAY] Updating interface %s event", port_msg->phy_interface.c_str()); + if (port_msg->is_add) { + if (std::find(interface_list.begin(), interface_list.end(), port_msg->phy_interface) == interface_list.end()) { + interface_list.push_back(port_msg->phy_interface); + } + phy_interface_alias_map[port_msg->phy_interface] = port_msg->alias; + } else { + auto it = std::find(interface_list.begin(), interface_list.end(), port_msg->phy_interface); + if (it != interface_list.end()) { + interface_list.erase(it); + } + phy_interface_alias_map.erase(port_msg->phy_interface); + } + delete port_msg; + } + } + } else { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to read config update: expected %lu bytes, got %zd bytes", sizeof(received_event), bytes_read); + } +} + +/** + * @code loop_relay(std::unordered_map &vlans); + * + * @brief main loop: configure sockets, create libevent base, start server listener thread + * + * @param vlans list of vlans retrieved from config_db + */ + +/** + * @brief Main loop for the DHCP relay functionality. + * + * This function sets up the necessary event base and socket listeners for + * handling DHCP relay operations. It initializes connections to the state + * and configuration databases, sets up event listeners for client and server + * sockets, and prepares VLAN-specific configurations. + * + * @param vlans A reference to an unordered map containing VLAN configurations. + * + */ +void loop_relay(std::unordered_map &vlans) { + base = event_base_new(); + if (base == NULL) { + syslog(LOG_ERR, "[DHCPV4_RELAY] libevent: Failed to create event base\n"); + exit(EXIT_FAILURE); + } + + /* Keep a list of physical interface available in config DB*/ + auto match_pattern = std::string("PORT|*"); + auto keys = config_db->keys(match_pattern); + + for (auto &itr : keys) { + auto found = itr.find_last_of('|'); + auto interface = itr.substr(found + 1); + interface_list.push_back(interface); + } + + // Create the pipe for inter-thread communication + if (pipe(config_pipe) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to create config update pipe"); + exit(EXIT_FAILURE); + } + + // Set the read-end of the pipe to non-blocking mode + fcntl(config_pipe[0], F_SETFL, O_NONBLOCK); + + // Add the pipe to libevent for async config updates + struct event *config_event = event_new(base, config_pipe[0], EV_READ | EV_PERSIST, + config_event_callback, reinterpret_cast(&vlans)); + if (!config_event) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to create event for config pipe"); + exit(EXIT_FAILURE); + } + + event_add(config_event, NULL); + syslog(LOG_INFO, "[DHCPV4_RELAY] Added event listener for config updates"); + + /* Open a socket with dhcp port, protocol filter */ + auto filter = sock_open(ðer_relay_fprog); + if (filter != -1) { + /* Register to the callbck func when there is new packet to the socket from client */ + auto event = event_new(base, filter, EV_READ | EV_PERSIST, pkt_in_callback, + reinterpret_cast(&vlans)); + if (event == NULL) { + syslog(LOG_ERR, "[DHCPV4_RELAY] libevent: Failed to create client listen event\n"); + exit(EXIT_FAILURE); + } + event_add(event, NULL); + syslog(LOG_INFO, "[DHCPV4_RELAY] libevent: Add client listen socket event\n"); + } else { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to create client listen socket"); + exit(EXIT_FAILURE); + } + + // Start thread for periodic counters updates to DB + dhcp_cntr_table.start_db_updates(); + + // Start thread for listening of config DB updates + dhcp_mgr.initialize_config_listner(); + + if (signal_init() == 0 && signal_start() == 0) { + shutdown_relay(); + if (filter != -1) { + close(filter); + } + } +} + +/** + * @code shutdown_relay(); + * + * @brief free signals and terminate threads + */ +void shutdown_relay() { + event_del(ev_sigint); + event_del(ev_sigterm); + event_free(ev_sigint); + event_free(ev_sigterm); + event_base_free(base); +} diff --git a/dhcp4relay/src/dhcp4relay.h b/dhcp4relay/src/dhcp4relay.h new file mode 100644 index 0000000..b4c4a7d --- /dev/null +++ b/dhcp4relay/src/dhcp4relay.h @@ -0,0 +1,298 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "dbconnector.h" +#include "dhcp4_sender.h" +#include "table.h" + +#define PACKED __attribute__((packed)) + +#define RELAY_PORT 67 +#define CLIENT_PORT 68 +#define HOP_LIMIT 4 +#define DHCPv4_OPTION_LIMIT 255 +#define RAWSOCKET_RECV_SIZE 1048576 +#define CLIENT_IF_PREFIX "Ethernet" +#define BUFFER_SIZE 9200 // TODO: change to dynamic size based on MTU +#define MAX_DHCP_PKT_SIZE 1472 // 1500 - (IP + UDP)headers +#define MAC_ADDR_STR_LEN 17 +#define DHCP_MAGIC_NUMBER 0x63538263 +#define MAX_HOP_COUNT 16 + +#define BOOTPREQUEST 1 +#define BOOTPREPLY 2 +#define BOOTP_HTYPE_ETHERNET 1 +#define BOOTP_HLEN_ETHERNET 6 +#define BOOTP_FLAGS_BROADCAST 0x8000 +#define DHCP_BROADCAST_IPADDR 0xFFFFFFFF + +#define DHCP_SUB_OPT_TLV_LENGTH_OFFSET 1 +#define DHCP_SUB_OPT_TLV_HEADER_LEN 2 + +#define lengthof(A) (sizeof(A) / sizeof(A)[0]) + +extern char vrf_single[IF_NAMESIZE]; +extern bool vrf_sock_set; +extern int config_pipe[2]; + +#define OPTION_RELAY_MSG 82 +#define OPTION82_SUBOPT_CIRCUIT_ID 1 +#define OPTION82_SUBOPT_REMOTE_ID 2 +#define OPTION82_SUBOPT_LINK_SELECTION 5 +#define OPTION82_SUBOPT_SERVER_OVERRIDE 11 +#define OPTION82_SUBOPT_VIRTUAL_SUBNET 151 + +#define DHCP_ETHERNET_HDR_LEN 14 +#define DHCP_IP_HDR_LEN 20 +#define DHCP_UDP_HDR_LEN 8 +#define DHCP_UDP_OVERHEAD_LEN (DHCP_ETHERNET_HDR_LEN + DHCP_IP_HDR_LEN + DHCP_UDP_HDR_LEN) +#define DHCP_SNAME_LEN 64 +#define DHCP_FILE_LEN 128 +#define DHCP_FIXED_NON_UDP_LEN 236 +#define DHCP_FIXED_LEN (DHCP_FIXED_NON_UDP_LEN + DHCP_UDP_OVERHEAD_LEN) +#define DHCP_MTU_MAX 9216 +#define DHCP_OPTION_LEN (DHCP_MTU_MAX - DHCP_FIXED_LEN) + +#define BATCH_SIZE 64 +#define VLAN_MASK 0x0FFF + +extern char loopback[IF_NAMESIZE]; + +struct VrfSocketInfo { + int sock; + uint16_t ref_count; +}; + +/* DHCPv4 message types */ +typedef enum { + DHCPv4_MESSAGE_TYPE_UNKNOWN, + DHCPv4_MESSAGE_TYPE_DISCOVER, + DHCPv4_MESSAGE_TYPE_OFFER, + DHCPv4_MESSAGE_TYPE_REQUEST, + DHCPv4_MESSAGE_TYPE_DECLINE, + DHCPv4_MESSAGE_TYPE_ACK, + DHCPv4_MESSAGE_TYPE_NAK, + DHCPv4_MESSAGE_TYPE_RELEASE, + DHCPv4_MESSAGE_TYPE_INFORM, + DHCPv4_MESSAGE_TYPE_MALFORMED, + DHCPv4_MESSAGE_TYPE_DROP, + + DHCPv4_MESSAGE_TYPE_COUNT +} dhcp_message_type_t; + +struct relay_config { + /* Client facing socket, use to send packet to client */ + int client_sock; + /* Server facing socket, use to send packet to server */ + int vrf_sock; + int filter; + sockaddr_in link_address; + sockaddr_in link_address_netmask; + sockaddr_in src_intf_sel_addr; + uint32_t link_ifindex; + std::shared_ptr state_db; + std::string vlan; + std::string phy_interface; + std::string vrf; // This is server VRF. + std::string source_interface; + std::string link_selection_opt; + std::string server_id_override_opt; + std::string vrf_selection_opt; + std::string agent_relay_mode; + uint8_t max_hop_count = MAX_HOP_COUNT; + std::vector servers; + std::vector servers_sock; + bool is_interface_id; + bool is_add; + std::shared_ptr config_db; +}; + +typedef enum { + DHCPv4_RELAY_CONFIG_UNKNOWN, + DHCPv4_RELAY_CONFIG_UPDATE, + DHCPv4_RELAY_INTERFACE_UPDATE, + DHCPv4_RELAY_VLAN_MEMBER_UPDATE, + DHCPv4_RELAY_VLAN_INTERFACE_UPDATE, + DHCPv4_SERVER_RELAY_CONFIG_UPDATE, + DHCPv4_SERVER_FEATURE_UPDATE, + DHCPv4_SERVER_IP_UPDATE, + DHCPv4_SERVER_IP_DELETE, + DHCPv4_RELAY_DUAL_TOR_UPDATE, + DHCPv4_RELAY_PORT_UPDATE +} event_type; + +struct event_config { + event_type type; + void *msg; +}; + +struct vlan_member_config { + std::string vlan; + std::string interface; + bool is_add; +}; + +struct vlan_interface_config { + std::string vlan; + std::string vrf; +}; + +struct port_config { + std::string phy_interface; + std::string alias; + bool is_add; +}; + +struct metadata_config { + std::string host_mac_addr; + std::string hostname = "sonic"; + uint32_t deployment_id; + bool is_dualTor; +}; + +/** + * @code sock_open(const struct sock_fprog *fprog); + * + * @brief prepare L2 socket to attach to "udp and port 67" filter + * + * @param fprog bpf filter "udp and port 67" + * + * @return socket descriptor + */ +int sock_open(const struct sock_fprog *fprog); + +/** + * @code prepare_vlan_sockets(relay_config &config); + * + * @brief prepare vlan L3 socket for sending + * + * @return int + */ +int prepare_vlan_sockets(relay_config &config); + +/** + * @code prepare_vrf_sockets(relay_config &config); + * + * @brief prepare vrf L3 socket for sending + * + * @return int + */ +int prepare_vrf_sockets(relay_config &config); + +/** + * @code prepare_relay_interface_config(relay_config &interface_config); + * + * @brief prepare for specified relay interface config + * + * @param interface_config pointer to relay config to be prepared + * + * @return none + */ +void prepare_relay_interface_config(relay_config &interface_config); + +/** + * @code prepare_relay_server_config(relay_config &interface_config); + * + * @brief prepare for specified relay server and link address + * + * @param interface_config pointer to relay config to be prepared + * + * @return none + */ +void prepare_relay_server_config(relay_config &interface_config); + +/** + * @code loop_relay(std::unordered_map &vlans); + * + * @brief main loop: configure sockets, create libevent base, start server listener thread + * + * @param vlans list of vlans retrieved from config_db + * @param state_db state_db connector + */ +void loop_relay(std::unordered_map &vlans); + +/** + * @code signal_init(); + * + * @brief initialize DHCPv4 Relay libevent signals + */ +int signal_init(); + +/** + * @code signal_start(); + * + * @brief start DHCPv4 Relay libevent base and add signals + */ +int signal_start(); + +/** + * @code dhcp4relay_stop(); + * + * @brief stop DHCPv4 Relay libevent loop upon signal + */ +void dhcp4relay_stop(); + +/** + * @code signal_callback(fd, event, arg); + * + * @brief signal handler for dhcp4relay. Initiate shutdown when signal is caught + * + * @param fd libevent socket + * @param event event triggered + * @param arg pointer to libevent base + * + * @return none + */ +void signal_callback(evutil_socket_t fd, short event, void *arg); + +/** + * @code shutdown(); + * + * @brief free signals and terminate threads + */ +void shutdown_relay(); + +/* Helper functions */ + +/** + * @code update_vlan_mapping(std::string vlan, bool is_add); + * + * @brief build vlan member interface to vlan mapping table + * + * @param vlan vlan name string + * @param is_add add or delete entry + * + * @return none + */ +void update_vlan_mapping(std::string vlan, bool is_add); + +/** + * @code pkt_in_callback(evutil_socket_t fd, short event, void *arg); + * + * @brief callback for libevent that is called everytime data is received at the filter socket + * this is expected to receive both server and client sent DHCP packets + * + * @param fd filter socket + * @param event libevent triggered event + * @param arg callback argument provided by user + * + * @return none + */ +void pkt_in_callback(evutil_socket_t fd, short event, void *arg); +void config_event_callback(evutil_socket_t fd, short event, void *arg); +uint8_t *decode_tlv(const uint8_t *buf, uint8_t t, uint8_t &l, uint32_t options_total_size); +uint8_t encode_tlv(uint8_t *buf, uint8_t t, uint8_t l, uint8_t *v); diff --git a/dhcp4relay/src/dhcp4relay_mgr.cpp b/dhcp4relay/src/dhcp4relay_mgr.cpp new file mode 100644 index 0000000..188e2c7 --- /dev/null +++ b/dhcp4relay/src/dhcp4relay_mgr.cpp @@ -0,0 +1,849 @@ +#include "dhcp4relay_mgr.h" + +#include +#include +constexpr auto DEFAULT_TIMEOUT_MSEC = 1000; + +std::unordered_map vlans_copy; + +#ifdef UNIT_TEST +using namespace swss; +#endif + +metadata_config m_config; + +bool feature_dhcp_server_enabled = false; +std::shared_ptr config_db_dhcp_server_ipv4_ptr = NULL; +std::shared_ptr state_db_dhcp_server_ipv4_ip_ptr = NULL; +std::shared_ptr config_db_relaymgr_table_ptr = NULL; +std::string global_dhcp_server_ip; +/** + * @brief Initializes the configuration listener for the DHCP manager. + * + * This function starts a new detached thread that listens for SWSS (Switch State Service) + * notifications by invoking the handle_swss_notification method. It also sets the stop_thread + * flag to false to indicate that the listener thread should be running. + * + * @note The spawned thread is detached, so it will run independently of the main thread. + */ +void DHCPMgr::initialize_config_listner() { + stop_thread = false; + std::thread m_swss_thread(&DHCPMgr::handle_swss_notification, this); + m_swss_thread.detach(); +} + +/** + * @brief Handles SWSS (Sonic Warehouse State Service) notifications for DHCPv4 relay manager. + * + * This method listens for configuration changes in various tables within the CONFIG_DB, + * such as DHCPV4_RELAY, INTERFACE, LOOPBACK_INTERFACE, PORTCHANNEL_INTERFACE, and DEVICE_METADATA. + * It uses a select loop to wait for notifications from these tables and processes them accordingly. + * + * The function continues to run until the `stop_thread` flag is set. For each notification, + * it determines the source table and invokes the appropriate handler to process the entries. + * Errors and unknown return values from the select operation are logged. + * + * Tables monitored: + * - DHCPV4_RELAY: Triggers relay notification processing. + * - INTERFACE, LOOPBACK_INTERFACE, PORTCHANNEL_INTERFACE: Triggers interface notification processing. + * - DEVICE_METADATA: Triggers device metadata notification processing. + * + * @note This function is intended to be run in a dedicated thread. + */ +void DHCPMgr::handle_swss_notification() { + std::shared_ptr config_db_ptr = std::make_shared("CONFIG_DB", 0); + std::shared_ptr state_db_ptr = std::make_shared("STATE_DB", 0); + config_db_relaymgr_table_ptr = std::make_shared(config_db_ptr.get(), CFG_DHCPV4_RELAY_TABLE_NAME); + swss::SubscriberStateTable config_db_interface_table(config_db_ptr.get(), "INTERFACE"); + swss::SubscriberStateTable config_db_loopback_table(config_db_ptr.get(), "LOOPBACK_INTERFACE"); + swss::SubscriberStateTable config_db_portchannel_table(config_db_ptr.get(), "PORTCHANNEL_INTERFACE"); + swss::SubscriberStateTable config_db_device_metadata_table(config_db_ptr.get(), "DEVICE_METADATA"); + swss::SubscriberStateTable config_db_vlan_member_table(config_db_ptr.get(), "VLAN_MEMBER"); + swss::SubscriberStateTable config_db_vlan_interface_table(config_db_ptr.get(), "VLAN_INTERFACE"); + swss::SubscriberStateTable config_db_feature_table(config_db_ptr.get(), "FEATURE"); + swss::SubscriberStateTable config_db_vlan_table(config_db_ptr.get(), "VLAN"); + config_db_dhcp_server_ipv4_ptr = std::make_shared(config_db_ptr.get(), CFG_DHCP_SERVER_IPV4_TABLE_NAME); + state_db_dhcp_server_ipv4_ip_ptr = std::make_shared(state_db_ptr.get(), STATE_DHCPV4_SERVER_IPV4_SERVER_IP_TABLE); + swss::SubscriberStateTable config_db_port_table(config_db_ptr.get(), "PORT"); + + std::deque entries; + swss::Select swss_select; + swss_select.addSelectable(config_db_relaymgr_table_ptr.get()); + swss_select.addSelectable(&config_db_interface_table); + swss_select.addSelectable(&config_db_loopback_table); + swss_select.addSelectable(&config_db_portchannel_table); + swss_select.addSelectable(&config_db_device_metadata_table); + swss_select.addSelectable(&config_db_vlan_member_table); + swss_select.addSelectable(&config_db_vlan_interface_table); + swss_select.addSelectable(&config_db_feature_table); + swss_select.addSelectable(&config_db_vlan_table); + swss_select.addSelectable(config_db_dhcp_server_ipv4_ptr.get()); + swss_select.addSelectable(state_db_dhcp_server_ipv4_ip_ptr.get()); + swss_select.addSelectable(&config_db_port_table); + + while (!stop_thread) { + swss::Selectable *selectable; + int ret = swss_select.select(&selectable, DEFAULT_TIMEOUT_MSEC); + + if (ret == swss::Select::ERROR) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Error had been returned in select"); + continue; + } else if (ret == swss::Select::TIMEOUT) { + continue; + } else if (ret != swss::Select::OBJECT) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Unknown return value from Select: %d", ret); + continue; + } + + if (!feature_dhcp_server_enabled) { + if (config_db_relaymgr_table_ptr && selectable == config_db_relaymgr_table_ptr.get()) { + config_db_relaymgr_table_ptr->pops(entries); + process_relay_notification(entries); + } else if (selectable == static_cast(&config_db_interface_table)) { + config_db_interface_table.pops(entries); + process_interface_notification(entries); + } else if (selectable == static_cast(&config_db_loopback_table)) { + config_db_loopback_table.pops(entries); + process_interface_notification(entries); + } else if (selectable == static_cast(&config_db_portchannel_table)) { + config_db_portchannel_table.pops(entries); + process_interface_notification(entries); + } + } else { + if (config_db_dhcp_server_ipv4_ptr && selectable == config_db_dhcp_server_ipv4_ptr.get()) { + config_db_dhcp_server_ipv4_ptr->pops(entries); + process_dhcp_server_ipv4_notification(entries); + } else if (state_db_dhcp_server_ipv4_ip_ptr && selectable == state_db_dhcp_server_ipv4_ip_ptr.get()) { + state_db_dhcp_server_ipv4_ip_ptr->pops(entries); + process_dhcp_server_ipv4_ip_notification(entries, swss_select, config_db_ptr); + } + } + + if (selectable == static_cast(&config_db_device_metadata_table)) { + config_db_device_metadata_table.pops(entries); + process_device_metadata_notification(entries); + } else if (selectable == static_cast(&config_db_vlan_member_table)) { + config_db_vlan_member_table.pops(entries); + process_vlan_member_notification(entries); + } else if (selectable == static_cast(&config_db_vlan_interface_table)) { + config_db_vlan_interface_table.pops(entries); + process_vlan_interface_notification(entries); + } else if (selectable == static_cast(&config_db_feature_table)) { + config_db_feature_table.pops(entries); + process_feature_notification(entries, swss_select, config_db_ptr, state_db_ptr); + } else if (selectable == static_cast(&config_db_vlan_table)) { + config_db_vlan_table.pops(entries); + process_vlan_notification(entries); + } else if (selectable == static_cast(&config_db_port_table)) { + config_db_port_table.pops(entries); + process_port_notification(entries); + } + } +} + +/** + * @brief Processes device metadata notifications and sends metadata update events if necessary. + * + * This function iterates over a deque of device metadata entries, checks for relevant updates, + * and sends a metadata update event through a configuration pipe if the entry corresponds to "localhost". + * It extracts fields such as hostname and MAC address from the metadata, constructs a relay_config object, + * and ensures a default hostname ("sonic") is set if not present. If memory allocation fails or writing + * to the pipe fails, appropriate error messages are logged. + * + * @param entries A deque of KeyOpFieldsValuesTuple containing device metadata notifications. + */ +void DHCPMgr::process_device_metadata_notification(std::deque &entries) { + for (auto &entry : entries) { + std::string key = kfvKey(entry); + std::vector field_values = kfvFieldsValues(entry); + std::string operation = kfvOp(entry); + + if (key != "localhost") { + continue; + } + bool subtype_found = false; + bool send_dualTor_event = false; + std::string subtype_value; + + for (auto &field : field_values) { + std::string f = fvField(field); + std::string v = fvValue(field); + + if (f == "hostname") { + m_config.hostname = v; + } else if (f == "mac") { + std::transform(v.begin(), v.end(), v.begin(), ::tolower); + m_config.host_mac_addr = v; + } else if (f == "deployment_id") { + m_config.deployment_id = static_cast(std::stoul(v)); + } else if (f == "subtype") { + subtype_found = true; + subtype_value = v; + } + + // Handle is_dualToR logic + if (subtype_found && subtype_value == "DualToR") { + m_config.is_dualTor = true; + send_dualTor_event = true; + } else if (m_config.is_dualTor) { + // Covers both 'subtype' deleted and any value other than "DualToR" + m_config.is_dualTor = false; + send_dualTor_event = true; + } + + if (send_dualTor_event) { + relay_config *relay_msg = nullptr; + try { + relay_msg = new relay_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + + if (m_config.is_dualTor) { + relay_msg->is_add = true; + } else { + relay_msg->is_add = false; + } + + event_config event; + event.type = DHCPv4_RELAY_DUAL_TOR_UPDATE; + event.msg = static_cast(relay_msg); + // Write the pointer address to the pipe + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to write to config update pipe: %s", strerror(errno)); + delete relay_msg; + } + } + } + /* Re-set hostname to default value if hostname is deleted */ + if (m_config.hostname.length() == 0) { + m_config.hostname = "sonic"; + } + } +} + +/** + * @brief Processes interface notifications and updates DHCP relay configuration accordingly. + * + * This method iterates over a deque of interface notification entries, each containing + * a key, operation, and associated values. For each entry, it parses the interface name + * and IP address, checks if the interface is configured as a DHCP relay source interface, + * and prepares a relay configuration update event. Depending on the operation ("SET" or "DEL"), + * it sets up the relay configuration to add or remove the interface. The configuration update + * event is then written to a pipe for further processing. + * + * Memory allocation failures and invalid IP addresses are logged as errors. + * + * @param entries A deque of KeyOpFieldsValuesTuple objects representing interface notifications. + */ +void DHCPMgr::process_interface_notification(std::deque &entries) { + for (auto &entry : entries) { + std::string key = kfvKey(entry); + std::string operation = kfvOp(entry); + + size_t found = key.find("|"); + + std::string intf_name; + std::string ip_with_mask; + std::string ip; + if (found != std::string::npos) { + intf_name = key.substr(0, found); + ip_with_mask = key.substr(found + 1); + ip = ip_with_mask.substr(0, ip_with_mask.find('/')); + } else { + continue; + } + + // Check the source interface is configured in dhcp relay config. + for (auto &vlan : vlans_copy) { + if (vlan.second.source_interface == intf_name) { + relay_config *relay_msg = nullptr; + try { + relay_msg = new relay_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + + relay_msg->vlan = vlan.second.vlan; + if (operation == "SET") { + relay_msg->is_add = true; + if (inet_pton(AF_INET, ip.c_str(), &relay_msg->src_intf_sel_addr.sin_addr) != 1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Invalid IP address"); + delete relay_msg; + return; + } + + relay_msg->src_intf_sel_addr.sin_family = AF_INET; + } else if (operation == "DEL") { + relay_msg->is_add = false; + } + + event_config event; + event.type = DHCPv4_RELAY_INTERFACE_UPDATE; + event.msg = static_cast(relay_msg); + // Write the pointer address to the pipe + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to write to config update pipe: %s", strerror(errno)); + delete relay_msg; + } + } + } + } +} + +/** + * @brief Processes a batch of relay configuration notifications for DHCPv4 relay. + * + * This method iterates over a deque of relay configuration entries, parses each entry, + * and updates the internal VLAN relay configuration cache accordingly. For "SET" operations, + * it creates or updates the relay configuration for the specified VLAN, parsing relevant fields + * such as DHCPv4 servers, VRF, source interface, and various relay options. For "DEL" operations, + * it removes the relay configuration for the specified VLAN from the cache. + * + * After processing each entry, it constructs an event containing the updated relay configuration + * and writes it to a configuration update pipe for further handling. The method also logs + * relevant information and errors using syslog. + * + * @param entries A deque of KeyOpFieldsValuesTuple objects representing relay configuration notifications. + */ +void DHCPMgr::process_relay_notification(std::deque &entries) { + for (auto &entry : entries) { + std::string vlan = kfvKey(entry); + std::string operation = kfvOp(entry); + std::vector field_values = kfvFieldsValues(entry); + relay_config *relay_msg = nullptr; + try { + relay_msg = new relay_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + + relay_msg->vlan = vlan; + + if (operation == "SET") { + relay_msg->is_add = true; + for (auto &field_value : field_values) { + std::string f = fvField(field_value); + std::string v = fvValue(field_value); + if (f == "dhcpv4_servers") { + std::stringstream ss(v); + while (ss.good()) { + std::string substr; + getline(ss, substr, ','); + relay_msg->servers.push_back(substr); + } + } else if (f == "server_vrf") { + relay_msg->vrf = v; + } else if (f == "source_interface") { + relay_msg->source_interface = v; + } else if (f == "link_selection") { + relay_msg->link_selection_opt = v; + } else if (f == "server_id_override") { + relay_msg->server_id_override_opt = v; + } else if (f == "vrf_selection") { + relay_msg->vrf_selection_opt = v; + } else if (f == "agent_relay_mode") { + relay_msg->agent_relay_mode = v; + } else if (f == "max_hop_count") { + relay_msg->max_hop_count = static_cast(std::stoi(v)); + } + syslog(LOG_DEBUG, "[DHCPV4_RELAY] key: %s, Operation: %s, f: %s, v: %s", vlan.c_str(), operation.c_str(), f.c_str(), v.c_str()); + } + + // Updating vrf value with client VRF if server vrf is not configured. + if (relay_msg->vrf.length() == 0) { + std::string value; + std::shared_ptr config_db = std::make_shared("CONFIG_DB", 0); + std::shared_ptr vlan_intf_tbl = std::make_shared(config_db.get(), CFG_VLAN_INTF_TABLE_NAME); + vlan_intf_tbl->hget(vlan, "vrf_name", value); + if (value.size() <= 0) { + relay_msg->vrf = "default"; + } else { + relay_msg->vrf = value; + } + } + + // Update the vlan cache entry + vlans_copy[relay_msg->vlan] = *relay_msg; + } else if (operation == "DEL") { + syslog(LOG_INFO, "[DHCPV4_RELAY] Received DELETE operation for VLAN %s", vlan.c_str()); + relay_msg->is_add = false; + // Remove the vlan cache entry + vlans_copy.erase(relay_msg->vlan); + } + + if (relay_msg->servers.empty() && operation != "DEL") { + syslog(LOG_WARNING, "[DHCPV4_RELAY] No servers found for VLAN %s, skipping configuration.", vlan.c_str()); + continue; + } + syslog(LOG_INFO, "[DHCPV4_RELAY] %s %s relay config\n", operation.c_str(), vlan.c_str()); + + event_config event; + event.type = DHCPv4_RELAY_CONFIG_UPDATE; + event.msg = static_cast(relay_msg); + + // Write the pointer address to the pipe + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to write to config update pipe: %s", strerror(errno)); + delete relay_msg; + } + } +} + +/** + * @brief Processes the feature table updates to configure the dhcp_server enabled/disabled. + * + * This method iterates over a deque of relay configuration entries, parses each entry, + * if the entry is for 'dhcp_server' then based on the 'state' value it will process the entry. + * If the "state" is "enable" then it will send the delete event to main thread to remove all the + * existing dhcp_relay config and then restart the listeners for dhcp_server related tables. + * If the "state" is "disable" then it will send the delete event to main thread to remove all the + * auto configured dhcp_server config and then restart the listeners dhcp_relay related table. + * + * The method will handle the clean up for the vlan cache entries and also logs relevant information + * and errors using syslog. + * + * @param entries A deque of KeyOpFieldsValuesTuple objects representing feature table notifications. + * config_db_ptr It represents the pointer for the CONFIG_DB + * state_db_ptr It represents the pointer for the STATE_DB + */ +void DHCPMgr::process_feature_notification(std::deque &entries, + swss::Select &select, std::shared_ptr config_db_ptr, + std::shared_ptr state_db_ptr) { + for (auto &entry : entries) { + if (kfvKey(entry) != "dhcp_server") { + continue; + } + + std::string state; + for (auto &field : kfvFieldsValues(entry)) { + if (fvField(field) == "state") { + state = fvValue(field); + break; + } + } + + if (state == "enabled" && !feature_dhcp_server_enabled) { + //Delete the existing vlan configs in main thread + event_config event; + event.type = DHCPv4_SERVER_FEATURE_UPDATE; + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send delete event for dhcp_server feature update"); + return; + } + vlans_copy.clear(); + feature_dhcp_server_enabled = true; + + if (config_db_dhcp_server_ipv4_ptr) { + select.removeSelectable(config_db_dhcp_server_ipv4_ptr.get()); + } + if (state_db_dhcp_server_ipv4_ip_ptr) { + select.removeSelectable(state_db_dhcp_server_ipv4_ip_ptr.get()); + } + + config_db_dhcp_server_ipv4_ptr = std::make_shared(config_db_ptr.get(), CFG_DHCP_SERVER_IPV4_TABLE_NAME); + state_db_dhcp_server_ipv4_ip_ptr = std::make_shared(state_db_ptr.get(), STATE_DHCPV4_SERVER_IPV4_SERVER_IP_TABLE); + + select.addSelectable(config_db_dhcp_server_ipv4_ptr.get()); + select.addSelectable(state_db_dhcp_server_ipv4_ip_ptr.get()); + } else if (state == "disabled" && feature_dhcp_server_enabled) { + syslog(LOG_INFO, "[DHCPV4_RELAY] Disabling DHCP server auto-config mode and cleaning up."); + feature_dhcp_server_enabled = false; + global_dhcp_server_ip.clear(); + vlans_copy.clear(); + //Delete the old auto generated relay config in main thread + event_config event; + event.type = DHCPv4_SERVER_FEATURE_UPDATE; + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send delete event for dhcp_server feature update"); + return; + } + + //re-add the dhcp relay listeners + if (config_db_relaymgr_table_ptr) { + select.removeSelectable(config_db_relaymgr_table_ptr.get()); + } + config_db_relaymgr_table_ptr = std::make_shared(config_db_ptr.get(), "DHCPV4_RELAY"); + select.addSelectable(config_db_relaymgr_table_ptr.get()); + } + } +} + +/** + * @brief Processes the dhcp_server_ip entry and stores the IP in the global parameter. + * + * This method iterates over a deque of dhcp_server_ip configuration entries, parses each entry, + * if the entry is for 'eth0' then only it will process the entry. + * If the operation is "SET" then it will stores the IP in global parameter and restart the dhcp_server + * related as it is the new configuration of the IP. + * If the operation is "DEL" then it will send the delete event to main thread to remove all the + * auto configured dhcp_server config, as without IP, the relay config can't be formed. + * + * The method will handle the clean up for the vlan cache entries and also logs relevant information + * and errors using syslog. + * + * @param entries A deque of KeyOpFieldsValuesTuple objects representing dhcp_server_ip table notifications. + * config_db_ptr It represents the pointer for the CONFIG_DB + */ +void DHCPMgr::process_dhcp_server_ipv4_ip_notification(std::deque &entries, + swss::Select &select, std::shared_ptr config_db_ptr) { + bool is_modify = false; + + for (auto &entry : entries) { + std::string server_intf = kfvKey(entry); + std::string operation = kfvOp(entry); + + if (server_intf != "eth0") { + continue; + } + + if (operation == "SET") { + std::string server_ip; + for (auto &fv : kfvFieldsValues(entry)) { + if (fvField(fv) == "ip") { + server_ip = fvValue(fv); + break; + } + } + if (server_ip.empty()) { + syslog(LOG_ERR, "[DHCPV4_RELAY] dhcp_server IP is not present in state DB"); + return; + } + //modification case + if (!global_dhcp_server_ip.empty() && (global_dhcp_server_ip != server_ip)) { + event_config event; + event.type = DHCPv4_SERVER_IP_UPDATE; + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send delete event for dhcp_server IP update"); + return; + } + is_modify = true; + } + global_dhcp_server_ip = server_ip; + //Since the server IP see newly added, restart the listener for the dhcp_server config. + if (!is_modify) { + syslog(LOG_INFO, "[DHCPV4_RELAY] Restarting the dhcp_server listener"); + if (config_db_dhcp_server_ipv4_ptr) { + select.removeSelectable(config_db_dhcp_server_ipv4_ptr.get()); + } + config_db_dhcp_server_ipv4_ptr = std::make_shared(config_db_ptr.get(), CFG_DHCP_SERVER_IPV4_TABLE_NAME); + select.addSelectable(config_db_dhcp_server_ipv4_ptr.get()); + } + } else { + //DHCP server IP deletion case, need to remove the existing configs in main thread + event_config event; + event.type = DHCPv4_SERVER_IP_DELETE; + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send delete event for dhcp_server IP delete"); + return; + } + global_dhcp_server_ip.clear(); + vlans_copy.clear(); + } + } +} + +void DHCPMgr::process_vlan_member_notification(std::deque &entries) { + for (auto &entry : entries) { + std::string key = kfvKey(entry); + std::string operation = kfvOp(entry); + + size_t pos = key.find('|'); + if (pos == std::string::npos) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Invalid string format"); + return; + } + + std::string vlan = key.substr(0, pos); + std::string interface = key.substr(pos + 1); + + //If the vlan is not configured in DHCPV4 table then skip the entry. + if (vlans_copy.find(vlan) == vlans_copy.end()) { + continue; + } + + vlan_member_config *msg = nullptr; + try { + msg = new vlan_member_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + + msg->vlan = vlan; + msg->interface = interface; + + if (operation == "SET") { + msg->is_add = true; + } else { + msg->is_add = false; + } + + event_config event; + event.type = DHCPv4_RELAY_VLAN_MEMBER_UPDATE; + event.msg = static_cast(msg); + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send vlan member update for vlan %s", vlan.c_str()); + delete msg; + } + } +} + +void DHCPMgr::process_vlan_interface_notification(std::deque &entries) { + for (auto &entry : entries) { + std::string key = kfvKey(entry); + + std::string vlan; + std::string vrf; + size_t pos = key.find('|'); + if (pos == std::string::npos) { + vlan = key; + vrf = "default"; + for (auto &fv : kfvFieldsValues(entry)) { + if (fvField(fv) == "vrf_name") { + vrf = fvValue(fv); + break; + } + } + } else { + vlan = key.substr(0, pos); + } + + //If the vlan is not configured in DHCPV4 table then skip the entry. + if (vlans_copy.find(vlan) == vlans_copy.end()) { + continue; + } + + vlan_interface_config *msg = nullptr; + try { + msg = new vlan_interface_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + msg->vlan = vlan; + msg->vrf = vrf; + + event_config event; + event.type = DHCPv4_RELAY_VLAN_INTERFACE_UPDATE; + event.msg = static_cast(msg); + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send vlan interface update for vlan %s", vlan.c_str()); + delete msg; + } + + } +} + +/** + * @brief Processes the dhcp_server table entry to form the dhcp_relay config. + * + * This method iterates over a deque of dhcp_server configuration entries, parses each entry, + * If the operation is "SET" then based on the "state" value it will proceed entry, + * it will adds the vlan and server IP to for the relay_config and send the event to main thread. + * If the server IP is not updated then it will get the entry from DB and fill it. + * related as it is the new configuration of the IP. + * If the Vlan is not present in the VLAN table then it will not send the config event to main thread. + * If the operation is "DEL" then it will send the delete entry event to the main thread. + * + * The method will handle the updating the vlan cache entries and also logs relevant information + * and errors using syslog. + * + * @param entries A deque of KeyOpFieldsValuesTuple objects representing dhcp_server table notifications. + */ +void DHCPMgr::process_dhcp_server_ipv4_notification(std::deque &entries) { + std::shared_ptr config_db = std::make_shared("CONFIG_DB", 0); + swss::Table vlan_tbl(config_db.get(), "VLAN"); + + for (auto &entry : entries) { + std::string vlan = kfvKey(entry); + std::string operation = kfvOp(entry); + + relay_config *relay_msg = nullptr; + try { + relay_msg = new relay_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + + relay_msg->vlan = vlan; + + if (operation == "SET") { + std::string state; + for (auto &fv : kfvFieldsValues(entry)) { + if (fvField(fv) == "state") { + state = fvValue(fv); + break; + } + } + + if (state == "enabled") { + if (global_dhcp_server_ip.empty()) { + std::shared_ptr state_db_ptr = std::make_shared("STATE_DB", 0); + swss::Table ip_tbl(state_db_ptr.get(), STATE_DHCPV4_SERVER_IPV4_SERVER_IP_TABLE); + + std::string ip; + ip_tbl.hget("eth0", "ip", ip); + if (!ip.empty()) { + global_dhcp_server_ip = ip; + syslog(LOG_INFO, "[DHCPV4_RELAY] Fetched DHCPv4 server IP from STATE_DB: %s", ip.c_str()); + } else { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to get DHCPv4 server IP from STATE_DB"); + continue; + } + } + relay_msg->is_add = true; + relay_msg->servers.push_back(global_dhcp_server_ip); + relay_msg->vrf = "default"; + } else if (state == "disabled") { + relay_msg->is_add = false; //In case of modify in state field need to delete the entry + } + } else { + relay_msg->is_add = false; + } + + // Update the vlan cache entry + if (relay_msg->is_add) { + vlans_copy[relay_msg->vlan] = *relay_msg; + } else { + vlans_copy.erase(relay_msg->vlan); + } + + /*Validation to check vlan is present in VLAN table or not */ + std::string value; + if (!vlan_tbl.hget(vlan, "vlanid", value)) { + delete relay_msg; + continue; + } + + event_config event; + event.type = DHCPv4_SERVER_RELAY_CONFIG_UPDATE; + event.msg = static_cast(relay_msg); + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send vlan table update for VLAN %s", vlan.c_str()); + delete relay_msg; + } + } +} + +/** + * @brief Processes the vlan table entry. + * + * This method iterates over a deque of vlan configuration entries, parses each entry, + * If any dhcp_relay/dhcp_server entry exists in the vlan cache list then based on the + * operation "SET" or "DEL" it will send the update event to main thread to process that entry. + * + * @param entries A deque of KeyOpFieldsValuesTuple objects representing vlan table notifications. + */ +void DHCPMgr::process_vlan_notification(std::deque &entries) { + for (auto &entry : entries) { + std::string vlan = kfvKey(entry); + std::string operation = kfvOp(entry); + + //If the vlan is not configured in DHCPV4 table then skip the entry. + if (vlans_copy.find(vlan) == vlans_copy.end()) { + continue; + } + + relay_config *relay_msg = nullptr; + try { + relay_msg = new relay_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + + relay_msg->vlan = vlan; + + if (operation == "SET") { + *relay_msg = vlans_copy[relay_msg->vlan]; + relay_msg->is_add = true; + } else { + relay_msg->is_add = false; + } + + event_config event; + if (feature_dhcp_server_enabled) { + event.type = DHCPv4_SERVER_RELAY_CONFIG_UPDATE; + } else { + event.type = DHCPv4_RELAY_CONFIG_UPDATE; + } + event.msg = static_cast(relay_msg); + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send vlan update event for vlan %s", vlan.c_str()); + delete relay_msg; + } + } +} + +void DHCPMgr::process_port_notification(std::deque &entries) { + for (auto &entry : entries) { + std::string interface = kfvKey(entry); + std::string operation = kfvOp(entry); + std::vector fv = kfvFieldsValues(entry); + port_config *port_msg = nullptr; + try { + port_msg = new port_config(); + } catch (const std::bad_alloc &e) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Memory allocation failed: %s", e.what()); + return; + } + + port_msg->phy_interface = interface; + if (operation == "SET") { + port_msg->is_add = true; + for (auto &fv : kfvFieldsValues(entry)) { + if (fvField(fv) == "alias") { + port_msg->alias = fvValue(fv); + break; + } + } + } else { + port_msg->is_add = false; + } + + event_config event; + event.type = DHCPv4_RELAY_PORT_UPDATE; + event.msg = static_cast(port_msg); + + if (write(config_pipe[1], &event, sizeof(event)) == -1) { + syslog(LOG_ERR, "[DHCPV4_RELAY] Failed to send port table update for interface %s", interface.c_str()); + delete port_msg; + } + } +} + +/** + * @code void DHCPMgr::stop_db_updates(); + * + * @brief Method to stop thread which will be listening to the DB updates.. + * + * @return none + */ + +void DHCPMgr::stop_db_updates() { + stop_thread = true; +} + +/** + * @code DHCPMgr::~DHCPMgr() + * + * @brief Destructor. + * + * @return none + */ +DHCPMgr::~DHCPMgr() { + stop_db_updates(); +} diff --git a/dhcp4relay/src/dhcp4relay_mgr.h b/dhcp4relay/src/dhcp4relay_mgr.h new file mode 100644 index 0000000..15065d4 --- /dev/null +++ b/dhcp4relay/src/dhcp4relay_mgr.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "dbconnector.h" +#include "dhcp4relay.h" +#include "select.h" +#include "subscriberstatetable.h" +#include "table.h" + +class DHCPMgr { + private: + std::atomic stop_thread; + + public: + DHCPMgr() : stop_thread(false) {} + ~DHCPMgr(); + + void initialize_config_listner(); + void handle_swss_notification(); + void stop_db_updates(); + void process_relay_notification(std::deque &entries); + void process_interface_notification(std::deque &entries); + void process_device_metadata_notification(std::deque &entries); + void process_vlan_member_notification(std::deque &entries); + void process_vlan_interface_notification(std::deque &entries); + void process_feature_notification(std::deque &entries, + swss::Select &select, std::shared_ptr config_db_ptr, + std::shared_ptr state_db_ptr); + void process_dhcp_server_ipv4_ip_notification(std::deque &entries, + swss::Select &select, std::shared_ptr config_db_ptr); + void process_dhcp_server_ipv4_notification(std::deque &entries); + void process_vlan_notification(std::deque &entries); + void process_port_notification(std::deque &entries); +}; diff --git a/dhcp4relay/src/dhcp4relay_stats.cpp b/dhcp4relay/src/dhcp4relay_stats.cpp new file mode 100644 index 0000000..cf46482 --- /dev/null +++ b/dhcp4relay/src/dhcp4relay_stats.cpp @@ -0,0 +1,238 @@ +#include "dhcp4relay_stats.h" + +#include "dbconnector.h" +#include "dhcp4relay.h" +#include "table.h" + +using namespace swss; + +/* DHCPv4 counter name map */ +std::map counter_map = { + {DHCPv4_MESSAGE_TYPE_UNKNOWN, "Unknown"}, + {DHCPv4_MESSAGE_TYPE_DISCOVER, "Discover"}, + {DHCPv4_MESSAGE_TYPE_OFFER, "Offer"}, + {DHCPv4_MESSAGE_TYPE_REQUEST, "Request"}, + {DHCPv4_MESSAGE_TYPE_DECLINE, "Decline"}, + {DHCPv4_MESSAGE_TYPE_ACK, "Acknowledge"}, + {DHCPv4_MESSAGE_TYPE_NAK, "NegativeAcknowledge"}, + {DHCPv4_MESSAGE_TYPE_RELEASE, "Release"}, + {DHCPv4_MESSAGE_TYPE_INFORM, "Inform"}, + {DHCPv4_MESSAGE_TYPE_MALFORMED, "Malformed"}, + {DHCPv4_MESSAGE_TYPE_DROP, "Dropped"}}; + +/** + * @code calculate_delta(uint64_t new_value, uint64_t old_value); + * + * @brief Helper function to calculate safe delta with overflow handling. + * + * @param new_value new uint64_t value + * @param old_value old uint64_t value + * + * @return delta value + */ +uint64_t calculate_delta(uint64_t new_value, uint64_t old_value) { + if (new_value >= old_value) { + return new_value - old_value; + } else { + // Handle overflow case + return (std::numeric_limits::max() - old_value) + new_value + 1; + } +} + +/** + * @code get_counters_data() + * + * @brief GET function to fetch data of private member interfaces_cntr_table + * + * @return std::unordered_map + */ +std::unordered_map DHCPCounter_table::get_counters_data() { + return interfaces_cntr_table; +} + +/** + * @brief Helper function to update RX/TX counters in the DB for a given interface and direction. + * + * @param cntr_table Shared pointer to the swss::Table for updating the DB. + * @param interface Name of the interface. + * @param direction "RX" or "TX". + * @param counters_map Reference to the map of counters (either RX or TX). + */ +void update_interface_counters_in_db( + std::shared_ptr cntr_table, + const std::string& interface, + const std::string& direction, + const std::unordered_map counters_map) +{ + std::vector existing_fields; + std::string key = interface + swss::TableBase::getTableSeparator(COUNTERS_DB) + direction; + cntr_table->get(key, existing_fields); + + std::vector fields; + std::unordered_map counter_map; + + // Populate existing counters + for (const auto& field : existing_fields) { + counter_map[fvField(field)] = std::stoi(fvValue(field)); + } + + // Update with new values + for (const auto& [type, value] : counters_map) { + counter_map[type] += value; + fields.emplace_back(type, std::to_string(counter_map[type])); + } + + cntr_table->set(key, fields); +} + +/** + * @code DHCPCounter_table::db_update_loop(); + * + * @brief Loop to update dhcp stats to the DB periodically. + * This loop is triggered by a new thread which is responsible to update stats to DB. + * + * Mutex lock is taken to copy the data locally, before parsing and setting to DB. + * + * @return none + */ +void DHCPCounter_table::db_update_loop() { + std::shared_ptr cntrs_db = std::make_shared("COUNTERS_DB", 0); + std::shared_ptr cntr_table = std::make_shared( + cntrs_db.get(), COUNTERS_DHCPV4_TABLE); + + while (!stop_thread) { + std::this_thread::sleep_for(std::chrono::seconds(DHCP_RELAY_DB_UPDATE_TIMER_VAL)); + + // Copy the data by taking lock for processing and populating to DB + std::unordered_map interfaces_copy; + { + std::lock_guard lock(interfaces_mutex); + interfaces_copy = interfaces_cntr_table; + } + + /* These steps are followed before updating to Redis: + 1. Fetch present values from redis - existing _fields + 2. Update counters with 'cache values' + 'existing_fields' + 3. Populate to DB + */ + for (const auto& [interface, counters] : interfaces_copy) { + update_interface_counters_in_db(cntr_table, interface, "RX", counters.RX); + update_interface_counters_in_db(cntr_table, interface, "TX", counters.TX); + } + + // Update local changes after syncing to Redis + // We will take the delta values of running data in interfaces_cntr_table + // and previously copied interfaces_copy values. + { + // Taking lock inside a block so that is released automatically + std::lock_guard lock(interfaces_mutex); + for (auto& [interface, counters] : interfaces_cntr_table) { + for (auto& [type, value] : counters.RX) { + value = calculate_delta(value, interfaces_copy[interface].RX[type]); + } + for (auto& [type, value] : counters.TX) { + value = calculate_delta(value, interfaces_copy[interface].TX[type]); + } + } + } + syslog(LOG_INFO, "DHCPV4_RELAY: DHCPCounter_table::db_update_loop() : Data Updated to DB \n"); + } +} + +/** + * @code DHCPCounter_table::start_db_updates(); + * + * @brief Method to start thread to update stats to DB periodically. + * + * @return none + */ +void DHCPCounter_table::start_db_updates() { + db_update_thread = std::thread(&DHCPCounter_table::db_update_loop, this); +} + +/** + * @code DHCPCounter_table::stop_db_updates(); + * + * @brief Method to stop thread which updates stats to DB periodically. + * + * @return none + */ +void DHCPCounter_table::stop_db_updates() { + stop_thread = true; + if (db_update_thread.joinable()) { + db_update_thread.join(); + } +} + +/** + * @code DHCPCounter_table::initialize_interface(std::string& interface); + * + * @brief Method to initialize counters in DB for a particular interface + * + * @param interface Name of the interface for which RX and TX counters need to be initialized + * + * @return none + */ +void DHCPCounter_table::initialize_interface(const std::string& interface) { + std::lock_guard lock(interfaces_mutex); + DHCPCounters counter; + for (const auto& type : counter_map) { + counter.RX[type.second] = 0; + counter.TX[type.second] = 0; + } + interfaces_cntr_table[interface] = counter; +} + +/** + * @code DHCPCounter_table::increment_counter(const std::string& interface, + * const std::string& direction, + * int msg_type); + * + * @brief Method to increment counters in DB for a particular interface, + * direction(RX|TX) and dhcp_message_type_t + * + * @param interface Name of the interface for which counters need to be incremented + * + * @return none + */ +void DHCPCounter_table::increment_counter(const std::string& interface, + const std::string& direction, + int msg_type) { + std::string type = counter_map.find(msg_type)->second; + // Initialize counters if not present + if (interfaces_cntr_table.find(interface) == interfaces_cntr_table.end()) + DHCPCounter_table::initialize_interface(interface); + + std::lock_guard lock(interfaces_mutex); + + if (direction == "RX") { + interfaces_cntr_table[interface].RX[type]++; + } else if (direction == "TX") { + interfaces_cntr_table[interface].TX[type]++; + } +} + +/** + * @code DHCPCounter_table::remove_interface(const std::string& interface); + * + * @brief Method to remove counters from global datastructure. + * + * @param interface Name of the interface for which counters need to be removed. + * + * @return none + */ +void DHCPCounter_table::remove_interface(const std::string& interface) { + std::lock_guard lock(interfaces_mutex); + interfaces_cntr_table.erase(interface); +} + +/** + * @code DHCPCounter_table:~DHCPCounter_table() + * + * @brief Destructor. + * + * @return none + */ +DHCPCounter_table::~DHCPCounter_table() { + stop_db_updates(); +} diff --git a/dhcp4relay/src/dhcp4relay_stats.h b/dhcp4relay/src/dhcp4relay_stats.h new file mode 100644 index 0000000..805baa4 --- /dev/null +++ b/dhcp4relay/src/dhcp4relay_stats.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#define DHCP_RELAY_DB_UPDATE_TIMER_VAL 30 + +extern std::map counter_map; + +struct DHCPCounters { + std::unordered_map RX; + std::unordered_map TX; +}; + +class DHCPCounter_table { +private: + std::unordered_map interfaces_cntr_table; + std::mutex interfaces_mutex; + std::atomic stop_thread{false}; + std::thread db_update_thread; + + void db_update_loop(); + +public: + void start_db_updates(); + void stop_db_updates(); + void initialize_interface(const std::string& interface); + void increment_counter(const std::string& interface, const std::string& direction, + int msg_type); + void remove_interface(const std::string& interface); + std::unordered_map get_counters_data(); + + ~DHCPCounter_table(); +}; + +uint64_t calculate_delta(uint64_t new_value, uint64_t old_value); diff --git a/dhcp4relay/src/main.cpp b/dhcp4relay/src/main.cpp new file mode 100644 index 0000000..8a4f67f --- /dev/null +++ b/dhcp4relay/src/main.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include + +#include "dhcp4relay.h" + +bool dual_tor_sock = false; +char loopback[IF_NAMESIZE] = "Loopback0"; + +int main(int argc, char *argv[]) { + try { + std::unordered_map vlans; + loop_relay(vlans); + } catch (std::exception &e) { + syslog(LOG_ERR, "An exception occurred.\n"); + return 1; + } + return 0; +} diff --git a/dhcp4relay/src/subdir.mk b/dhcp4relay/src/subdir.mk new file mode 100644 index 0000000..f6d21fa --- /dev/null +++ b/dhcp4relay/src/subdir.mk @@ -0,0 +1,6 @@ +SRCS += \ +src/dhcp4_sender.cpp \ +src/dhcp4relay.cpp \ +src/dhcp4relay_stats.cpp \ +src/dhcp4relay_mgr.cpp \ +src/main.cpp diff --git a/dhcp4relay/test/database_config.json b/dhcp4relay/test/database_config.json new file mode 100644 index 0000000..8ab44e2 --- /dev/null +++ b/dhcp4relay/test/database_config.json @@ -0,0 +1,32 @@ +{ + "INSTANCES": { + "redis":{ + "hostname" : "127.0.0.1", + "port" : 6379, + "unix_socket_path" : "/var/run/redis/redis.sock" + }, + "redis_chassis":{ + "hostname" : "redis_chassis.server", + "port" : 6380, + "unix_socket_path" : "/var/run/redis/redis_chassis.sock" + } + }, + "DATABASES" : { + "COUNTERS_DB" : { + "id" : 2, + "separator": "|", + "instance" : "redis" + }, + "CONFIG_DB" : { + "id" : 4, + "separator": "|", + "instance" : "redis" + }, + "STATE_DB" : { + "id" : 6, + "separator": "|", + "instance" : "redis" + } + }, + "VERSION" : "1.0" +} diff --git a/dhcp4relay/test/main.cpp b/dhcp4relay/test/main.cpp new file mode 100644 index 0000000..cc17eba --- /dev/null +++ b/dhcp4relay/test/main.cpp @@ -0,0 +1,9 @@ +#include "gtest/gtest.h" +#include + +int main(int argc, char* argv[]) +{ + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/dhcp4relay/test/mock_consumerstatetable.cpp b/dhcp4relay/test/mock_consumerstatetable.cpp new file mode 100644 index 0000000..2764bb1 --- /dev/null +++ b/dhcp4relay/test/mock_consumerstatetable.cpp @@ -0,0 +1,40 @@ +#include "consumerstatetable.h" + +namespace swss +{ + ConsumerStateTable::ConsumerStateTable(DBConnector *db, const std::string &tableName, int popBatchSize, int pri) : + ConsumerTableBase(db, tableName, popBatchSize, pri), + TableName_KeySet(tableName) + { + } + + void ConsumerStateTable::pops(std::deque &vkco, const std::string& /*prefix*/) + { + int count = 0; + swss::Table table(getDbConnector(), getTableName()); + std::vector keys; + table.getKeys(keys); + for (const auto &key: keys) + { + // pop with batch size + if (count < POP_BATCH_SIZE) + { + count++; + } + else + { + break; + } + + KeyOpFieldsValuesTuple kco; + kfvKey(kco) = key; + kfvOp(kco) = SET_COMMAND; + if (!table.get(key, kfvFieldsValues(kco))) + { + continue; + } + table.del(key); + vkco.push_back(kco); + } + } +} diff --git a/dhcp4relay/test/mock_dbconnector.cpp b/dhcp4relay/test/mock_dbconnector.cpp new file mode 100644 index 0000000..8c3aa0d --- /dev/null +++ b/dhcp4relay/test/mock_dbconnector.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include +#include + +#include + +#include "dbconnector.h" + +namespace swss +{ + DBConnector::DBConnector(int dbId, const std::string &hostname, int port, unsigned int timeout) : + m_dbId(dbId) + { + auto conn = (redisContext *)calloc(1, sizeof(redisContext)); + conn->connection_type = REDIS_CONN_TCP; + conn->tcp.host = strdup(hostname.c_str()); + conn->tcp.port = port; + conn->fd = socket(AF_UNIX, SOCK_DGRAM, 0); + setContext(conn); + } + + DBConnector::DBConnector(int dbId, const std::string &unixPath, unsigned int timeout) : + m_dbId(dbId) + { + auto conn = (redisContext *)calloc(1, sizeof(redisContext)); + conn->connection_type = REDIS_CONN_UNIX; + conn->unix_sock.path = strdup(unixPath.c_str()); + conn->fd = socket(AF_UNIX, SOCK_DGRAM, 0); + setContext(conn); + } + + DBConnector::DBConnector(const std::string& dbName, unsigned int timeout, bool isTcpConn) + : m_dbName(dbName) + { + if (swss::SonicDBConfig::isInit() == false) + swss::SonicDBConfig::initialize("./test/database_config.json"); + m_dbId = swss::SonicDBConfig::getDbId(dbName); + if (isTcpConn) + { + auto conn = (redisContext *)calloc(1, sizeof(redisContext)); + conn->connection_type = REDIS_CONN_TCP; + conn->tcp.host = strdup(swss::SonicDBConfig::getDbHostname(dbName).c_str()); + conn->tcp.port = swss::SonicDBConfig::getDbPort(dbName); + conn->fd = socket(AF_UNIX, SOCK_DGRAM, 0); + setContext(conn); + } + else + { + auto conn = (redisContext *)calloc(1, sizeof(redisContext)); + conn->connection_type = REDIS_CONN_UNIX; + conn->unix_sock.path = strdup(swss::SonicDBConfig::getDbSock(dbName).c_str()); + conn->fd = socket(AF_UNIX, SOCK_DGRAM, 0); + setContext(conn); + } + } + + int DBConnector::getDbId() const + { + return m_dbId; + } +} diff --git a/dhcp4relay/test/mock_hiredis.cpp b/dhcp4relay/test/mock_hiredis.cpp new file mode 100644 index 0000000..8c1c905 --- /dev/null +++ b/dhcp4relay/test/mock_hiredis.cpp @@ -0,0 +1,40 @@ +#include +#include +#include + +// Add a global redisReply for user to mock +redisReply *mockReply = nullptr; + +int redisGetReply(redisContext *c, void **reply) +{ + if (mockReply == nullptr) + { + *reply = calloc(sizeof(redisReply), 1); + ((redisReply *)*reply)->type = 3; + } + else + { + *reply = mockReply; + } + return 0; +} + +int redisAppendFormattedCommand(redisContext *c, const char *cmd, size_t len) +{ + return 0; +} + +int redisvAppendCommand(redisContext *c, const char *format, va_list ap) +{ + return 0; +} + +int redisAppendCommand(redisContext *c, const char *format, ...) +{ + return 0; +} + +int redisGetReplyFromReader(redisContext *c, void **reply) +{ + return 0; +} diff --git a/dhcp4relay/test/mock_redisreply.cpp b/dhcp4relay/test/mock_redisreply.cpp new file mode 100644 index 0000000..391b853 --- /dev/null +++ b/dhcp4relay/test/mock_redisreply.cpp @@ -0,0 +1,16 @@ +#include "redisreply.h" + +namespace swss +{ + void RedisReply::checkStatus(const char *status) + { + } + + void RedisReply::checkReply() + { + } + + void RedisReply::checkReplyType(int expectedType) + { + } +} \ No newline at end of file diff --git a/dhcp4relay/test/mock_relay.cpp b/dhcp4relay/test/mock_relay.cpp new file mode 100644 index 0000000..40fd7aa --- /dev/null +++ b/dhcp4relay/test/mock_relay.cpp @@ -0,0 +1,903 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "gtest/gtest.h" +#include "gmock/gmock.h" +#include "mock_relay.h" +#include + +#include +#include +#include +#include +#include +#include + +using namespace ::testing; +using namespace swss; + +MOCK_GLOBAL_FUNC1(getifaddrs, int(struct ifaddrs **)); +MOCK_GLOBAL_FUNC1(freeifaddrs, void(struct ifaddrs *)); +MOCK_GLOBAL_FUNC3(write, ssize_t(int, const void*, size_t)); +MOCK_GLOBAL_FUNC6(send_udp, bool(int, uint8_t *, struct sockaddr_in, uint32_t, in_addr, bool)); + +void encode_relay_option(pcpp::DhcpLayer *dhcp_pkt, relay_config *config); +void to_client(pcpp::DhcpLayer* dhcp_pkt, std::unordered_map *vlans, + std::string src_ip); +void from_client(pcpp::DhcpLayer *dhcp_pkt, relay_config &config); + +ssize_t RealWrite(int fd, const void *buf, size_t count) { + return syscall(SYS_write, fd, buf, count); +} + +struct ifaddrs *CreateMockIfaddrs(const std::string &vlan_ip, const std::string &vlan_mask, const std::string &vlan_name, + const std::string &src_ip, const std::string &src_name) { + struct ifaddrs *mock_ifaddrs = new ifaddrs; + memset(mock_ifaddrs, 0, sizeof(ifaddrs)); + + struct sockaddr_in *addr = new sockaddr_in; + struct sockaddr_in *mask = new sockaddr_in; + addr->sin_family = AF_INET; + addr->sin_addr.s_addr = inet_addr(vlan_ip.c_str()); + mask->sin_family = AF_INET; + mask->sin_addr.s_addr = inet_addr(vlan_mask.c_str()); + + mock_ifaddrs->ifa_addr = reinterpret_cast(addr); + mock_ifaddrs->ifa_netmask = reinterpret_cast(mask); + mock_ifaddrs->ifa_name = strdup(vlan_name.c_str()); + + struct ifaddrs *mock_ifaddrs_src = new ifaddrs; + memset(mock_ifaddrs_src, 0, sizeof(ifaddrs)); + + struct sockaddr_in *src_addr = new sockaddr_in; + src_addr->sin_family = AF_INET; + src_addr->sin_addr.s_addr = inet_addr(src_ip.c_str()); + + mock_ifaddrs_src->ifa_addr = reinterpret_cast(src_addr); + mock_ifaddrs_src->ifa_name = strdup(src_name.c_str()); + + mock_ifaddrs->ifa_next = mock_ifaddrs_src; + return mock_ifaddrs; +} + +void FreeMockIfaddrs(struct ifaddrs *mock_ifaddrs) { + if (mock_ifaddrs) { + delete reinterpret_cast(mock_ifaddrs->ifa_addr); + delete reinterpret_cast(mock_ifaddrs->ifa_netmask); + if (mock_ifaddrs->ifa_next) { + delete reinterpret_cast(mock_ifaddrs->ifa_next->ifa_addr); + free(mock_ifaddrs->ifa_next->ifa_name); + delete mock_ifaddrs->ifa_next; + } + free(mock_ifaddrs->ifa_name); + delete mock_ifaddrs; + } +} + +TEST(EncodeDecodeTLV, EncodeAndDecode) { + uint8_t buffer[10] = {}; + uint8_t value[3] = {0x11, 0x22, 0x33}; + uint8_t length = 0; + + uint8_t encoded_length = encode_tlv(buffer, 1, 3, value); + EXPECT_EQ(encoded_length, 5); + EXPECT_EQ(buffer[0], 1); + EXPECT_EQ(buffer[1], 3); + EXPECT_EQ(buffer[2], 0x11); + EXPECT_EQ(buffer[3], 0x22); + EXPECT_EQ(buffer[4], 0x33); + + uint8_t *decoded_value = decode_tlv(buffer, 1, length, 5); + ASSERT_NE(decoded_value, nullptr); + EXPECT_EQ(length, 3); + EXPECT_EQ(decoded_value[0], 0x11); + EXPECT_EQ(decoded_value[1], 0x22); + EXPECT_EQ(decoded_value[2], 0x33); +} + +TEST(sock, sock_open) { + struct sock_filter ether_relay_filter[] = { + { 0x6, 0, 0, 0x00040000 }, + }; + const struct sock_fprog ether_relay_fprog = { + lengthof(ether_relay_filter), + ether_relay_filter + }; + EXPECT_GE(sock_open(ðer_relay_fprog), 0); +} + +TEST(sock, sock_open_invalid_filter) { + const struct sock_fprog ether_relay_fprog = {0,{}}; + EXPECT_EQ(sock_open(ðer_relay_fprog), -1); +} + +TEST(prepareConfig, prepare_relay_server_config) { + struct relay_config config{}; + config.servers.push_back("192.168.1.1"); + config.servers.push_back("10.0.0.1"); + + prepare_relay_server_config(config); + + ASSERT_EQ(config.servers_sock.size(), 2); + + EXPECT_EQ(config.servers_sock[0].sin_family, AF_INET); + EXPECT_EQ(config.servers_sock[0].sin_port, htons(67)); + EXPECT_EQ(config.servers_sock[0].sin_addr.s_addr, inet_addr("192.168.1.1")); + + EXPECT_EQ(config.servers_sock[1].sin_family, AF_INET); + EXPECT_EQ(config.servers_sock[1].sin_port, htons(67)); + EXPECT_EQ(config.servers_sock[1].sin_addr.s_addr, inet_addr("10.0.0.1")); +} + +TEST(prepareConfig, prepare_relay_interface_config) { + struct ifaddrs *mock_ifaddrs = CreateMockIfaddrs("192.168.1.1", "255.255.255.0", "Vlan100", "192.168.1.2", "Ethernet4"); + struct relay_config interface_config{}; + interface_config.vlan = "Vlan100"; + interface_config.source_interface = "Ethernet4"; + + EXPECT_GLOBAL_CALL(getifaddrs, getifaddrs(_)).WillOnce(DoAll(testing::SetArgPointee<0>(mock_ifaddrs), Return(0))); + EXPECT_GLOBAL_CALL(freeifaddrs, freeifaddrs(_)).Times(1); + + prepare_relay_interface_config(interface_config); + + EXPECT_EQ(interface_config.link_address.sin_addr.s_addr, inet_addr("192.168.1.1")); + EXPECT_EQ(interface_config.link_address_netmask.sin_addr.s_addr, inet_addr("255.255.255.0")); + EXPECT_EQ(interface_config.src_intf_sel_addr.sin_addr.s_addr, inet_addr("192.168.1.2")); + + FreeMockIfaddrs(mock_ifaddrs); +} + +TEST(prepareConfig, prepare_vlan_sockets) { + struct relay_config config{}; + config.link_address.sin_addr.s_addr = htonl(0x01010101); + struct iphdr ip_hdr; + std::string s_addr = "1.1.1.1"; + inet_pton(AF_INET, s_addr.c_str(), &ip_hdr.saddr); + + config.servers.push_back("3.3.3.3"); + config.servers.push_back("4.4.4.4"); + + config.vlan = "Vlan200"; + EXPECT_EQ(prepare_vlan_sockets(config), 0); + EXPECT_GE(config.client_sock, 0); +} + +TEST(prepareConfig, prepare_vrf_sockets) { + struct relay_config config{}; + config.vrf = "default"; + + EXPECT_EQ(prepare_vrf_sockets(config), 0); + EXPECT_GE(config.vrf_sock, 0); + EXPECT_GE(vrf_sock_map["default"].sock, 0); + EXPECT_EQ(vrf_sock_map["default"].ref_count, 1); + vrf_sock_map.clear(); +} + +TEST(prepareConfig, update_vlan_mapping) { + swss::Table vlan_member_table(config_db.get(), "VLAN_MEMBER"); + swss::Table vlan_interface_table(config_db.get(), "VLAN_INTERFACE"); + + std::string key = "Vlan200|Ethernet8"; + std::vector> values = { + {"tagging_mode", "untagged"}, + }; + + std::string vlan_key = "Vlan200"; + std::vector> vlan_values = { + {"vrf_name", "VrfRed"}, + }; + + vlan_member_table.set(key, values); + vlan_interface_table.set(vlan_key, vlan_values); + + // add case + update_vlan_mapping(vlan_key, true); + + EXPECT_EQ(vlan_map["Ethernet8"], vlan_key); + EXPECT_EQ(vlan_vrf_map[vlan_key], "VrfRed"); + + //delete case + update_vlan_mapping(vlan_key, false); + + EXPECT_EQ(vlan_map.find("Ethernet8"), vlan_map.end()); + EXPECT_EQ(vlan_vrf_map.find(vlan_key), vlan_vrf_map.end()); +} + +TEST(relayConfig, handle_vlan_events) { + event_config event; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Invoke(RealWrite)); + std::unordered_map vlans; + + swss::Table vlan_member_table(config_db.get(), "VLAN_MEMBER"); + swss::Table vlan_interface_table(config_db.get(), "VLAN_INTERFACE"); + + std::string key = "Vlan200|Ethernet8"; + std::vector> values = { + {"tagging_mode", "untagged"}, + }; + + std::string vlan_key = "Vlan200"; + std::vector> vlan_values = { + {"vrf_name", "VrfRed"}, + }; + + vlan_member_table.set(key, values); + vlan_interface_table.set(vlan_key, vlan_values); + + struct ifaddrs *mock_ifaddrs = CreateMockIfaddrs("1.1.1.1", "255.255.255.0", "Vlan200", "2.2.2.2", "Ethernet8"); + + EXPECT_GLOBAL_CALL(getifaddrs, getifaddrs(_)).WillOnce(DoAll(testing::SetArgPointee<0>(mock_ifaddrs), Return(0))); + EXPECT_GLOBAL_CALL(freeifaddrs, freeifaddrs(_)).Times(1); + relay_config *config = new relay_config(); + config->vlan = "Vlan200"; + config->is_add = true; + config->source_interface = "Ethernet8"; + config->servers = {"192.168.1.1","10.0.0.1"}; + config->vrf = "default"; + event.type = DHCPv4_RELAY_CONFIG_UPDATE; + event.msg = static_cast(config); + + int pipe_fds[2]; + ASSERT_NE(pipe(pipe_fds), -1); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + + ASSERT_TRUE(vlans.find("Vlan200") != vlans.end()); + + ASSERT_EQ(vlans["Vlan200"].servers_sock.size(), 2); + + EXPECT_EQ(vlans["Vlan200"].servers_sock[0].sin_family, AF_INET); + EXPECT_EQ(vlans["Vlan200"].servers_sock[0].sin_port, htons(67)); + EXPECT_EQ(vlans["Vlan200"].servers_sock[0].sin_addr.s_addr, inet_addr("192.168.1.1")); + + EXPECT_EQ(vlans["Vlan200"].servers_sock[1].sin_family, AF_INET); + EXPECT_EQ(vlans["Vlan200"].servers_sock[1].sin_port, htons(67)); + EXPECT_EQ(vlans["Vlan200"].servers_sock[1].sin_addr.s_addr, inet_addr("10.0.0.1")); + + EXPECT_EQ(vlans["Vlan200"].link_address.sin_addr.s_addr, inet_addr("1.1.1.1")); + EXPECT_EQ(vlans["Vlan200"].link_address_netmask.sin_addr.s_addr, inet_addr("255.255.255.0")); + EXPECT_EQ(vlans["Vlan200"].src_intf_sel_addr.sin_addr.s_addr, inet_addr("2.2.2.2")); + + EXPECT_GE(vlans["Vlan200"].vrf_sock, 0); + EXPECT_GE(vrf_sock_map["default"].sock, 0); + EXPECT_EQ(vrf_sock_map["default"].ref_count, 1); + + EXPECT_GE(vlans["Vlan200"].client_sock, 0); + + /*Vlan deletion.*/ + relay_config *config_del = new relay_config(); + config_del->vlan = "Vlan200"; + config_del->is_add = false; + + vlans["Vlan200"].client_sock = -1; + vlans["Vlan200"].vrf_sock = -1; + event.msg = static_cast(config_del); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + ASSERT_TRUE(vlans.find("Vlan200") == vlans.end()); + close(pipe_fds[0]); + close(pipe_fds[1]); + FreeMockIfaddrs(mock_ifaddrs); +} + +TEST(relayConfig, handle_interface_events) { + int pipe_fds[2]; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Invoke(RealWrite)); + ASSERT_NE(pipe(pipe_fds), -1); + relay_config *config = new relay_config(); + std::unordered_map vlans; + config->vlan = "Vlan100"; + config->is_add = true; + config->src_intf_sel_addr.sin_family = AF_INET; + config->src_intf_sel_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); + + event_config event; + event.type = DHCPv4_RELAY_INTERFACE_UPDATE; + event.msg = static_cast(config); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + + ASSERT_TRUE(vlans.find("Vlan100") != vlans.end()); + EXPECT_EQ(vlans["Vlan100"].src_intf_sel_addr.sin_family, AF_INET); + EXPECT_EQ(vlans["Vlan100"].src_intf_sel_addr.sin_addr.s_addr, inet_addr("192.168.1.1")); + + event.type = DHCPv4_SERVER_IP_UPDATE; + global_dhcp_server_ip = "192.168.1.1"; + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_EQ(vlans["Vlan100"].servers_sock[0].sin_family, AF_INET); + EXPECT_EQ(vlans["Vlan100"].servers_sock[0].sin_port, htons(67)); + EXPECT_EQ(vlans["Vlan100"].servers_sock[0].sin_addr.s_addr, inet_addr("192.168.1.1")); + + event.type = DHCPv4_SERVER_FEATURE_UPDATE; + event.msg = NULL; + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + config_event_callback(pipe_fds[0], 0, &vlans); + + close(pipe_fds[0]); + close(pipe_fds[1]); +} + +TEST(relayConfig, handle_vlan_member_events) { + 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; + vlans["Vlan100"].vlan = "Vlan100"; + vlans["Vlan100"].client_sock = -1; + vlans["Vlan100"].is_add = true; + + vlan_member_config *vlan_config = new vlan_member_config(); + + vlan_config->is_add = true; + vlan_config->interface = "Ethernet12"; + vlan_config->vlan = "Vlan100"; + + event_config event; + event.type = DHCPv4_RELAY_VLAN_MEMBER_UPDATE; + event.msg = static_cast(vlan_config); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_EQ(vlan_map["Ethernet12"], "Vlan100"); + EXPECT_GE(vlans["Vlan100"].client_sock, 0); + + vlan_member_config *vlan_config_del = new vlan_member_config(); + + vlans["Vlan100"].client_sock = -1; + vlan_config_del->is_add = false ; + vlan_config_del->interface = "Ethernet12"; + vlan_config_del->vlan = "Vlan100"; + + event.msg = static_cast(vlan_config_del); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_NE(vlan_map["Ethernet12"], "Vlan100"); + EXPECT_GE(vlans["Vlan100"].client_sock, 0); + + close(pipe_fds[0]); + close(pipe_fds[1]); +} + +TEST(relayConfig, handle_vlan_interface_events) { + struct ifaddrs *mock_ifaddrs = CreateMockIfaddrs("192.168.5.5", "255.255.255.0", "Vlan100", "192.168.1.2", "Ethernet4"); + int pipe_fds[2]; + EXPECT_GLOBAL_CALL(getifaddrs, getifaddrs(_)).WillOnce(DoAll(testing::SetArgPointee<0>(mock_ifaddrs), Return(0))); + EXPECT_GLOBAL_CALL(freeifaddrs, freeifaddrs(_)).Times(1); + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Invoke(RealWrite)); + ASSERT_NE(pipe(pipe_fds), -1); + + std::unordered_map vlans; + vlans["Vlan100"].vlan = "Vlan100"; + vlans["Vlan100"].is_add = true; + + vlan_interface_config *vlan_config = new vlan_interface_config(); + + vlan_config->vlan = "Vlan100"; + vlan_config->vrf = "VrfRed"; + + event_config event; + event.type = DHCPv4_RELAY_VLAN_INTERFACE_UPDATE; + event.msg = static_cast(vlan_config); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_EQ(vlan_vrf_map["Vlan100"], "VrfRed"); + + vlan_interface_config *vlan_intf_config = new vlan_interface_config(); + + vlans["Vlan100"].client_sock = -1; + vlan_intf_config->vlan = "Vlan100"; + + event.msg = static_cast(vlan_intf_config); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_GE(vlans["Vlan100"].client_sock, 0); + EXPECT_EQ(vlans["Vlan100"].link_address.sin_addr.s_addr, inet_addr("192.168.5.5")); + EXPECT_EQ(vlans["Vlan100"].link_address_netmask.sin_addr.s_addr, inet_addr("255.255.255.0")); + + close(pipe_fds[0]); + close(pipe_fds[1]); +} + +TEST(relayConfig, handle_port_table_events) { + 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; + + port_config *port_msg = new port_config(); + + port_msg->phy_interface = "Ethernet12"; + port_msg->alias = "eth12"; + port_msg->is_add = true; + + event_config event; + event.type = DHCPv4_RELAY_PORT_UPDATE; + event.msg = static_cast(port_msg); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + + EXPECT_EQ(interface_list[0], "Ethernet12"); + EXPECT_EQ(phy_interface_alias_map["Ethernet12"], "eth12"); + + port_config *port_msg_del = new port_config(); + + port_msg_del->phy_interface = "Ethernet12"; + port_msg_del->is_add = false; + + event.msg = static_cast(port_msg_del); + + ASSERT_NE(write(pipe_fds[1], &event, sizeof(event)), -1); + + config_event_callback(pipe_fds[0], 0, &vlans); + EXPECT_EQ(std::find(interface_list.begin(), interface_list.end(), "Ethernet12"), interface_list.end()); + EXPECT_EQ(phy_interface_alias_map.count("Ethernet12"), 0); + + close(pipe_fds[0]); + close(pipe_fds[1]); +} +TEST(relay, signal_init) { + signal_init(); + EXPECT_NE((uintptr_t)ev_sigint, NULL); + EXPECT_NE((uintptr_t)ev_sigterm, NULL); +} + +MOCK_GLOBAL_FUNC1(event_base_dispatch, int(struct event_base *)); +MOCK_GLOBAL_FUNC2(event_add, int(struct event *, const struct timeval *)); + +TEST(relay, signal_start) { + EXPECT_GLOBAL_CALL(event_add, event_add(_, NULL)).Times(5) + .WillOnce(Return(-1)) + .WillOnce(Return(0)).WillOnce(Return(-1)) + .WillOnce(Return(0)).WillOnce(Return(0)); + EXPECT_EQ(signal_start(), -1); + EXPECT_EQ(signal_start(), -1); + EXPECT_GLOBAL_CALL(event_base_dispatch, event_base_dispatch(_)).Times(1).WillOnce(Return(-1)); + EXPECT_EQ(signal_start(), 0); +} + +TEST(DHCPMgrTest, initialize_config_listner) { + DHCPMgr dhcpMgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(-1)); + dhcpMgr.initialize_config_listner(); + + swss::Table dhcp_table(config_db.get(), CFG_DHCPV4_RELAY_TABLE_NAME); + swss::Table intf_table(config_db.get(), "INTERFACE"); + swss::Table loopback_intf_table(config_db.get(), "LOOPBACK_INTERFACE"); + swss::Table portchannel_intf_table(config_db.get(), "PORTCHANNEL_INTERFACE"); + swss::Table metadata_table(config_db.get(), "DEVICE_METADATA"); + swss::Table vlan_member_table(config_db.get(), "VLAN_MEMBER"); + swss::Table vlan_interface_table(config_db.get(), "VLAN_INTERFACE"); + swss::Table port_table(config_db.get(), "PORT"); + + std::string vlan_member_key = "Vlan200|Ethernet8"; + std::vector> vlan_member_values = { + {"tagging_mode", "untagged"}, + }; + + std::string vlan_interface_key = "Vlan200|200.200.200.1/24"; + std::vector> vlan_interface_values = { + {"vrf_name", "VrfRed"}, + }; + + std::string vlan = "Vlan200"; + std::vector> dhcp_values = { + {"dhcpv4_servers", "1.1.1.1,1.1.1.2"}, + {"server_vrf", "VrfRed"}, + {"source_interface", "Ethernet4"}, + {"agent_relay_mode", "discard"}, + {"link_selection", "enable"}, + {"server_id_override", "enable"}, + {"vrf_selection", "enable"}, + {"max_hop_count", "16"} + }; + + dhcp_table.set(vlan, dhcp_values); + + std::string intf_key = "Ethernet4|192.168.1.1/24"; + std::vector> intf_values = { + {"NULL", "NULL"}, + }; + + intf_table.set(intf_key, intf_values); + loopback_intf_table.set(intf_key, intf_values); + portchannel_intf_table.set(intf_key, intf_values); + + std::string metadata_key = "localhost"; + std::vector> metadata_values = { + {"hostname", "newHost"}, + {"mac", "00:11:22:33:44:55"} + }; + + std::vector> port_values = { + {"alias", "eth12"}, + }; + metadata_table.set("host", metadata_values); + metadata_table.set(metadata_key, metadata_values); + vlan_member_table.set(vlan_member_key, vlan_member_values); + vlan_interface_table.set(vlan_interface_key, intf_values); + vlan_interface_table.set(vlan, vlan_interface_values); + port_table.set("Ethernet12", port_values); + std::this_thread::sleep_for(std::chrono::seconds(1)); + dhcpMgr.stop_db_updates(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + EXPECT_EQ(vlans_copy[vlan].max_hop_count, 16); +} + +TEST(DHCPMgrTest, process_vlan_events) { + DHCPMgr dhcpMgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(-1)); + vlans_copy.clear(); + relay_config *config = new relay_config(); + config->vlan = "Vlan100"; + config->is_add = true; + vlans_copy["Vlan100"] = *config; + std::deque entries; + entries.emplace_back("Vlan100", "SET", std::vector{}); + dhcpMgr.process_vlan_notification(entries); +} + +TEST(DHCPMgrTest, dhcp_server_feature_enable) { + DHCPMgr dhcpMgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(0)); + dhcpMgr.initialize_config_listner(); + + std::shared_ptr state_db = std::make_shared ("STATE_DB", 0); + + swss::Table feature_table(config_db.get(), "FEATURE"); + swss::Table vlan_table(config_db.get(), "VLAN"); + swss::Table dhcp_server_table(config_db.get(), CFG_DHCP_SERVER_IPV4_TABLE_NAME); + swss::Table dhcp_server_ip_table(state_db.get(), STATE_DHCPV4_SERVER_IPV4_SERVER_IP_TABLE); + + std::string vlan = "Vlan200"; + std::vector> enable_dhcp_server = { + {"state", "enabled"}, + }; + + std::vector> server_ip_values = { + {"ip", "240.127.1.2"}, + }; + + std::vector> vlan_values = { + {"vlanid", "200"}, + }; + + vlan_table.set(vlan, vlan_values); + dhcp_server_ip_table.set("eth0", server_ip_values); + dhcp_server_table.set(vlan, enable_dhcp_server); + feature_table.set("dhcp_server", enable_dhcp_server); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + dhcpMgr.stop_db_updates(); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + EXPECT_EQ(global_dhcp_server_ip, "240.127.1.2"); + feature_dhcp_server_enabled = false; + global_dhcp_server_ip.clear(); +} + +TEST(DHCPMgrTest, dhcp_server_feature_disable) { + DHCPMgr dhcpMgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(0)); + dhcpMgr.initialize_config_listner(); + + swss::Table feature_table(config_db.get(), "FEATURE"); + std::vector> disable_dhcp_server = { + {"state", "disabled"}, + }; + feature_dhcp_server_enabled = true; + feature_table.set("dhcp_server", disable_dhcp_server); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + dhcpMgr.stop_db_updates();; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + feature_dhcp_server_enabled = false; +} + +TEST(DHCPMgrTest, dhcp_server_ip_modification) { + DHCPMgr dhcpMgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(0)); + global_dhcp_server_ip = "240.127.1.3"; + std::deque entries; + swss::Select select; + entries.emplace_back("eth0", "SET", std::vector{ + {"ip", "240.127.1.2"} + }); + + dhcpMgr.process_dhcp_server_ipv4_ip_notification(entries, select, config_db); + EXPECT_EQ(global_dhcp_server_ip, "240.127.1.2"); +} + +TEST(DHCPMgrTest, dhcp_server_ip_deletion) { + DHCPMgr dhcpMgr; + EXPECT_GLOBAL_CALL(write, write(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(0)); + std::deque entries; + swss::Select select; + entries.emplace_back("eth0", "DEL", std::vector{}); + + dhcpMgr.process_dhcp_server_ipv4_ip_notification(entries, select,config_db); + EXPECT_TRUE(global_dhcp_server_ip.empty()); + EXPECT_TRUE(vlans_copy.empty()); +} + +TEST(DHCPRelayTest, encode_relay_option) { + std::shared_ptr config_db = std::make_shared ("CONFIG_DB", 0); + pcpp::EthLayer ethLayer(pcpp::MacAddress("00:13:72:25:fa:cd"), pcpp::MacAddress("00:e0:b1:49:39:02")); + + pcpp::IPv4Address srcIp("172.22.178.234"); + pcpp::IPv4Address dstIp("10.10.8.240"); + pcpp::IPv4Layer ipLayer(srcIp, dstIp); + ipLayer.getIPv4Header()->ipId = htobe16(20370); + ipLayer.getIPv4Header()->timeToLive = 128; + + pcpp::UdpLayer udpLayer((uint16_t)67, (uint16_t)67); + + pcpp::MacAddress clientMac(std::string("00:0e:86:11:c0:75")); + pcpp::DhcpLayer dhcpLayer(pcpp::DHCP_DISCOVER, clientMac); + dhcpLayer.getDhcpHeader()->hops = 1; + + interface_list.push_back("Ethernet12"); + phy_interface_alias_map["Ethernet12"] = "eth12"; + + relay_config config = {}; + config.phy_interface = "Ethernet12"; + config.vlan = "Vlan10"; + config.link_selection_opt = "enable"; + config.server_id_override_opt = "enable"; + config.link_address.sin_addr.s_addr = inet_addr("192.168.10.10"); + config.link_address_netmask.sin_addr.s_addr = inet_addr("255.255.255.0"); + config.vrf_selection_opt = "enable"; + vlan_vrf_map["Vlan10"] = "Vrf01"; + + m_config.hostname = "cisco"; + m_config.host_mac_addr = "12:32:54:24:95:36"; + + encode_relay_option(&dhcpLayer, &config); + + auto agent_option = dhcpLayer.getOptionData(pcpp::DHCPOPT_DHCP_AGENT_OPTIONS); + auto options_ptr = agent_option.getValue(); + auto agent_option_size = agent_option.getDataSize(); + EXPECT_NE((uintptr_t)options_ptr, NULL); + + uint8_t circuit_id_len = 0; + auto circuit_id_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_CIRCUIT_ID, + circuit_id_len, agent_option_size); + std::string circuit_id((const char*)circuit_id_ptr, circuit_id_len); + + EXPECT_EQ(circuit_id, "cisco:eth12:Vlan10"); + uint8_t remote_id_len = 0; + auto remote_id_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_REMOTE_ID, + remote_id_len, agent_option_size); + + EXPECT_EQ(memcmp(m_config.host_mac_addr.c_str(), remote_id_ptr, 17), 0); + + uint8_t link_sel_len = 0; + auto link_sel_ip_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_LINK_SELECTION, + link_sel_len, agent_option_size); + auto link_sel_ip = *((uint32_t *)link_sel_ip_ptr); + EXPECT_EQ((config.link_address.sin_addr.s_addr & config.link_address_netmask.sin_addr.s_addr), link_sel_ip); + + auto srv_ovr_ride = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_SERVER_OVERRIDE, + link_sel_len, agent_option_size); + auto srv_ip = *((uint32_t *)srv_ovr_ride); + EXPECT_EQ(srv_ip, config.link_address.sin_addr.s_addr); + + uint8_t vrf_len = 0; + auto vrf_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_VIRTUAL_SUBNET, + vrf_len, agent_option_size); + std::string vrf((char *)vrf_ptr, vrf_len); + uint8_t vss_buf[32] = {0}; + uint8_t zero_encode = 0; + memcpy(vss_buf, &zero_encode, sizeof(uint8_t)); + memcpy((vss_buf + 1), (uint8_t*)vlan_vrf_map["Vlan10"].c_str(), (uint8_t)vlan_vrf_map["Vlan10"].length()); + + EXPECT_EQ(memcmp(vss_buf, vrf_ptr, 6), 0); +} + +TEST(DHCPRelayTest, encode_relay_option_server_client_same_vrf) { + std::shared_ptr config_db = std::make_shared ("CONFIG_DB", 0); + pcpp::EthLayer ethLayer(pcpp::MacAddress("00:13:72:25:fa:cd"), pcpp::MacAddress("00:e0:b1:49:39:02")); + + pcpp::IPv4Address srcIp("172.22.178.234"); + pcpp::IPv4Address dstIp("10.10.8.240"); + pcpp::IPv4Layer ipLayer(srcIp, dstIp); + ipLayer.getIPv4Header()->ipId = htobe16(20370); + ipLayer.getIPv4Header()->timeToLive = 128; + + pcpp::UdpLayer udpLayer((uint16_t)67, (uint16_t)67); + + pcpp::MacAddress clientMac(std::string("00:0e:86:11:c0:75")); + pcpp::DhcpLayer dhcpLayer(pcpp::DHCP_DISCOVER, clientMac); + dhcpLayer.getDhcpHeader()->hops = 1; + + interface_list.push_back("Ethernet12"); + phy_interface_alias_map["Ethernet12"] = "eth12"; + + relay_config config = {}; + config.phy_interface = "Ethernet12"; + config.vlan = "Vlan10"; + config.vrf = "Vrf01"; + config.link_selection_opt = "enable"; + config.server_id_override_opt = "enable"; + config.link_address.sin_addr.s_addr = inet_addr("192.168.10.10"); + config.link_address_netmask.sin_addr.s_addr = inet_addr("255.255.255.0"); + config.vrf_selection_opt = "enable"; + vlan_vrf_map["Vlan10"] = "Vrf01"; + + m_config.hostname = "cisco"; + m_config.host_mac_addr = "12:32:54:24:95:36"; + + encode_relay_option(&dhcpLayer, &config); + + auto agent_option = dhcpLayer.getOptionData(pcpp::DHCPOPT_DHCP_AGENT_OPTIONS); + auto options_ptr = agent_option.getValue(); + auto agent_option_size = agent_option.getDataSize(); + EXPECT_NE((uintptr_t)options_ptr, NULL); + + uint8_t circuit_id_len = 0; + auto circuit_id_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_CIRCUIT_ID, + circuit_id_len, agent_option_size); + std::string circuit_id((const char*)circuit_id_ptr, circuit_id_len); + + EXPECT_EQ(circuit_id, "cisco:eth12:Vlan10"); + uint8_t remote_id_len = 0; + auto remote_id_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_REMOTE_ID, + remote_id_len, agent_option_size); + + EXPECT_EQ(memcmp(m_config.host_mac_addr.c_str(), remote_id_ptr, 17), 0); + + uint8_t link_sel_len = 0; + auto link_sel_ip_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_LINK_SELECTION, + link_sel_len, agent_option_size); + auto link_sel_ip = *((uint32_t *)link_sel_ip_ptr); + EXPECT_EQ((config.link_address.sin_addr.s_addr & config.link_address_netmask.sin_addr.s_addr), link_sel_ip); + + auto srv_ovr_ride = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_SERVER_OVERRIDE, + link_sel_len, agent_option_size); + auto srv_ip = *((uint32_t *)srv_ovr_ride); + EXPECT_EQ(srv_ip, config.link_address.sin_addr.s_addr); + + uint8_t vrf_len = 0; + uint8_t *vrf_ptr = NULL; + vrf_ptr = decode_tlv((const uint8_t *)options_ptr, OPTION82_SUBOPT_VIRTUAL_SUBNET, + vrf_len, agent_option_size); + + EXPECT_EQ((uintptr_t)vrf_ptr, NULL); +} + +TEST(DHCPRelayTest, to_client) { + pcpp::EthLayer ethLayer(pcpp::MacAddress("00:13:72:25:fa:cd"), pcpp::MacAddress("00:e0:b1:49:39:02")); + std::unordered_map vlans; + + pcpp::IPv4Address srcIp("172.22.178.234"); + pcpp::IPv4Address dstIp("10.10.8.240"); + pcpp::IPv4Layer ipLayer(srcIp, dstIp); + ipLayer.getIPv4Header()->ipId = htobe16(20370); + ipLayer.getIPv4Header()->timeToLive = 128; + + pcpp::UdpLayer udpLayer((uint16_t)67, (uint16_t)67); + + pcpp::MacAddress clientMac(std::string("00:0e:86:11:c0:75")); + pcpp::DhcpLayer dhcpLayer(pcpp::DHCP_OFFER, clientMac); + dhcpLayer.getDhcpHeader()->hops = 1; + dhcpLayer.getDhcpHeader()->gatewayIpAddress = inet_addr("192.168.1.1"); + dhcpLayer.getDhcpHeader()->opCode = 1; + + interface_list.push_back("Ethernet12"); + phy_interface_alias_map["Ethernet12"] = "eth12"; + + relay_config config = {}; + config.phy_interface = "Ethernet12"; + config.vlan = "Vlan10"; + config.link_selection_opt = "enable"; + config.server_id_override_opt = "enable"; + config.link_address.sin_addr.s_addr = inet_addr("192.168.10.10"); + config.link_address_netmask.sin_addr.s_addr = inet_addr("255.255.255.0"); + config.vrf_selection_opt = "enable"; + vlan_vrf_map["Vlan10"] = "Vrf01"; + + m_config.host_mac_addr = "12:32:54:24:95:36"; + vlans["Vlan10"] = config; + encode_relay_option(&dhcpLayer, &config); + + struct ifaddrs *mock_ifaddrs = CreateMockIfaddrs("192.168.1.1", "255.255.255.0", "Vlan100", "192.168.1.2", "Ethernet4"); + EXPECT_GLOBAL_CALL(getifaddrs, getifaddrs(_)).WillOnce(DoAll(testing::SetArgPointee<0>(mock_ifaddrs), Return(0))); + EXPECT_GLOBAL_CALL(freeifaddrs, freeifaddrs(_)).Times(1); + EXPECT_GLOBAL_CALL(send_udp, send_udp(_, _, _, _, _, _)).WillOnce([] + (int sock, uint8_t* hdr, struct sockaddr_in target, uint32_t len, in_addr src_ip, bool use_src_ip) { + pcpp::dhcp_header* dhcp_hdr = (pcpp::dhcp_header*)hdr; + EXPECT_EQ((dhcp_hdr->opCode), 1); + EXPECT_EQ((dhcp_hdr->hops), 1); + EXPECT_EQ((dhcp_hdr->gatewayIpAddress), inet_addr("192.168.1.1")); + return true; + }); + to_client(&dhcpLayer, &vlans, "172.22.178.234"); +} + +TEST(DHCPRelayTest, from_client) { + + pcpp::MacAddress clientMac(std::string("00:0e:86:11:c0:75")); + pcpp::DhcpLayer dhcpLayer(pcpp::DHCP_DISCOVER, clientMac); + dhcpLayer.getDhcpHeader()->hops = 0; + dhcpLayer.getDhcpHeader()->gatewayIpAddress = inet_addr("192.168.1.1"); + dhcpLayer.getDhcpHeader()->opCode = 0; + + interface_list.push_back("Ethernet12"); + phy_interface_alias_map["Ethernet12"] = "eth12"; + + relay_config config = {}; + config.phy_interface = "Ethernet12"; + config.vlan = "Vlan10"; + config.link_selection_opt = "enable"; + config.server_id_override_opt = "enable"; + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("192.168.20.100"); + config.servers_sock = {addr}; + config.servers = {"192.168.20.100"}; + config.link_address.sin_addr.s_addr = inet_addr("192.168.10.10"); + config.link_address_netmask.sin_addr.s_addr = inet_addr("255.255.255.0"); + config.vrf_selection_opt = "enable"; + vlan_vrf_map["Vlan10"] = "Vrf01"; + + m_config.host_mac_addr = "12:32:54:24:95:36"; + encode_relay_option(&dhcpLayer, &config); + + EXPECT_GLOBAL_CALL(send_udp, send_udp(_, _, _, _, _, _)).WillOnce([] + (int sock, uint8_t* hdr, struct sockaddr_in target, uint32_t len, in_addr src_ip, bool use_src_ip) { + pcpp::dhcp_header* dhcp_hdr = (pcpp::dhcp_header*)hdr; + EXPECT_EQ((dhcp_hdr->opCode), 0); + EXPECT_EQ((dhcp_hdr->hops), 1); + EXPECT_EQ((dhcp_hdr->gatewayIpAddress), inet_addr("192.168.1.1")); + return true; + }); + from_client(&dhcpLayer, config); +} diff --git a/dhcp4relay/test/mock_relay.h b/dhcp4relay/test/mock_relay.h new file mode 100644 index 0000000..d492abe --- /dev/null +++ b/dhcp4relay/test/mock_relay.h @@ -0,0 +1,24 @@ +#pragma once + +#include "../src/dhcp4relay.h" +#include "../src/dhcp4relay_mgr.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "../../gmock_global/include/gmock-global/gmock-global.h" +#include +#include + +extern struct event_base *base; +extern struct event *ev_sigint; +extern struct event *ev_sigterm; +extern std::unordered_map vlan_map; +extern std::unordered_map vlan_vrf_map; +extern swss::Select swssSelect; +extern std::unordered_map vrf_sock_map; +extern std::unordered_map phy_interface_alias_map; +extern std::vector interface_list; +extern metadata_config m_config; +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; diff --git a/dhcp4relay/test/mock_relay_stats.cpp b/dhcp4relay/test/mock_relay_stats.cpp new file mode 100644 index 0000000..f4bd6e1 --- /dev/null +++ b/dhcp4relay/test/mock_relay_stats.cpp @@ -0,0 +1,230 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "mock_relay.h" +#include "../src/dhcp4relay_stats.h" + +using namespace swss; + +// Test fixture +class DHCPCounter_table_test : public ::testing::Test { +protected: + std::unique_ptr counter_table; + + void SetUp() override { + counter_table = std::make_unique(); + } + + void TearDown() override { + counter_table.reset(); + } +}; + +// Test the delta calculation function +TEST(Calculate_delta_test, Calculates_delta_correctly) { + // Normal case + EXPECT_EQ(calculate_delta(10, 5), 5); + + // Zero delta + EXPECT_EQ(calculate_delta(5, 5), 0); + + // Handle overflow case + uint64_t max_val = std::numeric_limits::max(); + uint64_t old_val = max_val - 5; + uint64_t new_val = 10; + + EXPECT_EQ(calculate_delta(new_val, old_val), 16); +} + +// Test interface initialization +TEST_F(DHCPCounter_table_test, Initialize_interface) { + const std::string interface = "Ethernet0"; + + // Initialize the interface + counter_table->initialize_interface(interface); + + // Increment counters + counter_table->increment_counter(interface, "RX", DHCPv4_MESSAGE_TYPE_DISCOVER); + counter_table->increment_counter(interface, "TX", DHCPv4_MESSAGE_TYPE_OFFER); + + // Verify Incremented counters from table + std::unordered_map interfaces_cntr_table = counter_table->get_counters_data(); + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_DISCOVER)->second], 1); + EXPECT_EQ(interfaces_cntr_table[interface].TX[counter_map.find(DHCPv4_MESSAGE_TYPE_OFFER)->second], 1); + + // If initialization failed, the above would likely crash + SUCCEED(); +} + +// Test counter incrementation +TEST_F(DHCPCounter_table_test, Increment_counter) { + const std::string interface = "Ethernet0"; + + // Initialize and increment counters + counter_table->initialize_interface(interface); + + // Increment RX counter multiple times + for (int i = 0; i < 5; i++) { + counter_table->increment_counter(interface, "RX", DHCPv4_MESSAGE_TYPE_DISCOVER); + } + + // Increment TX counter once + counter_table->increment_counter(interface, "TX", DHCPv4_MESSAGE_TYPE_ACK); + + // Verify Incremented counters from table + std::unordered_map interfaces_cntr_table = counter_table->get_counters_data(); + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_DISCOVER)->second], 5); + EXPECT_EQ(interfaces_cntr_table[interface].TX[counter_map.find(DHCPv4_MESSAGE_TYPE_ACK)->second], 1); + + // Negative case - other counters should NOT have incremented + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_REQUEST)->second], 0); + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_OFFER)->second], 0); + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_ACK)->second], 0); + EXPECT_EQ(interfaces_cntr_table[interface].TX[counter_map.find(DHCPv4_MESSAGE_TYPE_DECLINE)->second], 0); + EXPECT_EQ(interfaces_cntr_table[interface].TX[counter_map.find(DHCPv4_MESSAGE_TYPE_INFORM)->second], 0); + + SUCCEED(); +} + +// Test with uninitialized interface +TEST_F(DHCPCounter_table_test, Auto_initialize_interface) { + const std::string interface = "Ethernet1"; + + // Try to increment without explicitly initializing + counter_table->increment_counter(interface, "RX", DHCPv4_MESSAGE_TYPE_DISCOVER); + + // If auto-initialization works, this should succeed + counter_table->increment_counter(interface, "TX", DHCPv4_MESSAGE_TYPE_ACK); + + // Verify Incremented counters from table + std::unordered_map interfaces_cntr_table = counter_table->get_counters_data(); + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_DISCOVER)->second], 1); + EXPECT_EQ(interfaces_cntr_table[interface].TX[counter_map.find(DHCPv4_MESSAGE_TYPE_ACK)->second], 1); + + SUCCEED(); +} + +// Test interface removal +TEST_F(DHCPCounter_table_test, Remove_interface) { + const std::string interface = "Ethernet0"; + + // Initialize and increment counters + counter_table->initialize_interface(interface); + counter_table->increment_counter(interface, "RX", DHCPv4_MESSAGE_TYPE_DISCOVER); + + // Verify Incremented counters from table + std::unordered_map interfaces_cntr_table = counter_table->get_counters_data(); + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_DISCOVER)->second], 1); + + // Remove the interface + counter_table->remove_interface(interface); + + // Now add it again - if removal worked, this should reinitialize from scratch + // and should not crash + counter_table->initialize_interface(interface); + + // Increment the same counter and verify + counter_table->increment_counter(interface, "RX", DHCPv4_MESSAGE_TYPE_DISCOVER); + // verify counter + interfaces_cntr_table = counter_table->get_counters_data(); + EXPECT_EQ(interfaces_cntr_table[interface].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_DISCOVER)->second], 1); + + SUCCEED(); +} + +// Test starting and stopping the DB update thread +TEST_F(DHCPCounter_table_test, Start_stop_db_updates) { + // Start the DB update thread + counter_table->start_db_updates(); + + // Sleep briefly to allow thread to execute + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Stop the thread + counter_table->stop_db_updates(); + + // If the start/stop mechanisms work correctly, this will complete without hanging + SUCCEED(); +} + +// Integration test for database update loop +TEST_F(DHCPCounter_table_test, DBUpdate_loop_integration) { + const std::string interface = "Ethernet0"; + + // Initialize and increment counters + counter_table->initialize_interface(interface); + + // Add some counter increments + counter_table->increment_counter(interface, "RX", DHCPv4_MESSAGE_TYPE_DISCOVER); + counter_table->increment_counter(interface, "RX", DHCPv4_MESSAGE_TYPE_REQUEST); + counter_table->increment_counter(interface, "TX", DHCPv4_MESSAGE_TYPE_OFFER); + counter_table->increment_counter(interface, "TX", DHCPv4_MESSAGE_TYPE_ACK); + + // Start DB updates + counter_table->start_db_updates(); + + // Let the update thread run briefly (less than the normal interval for test speed) + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Stop the updates + counter_table->stop_db_updates(); + + std::shared_ptr cntrs_db = std::make_shared ("COUNTERS_DB", 0); + swss::Table cntr_table(cntrs_db.get(), COUNTERS_DHCPV4_TABLE); + std::vector existing_rx_fields; + std::vector existing_tx_fields; + cntr_table.get(interface+"|RX", existing_rx_fields); + cntr_table.get(interface+"|TX", existing_tx_fields); + + //Verify Incremented Rx fields + for (const auto& field : existing_rx_fields) { + if ( (fvField(field)) == "Discover" || (fvField(field)) == "Request") { + EXPECT_EQ(std::stoi(fvValue(field)), 1); + } + } + //Verify Incremented Tx fields + for (const auto& field : existing_tx_fields) { + if ( (fvField(field)) == "Offer" || (fvField(field)) == "Acknowledge") { + EXPECT_EQ(std::stoi(fvValue(field)), 1); + } + } + + SUCCEED(); +} + +// Test for handling multiple interfaces +TEST_F(DHCPCounter_table_test, Multiple_interfaces) { + const std::vector interfaces = {"Ethernet0", "Ethernet1", "Ethernet2"}; + + // Initialize multiple interfaces + for (const auto& intf : interfaces) { + counter_table->initialize_interface(intf); + } + + // Increment counters for different interfaces + counter_table->increment_counter(interfaces[0], "RX", DHCPv4_MESSAGE_TYPE_DISCOVER); + counter_table->increment_counter(interfaces[1], "RX", DHCPv4_MESSAGE_TYPE_REQUEST); + counter_table->increment_counter(interfaces[2], "TX", DHCPv4_MESSAGE_TYPE_ACK); + + // Verify Incremented counters from table + std::unordered_map interfaces_cntr_table = counter_table->get_counters_data(); + EXPECT_EQ(interfaces_cntr_table[interfaces[0]].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_DISCOVER)->second], 1); + EXPECT_EQ(interfaces_cntr_table[interfaces[1]].RX[counter_map.find(DHCPv4_MESSAGE_TYPE_REQUEST)->second], 1); + EXPECT_EQ(interfaces_cntr_table[interfaces[2]].TX[counter_map.find(DHCPv4_MESSAGE_TYPE_ACK)->second], 1); + + // Remove one interface + counter_table->remove_interface(interfaces[1]); + + // Try incrementing the removed interface (should auto-initialize) + counter_table->increment_counter(interfaces[1], "TX", DHCPv4_MESSAGE_TYPE_OFFER); + // Verify incremented counter + interfaces_cntr_table = counter_table->get_counters_data(); + EXPECT_EQ(interfaces_cntr_table[interfaces[1]].TX[counter_map.find(DHCPv4_MESSAGE_TYPE_OFFER)->second], 1); + + SUCCEED(); +} diff --git a/dhcp4relay/test/mock_table.cpp b/dhcp4relay/test/mock_table.cpp new file mode 100644 index 0000000..7dc77d4 --- /dev/null +++ b/dhcp4relay/test/mock_table.cpp @@ -0,0 +1,183 @@ +#include "table.h" +#include "producerstatetable.h" +#include "producertable.h" +#include +#include + +using TableDataT = std::map>; +using TablesT = std::map; + +namespace testing_db +{ + + TableDataT gTableData; + TablesT gTables; + std::map gDB; + + void reset() + { + gDB.clear(); + } +} + +namespace swss +{ + + using namespace testing_db; + + void merge_values(std::vector &existing_values, const std::vector &values) + { + std::vector new_values(values); + std::set field_set; + for (auto &value : values) + { + field_set.insert(fvField(value)); + } + for (auto &value : existing_values) + { + auto &field = fvField(value); + if (field_set.find(field) != field_set.end()) + { + continue; + } + new_values.push_back(value); + } + existing_values.swap(new_values); + } + + bool _hget(int dbId, const std::string &tableName, const std::string &key, const std::string &field, std::string &value) + { + auto table = gDB[dbId][tableName]; + if (table.find(key) == table.end()) + { + return false; + } + + for (const auto &it : table[key]) + { + if (it.first == field) + { + value = it.second; + return true; + } + } + + return false; + } + + bool Table::get(const std::string &key, std::vector &ovalues) + { + auto table = gDB[m_pipe->getDbId()][getTableName()]; + if (table.find(key) == table.end()) + { + return false; + } + + ovalues = table[key]; + return true; + } + + bool Table::hget(const std::string &key, const std::string &field, std::string &value) + { + return _hget(m_pipe->getDbId(), getTableName(), key, field, value); + } + + void Table::set(const std::string &key, + const std::vector &values, + const std::string &op, + const std::string &prefix) + { + auto &table = gDB[m_pipe->getDbId()][getTableName()]; + auto iter = table.find(key); + if (iter == table.end()) + { + table[key] = values; + } + else + { + merge_values(iter->second, values); + } + } + + void Table::getKeys(std::vector &keys) + { + keys.clear(); + auto table = gDB[m_pipe->getDbId()][getTableName()]; + for (const auto &it : table) + { + keys.push_back(it.first); + } + } + + void Table::del(const std::string &key, const std::string& /* op */, const std::string& /*prefix*/) + { + auto table = gDB[m_pipe->getDbId()].find(getTableName()); + if (table != gDB[m_pipe->getDbId()].end()){ + table->second.erase(key); + } + } + + void ProducerStateTable::set(const std::string &key, + const std::vector &values, + const std::string &op, + const std::string &prefix) + { + auto &table = gDB[m_pipe->getDbId()][getTableName()]; + auto iter = table.find(key); + if (iter == table.end()) + { + table[key] = values; + } + else + { + merge_values(iter->second, values); + } + } + + void ProducerStateTable::del(const std::string &key, + const std::string &op, + const std::string &prefix) + { + auto &table = gDB[m_pipe->getDbId()][getTableName()]; + table.erase(key); + } + + std::shared_ptr DBConnector::hget(const std::string &key, const std::string &field) + { + std::string value; + if (_hget(getDbId(), key, "", field, value)) + { + std::shared_ptr ptr(new std::string(value)); + return ptr; + } + else + { + return std::shared_ptr(NULL); + } + } + + void ProducerTable::set(const std::string &key, + const std::vector &values, + const std::string &op, + const std::string &prefix) + { + auto &table = gDB[m_pipe->getDbId()][getTableName()]; + auto iter = table.find(key); + if (iter == table.end()) + { + table[key] = values; + } + else + { + merge_values(iter->second, values); + } + } + + void ProducerTable::del(const std::string &key, + const std::string &op, + const std::string &prefix) + { + auto &table = gDB[m_pipe->getDbId()][getTableName()]; + table.erase(key); + } +} diff --git a/dhcp4relay/test/mock_table.h b/dhcp4relay/test/mock_table.h new file mode 100644 index 0000000..88aed22 --- /dev/null +++ b/dhcp4relay/test/mock_table.h @@ -0,0 +1,8 @@ +#pragma once + +#include "table.h" + +namespace testing_db +{ + void reset(); +} diff --git a/dhcp4relay/test/subdir.mk b/dhcp4relay/test/subdir.mk new file mode 100644 index 0000000..51ba58a --- /dev/null +++ b/dhcp4relay/test/subdir.mk @@ -0,0 +1,13 @@ +TEST_SRCS += \ +test/main.cpp \ +test/mock_relay.cpp \ +src/dhcp4relay_mgr.cpp \ +src/dhcp4relay_stats.cpp \ +src/dhcp4relay.cpp \ +src/dhcp4_sender.cpp \ +test/mock_dbconnector.cpp \ +test/mock_table.cpp \ +test/mock_consumerstatetable.cpp \ +test/mock_hiredis.cpp \ +test/mock_redisreply.cpp \ +test/mock_relay_stats.cpp