diff --git a/Makefile.in b/Makefile.in index c3486e39..0fdaa364 100644 --- a/Makefile.in +++ b/Makefile.in @@ -59,7 +59,7 @@ BIN_SRCS := src/main.cpp LIB_OBJS := $(patsubst %cpp,%o,$(LIB_SRCS)) BIN_OBJS := $(patsubst %cpp,%o,$(BIN_SRCS)) TESTS := StringTest ConfigTest UtilsTest ThreadTest NickTest ClientTest NetworkTest \ - MessageTest ModulesTest IRCSockTest QueryTest BufferTest + MessageTest ModulesTest IRCSockTest QueryTest BufferTest UserTest TESTS := $(addprefix test/,$(addsuffix .o,$(TESTS))) CLEAN := znc src/*.o test/*.o core core.* .version_extra .depend modules/.depend \ unittest $(LIBZNC) diff --git a/src/User.cpp b/src/User.cpp index ae4cbd52..55e9aa79 100644 --- a/src/User.cpp +++ b/src/User.cpp @@ -852,6 +852,92 @@ bool CUser::IsHostAllowed(const CString& sHostMask) const { for (const CString& sHost : m_ssAllowedHosts) { if (sHostMask.WildCmp(sHost)) { return true; + } else { + // CIDR notation checker, e.g. "192.0.2.0/24" or "2001:db8::/32" + addrinfo *aiHost, *aiRange; + addrinfo aiHints; + in6_addr in6aBitmask; + + memset(&in6aBitmask, 0, sizeof(in6aBitmask)); + memset(&aiHints, 0, sizeof(addrinfo)); + + aiHints.ai_family = AF_UNSPEC; // Allow any address family + aiHints.ai_socktype = 0; // Any socket + aiHints.ai_flags = AI_NUMERICHOST | AI_ADDRCONFIG; + aiHints.ai_protocol = 0; // Any protocol + aiHints.ai_canonname = NULL; + aiHints.ai_addr = NULL; + aiHints.ai_next = NULL; + + // Try to split the string into an IP and routing prefix + VCString vsSplitCIDR; + const int iSplitCIDRCount = sHost.Split("/", vsSplitCIDR, false); + const int iRoutingPrefix = vsSplitCIDR.back().ToInt(); + + if (iSplitCIDRCount != 2 || iRoutingPrefix < 0) continue; + + int iIsHostValid, iIsRangeValid; + iIsHostValid = + getaddrinfo(sHostMask.c_str(), NULL, &aiHints, &aiHost); + if (iIsHostValid != 0) continue; + + aiHints.ai_family = aiHost->ai_family; // Host and range must be in + // the same address family + + iIsRangeValid = getaddrinfo(vsSplitCIDR.front().c_str(), NULL, + &aiHints, &aiRange); + if (iIsRangeValid != 0) continue; + + // "/0" allows all IPv[4|6] addresses + if (iRoutingPrefix == 0) 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) { + 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 + 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/test/UserTest.cpp b/test/UserTest.cpp new file mode 100644 index 00000000..393480be --- /dev/null +++ b/test/UserTest.cpp @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2004-2015 ZNC, see the NOTICE file for details. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +class UserTest : public ::testing::Test { + protected: + UserTest() { CZNC::CreateInstance(); } + ~UserTest() { CZNC::DestroyInstance(); } +}; + +TEST_F(UserTest, IsHostAllowed) { + CUser* m_pTestUser; + + struct hostTest { + CString sTestHost; + CString sIP; + bool bExpectedResult; + }; + + hostTest aHostTests[] = { + {"127.0.0.1", "127.0.0.1", true}, + {"127.0.0.20", "127.0.0.1", false}, + {"127.0.0.20/24", "127.0.0.1", true}, + {"127.0.0.20/0", "127.0.0.1", true}, + {"127.0.0.20/32", "127.0.0.1", false}, + + {"127.0.0.1", "127.0.0.0", false}, + {"127.0.0.20", "127.0.0.0", false}, + {"127.0.0.20/24", "127.0.0.0", true}, + {"127.0.0.20/0", "127.0.0.0", true}, + {"127.0.0.20/32", "127.0.0.0", false}, + + {"127.0.0.1", "127.0.0.255", false}, + {"127.0.0.20", "127.0.0.255", false}, + {"127.0.0.20/24", "127.0.0.255", true}, + {"127.0.0.20/0", "127.0.0.255", true}, + {"127.0.0.20/32", "127.0.0.255", false}, + + {"127.0.0.1", "127.0.1.1", false}, + {"127.0.0.20", "127.0.1.1", false}, + {"127.0.0.20/24", "127.0.1.1", false}, + {"127.0.0.20/16", "127.0.1.1", true}, + {"127.0.0.20/0", "127.0.1.1", true}, + {"127.0.0.20/32", "127.0.1.1", false}, + + {"127.0.0.1", "0.0.0.0", false}, + {"127.0.0.20", "0.0.0.0", false}, + {"127.0.0.20/24", "0.0.0.0", false}, + {"127.0.0.20/16", "0.0.0.0", false}, + {"127.0.0.20/0", "0.0.0.0", true}, + {"127.0.0.20/32", "0.0.0.0", false}, + + {"127.0.0.1", "255.255.255.255", false}, + {"127.0.0.20", "255.255.255.255", false}, + {"127.0.0.20/24", "255.255.255.255", false}, + {"127.0.0.20/16", "255.255.255.255", false}, + {"127.0.0.20/0", "255.255.255.255", true}, + {"127.0.0.20/32", "255.255.255.255", false}, + + {"127.0.0.1", "::1", false}, + {"::1", "::1", true}, + {"::20/120", "::1", true}, + {"::20/120", "::ffff", false}, + {"::ff/0", "::1", true}, + {"::ff/0", "127.0.0.1", false}, + {"127.0.0.20/0", "::1", false}, + + {"127.0.0.1/-1", "127.0.0.1", false}, + {"::0/-1", "::0", false}, + {"127.0.0.a/0", "127.0.0.1", false}, + {"::g/0", "::0", false}, + {"127.0.0.1/0", "127.0.0.a", false}, + {"::0/0", "::g", false}, + + {"::0/0/0", "::0", false}, + {"2001:db8::/33", "2001:db9:0::", false}, + {"2001:db8::/32", "2001:db8:8000::", true}, + {"2001:db8::/33", "2001:db8:8000::", false}, + }; + for (int i = 0; i < (int)(sizeof(aHostTests) / sizeof(aHostTests[0])); + ++i) { + m_pTestUser = new CUser("user"); + hostTest* h = aHostTests + i; + m_pTestUser->AddAllowedHost(h->sTestHost); + CString should = h->bExpectedResult ? "" : " not"; + CString error = "Allow-host string \"" + h->sTestHost + "\" should" + + should + " allow host \"" + h->sIP + "\""; + EXPECT_EQ(m_pTestUser->IsHostAllowed(aHostTests[i].sIP), + aHostTests[i].bExpectedResult) + << error; + delete m_pTestUser; + } +}