Copilot commented on code in PR #13586:
URL: https://github.com/apache/trafficserver/pull/13586#discussion_r3907499387
##########
plugins/experimental/jax_fingerprint/ja4/tls_client_hello_summary.cc:
##########
@@ -23,14 +23,18 @@
*/
#include "ts/ts.h"
-#include <plugin.h>
#include "ja4.h"
#include "tls_client_hello_summary.h"
-#include <openssl/sha.h>
-#include <cstdint>
#include <algorithm>
+#include <cstdint>
+#include <openssl/sha.h>
+
+namespace
+{
+DbgCtl dbg_ctl{"jax_fingerprint"};
+}
Review Comment:
This file uses DbgCtl, but it no longer includes a header that declares it.
ts/ts.h does not define DbgCtl, so this will fail to compile unless another
header happens to be pulled in indirectly.
##########
include/tsutil/UdiTable.h:
##########
@@ -0,0 +1,493 @@
+/** @file
+
+ Fixed-size table for tracking frequently observed keys with bounded memory.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ agreements. See the NOTICE file distributed with this work for additional
information regarding
+ copyright ownership. The ASF licenses this file to you 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.
+*/
+
+#pragma once
+
+#include <algorithm>
+#include <atomic>
+#include <chrono>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+namespace ts
+{
+
+/** Default eviction policy for UdiTable. */
+template <typename Data> struct UdiTableAlwaysEvictable {
+ bool
+ operator()(Data const &) const noexcept
+ {
+ return true;
+ }
+};
+
+/** A fixed-size hash table using the Udi "King of the Hill" algorithm.
+ *
+ * Instantiations of this table track the keys/entities (IPs, URLs, etc.) with
+ * the highest rates of events (e.g. number of requests, number of errors,
etc.).
+ *
+ * Key properties:
+ * - Fixed memory: N slots = bounded memory, no unbounded growth
+ * - Self-cleaning: No cleanup thread needed, table manages itself
+ * - Hot tracking: High-score entries naturally stay in the table
+ * - Simple locking: Single mutex for all operations
+ * - Safe references: Returns shared_ptr so data survives eviction
+ *
+ * @tparam Key Default-constructible entity type (e.g., IP address, URL)
+ * @tparam Data Default-constructible user data type stored with each key.
+ * @tparam Hash Hash function for keys (defaults to std::hash)
+ * @tparam CanEvict Predicate callable with @c Data @c const& before replacing
+ * an occupied slot. The default permits every occupied slot to participate.
+ *
+ * The table owns the key and score for each entry. Users provide only their
custom
+ * Data type which is stored in a shared_ptr for safe access.
+ *
+ * Thread Safety:
+ * All operations are protected by a single mutex and are serialized.
+ * Returned shared_ptr<Data> remains valid even after the slot is evicted.
+ * The eviction predicate is invoked while the mutex is held and must not
+ * reenter the same table.
+ *
+ * Example usage:
+ * @code
+ * struct MyData {
+ * std::atomic<uint32_t> error_count{0};
+ * std::atomic<uint32_t> success_count{0};
+ * };
+ *
+ * ts::UdiTable<std::string, MyData> table(10000);
+ *
+ * auto data = table.process_event("some_key", 1);
+ * if (data) {
+ * data->error_count.fetch_add(1);
+ * }
+ * @endcode
+ */
+template <typename Key, typename Data, typename Hash = std::hash<Key>,
typename CanEvict = UdiTableAlwaysEvictable<Data>>
+class UdiTable
+{
+public:
+ using key_type = Key;
+ using data_type = Data;
+ using data_ptr = std::shared_ptr<Data>;
+
+ enum class ProcessStatus {
+ TRACKED, ///< Existing entry or successful contest.
+ CONTEST_LOST, ///< Ordinary score-based contest loss.
+ NO_CANDIDATE, ///< Bounded eviction-predicate scan found no candidate.
+ };
+
+ using const_data_ptr = std::shared_ptr<Data const>;
+ using data_format_fn = std::function<std::string(Key const &, uint32_t,
const_data_ptr const &)>;
+
+ // =========================================================================
+ // Public API - Declarations
+ // =========================================================================
+
+ /**
+ * @param[in] num_slots Total number of slots to allocate.
+ * @param[in] can_evict Predicate that decides whether occupied slots may be
replaced.
+ */
+ explicit UdiTable(size_t num_slots, CanEvict can_evict = {});
+
+ // No copying or moving
+ UdiTable(UdiTable const &) = delete;
+ UdiTable &operator=(UdiTable const &) = delete;
+ UdiTable(UdiTable &&) = delete;
+ UdiTable &operator=(UdiTable &&) = delete;
+
+ /** Retrieve the configured data for a given key.
+ *
+ * @param[in] key The key to look up.
+ * @return The data associated with @a key if found, nullptr otherwise.
+ *
+ * Thread-safe: Uses mutex lock.
+ * The returned shared_ptr remains valid even if the slot is later evicted.
+ */
+ data_ptr find(Key const &key);
+ const_data_ptr find(Key const &key) const;
+
+ /** Process an event for a key, creating a slot if needed via @a contest.
+ *
+ * If the key is already tracked, increments its score and returns the Data.
+ * If not, a call to @a contest is made to see whether the @a key should
evict
+ * an entry and take its place.
+ *
+ * @param[in] key The key for which an event is being processed.
+ * @param[in] score_delta Score to add (typically 1 for events).
+ * @param[out] status Optional result that distinguishes an ordinary contest
+ * loss from exhausting the bounded protected-slot scan.
+ * @return The data for @a key if tracked or contest won, nullptr otherwise.
+ *
+ * Thread-safe: Uses mutex lock.
+ * The returned shared_ptr remains valid even if the slot is later evicted.
+ */
+ data_ptr process_event(Key const &key, uint32_t score_delta = 1,
ProcessStatus *status = nullptr);
+
+ /** Remove a key from the table.
+ *
+ * @param[in] key The key to remove.
+ * @return Whether @a key was found and removed.
+ *
+ * Note: Existing shared_ptr references to the removed Data remain valid.
+ */
+ bool remove(Key const &key);
+
+ // Statistics
+ size_t num_slots() const;
+ size_t slots_used() const;
+ uint64_t contests() const;
+ uint64_t contests_won() const;
+ uint64_t evictions() const;
+
+ /** Reset table-level metrics to zero.
+ *
+ * Does NOT modify any entries in the table.
+ */
+ void reset_metrics();
+
+ /** Get the timestamp of the last reset (or table creation).
+ */
+ std::chrono::system_clock::time_point last_reset_time() const;
+
+ /** Get the number of seconds since last reset (or table creation).
+ */
+ uint64_t seconds_since_reset() const;
+
+ /** Dump all entries to a string.
+ *
+ * This may be useful to evaluate the behavior of the table for debugging or
+ * diagnostic purposes.
+ *
+ * @param[in] format_data Optional function to format each entry (key,
score, data).
+ * @return A string representation of all table entries.
+ */
+ std::string dump(data_format_fn format_data = nullptr) const;
+
+private:
+ /** The data used to track the key and score used to determine eviction.
+ */
+ struct Slot {
+ Key key{};
+ uint32_t score{0};
+ data_ptr data;
+
+ /** Check whether there is an entity associated with this slot.
+ *
+ * Note that a score of zero does not necessarily mean that the slot is
+ * empty. Upon table initialization, all slots are empty.
+ *
+ * @return Whether the slot has no entity assigned.
+ */
+ bool
+ is_empty() const
+ {
+ return !data;
+ }
+
+ /** Remove the entity associated with this slot.
+ */
+ void
+ clear()
+ {
+ key = Key{};
+ score = 0;
+ data.reset();
+ }
+ };
+
+ /** Determine whether the given key should evict a slot.
+ *
+ * Assumes @a mutex_ already held exclusively.
+ *
+ * @param[in] key The key trying to enter.
+ * @param[in] incoming_score The score of the incoming key.
+ * @return The data for @a key if contest won, nullptr if contest lost.
+ */
+ data_ptr contest(Key const &key, uint32_t incoming_score, ProcessStatus
*status);
+
+ static constexpr size_t MAX_CONTEST_PROBES = 1024;
+
+ /// Single global mutex for all operations
+ mutable std::mutex mutex_;
+
+ /// Lookup map: key -> slot index
+ std::unordered_map<Key, size_t, Hash> lookup_;
+
+ /// The slots representing the table.
+ std::vector<Slot> slots_;
+
+ /// The contest pointer - rotates through all slots as @a contest() is
called.
+ size_t contest_ptr_{0};
+
+ /// Policy for determining whether an occupied slot may participate in a
contest.
+ CanEvict can_evict_;
+
+ /// Metrics.
+ std::atomic<uint64_t> metric_contests_{0};
+ std::atomic<uint64_t> metric_contests_won_{0};
+ std::atomic<uint64_t> metric_evictions_{0};
+
+ /// Timestamp of last reset (or construction)
+ std::chrono::system_clock::time_point last_reset_time_;
+};
+
+// ===========================================================================
+// Implementation
+// ===========================================================================
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+UdiTable<Key, Data, Hash, CanEvict>::UdiTable(size_t num_slots, CanEvict
can_evict)
+ : slots_(num_slots), can_evict_(std::move(can_evict)),
last_reset_time_(std::chrono::system_clock::now())
+{
+ lookup_.reserve(num_slots);
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+std::shared_ptr<Data>
+UdiTable<Key, Data, Hash, CanEvict>::find(Key const &key)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+
+ auto it = lookup_.find(key);
+ if (it != lookup_.end()) {
+ return slots_[it->second].data;
+ }
+ return nullptr;
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+std::shared_ptr<Data const>
+UdiTable<Key, Data, Hash, CanEvict>::find(Key const &key) const
+{
+ return const_cast<UdiTable *>(this)->find(key);
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+std::shared_ptr<Data>
+UdiTable<Key, Data, Hash, CanEvict>::process_event(Key const &key, uint32_t
score_delta, ProcessStatus *status)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+
+ if (status) {
+ *status = ProcessStatus::TRACKED;
+ }
+
+ // Check if already tracked
+ auto it = lookup_.find(key);
+ if (it != lookup_.end()) {
+ Slot &slot = slots_[it->second];
+ slot.score = score_delta > UINT32_MAX - slot.score ? UINT32_MAX :
slot.score + score_delta;
+ return slot.data;
+ }
+
+ // Not tracked - contest for a slot
+ return contest(key, score_delta, status);
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+bool
+UdiTable<Key, Data, Hash, CanEvict>::remove(Key const &key)
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+
+ auto it = lookup_.find(key);
+ if (it == lookup_.end()) {
+ return false;
+ }
+
+ slots_[it->second].clear();
+ lookup_.erase(it);
+ return true;
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+size_t
+UdiTable<Key, Data, Hash, CanEvict>::num_slots() const
+{
+ return slots_.size();
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+size_t
+UdiTable<Key, Data, Hash, CanEvict>::slots_used() const
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ return lookup_.size();
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+uint64_t
+UdiTable<Key, Data, Hash, CanEvict>::contests() const
+{
+ return metric_contests_.load(std::memory_order_relaxed);
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+uint64_t
+UdiTable<Key, Data, Hash, CanEvict>::contests_won() const
+{
+ return metric_contests_won_.load(std::memory_order_relaxed);
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+uint64_t
+UdiTable<Key, Data, Hash, CanEvict>::evictions() const
+{
+ return metric_evictions_.load(std::memory_order_relaxed);
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+void
+UdiTable<Key, Data, Hash, CanEvict>::reset_metrics()
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ metric_contests_.store(0, std::memory_order_relaxed);
+ metric_contests_won_.store(0, std::memory_order_relaxed);
+ metric_evictions_.store(0, std::memory_order_relaxed);
+ last_reset_time_ = std::chrono::system_clock::now();
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+std::chrono::system_clock::time_point
+UdiTable<Key, Data, Hash, CanEvict>::last_reset_time() const
+{
+ std::lock_guard<std::mutex> lock(mutex_);
+ return last_reset_time_;
+}
+
+template <typename Key, typename Data, typename Hash, typename CanEvict>
+uint64_t
+UdiTable<Key, Data, Hash, CanEvict>::seconds_since_reset() const
+{
+ auto reset_time = last_reset_time();
+ auto now = std::chrono::system_clock::now();
+ auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now -
reset_time);
+ return static_cast<uint64_t>(elapsed.count());
+}
Review Comment:
seconds_since_reset() uses system_clock and casts elapsed.count() (a signed
value) to uint64_t without guarding against clock adjustments. If system time
moves backward, (now - reset_time) can be negative and the cast will produce a
huge value.
##########
plugins/experimental/jax_fingerprint/fingerprint_registry.h:
##########
@@ -0,0 +1,100 @@
+/** @file
+
+ Versioned inter-plugin registry for JAx fingerprint results.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <string_view>
+#include <type_traits>
+
+namespace jax_fingerprint
+{
+
+inline constexpr uint32_t REGISTRY_MAGIC = 0x4a415852; // "JAXR"
+inline constexpr uint16_t REGISTRY_ABI_VERSION = 1;
+inline constexpr char REGISTRY_DESCRIPTION[] = "JAx fingerprint registry
ABI v1";
+
+/** One immutable method/value pair exported for a connection or transaction.
*/
+struct RegistryEntryV1 {
+ const char *method;
+ const char *value;
+ uint32_t method_length;
+ uint32_t value_length;
+};
+
+/** A read-only view of all JAx fingerprints associated with one ATS object.
+ *
+ * The producer owns the registry, its entry array, and all referenced strings.
+ * Consumers must not retain pointers beyond the lifetime of the VConn or
+ * transaction from which the registry was retrieved.
+ */
+struct RegistryV1 {
+ uint32_t magic;
+ uint16_t abi_version;
+ uint16_t struct_size;
+ uint16_t entry_size;
+ uint16_t reserved;
+ uint32_t entry_count;
+ const RegistryEntryV1 *entries;
+};
+
+static_assert(std::is_standard_layout_v<RegistryEntryV1>);
+static_assert(std::is_standard_layout_v<RegistryV1>);
+
+inline bool
+is_valid(const RegistryV1 *registry)
+{
+ return registry != nullptr && registry->magic == REGISTRY_MAGIC &&
registry->abi_version == REGISTRY_ABI_VERSION &&
+ registry->struct_size >= sizeof(RegistryV1) && registry->entry_size
>= sizeof(RegistryEntryV1) &&
+ registry->entry_size % alignof(RegistryEntryV1) == 0 &&
(registry->entry_count == 0 || registry->entries != nullptr);
Review Comment:
fingerprint_registry::is_valid() checks entry_size alignment/stride, but it
doesn't verify that the entries base pointer itself is aligned for
RegistryEntryV1. entry_at() then reinterpret_casts and dereferences entries,
which is undefined behavior if a producer provides a misaligned entries pointer.
##########
plugins/experimental/abuse_shield/abuse_shield.cc:
##########
@@ -0,0 +1,1278 @@
+/** @file
+
+ Abuse Shield Plugin - HTTP/2 error tracking and IP-based abuse detection.
+
+ Uses the Udi "King of the Hill" algorithm for efficient, bounded-memory IP
tracking.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ agreements. See the NOTICE file distributed with this work for additional
information regarding
+ copyright ownership. The ASF licenses this file to you 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 <algorithm>
+#include <chrono>
+#include <cinttypes>
+#include <cstring>
+#include <ctime>
+#include <exception>
+#include <iomanip>
+#include <memory>
+#include <mutex>
+#include <shared_mutex>
+#include <sstream>
+#include <string>
+#include <type_traits>
+#include <unordered_map>
+
+#include <sys/socket.h>
+
+#include "ts/ts.h"
+#include "swoc/IPRange.h"
+#include "swoc/BufferWriter.h"
+#include "swoc/bwf_ip.h"
+
+#include "config.h"
+#include "fingerprint.h"
+#include "fingerprint_registry.h"
+#include "ip_data.h"
+#include "stats.h"
+#include "logging.h"
+
+namespace
+{
+using abuse_shield::dbg_ctl;
+using abuse_shield::PLUGIN_NAME;
+
+// Optional log file for LOG action output.
+TSTextLogObject g_log_object = nullptr;
+
+// Global action stats.
+abuse_shield::ActionStats g_action_stats;
+
+// Named JAx VConn user-arg selected by global.fingerprint_registry.
+int g_fingerprint_registry_index = -1;
+
+// Per-tracker stats.
+abuse_shield::TrackerStats g_txn_stats;
+abuse_shield::TrackerStats g_conn_stats;
+abuse_shield::TrackerStats g_h2_stats;
+
+// Helper to convert IPAddr to string.
+std::string
+ip_to_string(const swoc::IPAddr &ip)
+{
+ swoc::LocalBufferWriter<64> writer;
+ writer.print("{}", ip);
+ return std::string(writer.view());
+}
+
+std::string
+format_local_time(std::time_t time, const char *format)
+{
+ struct tm time_parts;
+
+ if (localtime_r(&time, &time_parts) == nullptr) {
+ return "-";
+ }
+
+ std::ostringstream oss;
+ oss << std::put_time(&time_parts, format);
+ return oss.str();
+}
+
+// Helper to get current wall clock time as string.
+std::string
+current_time_str()
+{
+ auto now = std::chrono::system_clock::now();
+ auto now_t = std::chrono::system_clock::to_time_t(now);
+ return format_local_time(now_t, "%Y-%m-%dT%H:%M:%S");
+}
+
+// ============================================================================
+// Global state
+// ============================================================================
+
+// Separate UDI tables for different event types, each with its own data type.
+std::unique_ptr<abuse_shield::TxnTable> g_txn_tracker; ///<
Transaction/request rate tracking
+std::unique_ptr<abuse_shield::ConnTable> g_conn_tracker; ///< Connection rate
tracking
+std::unique_ptr<abuse_shield::H2Table> g_h2_tracker; ///< HTTP/2 error
tracking
+
+/** Bounded block state which never evicts an unexpired block. */
+class BlockedIpTable
+{
+public:
+ explicit BlockedIpTable(size_t capacity) : capacity_(capacity) {
blocked_.reserve(capacity); }
+
+ bool
+ block(const swoc::IPAddr &ip, uint64_t until_ms)
+ {
+ std::lock_guard lock(mutex_);
+ auto spot = blocked_.find(ip);
+ if (spot != blocked_.end()) {
+ spot->second = std::max(spot->second, until_ms);
+ return true;
+ }
+
+ if (blocked_.size() >= capacity_) {
+ uint64_t now = abuse_shield::now_ms();
+ std::erase_if(blocked_, [now](auto const &item) { return item.second <=
now; });
+ }
+ if (blocked_.size() >= capacity_) {
+ return false;
+ }
+
+ blocked_.emplace(ip, until_ms);
+ return true;
+ }
+
+ bool
+ is_blocked(const swoc::IPAddr &ip)
+ {
+ std::lock_guard lock(mutex_);
+ auto spot = blocked_.find(ip);
+ if (spot == blocked_.end()) {
+ return false;
+ }
+ if (spot->second <= abuse_shield::now_ms()) {
+ blocked_.erase(spot);
+ return false;
+ }
+ return true;
+ }
+
+private:
+ size_t capacity_;
+ std::mutex mutex_;
+ std::unordered_map<swoc::IPAddr, uint64_t> blocked_;
+};
+
+std::unique_ptr<BlockedIpTable> g_blocked_ips;
+
+std::shared_ptr<abuse_shield::Config> g_config;
+std::shared_mutex g_config_mutex; // Protects g_config
pointer swaps
+
+// Sync the metrics from a single Udi table to its stats.
+void
+sync_tracker_stats(abuse_shield::TxnTable *tracker, abuse_shield::TrackerStats
&stats)
+{
+ if (tracker) {
+ TSStatIntSet(stats.slots_used,
static_cast<int64_t>(tracker->slots_used()));
+ TSStatIntSet(stats.contests, static_cast<int64_t>(tracker->contests()));
+ TSStatIntSet(stats.contests_won,
static_cast<int64_t>(tracker->contests_won()));
+ TSStatIntSet(stats.evictions, static_cast<int64_t>(tracker->evictions()));
+ }
+}
+
+void
+sync_tracker_stats(abuse_shield::ConnTable *tracker,
abuse_shield::TrackerStats &stats)
+{
+ if (tracker) {
+ TSStatIntSet(stats.slots_used,
static_cast<int64_t>(tracker->slots_used()));
+ TSStatIntSet(stats.contests, static_cast<int64_t>(tracker->contests()));
+ TSStatIntSet(stats.contests_won,
static_cast<int64_t>(tracker->contests_won()));
+ TSStatIntSet(stats.evictions, static_cast<int64_t>(tracker->evictions()));
+ }
+}
+
+void
+sync_tracker_stats(abuse_shield::H2Table *tracker, abuse_shield::TrackerStats
&stats)
+{
+ if (tracker) {
+ TSStatIntSet(stats.slots_used,
static_cast<int64_t>(tracker->slots_used()));
+ TSStatIntSet(stats.contests, static_cast<int64_t>(tracker->contests()));
+ TSStatIntSet(stats.contests_won,
static_cast<int64_t>(tracker->contests_won()));
+ TSStatIntSet(stats.evictions, static_cast<int64_t>(tracker->evictions()));
+ }
+}
+
+// Sync the metrics from all Udi tables.
+void
+sync_all_tracker_stats()
+{
+ sync_tracker_stats(g_txn_tracker.get(), g_txn_stats);
+ sync_tracker_stats(g_conn_tracker.get(), g_conn_stats);
+ sync_tracker_stats(g_h2_tracker.get(), g_h2_stats);
+}
+
+template <typename Table>
+void
+reset_tracker_stats(Table *tracker, abuse_shield::TrackerStats &stats)
+{
+ if (tracker) {
+ tracker->reset_metrics();
+ }
+ TSStatIntSet(stats.events, 0);
+ TSStatIntSet(stats.events_untracked, 0);
+ TSStatIntSet(stats.scan_exhausted, 0);
+ sync_tracker_stats(tracker, stats);
+}
+
+// ============================================================================
+// Rule evaluation
+// ============================================================================
+
+int
+metric_rate(const abuse_shield::RuleFilter &filter, abuse_shield::RateMetric
metric)
+{
+ switch (metric) {
+ case abuse_shield::RateMetric::REQUEST:
+ return filter.max_req_rate;
+ case abuse_shield::RateMetric::CONNECTION:
+ return filter.max_conn_rate;
+ case abuse_shield::RateMetric::H2_ERROR:
+ return filter.max_h2_error_rate;
+ }
+ return 0;
+}
+
+double
+metric_burst_multiplier(const abuse_shield::RuleFilter &filter,
abuse_shield::RateMetric metric)
+{
+ switch (metric) {
+ case abuse_shield::RateMetric::REQUEST:
+ return filter.req_burst_multiplier;
+ case abuse_shield::RateMetric::CONNECTION:
+ return filter.conn_burst_multiplier;
+ case abuse_shield::RateMetric::H2_ERROR:
+ return filter.h2_burst_multiplier;
+ }
+ return 1.0;
+}
+
+template <typename Table>
+bool
+rate_exceeded(Table *tracker, const abuse_shield::Rule &rule, const
swoc::IPAddr &ip)
+{
+ auto slot = tracker ? tracker->find(ip) : nullptr;
+ return slot && slot->buckets.exceeded(rule.name);
+}
+
+template <typename Table>
+void
+consume_rule_buckets(Table *tracker, const swoc::IPAddr &ip, const
abuse_shield::Config &config, abuse_shield::RateMetric metric,
+ abuse_shield::TrackerStats &stats, uint64_t error_code =
0)
+{
+ typename Table::data_ptr slot;
+ bool consumed = false;
+
+ for (const auto &rule : config.rules()) {
+ int rate = metric_rate(rule.filter, metric);
+ if (rate <= 0 || !config.rule_applies_to_ip(rule, ip)) {
+ continue;
+ }
+
+ if (!slot) {
+ typename Table::ProcessStatus status;
+ slot = tracker->process_event(ip, 1, &status);
+ if (!slot) {
+ TSStatIntIncrement(stats.events_untracked, 1);
+ if (status == Table::ProcessStatus::NO_CANDIDATE) {
+ TSStatIntIncrement(stats.scan_exhausted, 1);
+ }
+ return;
+ }
+ }
+
+ int burst = static_cast<int>(static_cast<double>(rate) *
metric_burst_multiplier(rule.filter, metric));
+ if constexpr (std::is_same_v<typename Table::data_type,
abuse_shield::H2Data>) {
+ slot->consume(rule.name, rate, burst, error_code);
+ } else {
+ slot->consume(rule.name, rate, burst);
+ }
+ consumed = true;
+ }
+
+ if (consumed) {
+ TSStatIntIncrement(stats.events, 1);
+ }
+}
+
+/** Check if a rule's filter criteria match for the given IP.
+ *
+ * A rule matches only if ALL enabled filter criteria are satisfied (AND
logic).
+ * Each filter uses token bucket rate limiting - a rate is exceeded when
tokens < 0.
+ * Fingerprint values and methods use OR logic with one another. The resulting
+ * fingerprint criterion is ANDed with any enabled rate criteria.
+ *
+ * @param[in] rule The rule containing filter criteria to check.
+ * @param[in] ip The IP address to evaluate.
+ * @return True if all enabled filter criteria are satisfied, false otherwise.
+ */
+bool
+rule_matches(const abuse_shield::Rule &rule, const swoc::IPAddr &ip, const
abuse_shield::Config &config,
+ const abuse_shield::FingerprintResults *fingerprints,
std::string_view &matched_method,
+ std::string_view &matched_fingerprint)
+{
+ const auto &f = rule.filter;
+
+ if (!config.rule_applies_to_ip(rule, ip)) {
+ return false;
+ }
+
+ if (f.max_req_rate == 0 && f.max_conn_rate == 0 && f.max_h2_error_rate == 0
&& !f.has_fingerprints()) {
+ return false;
+ }
+
+ if (f.has_fingerprints()) {
+ if (!fingerprints) {
+ return false;
+ }
+
+ bool fingerprint_matched = false;
+ for (const auto &[method, configured_values] : f.fingerprints) {
+ auto computed = fingerprints->find(method);
+ if (computed != fingerprints->end() &&
configured_values.contains(computed->second)) {
+ matched_method = computed->first;
+ matched_fingerprint = computed->second;
+ fingerprint_matched = true;
+ break;
+ }
+ }
+ if (!fingerprint_matched) {
+ return false;
+ }
+ }
+
+ if (f.max_req_rate > 0 && !rate_exceeded(g_txn_tracker.get(), rule, ip)) {
+ return false;
+ }
+ if (f.max_conn_rate > 0 && !rate_exceeded(g_conn_tracker.get(), rule, ip)) {
+ return false;
+ }
+ if (f.max_h2_error_rate > 0 && !rate_exceeded(g_h2_tracker.get(), rule, ip))
{
+ return false;
+ }
+
+ return true; // All enabled filters matched
+}
+
+abuse_shield::RuleMatch
+evaluate_rate_rules(const swoc::IPAddr &ip, const abuse_shield::Config &config)
+{
+ for (const auto &rule : config.rules()) {
+ if (rule.filter.has_fingerprints()) {
+ continue;
+ }
+
+ std::string_view matched_method;
+ std::string_view matched_fingerprint;
+ if (rule_matches(rule, ip, config, nullptr, matched_method,
matched_fingerprint)) {
+ Dbg(dbg_ctl, "Rule matched: %s", rule.name.c_str());
+ return abuse_shield::RuleMatch{&rule, rule.actions, {}, {}};
+ }
+ }
+ return abuse_shield::RuleMatch{};
+}
+
+abuse_shield::RuleMatch
+evaluate_fingerprint_rules(const swoc::IPAddr &ip, const abuse_shield::Config
&config,
+ const abuse_shield::FingerprintResults
&fingerprints)
+{
+ for (const auto &rule : config.rules()) {
+ if (!rule.filter.has_fingerprints()) {
+ continue;
+ }
+
+ std::string_view matched_method;
+ std::string_view matched_fingerprint;
+ if (rule_matches(rule, ip, config, &fingerprints, matched_method,
matched_fingerprint)) {
+ Dbg(dbg_ctl, "Fingerprint rule matched: %s", rule.name.c_str());
+ return abuse_shield::RuleMatch{&rule, rule.actions, matched_method,
matched_fingerprint};
+ }
+ }
+ return abuse_shield::RuleMatch{};
+}
+
+// ============================================================================
+// Action execution
+// ============================================================================
+
+/** Store block state independently from the evictable rate tables. */
+bool
+block_ip(const swoc::IPAddr &ip, uint64_t until_ms)
+{
+ return g_blocked_ips && g_blocked_ips->block(ip, until_ms);
+}
+
+/** Execute the log action while respecting the log rate limit.
+ *
+ * @param[in] match The rule match result.
+ * @param[in] ip The client IP address.
+ * @param[in] config The current configuration.
+ */
+void
+execute_log_action(const abuse_shield::RuleMatch &match, const swoc::IPAddr
&ip, const abuse_shield::Config &config)
+{
+ uint64_t log_interval_ms = static_cast<uint64_t>(config.log_interval_sec())
* 1000;
+ uint64_t now = abuse_shield::now_ms();
+
+ // Find the most recent log time across all trackers for this IP.
+ uint64_t most_recent_log = 0;
+ auto txn_slot = g_txn_tracker ? g_txn_tracker->find(ip) : nullptr;
+ auto conn_slot = g_conn_tracker ? g_conn_tracker->find(ip) :
nullptr;
+ auto h2_slot = g_h2_tracker ? g_h2_tracker->find(ip) : nullptr;
+
+ // Fingerprint-only rules do not otherwise need a rate-tracking slot. Use the
+ // bounded connection table to retain their per-IP log interval state.
+ if (!txn_slot && !conn_slot && !h2_slot && g_conn_tracker) {
+ conn_slot = g_conn_tracker->process_event(ip, 1);
+ }
+
+ if (txn_slot) {
+ most_recent_log = std::max(most_recent_log,
txn_slot->last_logged.load(std::memory_order_relaxed));
+ }
+ if (conn_slot) {
+ most_recent_log = std::max(most_recent_log,
conn_slot->last_logged.load(std::memory_order_relaxed));
+ }
+ if (h2_slot) {
+ most_recent_log = std::max(most_recent_log,
h2_slot->last_logged.load(std::memory_order_relaxed));
+ }
+
+ // Only log if enough time has passed since the last log for this IP.
+ if (!abuse_shield::log_interval_elapsed(now, most_recent_log,
log_interval_ms)) {
+ return;
+ }
+
+ // Try to claim the log opportunity atomically using the first available
slot.
+ bool claimed = false;
+
+ if (txn_slot) {
+ claimed = abuse_shield::claim_log_interval(txn_slot->last_logged, now,
log_interval_ms);
+ } else if (conn_slot) {
+ claimed = abuse_shield::claim_log_interval(conn_slot->last_logged, now,
log_interval_ms);
+ } else if (h2_slot) {
+ claimed = abuse_shield::claim_log_interval(h2_slot->last_logged, now,
log_interval_ms);
+ }
Review Comment:
execute_log_action() can double-log under concurrency because different call
sites can successfully claim the log interval on different per-table atomics
(txn_slot vs conn_slot vs h2_slot) before any of them store the new timestamp
into the others. That defeats the intended per-IP log rate limit.
Prefer claiming on a single per-IP atomic consistently (e.g., always
use/create conn_slot when possible), and only fall back to txn/h2 when the
connection slot cannot be obtained.
##########
plugins/experimental/abuse_shield/ip_data.cc:
##########
@@ -0,0 +1,137 @@
+/** @file
+
+ Token bucket rate limiting implementation for abuse detection.
+
+ @section license License
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
contributor license
+ agreements. See the NOTICE file distributed with this work for additional
information regarding
+ copyright ownership. The ASF licenses this file to you 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 "ip_data.h"
+
+#include <algorithm>
+#include <limits>
+
+namespace abuse_shield
+{
+
+namespace
+{
+ uint64_t
+ pack_state(uint32_t update_ms, int32_t tokens)
+ {
+ // Bias the token bits so a legitimate timestamp/token pair never collides
+ // with the all-zero uninitialized sentinel.
+ return (static_cast<uint64_t>(update_ms) << 32) |
(static_cast<uint32_t>(tokens) ^ 0x80000000U);
+ }
+
+ uint32_t
+ state_update_ms(uint64_t state)
+ {
+ return static_cast<uint32_t>(state >> 32);
+ }
+
+ int32_t
+ state_tokens(uint64_t state)
+ {
+ return static_cast<int32_t>(static_cast<uint32_t>(state) ^ 0x80000000U);
+ }
+} // namespace
+
+int32_t
+TokenBucket::consume(int rate_per_sec, int burst_limit)
+{
+ uint64_t old = state_.load(std::memory_order_relaxed);
+
+ while (true) {
+ // Refresh after every failed CAS so this thread never computes from a
+ // timestamp older than the state returned by compare_exchange_weak.
+ uint32_t now = static_cast<uint32_t>(now_ms());
+ int64_t current;
+
+ if (old == 0) {
+ current = burst_limit;
+ } else {
+ uint32_t elapsed_ms = now - state_update_ms(old);
+ uint64_t replenish = (static_cast<uint64_t>(elapsed_ms) *
static_cast<uint64_t>(rate_per_sec)) / 1000;
+
+ current = state_tokens(old);
+ if (replenish >= static_cast<uint64_t>(std::max<int64_t>(0,
static_cast<int64_t>(burst_limit) - current))) {
+ current = burst_limit;
+ } else {
+ current += static_cast<int64_t>(replenish);
+ }
+ }
+
+ current = std::max<int64_t>(std::numeric_limits<int32_t>::min(),
current - 1);
+ uint64_t desired = pack_state(now, static_cast<int32_t>(current));
+ if (state_.compare_exchange_weak(old, desired, std::memory_order_relaxed))
{
+ return static_cast<int32_t>(current);
+ }
+ }
+}
+
+int32_t
+TokenBucket::tokens() const
+{
+ return state_tokens(state_.load(std::memory_order_relaxed));
+}
Review Comment:
TokenBucket::tokens() decodes the packed state even when state_ is still the
all-zero sentinel. With the current encoding, state_==0 decodes to INT32_MIN
tokens, which can incorrectly report "debt" for a bucket that has never been
consumed and makes tokens() surprising for callers.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]