diff --git a/include/znc/User.h b/include/znc/User.h index f1bc04b0..74c248b8 100644 --- a/include/znc/User.h +++ b/include/znc/User.h @@ -65,7 +65,7 @@ class CUser { bool AddAllowedHost(const CString& sHostMask); bool RemAllowedHost(const CString& sHostMask); void ClearAllowedHosts(); - bool IsHostAllowed(const CString& sHostMask) const; + bool IsHostAllowed(const CString& sHost) const; bool IsValid(CString& sErrMsg, bool bSkipPass = false) const; static bool IsValidUserName(const CString& sUserName); static CString MakeCleanUserName(const CString& sUserName); diff --git a/include/znc/Utils.h b/include/znc/Utils.h index 5251ad09..e64560df 100644 --- a/include/znc/Utils.h +++ b/include/znc/Utils.h @@ -83,6 +83,11 @@ class CUtils { static timeval ParseServerTime(const CString& sTime); static SCString GetTimezones(); static SCString GetEncodings(); + /** CIDR notation checker, e.g. "192.0.2.0/24" or "2001:db8::/32" + * + * For historical reasons also allows wildcards, e.g. "192.168.*" + */ + static bool CheckCIDR(const CString& sIP, const CString& sRange); /// @deprecated Use CMessage instead static MCString GetMessageTags(const CString& sLine); diff --git a/src/HTTPSock.cpp b/src/HTTPSock.cpp index 07b53905..f6cf1818 100644 --- a/src/HTTPSock.cpp +++ b/src/HTTPSock.cpp @@ -163,7 +163,7 @@ void CHTTPSock::ReadLine(const CString& sData) { // check if sIP is trusted proxy bool bTrusted = false; for (const CString& sTrustedProxy : vsTrustedProxies) { - if (sIP.WildCmp(sTrustedProxy)) { + if (CUtils::CheckCIDR(sIP, sTrustedProxy)) { bTrusted = true; break; } diff --git a/src/User.cpp b/src/User.cpp index 806a92cb..b3a20def 100644 --- a/src/User.cpp +++ b/src/User.cpp @@ -849,112 +849,14 @@ bool CUser::RemAllowedHost(const CString& sHostMask) { } void CUser::ClearAllowedHosts() { m_ssAllowedHosts.clear(); } -bool CUser::IsHostAllowed(const CString& sHostMask) const { +bool CUser::IsHostAllowed(const CString& sHost) const { if (m_ssAllowedHosts.empty()) { return true; } - for (const CString& sHost : m_ssAllowedHosts) { - if (sHostMask.WildCmp(sHost)) { + for (const CString& sAllowedHost : m_ssAllowedHosts) { + if (CUtils::CheckCIDR(sHost, sAllowedHost)) { return true; - } else { - // CIDR notation checker, e.g. "192.0.2.0/24" or "2001:db8::/32" - - // Try to split the string into an IP and routing prefix - VCString vsSplitCIDR; - if (sHost.Split("/", vsSplitCIDR, false) != 2) continue; - const CString sRoutingPrefix = vsSplitCIDR.back(); - const int iRoutingPrefix = sRoutingPrefix.ToInt(); - if (iRoutingPrefix < 0 || iRoutingPrefix > 128) continue; - - // If iRoutingPrefix is 0, it could be due to ToInt() failing, so - // sRoutingPrefix needs to be all zeroes - if (iRoutingPrefix == 0 && sRoutingPrefix != "0") continue; - - // Convert each IP from a numeric string to an addrinfo - addrinfo aiHints; - memset(&aiHints, 0, sizeof(addrinfo)); - aiHints.ai_flags = AI_NUMERICHOST; - - addrinfo* aiHost; - int iIsHostValid = - getaddrinfo(sHostMask.c_str(), nullptr, &aiHints, &aiHost); - if (iIsHostValid != 0) continue; - - aiHints.ai_family = aiHost->ai_family; // Host and range must be in - // the same address family - - addrinfo* aiRange; - int iIsRangeValid = getaddrinfo(vsSplitCIDR.front().c_str(), - nullptr, &aiHints, &aiRange); - if (iIsRangeValid != 0) { - freeaddrinfo(aiHost); - continue; - } - - // "/0" allows all IPv[4|6] addresses - if (iRoutingPrefix == 0) { - freeaddrinfo(aiHost); - freeaddrinfo(aiRange); - return true; - } - - // If both IPs are valid and of the same type, make a bit field mask - // from the routing prefix, AND it to the host and range, and see if - // they match - bool bIsHostInRange = false; - if (aiHost->ai_family == AF_INET) { - if (iRoutingPrefix > 32) { - freeaddrinfo(aiHost); - freeaddrinfo(aiRange); - continue; - } - - const sockaddr_in* saHost = (sockaddr_in*)(aiHost->ai_addr); - const sockaddr_in* saRange = (sockaddr_in*)(aiRange->ai_addr); - - // Make IPv4 bitmask - const in_addr_t inBitmask = - htonl((~0u) << (32 - iRoutingPrefix)); - - // Compare masked IPv4s - bIsHostInRange = ((inBitmask & saHost->sin_addr.s_addr) == - (inBitmask & saRange->sin_addr.s_addr)); - } else if (aiHost->ai_family == AF_INET6) { - // Make IPv6 bitmask - in6_addr in6aBitmask; - memset(&in6aBitmask, 0, sizeof(in6aBitmask)); - - for (int i = 0, iBitsLeft = iRoutingPrefix; iBitsLeft > 0; - ++i, iBitsLeft -= 8) { - if (iBitsLeft >= 8) { - in6aBitmask.s6_addr[i] = (uint8_t)(~0u); - } else { - in6aBitmask.s6_addr[i] = (uint8_t)(~0u) - << (8 - iBitsLeft); - } - } - - // Compare masked IPv6s - bIsHostInRange = true; - const sockaddr_in6* sa6Host = (sockaddr_in6*)(aiHost->ai_addr); - const sockaddr_in6* sa6Range = - (sockaddr_in6*)(aiRange->ai_addr); - - for (int i = 0; i < 16; ++i) { - if ((in6aBitmask.s6_addr[i] & - sa6Host->sin6_addr.s6_addr[i]) != - (in6aBitmask.s6_addr[i] & - sa6Range->sin6_addr.s6_addr[i])) { - bIsHostInRange = false; - } - } - } - - freeaddrinfo(aiHost); - freeaddrinfo(aiRange); - - if (bIsHostInRange) return true; } } diff --git a/src/Utils.cpp b/src/Utils.cpp index 68bcf640..9c183ae8 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -32,6 +32,10 @@ #include #include +#include +#include +#include + #ifdef HAVE_TCSETATTR #include #endif @@ -585,6 +589,101 @@ SCString CUtils::GetEncodings() { return ssResult; } +bool CUtils::CheckCIDR(const CString& sIP, const CString& sRange) { + if (sIP.WildCmp(sRange)) { + return true; + } + // Try to split the string into an IP and routing prefix + VCString vsSplitCIDR; + if (sRange.Split("/", vsSplitCIDR, false) != 2) return false; + const CString sRoutingPrefix = vsSplitCIDR.back(); + const int iRoutingPrefix = sRoutingPrefix.ToInt(); + if (iRoutingPrefix < 0 || iRoutingPrefix > 128) return false; + + // If iRoutingPrefix is 0, it could be due to ToInt() failing, so + // sRoutingPrefix needs to be all zeroes + if (iRoutingPrefix == 0 && sRoutingPrefix != "0") return false; + + // Convert each IP from a numeric string to an addrinfo + addrinfo aiHints; + memset(&aiHints, 0, sizeof(addrinfo)); + aiHints.ai_flags = AI_NUMERICHOST; + + addrinfo* aiHost; + int iIsHostValid = getaddrinfo(sIP.c_str(), nullptr, &aiHints, &aiHost); + if (iIsHostValid != 0) return false; + + aiHints.ai_family = aiHost->ai_family; // Host and range must be in + // the same address family + + addrinfo* aiRange; + int iIsRangeValid = + getaddrinfo(vsSplitCIDR.front().c_str(), nullptr, &aiHints, &aiRange); + if (iIsRangeValid != 0) { + freeaddrinfo(aiHost); + return false; + } + + // "/0" allows all IPv[4|6] addresses + if (iRoutingPrefix == 0) { + freeaddrinfo(aiHost); + freeaddrinfo(aiRange); + return true; + } + + // If both IPs are valid and of the same type, make a bit field mask + // from the routing prefix, AND it to the host and range, and see if + // they match + bool bIsHostInRange = false; + if (aiHost->ai_family == AF_INET) { + if (iRoutingPrefix > 32) { + freeaddrinfo(aiHost); + freeaddrinfo(aiRange); + return false; + } + + const sockaddr_in* saHost = (sockaddr_in*)(aiHost->ai_addr); + const sockaddr_in* saRange = (sockaddr_in*)(aiRange->ai_addr); + + // Make IPv4 bitmask + const in_addr_t inBitmask = htonl((~0u) << (32 - iRoutingPrefix)); + + // Compare masked IPv4s + bIsHostInRange = ((inBitmask & saHost->sin_addr.s_addr) == + (inBitmask & saRange->sin_addr.s_addr)); + } else if (aiHost->ai_family == AF_INET6) { + // Make IPv6 bitmask + in6_addr in6aBitmask; + memset(&in6aBitmask, 0, sizeof(in6aBitmask)); + + for (int i = 0, iBitsLeft = iRoutingPrefix; iBitsLeft > 0; + ++i, iBitsLeft -= 8) { + if (iBitsLeft >= 8) { + in6aBitmask.s6_addr[i] = (uint8_t)(~0u); + } else { + in6aBitmask.s6_addr[i] = (uint8_t)(~0u) << (8 - iBitsLeft); + } + } + + // Compare masked IPv6s + bIsHostInRange = true; + const sockaddr_in6* sa6Host = (sockaddr_in6*)(aiHost->ai_addr); + const sockaddr_in6* sa6Range = (sockaddr_in6*)(aiRange->ai_addr); + + for (int i = 0; i < 16; ++i) { + if ((in6aBitmask.s6_addr[i] & sa6Host->sin6_addr.s6_addr[i]) != + (in6aBitmask.s6_addr[i] & sa6Range->sin6_addr.s6_addr[i])) { + bIsHostInRange = false; + } + } + } + + freeaddrinfo(aiHost); + freeaddrinfo(aiRange); + + return bIsHostInRange; +} + MCString CUtils::GetMessageTags(const CString& sLine) { if (sLine.StartsWith("@")) { return CMessage(sLine).GetTags(); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3f8a383f..fefc6c6d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -60,7 +60,8 @@ add_executable(unittest_bin EXCLUDE_FROM_ALL "${GMOCK_ROOT}/src/gmock-all.cc" "ThreadTest.cpp" "NickTest.cpp" "ClientTest.cpp" "NetworkTest.cpp" "MessageTest.cpp" "ModulesTest.cpp" "IRCSockTest.cpp" "QueryTest.cpp" - "StringTest.cpp" "ConfigTest.cpp" "BufferTest.cpp" "UtilsTest.cpp") + "StringTest.cpp" "ConfigTest.cpp" "BufferTest.cpp" "UtilsTest.cpp" + "UserTest.cpp") target_link_libraries(unittest_bin PRIVATE znclib) target_include_directories(unittest_bin PRIVATE "${GTEST_ROOT}" "${GTEST_ROOT}/include" diff --git a/test/UserTest.cpp b/test/UserTest.cpp index f5347406..e9ece70d 100644 --- a/test/UserTest.cpp +++ b/test/UserTest.cpp @@ -27,7 +27,7 @@ class UserTest : public ::testing::Test { TEST_F(UserTest, IsHostAllowed) { struct hostTest { - CString sTestHost; + CString sMask; CString sIP; bool bExpectedResult; }; @@ -108,12 +108,14 @@ TEST_F(UserTest, IsHostAllowed) { {"::2/00000000000", "::1", false}, {"::2/0a", "::1", false}, + + {"192.168.*", "192.168.0.1", true}, }; for (const hostTest& h : aHostTests) { CUser user("user"); - user.AddAllowedHost(h.sTestHost); + user.AddAllowedHost(h.sMask); EXPECT_EQ(h.bExpectedResult, user.IsHostAllowed(h.sIP)) - << "Allow-host is " << h.sTestHost; + << "Allow-host is " << h.sMask; } }