-
Notifications
You must be signed in to change notification settings - Fork 602
Replace Auth::User IP-cache with a ClpMap cache #2221
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<AuthUserIP *>(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<AuthUserIP *>(ip_list.head->data); | ||
| bool found = false; | ||
| while (ipdata) { | ||
| AuthUserIP *tempnode = static_cast<AuthUserIP *>(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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume you have tested this new code with both
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am relying on the fact that we exhaustively audited and tested |
||
| } | ||
|
|
||
| Auth::User::~User() | ||
|
|
@@ -125,104 +87,35 @@ Auth::User::~User() | |
| /* free cached acl results */ | ||
| aclCacheMatchFlush(&proxy_match_cache); | ||
|
|
||
| /* free seen ip address's */ | ||
| clearIp(); | ||
|
|
||
| if (username_) | ||
| xfree((char*)username_); | ||
|
|
||
| /* prevent accidental reuse */ | ||
| 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; | ||
|
Comment on lines
+101
to
+105
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TBH I feel like this would belong better to the Ip::Address class.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, I recommend addressing my change request about ipList key type before resuming work on this function. |
||
| } | ||
|
|
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<SBuf, Ip::Address> ipList; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not understand what this map key (represented by SBuf type) is. In other words, I do not understand what ipList maps to an IP address. Please clarify. PR BuildIpKey() code suggests that the key is an IP address (represented by Ip::Address), but that does not help answering the above question and raises another: Why convert Ip::Address to SBuf instead of using Ip::Address itself?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In theory we could use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If this map key is an IP address, we should use Ip::Address instead of SBuf for its type (more on that below). However, that raises a second question: If the key is an IP address and the value is the same IP address, then why map X to X?! AFAICT, this PR proposes to use a map for what is really a set of unique IP addresses (with LRU-driven expiration and other management features implemented by ClpMap). If so, then we should refactor to clarify this ClpMap abuse and reduce its overheads (unless you want to implement a ClpSet in a prerequisite PR). I can help with that refactoring, but I want to make sure we are on the same page here before we discuss specific code adjustments.
I do not think we should use in6_addr directly because high-level Squid code uses Ip::Address to represent IP addresses. This authentication cache is high-level code (for this discussion context). It is regrettable that Ip::Address contains port information as well, but we can (and should) ignore that extra information in this PR context. Can Ip::Address be uses as a ClpMap key type already? If not, what APIs do we need to add or adjust?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Per /// Key must come with std::hash<Key> and std::equal_to<Key> instantiations.
/// Key::length() must return the number of memory bytes in use by the key.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please note that the minor questions discussed below are secondary to the primary concern raised in this change request. Again, we need to "make sure that we are on the same page here before we discuss specific code adjustments". Please see "If the key is an IP address and the value is the same IP address, then why map X to X?!" paragraph for more details.
/// Key must come with std::hash<Key> and std::equal_to<Key> instantiations.
I hope that /// Key::length() must return the number of memory bytes in use by the key.I suggest investigating replacing the existing |
||
| }; | ||
|
|
||
| } // namespace Auth | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<Key, Value, MemoryUsedBy> &other); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please rename this method to importFresh(). There are many reasonable ways to merge two maps, and we should give this specific merging algorithm a more specific name. While renaming, please avoid the term "list" when mentioning this new method argument -- ClpMap is an associative container (i.e. a map) rather than a list.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure that name is any better.
If you want to be explicit about what operation the merge is doing. How about FWIW, I have tried to implement
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, and that is why nobody is proposing to name this method import(). mergeFresh() or addFresh() would work.
This code does not add expired entries (from this code point of view), so "fresh" is very appropriate IMO. I still think importFresh() is better than addOrUpdate() [because it more clearly indicates which map dominates], but addOrUpdate() is better than merge(). Using either of those two names will address this change request. "Update" clashes with the method description that promises to replace (rather than update) existing same-key entries. Please rephrase that bullet if you decide to go with addOrUpdate().
Those "small changes" are actually what differentiates the proposed method from other possible merging methods. That "sorting and uniqueness" is why we use ClpMap instead of std::map (and its merge() method) here. The name should inform the caller which of the two maps dominates as far as LRU replacements are concerned. If we were to call this merge(), then the method would have to truly merge (i.e. interleave as needed to preserve relative use order) LRU indexes instead (and our ClpMap does not have enough information to do that IIRC). |
||
|
|
||
| /// 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<Key, Value, MemoryUsedBy>::setMemLimit(const uint64_t newLimit) | |
| memLimit_ = newLimit; | ||
| } | ||
|
|
||
| template <class Key, class Value, uint64_t MemoryUsedBy(const Value &)> | ||
| void | ||
| ClpMap<Key, Value, MemoryUsedBy>::clear() | ||
| { | ||
| index.clear(); | ||
| entries.clear(); | ||
| memUsed_ = 0; | ||
| } | ||
|
|
||
| /// \returns the index position of an entry identified by its key (or end()) | ||
| template <class Key, class Value, uint64_t MemoryUsedBy(const Value &)> | ||
| typename ClpMap<Key, Value, MemoryUsedBy>::IndexIterator | ||
|
|
@@ -272,6 +292,20 @@ ClpMap<Key, Value, MemoryUsedBy>::del(const Key &key) | |
| erase(i); | ||
| } | ||
|
|
||
| template <class Key, class Value, uint64_t MemoryUsedBy(const Value &)> | ||
| void | ||
| ClpMap<Key, Value, MemoryUsedBy>::merge(const ClpMap<Key, Value, MemoryUsedBy> &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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this merge() method survives, we should try to refactor so that its code does not convert expiration values to TTL values (here) and then back to the original expiration value (in Entry constructor). In other words, there should be no |
||
| add(i->key, i->value, newTtl); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// purges entries to make free memory large enough to fit wantSpace bytes | ||
| template <class Key, class Value, uint64_t MemoryUsedBy(const Value &)> | ||
| void | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here, we ignore any same-key annotations found
from. In merge() below, we use same-key annotations infromto replace the ones inthis. If this difference is intentional, please add a C++ comment to highlight and explain the difference in merging two containers (notesandipList).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure why you are equating annotations with IP addresses. They are different, their handling is different.