diff --git a/CMakeLists.txt b/CMakeLists.txt index ffeabfee..dbdd2422 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,7 +77,14 @@ endif(NOT TARGET fmt) find_package(Protobuf REQUIRED) + +find_package(Microsoft.GSL REQUIRED) +if(Microsoft.GSL_FOUND) + include_directories(${Microsoft.GSL_INCLUDE_DIRS}) +endif() + set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + if(BUILD_TESTS OR BUILD_BPF_TESTS) include(FetchContent) FetchContent_Declare(googletest URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip) diff --git a/conanfile.txt b/conanfile.txt index ac15d13f..94739222 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,6 +1,7 @@ [requires] protobuf/3.21.9 protobuf-c/1.4.1 +ms-gsl/4.0.0 fmt/10.1.1 spdlog/1.12.0 gtest/1.14.0 diff --git a/libebpfdiscovery/CMakeLists.txt b/libebpfdiscovery/CMakeLists.txt index 6888c764..6f5c0ff0 100644 --- a/libebpfdiscovery/CMakeLists.txt +++ b/libebpfdiscovery/CMakeLists.txt @@ -3,6 +3,8 @@ list( SOURCES src/Config.cpp src/Discovery.cpp + src/IpAddressChecker.cpp + src/NetlinkCalls.cpp src/Session.cpp src/StringFunctions.cpp ) @@ -11,6 +13,7 @@ set(TARGET ebpfdiscovery) add_library(${TARGET} STATIC ${SOURCES}) target_include_directories(${TARGET} PRIVATE src PUBLIC headers) + target_link_libraries(${TARGET} bpfload) target_link_libraries(${TARGET} ebpfdiscoveryshared) target_link_libraries(${TARGET} ebpfdiscoveryskel) @@ -19,11 +22,11 @@ target_link_libraries(${TARGET} httpparser) target_link_libraries(${TARGET} ebpfdiscoveryproto) if(BUILD_TESTS) - list(APPEND TEST_SOURCES test/StringFunctionsTest.cpp test/LRUCacheTest.cpp) + list(APPEND TEST_SOURCES test/StringFunctionsTest.cpp test/LRUCacheTest.cpp test/IpAddressCheckerTest.cpp) set(TEST_TARGET test${TARGET}) add_executable(${TEST_TARGET} ${TEST_SOURCES}) - target_link_libraries(${TEST_TARGET} GTest::gtest_main ${TARGET}) + target_link_libraries(${TEST_TARGET} GTest::gtest_main GTest::gmock_main ${TARGET}) target_include_directories(${TEST_TARGET} PRIVATE src) gtest_discover_tests(${TEST_TARGET}) endif() diff --git a/libebpfdiscovery/headers/ebpfdiscovery/IpAddressChecker.h b/libebpfdiscovery/headers/ebpfdiscovery/IpAddressChecker.h new file mode 100644 index 00000000..c55408a6 --- /dev/null +++ b/libebpfdiscovery/headers/ebpfdiscovery/IpAddressChecker.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include "ebpfdiscovery/NetlinkCalls.h" +#include +#include + + +namespace ebpfdiscovery { + +using IPv4int = uint32_t; + +struct IpIfce { + std::vector ip; + std::vector broadcast; + uint32_t mask; + int index; + bool isLocalBridge; +}; + +class IpAddressChecker { + std::vector interfaces; + std::vector::iterator bridgeEnd = interfaces.end(); + const NetlinkCalls& netlink; + + bool readAllIpAddrs(); + bool markLocalBridges(); + bool isLoopback(const IpIfce&); + void addIpIfce(IpIfce&& ifce); + void markBridge(int idx); +protected: + void moveBridges(); +public: + IpAddressChecker(const NetlinkCalls &calls); + IpAddressChecker(std::initializer_list config, const NetlinkCalls &calls); + bool isAddressExternalLocal(IPv4int addr); + bool readNetworks(); +}; +} // namespace ebpfdiscovery + diff --git a/libebpfdiscovery/headers/ebpfdiscovery/NetlinkCalls.h b/libebpfdiscovery/headers/ebpfdiscovery/NetlinkCalls.h new file mode 100644 index 00000000..3b114cbb --- /dev/null +++ b/libebpfdiscovery/headers/ebpfdiscovery/NetlinkCalls.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once +#include +#include + +struct sockaddr_nl; + +namespace ebpfdiscovery { + +class NetlinkCalls { +public: + virtual int sendIpAddrRequest(int fd, sockaddr_nl* dst, int domain) const; + virtual int sendBridgesRequest(int fd, sockaddr_nl* dst, int domain) const; + virtual int receive(int fd, sockaddr_nl* dst, void* buf, size_t len) const; +}; +} // namespace ebpfdiscovery + diff --git a/libebpfdiscovery/src/IpAddressChecker.cpp b/libebpfdiscovery/src/IpAddressChecker.cpp new file mode 100644 index 00000000..e75ff06e --- /dev/null +++ b/libebpfdiscovery/src/IpAddressChecker.cpp @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "ebpfdiscovery/IpAddressChecker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr uint32_t BUFLEN{4096}; +static constexpr uint32_t IP_CLASS_C{0x0000a8c0}; // 192.168.*.* +static constexpr uint32_t MASK_CLASS_C{0x0000ffff}; +static constexpr uint32_t IP_CLASS_B{0x000010ac}; // 172.16-31.*.* +static constexpr uint32_t MASK_CLASS_B{0x0000f0ff}; +static constexpr uint32_t IP_CLASS_A{0x0000000a}; // 10.*.*.* +static constexpr uint32_t MASK_CLASS_A{0x000000ff}; +static constexpr uint32_t IP_LINK_LOCAL{0x0000fea9}; // 169.254.*.* +static constexpr uint32_t MASK_LINK_LOCAL{0x0000ffff}; +static constexpr uint32_t IP_LOOPBACK{0x0000007f}; // 127.0.*.* +static constexpr uint32_t MASK_LOOPBACK{0x00ffffff}; + +static void logErrorFromErrno(std::string_view prefix) { + std::cout << prefix << ": " << strerror(errno) << "\n"; +} + +static ebpfdiscovery::IpIfce parseIfceIPv4(void* data, size_t len) { + ebpfdiscovery::IpIfce ifce{}; + ifaddrmsg* ifa = reinterpret_cast(data); + ifce.index = ifa->ifa_index; + ifce.mask = htonl(-1 << (32 - ifa->ifa_prefixlen)); + rtattr* rta = reinterpret_cast(IFA_RTA(data)); + + for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { + in_addr* addr = reinterpret_cast(RTA_DATA(rta)); + + if (rta->rta_type == IFA_ADDRESS) { + ifce.ip.push_back(addr->s_addr); + } + + if (rta->rta_type == IFA_BROADCAST) { + ifce.broadcast.push_back(addr->s_addr); + } + } + + return ifce; +} + +static ebpfdiscovery::IpIfce parseIfce(void* data, size_t len) { + if (reinterpret_cast(data)->ifa_family != AF_INET) { + return {}; + } + + return parseIfceIPv4(data, len); +} + +static int getIfIndex(void* data) { + ifinfomsg* ifa = reinterpret_cast(data); + return ifa->ifi_index; +} + +namespace ebpfdiscovery { + +IpAddressChecker::IpAddressChecker(std::initializer_list config, const NetlinkCalls &calls) :netlink(calls) { + interfaces.insert(interfaces.end(), config.begin(), config.end()); +} + +IpAddressChecker::IpAddressChecker(const NetlinkCalls &calls) :netlink(calls) { +} + +bool IpAddressChecker::readNetworks() { + const bool ret = readAllIpAddrs(); + if (markLocalBridges()) { + moveBridges(); + } else { + bridgeEnd = interfaces.end(); + } + + return ret; +} + +void IpAddressChecker::moveBridges() { + bridgeEnd = std::partition(interfaces.begin(), interfaces.end(), [](const auto& it) { return it.isLocalBridge; }); +} + +template +static uint32_t parseNlMsg(void* buf, size_t len, P parse) { + const nlmsghdr* nl = reinterpret_cast(buf); + + for (; NLMSG_OK(nl, len) && nl->nlmsg_type != NLMSG_DONE; nl = NLMSG_NEXT(nl, len)) { + if (nl->nlmsg_type == NLMSG_ERROR) { + return -1; + } + + if (nl->nlmsg_type == RTM_NEWADDR || nl->nlmsg_type == RTM_NEWLINK) { + parse(NLMSG_DATA(nl), IFA_PAYLOAD(nl)); + continue; + } + } + + return nl->nlmsg_type; +} + +template +static bool handleNetlink(S send, R receive, P parse, int domain) { + int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); + if (fd < 0) { + logErrorFromErrno("socket"); + return false; + } + + auto const _ = gsl::finally([fd]() { ::close(fd); }); + sockaddr_nl sa{}; + sa.nl_family = AF_NETLINK; + + int len = send(fd, &sa, domain); + if (len <= 0) { + logErrorFromErrno("send"); + return false; + } + + uint32_t nlMsgType; + do { + std::array buf{}; + len = receive(fd, &sa, buf.data(), BUFLEN); + if (len <= 0) { + logErrorFromErrno("receive"); + break; + } + + nlMsgType = parseNlMsg(buf.data(), len, parse); + } while (nlMsgType != NLMSG_DONE && nlMsgType != NLMSG_ERROR); + + return true; +} + +bool IpAddressChecker::readAllIpAddrs() { + return handleNetlink( + [this](int fd, sockaddr_nl* sa, int domain) { return netlink.sendIpAddrRequest(fd, sa, AF_INET); }, + [this](int fd, sockaddr_nl* dst, void* buf, size_t len) { return netlink.receive(fd, dst, buf, len); }, + [this](void* buf, size_t len) { addIpIfce(parseIfce(buf, len)); }, AF_INET); +} + +bool IpAddressChecker::markLocalBridges() { + return handleNetlink( + [this](int fd, sockaddr_nl* sa, int domain) { return netlink.sendBridgesRequest(fd, sa, AF_INET); }, + [this](int fd, sockaddr_nl* dst, void* buf, size_t len) { return netlink.receive(fd, dst, buf, len); }, + [this](void* buf, size_t len) { markBridge(getIfIndex(buf)); }, AF_INET); +} + +void IpAddressChecker::addIpIfce(IpIfce&& ifce) { + if(!isLoopback(ifce)){ + interfaces.push_back(ifce); + } +} + +void IpAddressChecker::markBridge(int idx) { + auto ifc = std::find_if(interfaces.begin(), interfaces.end(), [idx](const auto& it) { return it.index == idx; }); + if (ifc == interfaces.end()) { + return; + } + + ifc->isLocalBridge = true; +} + +bool IpAddressChecker::isLoopback(const IpIfce& ifce) { + + return std::all_of(ifce.ip.begin(), ifce.ip.end(), [](const auto& ipv4) { + return ((ipv4 & MASK_LOOPBACK) == IP_LOOPBACK); + }); +} + +bool IpAddressChecker::isAddressExternalLocal(IPv4int addr) { + const bool isPublic = ((addr & MASK_CLASS_A) != IP_CLASS_A) && ((addr & MASK_CLASS_B) != IP_CLASS_B) && ((addr & MASK_CLASS_C) != IP_CLASS_C); + + if (isPublic) { + return false; + } + + if ((addr & MASK_LINK_LOCAL) == IP_LINK_LOCAL) { + return false; + } + + const bool bridgeRelated = std::any_of(interfaces.begin(), bridgeEnd, [addr](const auto& it) { + return std::any_of(it.ip.begin(), it.ip.end(), [addr, mask = it.mask](const auto& ip) { return (addr & mask) == (ip & mask); }); + }); + + if (bridgeRelated) { + return false; + } + + return true; +} +} // namespace ebpfdiscovery diff --git a/libebpfdiscovery/src/NetlinkCalls.cpp b/libebpfdiscovery/src/NetlinkCalls.cpp new file mode 100644 index 00000000..a6c91057 --- /dev/null +++ b/libebpfdiscovery/src/NetlinkCalls.cpp @@ -0,0 +1,84 @@ +#include "ebpfdiscovery/NetlinkCalls.h" +#include +#include +#include +#include +#include + +namespace ebpfdiscovery { + +static constexpr uint32_t BUFFLEN{4096}; + +static void addNetlinkMsg(nlmsghdr* nh, int type, const void* data, int dataLen) { + struct rtattr* rta; + int rta_length = RTA_LENGTH(dataLen); + + rta = reinterpret_cast((char*)nh + NLMSG_ALIGN(nh->nlmsg_len)); + + rta->rta_type = type; + rta->rta_len = rta_length; + nh->nlmsg_len = NLMSG_ALIGN(nh->nlmsg_len) + RTA_ALIGN(rta_length); + + memcpy(RTA_DATA(rta), data, dataLen); +} + +int NetlinkCalls::sendIpAddrRequest(int fd, sockaddr_nl* dst, int domain) const { + std::array buf{}; + + nlmsghdr* nl; + nl = reinterpret_cast(buf.data()); + nl->nlmsg_len = NLMSG_LENGTH(sizeof(ifaddrmsg)); + nl->nlmsg_type = RTM_GETADDR; + nl->nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT; + + ifaddrmsg* ifa; + ifa = reinterpret_cast(NLMSG_DATA(nl)); + ifa->ifa_family = domain; // ipv4 or ipv6 + + iovec iov = {nl, nl->nlmsg_len}; + msghdr msg = {dst, sizeof(*dst), &iov, 1, NULL, 0, 0}; + + return sendmsg(fd, &msg, 0); +} + +int NetlinkCalls::sendBridgesRequest(int fd, sockaddr_nl* dst, int domain) const { + struct { + struct nlmsghdr n; + struct ifinfomsg i; + char _[1024]; // space for rtattr array + } r{}; + + const char* dev_type = "bridge"; + + r.n.nlmsg_len = NLMSG_LENGTH(sizeof(ifinfomsg)); + r.n.nlmsg_type = RTM_GETLINK; + r.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP; + r.i.ifi_family = AF_PACKET; + r.n.nlmsg_pid = 0; + r.n.nlmsg_seq = 0; + + rtattr* linkinfo = reinterpret_cast((char*)&r.n + NLMSG_ALIGN(r.n.nlmsg_len)); + addNetlinkMsg(&r.n, IFLA_LINKINFO, NULL, 0); + addNetlinkMsg(&r.n, IFLA_INFO_KIND, dev_type, strlen(dev_type) + 1); + linkinfo->rta_len = (int)(reinterpret_cast(&r.n) + NLMSG_ALIGN(r.n.nlmsg_len) - reinterpret_cast(linkinfo)); + + iovec iov = {&r.n, r.n.nlmsg_len}; + msghdr msg = {dst, sizeof(*dst), &iov, 1, NULL, 0, 0}; + + return sendmsg(fd, &msg, 0); +} + +int NetlinkCalls::receive(int fd, sockaddr_nl* dst, void* buf, size_t len) const { + iovec iov; + msghdr msg{}; + iov.iov_base = buf; + iov.iov_len = len; + + msg.msg_name = dst; + msg.msg_namelen = sizeof(*dst); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + return recvmsg(fd, &msg, 0); +} +} // namespace ebpfdiscovery diff --git a/libebpfdiscovery/test/IpAddressCheckerTest.cpp b/libebpfdiscovery/test/IpAddressCheckerTest.cpp new file mode 100644 index 00000000..d248cfd7 --- /dev/null +++ b/libebpfdiscovery/test/IpAddressCheckerTest.cpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "ebpfdiscovery/IpAddressChecker.h" +#include +#include +#include + +using namespace ebpfdiscovery; +using namespace ::testing; + +class NetlinkCallsMock : public NetlinkCalls { +public: + MOCK_METHOD(int, sendIpAddrRequest, (int fd, sockaddr_nl* dst, int domain), (const, override)); + MOCK_METHOD(int, sendBridgesRequest, (int fd, sockaddr_nl* dst, int domain), (const, override)); + MOCK_METHOD(int, receive, (int fd, sockaddr_nl* dst, void* buf, size_t len), (const, override)); +}; + +class IpAddressCheckerTest : public IpAddressChecker { +protected: +public: + using IpAddressChecker::IpAddressChecker; + IpAddressCheckerTest(std::initializer_list config, const NetlinkCallsMock& mc) : IpAddressChecker(config, mc) { + moveBridges(); + } +}; + +TEST(TestAddressChecker, LocalBridgeIp) { + const NetlinkCallsMock netlinkMock; + IpAddressCheckerTest u( + {{{inet_addr("10.2.4.5")}, {}, 0x0000ffff, 0, true}, {{inet_addr("10.7.4.5")}, {}, 0x0000ffff, 0, false}}, netlinkMock); + EXPECT_FALSE(u.isAddressExternalLocal(inet_addr("10.2.6.5"))); +} + +TEST(TestAddressChecker, NoReadIfSendFailed) { + const NetlinkCallsMock netlinkMock; + EXPECT_CALL(netlinkMock, sendIpAddrRequest).WillOnce(Return(-1)); + EXPECT_CALL(netlinkMock, sendBridgesRequest).WillOnce(Return(-1)); + IpAddressCheckerTest u({}, netlinkMock); + u.readNetworks(); +} + +TEST(TestAddressChecker, ReadAfterSuccessfullSend) { + const NetlinkCallsMock netlinkMock; + EXPECT_CALL(netlinkMock, sendIpAddrRequest).WillOnce(Return(1)); + EXPECT_CALL(netlinkMock, sendBridgesRequest).WillOnce(Return(1)); + EXPECT_CALL(netlinkMock, receive).Times(2).WillRepeatedly(Return(0)); + IpAddressCheckerTest u({}, netlinkMock); + u.readNetworks(); +} + +TEST(TestAddressChecker, ReadUntilGreaterThan0) { + const NetlinkCallsMock netlinkMock; + EXPECT_CALL(netlinkMock, sendIpAddrRequest).WillOnce(Return(1)); + EXPECT_CALL(netlinkMock, sendBridgesRequest).WillOnce(Return(1)); + EXPECT_CALL(netlinkMock, receive).WillOnce(Return(1)).WillOnce(Return(0)).WillOnce(Return(1)).WillOnce(Return(0)); + IpAddressCheckerTest u({}, netlinkMock); + u.readNetworks(); +} + +TEST(TestAddressChecker, NOTLocalBridgeIp) { + const NetlinkCallsMock netlinkMock; + IpAddressCheckerTest u({{{inet_addr("10.2.6.5")}, {}, 0x0000ffff, 0, true}}, netlinkMock); + EXPECT_TRUE(u.isAddressExternalLocal(inet_addr("10.3.34.2"))); +} + +TEST(TestAddressChecker, SimpleClassATest) { + NetlinkCalls calls; + IpAddressCheckerTest u(calls); + EXPECT_TRUE(u.isAddressExternalLocal(inet_addr("192.168.1.2"))); +} + +TEST(TestAddressChecker, SimpleClassBTest) { + NetlinkCalls calls; + IpAddressCheckerTest u(calls); + EXPECT_TRUE(u.isAddressExternalLocal(inet_addr("172.20.21.2"))); +} + +TEST(TestAddressChecker, SimpleClassCtest) { + NetlinkCalls calls; + IpAddressCheckerTest u(calls); + EXPECT_TRUE(u.isAddressExternalLocal(inet_addr("10.2.4.5"))); +} + +TEST(TestAddressChecker, SimpleLinkLocal) { + NetlinkCalls calls; + IpAddressCheckerTest u(calls); + EXPECT_FALSE(u.isAddressExternalLocal(inet_addr("169.254.76.6"))); +} + +TEST(TestAddressChecker, SimplePublicIp) { + NetlinkCalls calls; + IpAddressCheckerTest u(calls); + EXPECT_FALSE(u.isAddressExternalLocal(inet_addr("170.254.76.6"))); +}