Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement-handling-of-ipv6-addresses-reserved-ranges #84

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ Discovered services are popped from the program in a specified time interval, se
* clang >= 17
* libelf-dev

Add a new remote:
```
conan remote add conancenter https://center.conan.io
```

**Build**

```
Expand Down
15 changes: 15 additions & 0 deletions libservice/headers/service/IpAddressNetlinkChecker.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

#pragma once

#include <string>
#include <netinet/in.h>
#include <linux/netfilter.h>
#include <optional>

#include "IpAddressChecker.h"
#include "NetlinkCalls.h"

Expand All @@ -29,7 +34,17 @@ class IpAddressNetlinkChecker : public IpAddressChecker {

bool isV6AddressExternal(const in6_addr& addr) const override;

struct ipv6Range {
std::string ipv6Address;
int prefixLength;
};
private:
ipv6Range parseIpv6Range(const std::string& range) const;
bool isInRange(const in6_addr& addr, const std::string& range) const;
bool checkSubnet(const in6_addr& addrToCheck, const in6_addr& interfaceIpv6Addr, const in6_addr& interfaceMask) const;
bool ipv6AddressContainsMappedIpv4Address(const in6_addr& addr) const;
std::optional<IPv4int> getMappedIPv4Addr(const in6_addr& addr) const;

void readNetworks();

void printNetworkInterfacesInfo();
Expand Down
7 changes: 7 additions & 0 deletions libservice/headers/service/NetlinkCalls.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <stddef.h>
#include <unordered_map>
#include <vector>
#include <netinet/in.h>

struct sockaddr_nl;

Expand All @@ -41,7 +42,13 @@ class NetlinkCalls {
public:
virtual ~NetlinkCalls() = default;

struct Ipv6Interface {
lnarolski marked this conversation as resolved.
Show resolved Hide resolved
in6_addr interfaceIpv6Addr;
in6_addr interfaceMask;
};

virtual IpInterfaces collectIpInterfaces() const;
virtual std::vector<Ipv6Interface> collectIpv6Interfaces() const;
virtual BridgeIndices collectBridgeIndices() const;
};

Expand Down
76 changes: 73 additions & 3 deletions libservice/src/IpAddressNetlinkChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <boost/algorithm/string/join.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <iostream>
#include <ifaddrs.h>

namespace service {

Expand Down Expand Up @@ -115,14 +116,21 @@ bool IpAddressNetlinkChecker::isV4AddressExternal(IPv4int addr) const {
return true;
}

static bool ipv6AddressContainsMappedIpv4Address(const in6_addr& addr) {
bool IpAddressNetlinkChecker::ipv6AddressContainsMappedIpv4Address(const in6_addr& addr) const {
for (const auto& internalRange : {"::ffff:0:0:0/96", "64:ff9b::/96"}) {
if (isInRange(addr, internalRange)) {
lnarolski marked this conversation as resolved.
Show resolved Hide resolved
return true;
pawsten marked this conversation as resolved.
Show resolved Hide resolved
}
}

lnarolski marked this conversation as resolved.
Show resolved Hide resolved
if (!std::all_of(addr.s6_addr, addr.s6_addr + 9, [](auto byte) { return byte == 0; })) {
return false;
}

return (addr.s6_addr[10] == 0xFF && addr.s6_addr[11] == 0xFF);
}

static std::optional<IPv4int> getMappedIPv4Addr(const in6_addr& addr) {
std::optional<IPv4int> IpAddressNetlinkChecker::getMappedIPv4Addr(const in6_addr& addr) const {
if (!ipv6AddressContainsMappedIpv4Address(addr)) {
return std::nullopt;
}
Expand All @@ -131,11 +139,73 @@ static std::optional<IPv4int> getMappedIPv4Addr(const in6_addr& addr) {
return ipv4Binary;
}

IpAddressNetlinkChecker::ipv6Range IpAddressNetlinkChecker::parseIpv6Range(const std::string& range) const {
pawsten marked this conversation as resolved.
Show resolved Hide resolved
ipv6Range rangeStruct;
const auto slashPos{range.find('/')};
rangeStruct.ipv6Address = range.substr(0, slashPos);
rangeStruct.prefixLength = slashPos != std::string::npos ? std::stoi(range.substr(slashPos + 1)) : 0;
return rangeStruct;
}

bool IpAddressNetlinkChecker::isInRange(const in6_addr& addr, const std::string& range) const {
auto rangeStruct{IpAddressNetlinkChecker::parseIpv6Range(range)};

in6_addr rangeIpv6Address{};
inet_pton(AF_INET6, rangeStruct.ipv6Address.c_str(), &rangeIpv6Address);

// Create mask
in6_addr mask{};
for (size_t i = 0; i < sizeof(mask.s6_addr); i++) {
if (rangeStruct.prefixLength >= 8) {
mask.s6_addr[i] = 0xFF;
rangeStruct.prefixLength -= 8;
} else if (rangeStruct.prefixLength > 0) {
mask.s6_addr[i] = (uint8_t)(0xFF << (8 - rangeStruct.prefixLength));
rangeStruct.prefixLength = 0;
} else {
mask.s6_addr[i] = 0;
}
}

// Check mask and addr
in6_addr andResult{};
for (size_t i = 0; i < sizeof(andResult.s6_addr); ++i) {
andResult.s6_addr[i] = addr.s6_addr[i] & mask.s6_addr[i];
}

// Compare andResult with IPv6 address of the rangeStruct
return memcmp(&andResult, &rangeIpv6Address, sizeof(andResult)) == 0;
}

bool IpAddressNetlinkChecker::checkSubnet(
const in6_addr& addrToCheck, const in6_addr& interfaceIpv6Addr, const in6_addr& interfaceMask) const {
const auto s6Addr32ArraySize = sizeof(addrToCheck.s6_addr32) / sizeof(addrToCheck.s6_addr32[0]);
for (size_t i = 0; i < s6Addr32ArraySize; ++i) {
if ((addrToCheck.s6_addr32[i] & interfaceMask.s6_addr32[i]) != (interfaceIpv6Addr.s6_addr32[i] & interfaceMask.s6_addr32[i])) {
return false;
}
}
return true;
}

bool IpAddressNetlinkChecker::isV6AddressExternal(const in6_addr& addr) const {
lnarolski marked this conversation as resolved.
Show resolved Hide resolved
if (auto mappedV4Addr = getMappedIPv4Addr(addr); mappedV4Addr) {
return isV4AddressExternal(*mappedV4Addr);
}
throw std::runtime_error("IPv6 is only supported for IPv4 mapped addresses");

for (auto& ipv6Interface : netlink.collectIpv6Interfaces()) {
lnarolski marked this conversation as resolved.
Show resolved Hide resolved
if (checkSubnet(addr, ipv6Interface.interfaceIpv6Addr, ipv6Interface.interfaceMask)) {
return false;
}
}

for (const auto& internalRange : {"fc00::/7", "fec0::/10", "fe80::/10", "::1/128"}) {
if (IpAddressNetlinkChecker::isInRange(addr, internalRange)) {
return false;
}
}

return true;
}

} // namespace service
21 changes: 20 additions & 1 deletion libservice/src/NetlinkCalls.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
#include <arpa/inet.h>
#include <array>
#include <cstring>
#include <ifaddrs.h>
#include <linux/rtnetlink.h>
#include <net/if.h>

#include "NetlinkSocket.h"
#include "logging/Logger.h"
Expand Down Expand Up @@ -237,6 +237,25 @@ IpInterfaces NetlinkCalls::collectIpInterfaces() const {
return handleNetlink<decltype(sendIpAddrRequest), decltype(receiveIpAddr), IpInterfaces>(sendIpAddrRequest, receiveIpAddr, AF_INET);
}

std::vector<NetlinkCalls::Ipv6Interface> NetlinkCalls::collectIpv6Interfaces() const {
std::vector<Ipv6Interface> collectedIpv6Interfaces{};

ifaddrs* ifAddressStruct = nullptr;
if (getifaddrs(&ifAddressStruct) == 0) {
for (ifaddrs* ifa = ifAddressStruct; ifa != nullptr; ifa = ifa->ifa_next) {
if (ifa->ifa_addr != nullptr && ifa->ifa_addr->sa_family == AF_INET6) {
in6_addr interfaceIpv6Addr = reinterpret_cast<sockaddr_in6*>(ifa->ifa_addr)->sin6_addr;
in6_addr interfaceMask = reinterpret_cast<sockaddr_in6*>(ifa->ifa_netmask)->sin6_addr;

collectedIpv6Interfaces.emplace_back(Ipv6Interface{interfaceIpv6Addr, interfaceMask});
}
}
freeifaddrs(ifAddressStruct);
}

return collectedIpv6Interfaces;
}

BridgeIndices NetlinkCalls::collectBridgeIndices() const {
return handleNetlink<decltype(sendBridgesRequest), decltype(receiveBridgeIndex), BridgeIndices>(
sendBridgesRequest, receiveBridgeIndex, AF_INET);
Expand Down
70 changes: 67 additions & 3 deletions libservice/test/IpAddressCheckerTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ TEST_F(IpAddressCheckerTest, LocalIfceIpSrc) {
EXPECT_FALSE(ipAddressNetlinkChecker.isV4AddressExternal(inet_addr("115.89.3.7")));
}

static in6_addr getV6AddrBinary(std::string addr) {
static in6_addr getV6AddrBinary(const std::string& addr) {
in6_addr clientAddrBinary{};
inet_pton(AF_INET6, addr.c_str(), &clientAddrBinary);
return clientAddrBinary;
Expand All @@ -279,6 +279,70 @@ TEST_F(IpAddressCheckerTest, TestMapped) {
EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("::FFFF:192.168.0.5")));

// regular IPv6 addr
EXPECT_THROW(
ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("2001:0db8:85a3:0000:0000:8a2e:0370:7334")), std::runtime_error);
EXPECT_TRUE(
ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("2001:0db8:85a3:0000:0000:8a2e:0370:7334")));

// IPv4-translated Address
EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("::ffff:0:0:0")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("0000:0000:0000:0000:fffe:ffff:ffff:ffff")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("0000:0000:0000:0000:ffff:0001:ffff:ffff")));

// IPv4-IPv6 Translatable Address
EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("64:ff9b::")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("0064:ff9a:ffff:ffff:ffff:ffff:ffff:ffff")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("0064:ff9b:0000:0000:0000:0001:0000:0000")));
}

// unique local address (prefix fc00::/7)
TEST_F(IpAddressCheckerTest, UniqueLocalAddress) {
IpAddressNetlinkChecker ipAddressNetlinkChecker{netlinkMock};

EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("fc00::1")));
EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("fbff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("fe00::")));
}

// site local addresses (fec0::/10 - deprecated)
TEST_F(IpAddressCheckerTest, SiteLocalAddress) {
IpAddressNetlinkChecker ipAddressNetlinkChecker{netlinkMock};

EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("fec0::")));
EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("feff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("ff00::")));
}

// link local addresses (fe80::/10)
TEST_F(IpAddressCheckerTest, LinkLocalAddress) {
IpAddressNetlinkChecker ipAddressNetlinkChecker{netlinkMock};

EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("fe80::")));
EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("fe7f:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
}

// loopback addresses (::1/128)
TEST_F(IpAddressCheckerTest, LoopbackAddress) {
IpAddressNetlinkChecker ipAddressNetlinkChecker{netlinkMock};

EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("::1")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("::")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("::2")));
}

// address is from the same subnet as any local ipv6 interfaces
TEST_F(IpAddressCheckerTest, Ipv6InterfaceSubnet) {
in6_addr ipv6InterfaceAddr{};
in6_addr ipv6InterfaceMask{};

inet_pton(AF_INET6, "2001:db8:85a3::8a2e:370:7336", &ipv6InterfaceAddr);
inet_pton(AF_INET6, "ffff:ffff:ffff:ffff::", &ipv6InterfaceMask);

std::vector<service::NetlinkCalls::Ipv6Interface> interfaces = {service::NetlinkCalls::Ipv6Interface{ipv6InterfaceAddr, ipv6InterfaceMask}};
IpAddressNetlinkChecker ipAddressNetlinkChecker{netlinkMock};
EXPECT_CALL(netlinkMock, collectIpv6Interfaces).Times(3).WillRepeatedly(Return(interfaces));

EXPECT_FALSE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("2001:0db8:85a3:0000:0000:0000:0000:0000")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("2001:0db8:85a3:0001:0000:0000:0000:0000")));
EXPECT_TRUE(ipAddressNetlinkChecker.isV6AddressExternal(getV6AddrBinary("2001:0db8:85a2:ffff:ffff:ffff:ffff:ffff")));
}
1 change: 1 addition & 0 deletions libservice/test/NetlinkCallsMock.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class NetlinkCallsMock : public NetlinkCalls {
public:
MOCK_CONST_METHOD0(collectIpInterfaces, IpInterfaces());
MOCK_CONST_METHOD0(collectBridgeIndices, BridgeIndices());
MOCK_CONST_METHOD0(collectIpv6Interfaces, std::vector<Ipv6Interface>());
};

} // namespace service
13 changes: 13 additions & 0 deletions test/component/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,16 @@ def test_request_forwarded_to_public_ip_matching_local_interface_mask(network_in
assert discovered_service_has_clients(run_ebpf_discovery, url,
internal_clients_number=0,
external_clients_number=1)

@pytest.mark.parametrize('network_interfaces', [[('dummy', 'eth17', '2001:db8:85a3::8a2e:370:7336', 64)]], indirect=True)
def test_request_forwarded_to_public_ipv6_matching_local_interface_mask(network_interfaces, run_ebpf_discovery, run_http_service):
url = run_http_service + "/forwarded"
send_http_requests(url, 1, "2001:0db8:85a3:0000:0000:0000:0000:0000")
send_http_requests(url, 1, "[2001:0db8:85a3::]:420")
send_http_requests(url, 1, "2001:0db8:85a3:0001:0000:0000:0000:0000")
send_http_requests(url, 1, "[2001:0db8:85a3:0001::]:12345")
send_http_requests(url, 1, "2001:0db8:85a2:ffff:ffff:ffff:ffff:ffff")
send_http_requests(url, 1, "[2001:0db8:85a2:ffff:ffff:ffff:ffff:ffff]:111")
lnarolski marked this conversation as resolved.
Show resolved Hide resolved
assert discovered_service_has_clients(run_ebpf_discovery, url,
internal_clients_number=2,
external_clients_number=4)