diff --git a/src/Makefile.am b/src/Makefile.am index 5d85a969817..de3b2dba2f9 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1711,6 +1711,7 @@ nodist_tests_testACLMaxUserIP_SOURCES = \ tests/stub_libhttp.cc \ tests/stub_libmem.cc \ tests/stub_libsecurity.cc \ + tests/stub_libtime.cc \ tests/stub_neighbors.cc tests_testACLMaxUserIP_LDADD = \ $(AUTH_ACL_LIBS) \ diff --git a/src/auth/User.cc b/src/auth/User.cc index 7b274386aac..77854fff1e2 100644 --- a/src/auth/User.cc +++ b/src/auth/User.cc @@ -20,17 +20,24 @@ #include "globals.h" #include "Store.h" +constexpr auto SizeOfUserIPCacheEntry = (sizeof(SBuf)+MAX_IPSTRLEN) /* key */ + + sizeof(Ip::Address) /* value */ + + sizeof(time_t) /* expiry */; + Auth::User::User(Auth::SchemeConfig *aConfig, const char *aRequestRealm) : auth_type(Auth::AUTH_UNKNOWN), config(aConfig), - ipcount(0), expiretime(0), credentials_state(Auth::Unchecked), username_(nullptr), - requestRealm_(aRequestRealm) + requestRealm_(aRequestRealm), + /* IPv6 "anonymization" rotates IPv6 every 5min. + * This 1200 cache entries allows each user 4 such devices + * with 24hrs of TTL before the cache starts to trim records. + */ + ipList(1200 * SizeOfUserIPCacheEntry) { proxy_match_cache.head = proxy_match_cache.tail = nullptr; - ip_list.head = ip_list.tail = nullptr; debugs(29, 5, "Initialised auth_user '" << this << "'."); } @@ -69,52 +76,7 @@ Auth::User::absorb(Auth::User::Pointer from) notes.appendNewOnly(&from->notes); /* absorb the list of IP address sources (for max_user_ip controls) */ - AuthUserIP *new_ipdata; - while (from->ip_list.head != nullptr) { - new_ipdata = static_cast(from->ip_list.head->data); - - /* If this IP has expired - ignore the expensive merge actions. */ - if (new_ipdata->ip_expiretime <= squid_curtime) { - /* This IP has expired - remove from the source list */ - dlinkDelete(&new_ipdata->node, &(from->ip_list)); - delete new_ipdata; - /* catch incipient underflow */ - -- from->ipcount; - } else { - /* add to our list. replace if already present. */ - AuthUserIP *ipdata = static_cast(ip_list.head->data); - bool found = false; - while (ipdata) { - AuthUserIP *tempnode = static_cast(ipdata->node.next->data); - - if (ipdata->ipaddr == new_ipdata->ipaddr) { - /* This IP has already been seen. */ - found = true; - /* update IP ttl and stop searching. */ - ipdata->ip_expiretime = max(ipdata->ip_expiretime, new_ipdata->ip_expiretime); - break; - } else if (ipdata->ip_expiretime <= squid_curtime) { - /* This IP has expired - cleanup the destination list */ - dlinkDelete(&ipdata->node, &ip_list); - delete ipdata; - /* catch incipient underflow */ - assert(ipcount); - -- ipcount; - } - - ipdata = tempnode; - } - - if (!found) { - /* This ip is not in the seen list. Add it. */ - dlinkAddTail(&new_ipdata->node, &ipdata->node, &ip_list); - ++ipcount; - /* remove from the source list */ - dlinkDelete(&new_ipdata->node, &(from->ip_list)); - ++from->ipcount; - } - } - } + ipList.merge(from->ipList); } Auth::User::~User() @@ -125,9 +87,6 @@ Auth::User::~User() /* free cached acl results */ aclCacheMatchFlush(&proxy_match_cache); - /* free seen ip address's */ - clearIp(); - if (username_) xfree((char*)username_); @@ -135,94 +94,28 @@ Auth::User::~User() auth_type = Auth::AUTH_UNKNOWN; } -void -Auth::User::clearIp() +/// generate the cache key for an ipList entry +static const SBuf +BuildIpKey(const Ip::Address &ip) { - AuthUserIP *ipdata, *tempnode; - - ipdata = (AuthUserIP *) ip_list.head; - - while (ipdata) { - tempnode = (AuthUserIP *) ipdata->node.next; - /* walk the ip list */ - dlinkDelete(&ipdata->node, &ip_list); - delete ipdata; - /* catch incipient underflow */ - assert(ipcount); - -- ipcount; - ipdata = tempnode; - } - - /* integrity check */ - assert(ipcount == 0); + SBuf key; + auto *buf = key.rawAppendStart(MAX_IPSTRLEN); + const auto len = ip.toHostStr(buf, MAX_IPSTRLEN); + key.rawAppendFinish(buf, len); + return key; } void -Auth::User::removeIp(Ip::Address ipaddr) +Auth::User::removeIp(const Ip::Address &ip) { - AuthUserIP *ipdata = (AuthUserIP *) ip_list.head; - - while (ipdata) { - /* walk the ip list */ - - if (ipdata->ipaddr == ipaddr) { - /* remove the node */ - dlinkDelete(&ipdata->node, &ip_list); - delete ipdata; - /* catch incipient underflow */ - assert(ipcount); - -- ipcount; - return; - } - - ipdata = (AuthUserIP *) ipdata->node.next; - } - + ipList.del(BuildIpKey(ip)); } void -Auth::User::addIp(Ip::Address ipaddr) +Auth::User::addIp(const Ip::Address &ip) { - AuthUserIP *ipdata = (AuthUserIP *) ip_list.head; - int found = 0; - - /* - * we walk the entire list to prevent the first item in the list - * preventing old entries being flushed and locking a user out after - * a timeout+reconfigure - */ - while (ipdata) { - AuthUserIP *tempnode = (AuthUserIP *) ipdata->node.next; - /* walk the ip list */ - - if (ipdata->ipaddr == ipaddr) { - /* This ip has already been seen. */ - found = 1; - /* update IP ttl */ - ipdata->ip_expiretime = squid_curtime + Auth::TheConfig.ipTtl; - } else if (ipdata->ip_expiretime <= squid_curtime) { - /* This IP has expired - remove from the seen list */ - dlinkDelete(&ipdata->node, &ip_list); - delete ipdata; - /* catch incipient underflow */ - assert(ipcount); - -- ipcount; - } - - ipdata = tempnode; - } - - if (found) - return; - - /* This ip is not in the seen list */ - ipdata = new AuthUserIP(ipaddr, squid_curtime + Auth::TheConfig.ipTtl); - - dlinkAddTail(ipdata, &ipdata->node, &ip_list); - - ++ipcount; - - debugs(29, 2, "user '" << username() << "' has been seen at a new IP address (" << ipaddr << ")"); + ipList.add(BuildIpKey(ip), ip, Auth::TheConfig.ipTtl); + debugs(29, 2, "user '" << username() << "' has been seen at a new IP address (" << ip << ")"); } SBuf diff --git a/src/auth/User.h b/src/auth/User.h index 60503d13351..60243f733ac 100644 --- a/src/auth/User.h +++ b/src/auth/User.h @@ -15,10 +15,12 @@ #include "auth/forward.h" #include "auth/Type.h" #include "base/CbcPointer.h" +#include "base/ClpMap.h" #include "base/RefCount.h" #include "dlink.h" #include "ip/Address.h" #include "Notes.h" +#include "sbuf/Algorithms.h" #include "sbuf/SBuf.h" class StoreEntry; @@ -49,7 +51,6 @@ class User : public RefCountable /** the config for this user */ Auth::SchemeConfig *config; dlink_list proxy_match_cache; - size_t ipcount; long expiretime; /// list of key=value pairs the helper produced @@ -72,9 +73,13 @@ class User : public RefCountable virtual int32_t ttl() const = 0; /* Manage list of IPs using this username */ - void clearIp(); - void removeIp(Ip::Address); - void addIp(Ip::Address); + void clearIp() { ipList.clear(); } + void removeIp(const Ip::Address &); + void addIp(const Ip::Address &); + /// How many unique IPs this client has been seen at. + /// Count may be limited to the prior authenticate_ip_ttl seconds + /// if more than 1200 IPs are seen. + size_t ipCount() const { return ipList.entries(); } /// add the Auth::User to the protocol-specific username cache. virtual void addToNameCache() = 0; @@ -117,8 +122,8 @@ class User : public RefCountable */ SBuf userKey_; - /** what ip addresses has this user been seen at?, plus a list length cache */ - dlink_list ip_list; + /// what IP addresses has this user been seen at? + ClpMap ipList; }; } // namespace Auth diff --git a/src/auth/UserRequest.cc b/src/auth/UserRequest.cc index 6a640ea7105..e1361fbf673 100644 --- a/src/auth/UserRequest.cc +++ b/src/auth/UserRequest.cc @@ -180,7 +180,7 @@ authenticateAuthUserRequestIPCount(Auth::UserRequest::Pointer auth_user_request) { assert(auth_user_request != nullptr); assert(auth_user_request->user() != nullptr); - return auth_user_request->user()->ipcount; + return auth_user_request->user()->ipCount(); } /* diff --git a/src/auth/UserRequest.h b/src/auth/UserRequest.h index a74e1d92ef3..66871f357d6 100644 --- a/src/auth/UserRequest.h +++ b/src/auth/UserRequest.h @@ -30,29 +30,6 @@ class HttpRequest; // XXX: Keep in sync with all others: bzr grep 'define MAX_AUTHTOKEN_LEN' #define MAX_AUTHTOKEN_LEN 65535 -/** - * Node used to link an IP address to some user credentials - * for the max_user_ip ACL feature. - */ -class AuthUserIP -{ - MEMPROXY_CLASS(AuthUserIP); - -public: - AuthUserIP(const Ip::Address &ip, time_t t) : ipaddr(ip), ip_expiretime(t) {} - - dlink_node node; - - /// IP address this user authenticated from - Ip::Address ipaddr; - - /** When this IP should be forgotten. - * Set to the time of last request made from this - * (user,IP) pair plus authenticate_ip_ttl seconds - */ - time_t ip_expiretime; -}; - // TODO: make auth schedule AsyncCalls? typedef void AUTHCB(void*); diff --git a/src/base/ClpMap.h b/src/base/ClpMap.h index 4ddfed6cfe6..39a916e975f 100644 --- a/src/base/ClpMap.h +++ b/src/base/ClpMap.h @@ -93,6 +93,14 @@ class ClpMap /// Remove the corresponding entry (if any) void del(const Key &); + /// Merge entries from another ClpMap. + /// * Entries imported from the other list will retain their LRU order + /// and be placed before existing entries. + /// * Entries which have expired will be ignored. + /// * When an key collision occurs the existing entry will be replaced + /// and the new entry given the largest of the two TTL values. + void merge(const ClpMap &other); + /// Reset the memory capacity for this map, purging if needed void setMemLimit(uint64_t newLimit); @@ -108,6 +116,9 @@ class ClpMap /// The number of currently stored entries, including expired ones size_t entries() const { return entries_.size(); } + /// Erases all elements from the container. After this call, entries() returns zero. + void clear(); + /// Read-only traversal of all cached entries in LRU order, least recently /// used entry first. Stored expired entries (if any) are included. Any map /// modification may invalidate these iterators and their derivatives. @@ -162,6 +173,15 @@ ClpMap::setMemLimit(const uint64_t newLimit) memLimit_ = newLimit; } +template +void +ClpMap::clear() +{ + index.clear(); + entries.clear(); + memUsed_ = 0; +} + /// \returns the index position of an entry identified by its key (or end()) template typename ClpMap::IndexIterator @@ -272,6 +292,20 @@ ClpMap::del(const Key &key) erase(i); } +template +void +ClpMap::merge(const ClpMap &other) +{ + // reverse-order across other - to preserve old LRU + for (auto i = other.entries_.rbegin(); i != other.entries_.rend(); ++i) { + if (!i->expired()) { + const auto ours = find(i->key); + const auto newTtl = (ours != index_.cend() ? max(i->expires, ours->second->expires) : i->expires) - squid_curtime; + add(i->key, i->value, newTtl); + } + } +} + /// purges entries to make free memory large enough to fit wantSpace bytes template void diff --git a/src/tests/stub_libauth.cc b/src/tests/stub_libauth.cc index 2e55410bae7..29752abc3bc 100644 --- a/src/tests/stub_libauth.cc +++ b/src/tests/stub_libauth.cc @@ -39,14 +39,14 @@ void Auth::Scheme::FreeAll() STUB void Auth::SchemesConfig::expand() STUB #include "auth/User.h" -Auth::User::User(Auth::SchemeConfig *, const char *) STUB +Auth::User::User(Auth::SchemeConfig *, const char *) : ipList(0) {STUB} Auth::CredentialState Auth::User::credentials() const STUB_RETVAL(credentials_state) void Auth::User::credentials(CredentialState) STUB void Auth::User::absorb(Auth::User::Pointer) STUB Auth::User::~User() STUB_NOP void Auth::User::clearIp() STUB -void Auth::User::removeIp(Ip::Address) STUB -void Auth::User::addIp(Ip::Address) STUB +void Auth::User::removeIp(const Ip::Address &) STUB +void Auth::User::addIp(const Ip::Address &) STUB void Auth::User::CredentialsCacheStats(StoreEntry *) STUB #include "auth/UserRequest.h"