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 all 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
16 changes: 16 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 All @@ -44,6 +59,7 @@ class IpAddressNetlinkChecker : public IpAddressChecker {

const NetlinkCalls& netlink;
IpInterfaces ipInterfaces;
std::vector<NetlinkCalls::Ipv6Network> ipv6Networks;
std::unordered_map<int, bool> isLocalBridgeMap;
};
} // namespace service
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 Ipv6Network {
in6_addr networkIpv6Addr;
in6_addr networkMask;
};

virtual IpInterfaces collectIpInterfaces() const;
virtual std::vector<Ipv6Network> collectIpv6Networks() const;
virtual BridgeIndices collectBridgeIndices() const;
};

Expand Down
88 changes: 82 additions & 6 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 All @@ -40,6 +41,8 @@ void IpAddressNetlinkChecker::readNetworks() {
isLocalBridgeMap[index] = true;
}

ipv6Networks = netlink.collectIpv6Networks();

printNetworkInterfacesInfo();
}

Expand All @@ -54,6 +57,14 @@ void IpAddressNetlinkChecker::printNetworkInterfacesInfo() {
", ")};
LOG_INFO("index: {}, IP addresses: {}{}", index, ipAddresses, isLocalBridge(index) ? " (local bridge)" : "");
}
LOG_INFO("{} IPv6 networks have been discovered:", ipv6Networks.size());
for (const auto& ipv6Network : ipv6Networks) {
char ipv6NetworkAddrString[INET6_ADDRSTRLEN];
char ipv6NetworkMaskString[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &(ipv6Network.networkIpv6Addr), ipv6NetworkAddrString, INET6_ADDRSTRLEN);
inet_ntop(AF_INET6, &(ipv6Network.networkMask), ipv6NetworkMaskString, INET6_ADDRSTRLEN);
LOG_INFO("Detected IPv6 network: {}, Mask: {}", ipv6NetworkAddrString, ipv6NetworkMaskString);
}
}

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

static bool ipv6AddressContainsMappedIpv4Address(const in6_addr& addr) {
if (!std::all_of(addr.s6_addr, addr.s6_addr + 9, [](auto byte) { return byte == 0; })) {
return false;
bool IpAddressNetlinkChecker::ipv6AddressContainsMappedIpv4Address(const in6_addr& addr) const {
for (const auto& internalRange : {"::ffff:0:0/96", "::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
}
}
return (addr.s6_addr[10] == 0xFF && addr.s6_addr[11] == 0xFF);

lnarolski marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

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 +145,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& ipv6Network : ipv6Networks) {
if (checkSubnet(addr, ipv6Network.networkIpv6Addr, ipv6Network.networkMask)) {
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::Ipv6Network> NetlinkCalls::collectIpv6Networks() const {
std::vector<Ipv6Network> collectedIpv6Networks{};

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 networkIpv6Addr = reinterpret_cast<sockaddr_in6*>(ifa->ifa_addr)->sin6_addr;
in6_addr networkMask = reinterpret_cast<sockaddr_in6*>(ifa->ifa_netmask)->sin6_addr;

collectedIpv6Networks.emplace_back(Ipv6Network{networkIpv6Addr, networkMask});
}
}
freeifaddrs(ifAddressStruct);
}

return collectedIpv6Networks;
}

BridgeIndices NetlinkCalls::collectBridgeIndices() const {
return handleNetlink<decltype(sendBridgesRequest), decltype(receiveBridgeIndex), BridgeIndices>(
sendBridgesRequest, receiveBridgeIndex, AF_INET);
Expand Down
72 changes: 69 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,72 @@ 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, Ipv6NetworkSubnet) {
in6_addr ipv6NetworkAddr{};
in6_addr ipv6NetworkMask{};

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

EXPECT_CALL(netlinkMock, collectIpv6Networks).WillOnce(Return(std::vector<service::NetlinkCalls::Ipv6Network>{{service::NetlinkCalls::Ipv6Network{ipv6NetworkAddr, ipv6NetworkMask}}}));
EXPECT_CALL(netlinkMock, collectIpInterfaces).WillOnce(Return(IpInterfaces{0}));
EXPECT_CALL(netlinkMock, collectBridgeIndices).WillOnce(Return(BridgeIndices{0}));

IpAddressNetlinkChecker ipAddressNetlinkChecker{netlinkMock};

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(collectIpv6Networks, std::vector<Ipv6Network>());
};

} // namespace service
20 changes: 20 additions & 0 deletions test/component/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,23 @@ 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_network_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")
assert discovered_service_has_clients(run_ebpf_discovery, url,
internal_clients_number=2,
external_clients_number=0)

@pytest.mark.parametrize('network_interfaces', [[('dummy', 'eth18', '2001:db8:85a3::8a2e:370:7337', 64)]], indirect=True)
def test_request_forwarded_to_public_ipv6_not_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: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=0,
external_clients_number=4)