diff --git a/configs/records.yaml.default.in b/configs/records.yaml.default.in index f59a1ffbdc8..225ae802399 100644 --- a/configs/records.yaml.default.in +++ b/configs/records.yaml.default.in @@ -130,6 +130,7 @@ records: ############################################################################## parent_proxy: retry_time: 300 + consistent_hash_algorithm: siphash24 ############################################################################## # Security. Docs: diff --git a/configs/strategies.schema.json b/configs/strategies.schema.json index b9a8500c41c..8f1116c3a13 100644 --- a/configs/strategies.schema.json +++ b/configs/strategies.schema.json @@ -157,6 +157,29 @@ "url" ] }, + "hash_algorithm": { + "type": "string", + "description": "when using consistent_hash, this specifies the hash algorithm to use", + "enum": [ + "siphash24", + "siphash13" + ] + }, + "hash_seed0": { + "type": "integer", + "description": "when using consistent_hash, this specifies the first 64 bits of the hash seed (decimal integer)", + "minimum": 0 + }, + "hash_seed1": { + "type": "integer", + "description": "when using consistent_hash, this specifies the second 64 bits of the hash seed (decimal integer)", + "minimum": 0 + }, + "hash_replicas": { + "type": "integer", + "description": "when using consistent_hash, this specifies the number of virtual nodes (replicas) per host", + "minimum": 1 + }, "go_direct": { "type": "boolean", "description": "wether, true/false, users of the strategy may bypass parents and go directly to the origin" diff --git a/doc/admin-guide/files/parent.config.en.rst b/doc/admin-guide/files/parent.config.en.rst index 9fe59116ef6..7b8fa4a56d3 100644 --- a/doc/admin-guide/files/parent.config.en.rst +++ b/doc/admin-guide/files/parent.config.en.rst @@ -278,6 +278,12 @@ The following list shows the possible actions and their allowed values. The other traffic is unaffected. Once the downed parent becomes available, the traffic distribution returns to the pre-down state. + + The hash algorithm used for consistent hashing can be configured via + :ts:cv:`proxy.config.http.parent_proxy.consistent_hash_algorithm`. Available + algorithms are ``siphash24`` (default) and ``siphash13`` (faster). + See the records.yaml documentation for details. + - ``latched`` - The first parent in the list is marked as primary and is always chosen until connection errors cause it to be marked down. When this occurs the next parent in the list then becomes primary. The primary @@ -318,6 +324,62 @@ The following list shows the possible actions and their allowed values. - ``false`` - The default. Do not ignore the host status. +.. _parent-config-format-hash-algorithm: + +``hash_algorithm`` + When using ``round_robin=consistent_hash``, this specifies the hash algorithm to use + for this specific rule, overriding the global default set in :ts:cv:`proxy.config.http.parent_proxy.consistent_hash_algorithm`. + + Allowed values: + + - ``siphash24`` - SipHash-2-4 (default). Cryptographically strong, DoS-resistant. + - ``siphash13`` - SipHash-1-3. ~50% faster than SipHash-2-4, still DoS-resistant. + + Example:: + + dest_domain=. parent="p1.x.com:80,p2.x.com:80" round_robin=consistent_hash hash_algorithm=siphash13 + +.. _parent-config-format-hash-seed0: + +``hash_seed0`` + When using ``round_robin=consistent_hash``, this specifies the first 64 bits of the hash seed (key). + + - For SipHash algorithms, this is the first half of the 128-bit cryptographic key (k0) + - For future 64-bit algorithms, this will be the full seed value + + Value must be specified as a decimal integer (e.g. ``12345678901234567``). Default is ``0``. + + Example:: + + dest_domain=. parent="p1.x.com:80,p2.x.com:80" round_robin=consistent_hash hash_seed0=1234567890 + +.. _parent-config-format-hash-seed1: + +``hash_seed1`` + When using ``round_robin=consistent_hash``, this specifies the second 64 bits of the hash seed (key). + + - For SipHash algorithms, this is the second half of the 128-bit cryptographic key (k1) + - For future 64-bit algorithms, this value is ignored + + Value must be specified as a decimal integer (e.g. ``9876543210987654321``). Default is ``0``. + + Example:: + + dest_domain=. parent="p1.x.com:80,p2.x.com:80" round_robin=consistent_hash hash_seed0=1234567890 hash_seed1=9876543210 + +.. _parent-config-format-hash-replicas: + +``hash_replicas`` + When using ``round_robin=consistent_hash``, this specifies the number of virtual nodes (replicas) + per parent host on the consistent hash ring. + + Increasing the replica count improves the distribution of requests across parent proxies but uses + more memory. Must be greater than 0. Default is ``1024``. + + Example:: + + dest_domain=. parent="p1.x.com:80,p2.x.com:80" round_robin=consistent_hash hash_replicas=2048 + Examples ======== diff --git a/doc/admin-guide/files/records.yaml.en.rst b/doc/admin-guide/files/records.yaml.en.rst index 920ec5d7d40..4f7d7d187bb 100644 --- a/doc/admin-guide/files/records.yaml.en.rst +++ b/doc/admin-guide/files/records.yaml.en.rst @@ -1471,6 +1471,63 @@ Parent Proxy Configuration ``2`` Mark the host down. This is the default. ===== ====================================================================== +.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_algorithm STRING siphash24 + + Selects the hash algorithm used for consistent hash parent selection. This setting + only affects parent selection when ``round_robin=consistent_hash`` is configured in + :file:`parent.config`. The hash algorithm determines how requests are distributed + across parent proxies. + + ============== ================================================================================ + Value Description + ============== ================================================================================ + ``siphash24`` SipHash-2-4 (default). Cryptographically strong, DoS-resistant hash function. + ``siphash13`` SipHash-1-3. ~50% faster than SipHash-2-4, still DoS-resistant. + ============== ================================================================================ + + .. warning:: + + Changing this setting will cause requests to be redistributed differently across + parent proxies. This can lead to cache churn and increased origin load during the + transition period. Plan the migration carefully and consider doing it during + low-traffic periods. + +.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_seed0 INT 0 + + The first 64 bits of the hash seed (key) for consistent hash parent selection. + This setting only affects parent selection when ``round_robin=consistent_hash`` is configured. + + - For SipHash algorithms, this forms the first half of the 128-bit cryptographic key (k0) + - For future 64-bit hash algorithms (like XXH3), this is the full seed value + + The value must be specified as a decimal integer (e.g. ``12345678901234567``). Default is ``0``. + Per-rule configuration is available in :file:`parent.config` using ``hash_seed0=``. + Per-strategy configuration is available in :file:`strategies.yaml` using ``hash_seed0: ``. + +.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_seed1 INT 0 + + The second 64 bits of the hash seed (key) for consistent hash parent selection. + This setting only affects parent selection when ``round_robin=consistent_hash`` is configured. + + - For SipHash algorithms, this forms the second half of the 128-bit cryptographic key (k1) + - For future 64-bit hash algorithms, this value is ignored + + The value must be specified as a decimal integer (e.g. ``9876543210987654321``). Default is ``0``. + Per-rule configuration is available in :file:`parent.config` using ``hash_seed1=``. + Per-strategy configuration is available in :file:`strategies.yaml` using ``hash_seed1: ``. + +.. ts:cv:: CONFIG proxy.config.http.parent_proxy.consistent_hash_replicas INT 1024 + + The number of virtual nodes (replicas) per parent host on the consistent hash ring. + This setting only affects parent selection when ``round_robin=consistent_hash`` is configured. + + Increasing the replica count improves the distribution of requests across parent proxies + but uses more memory. The default value of 1024 provides good distribution in most scenarios. + + Must be greater than 0. Default is ``1024``. + Per-rule configuration is available in :file:`parent.config` using ``hash_replicas=``. + Per-strategy configuration is available in :file:`strategies.yaml` using ``hash_replicas: ``. + .. ts:cv:: CONFIG proxy.config.http.parent_proxy.enable_parent_timeout_markdowns INT 0 :reloadable: :overridable: diff --git a/doc/admin-guide/files/strategies.yaml.en.rst b/doc/admin-guide/files/strategies.yaml.en.rst index 5f3fad0c0dc..ec7c223c8a6 100644 --- a/doc/admin-guide/files/strategies.yaml.en.rst +++ b/doc/admin-guide/files/strategies.yaml.en.rst @@ -201,6 +201,62 @@ Each **strategy** in the list may using the following parameters: #. **parent**: Use the parent URL as set via the API :cpp:func:`TSHttpTxnParentSelectionUrlSet`. This again is likely set via an existing plugin such as the **cachekey** plugin. +- **hash_algorithm**: The hash algorithm used by the **consistent_hash** policy. This parameter allows + per-strategy selection of the hash algorithm. If not specified, defaults to **siphash24** (or the value + set in :ts:cv:`proxy.config.http.parent_proxy.consistent_hash_algorithm`). Use one of: + + #. **siphash24**: (**default**) SipHash-2-4. Cryptographically strong, DoS-resistant hash function. + #. **siphash13**: SipHash-1-3. ~50% faster than SipHash-2-4, still DoS-resistant. + + Example: + + .. code-block:: yaml + + policy: consistent_hash + hash_algorithm: siphash13 + +- **hash_seed0**: The first 64 bits of the hash seed (key) for the **consistent_hash** policy. + + - For SipHash algorithms, this forms the first half of the 128-bit cryptographic key (k0) + - For future 64-bit hash algorithms, this is the full seed value + + Value must be specified as a decimal integer. Default is **0**. + + Example: + + .. code-block:: yaml + + policy: consistent_hash + hash_seed0: 12345678901234567 + +- **hash_seed1**: The second 64 bits of the hash seed (key) for the **consistent_hash** policy. + + - For SipHash algorithms, this forms the second half of the 128-bit cryptographic key (k1) + - For future 64-bit hash algorithms, this value is ignored + + Value must be specified as a decimal integer. Default is **0**. + + Example: + + .. code-block:: yaml + + policy: consistent_hash + hash_seed0: 12345678901234567 + hash_seed1: 9876543210987654321 + +- **hash_replicas**: The number of virtual nodes (replicas) per host on the consistent hash ring for + the **consistent_hash** policy. + + Increasing the replica count improves the distribution of requests across hosts but uses more memory. + Must be greater than 0. Default is **1024**. + + Example: + + .. code-block:: yaml + + policy: consistent_hash + hash_replicas: 2048 + - **go_direct**: A boolean value indicating whether a transaction may bypass proxies and go direct to the origin. Defaults to **true** - **parent_is_proxy**: A boolean value which indicates if the groups of hosts are proxy caches or origins. **true** (default) means all the hosts used in the remap are |TS| caches. **false** means the hosts are origins that the next hop strategies may use for load balancing and/or failover. - **cache_peer_result**: A boolean value that is only used when the **policy** is 'consistent_hash' and a **peering_ring** mode is used for the strategy. When set to true, the default, all responses from upstream and peer endpoints are allowed to be cached. Setting this to false will disable caching responses received from a peer host. Only responses from upstream origins or parents will be cached for this strategy. diff --git a/include/proxy/ParentConsistentHash.h b/include/proxy/ParentConsistentHash.h index afcc3873936..4a38c16a615 100644 --- a/include/proxy/ParentConsistentHash.h +++ b/include/proxy/ParentConsistentHash.h @@ -38,14 +38,15 @@ // class ParentConsistentHash : public ParentSelectionStrategy { - // there are two hashes PRIMARY parents - // and SECONDARY parents. - ATSHash64Sip24 hash[2]; + std::unique_ptr hash[2]; std::unique_ptr chash[2]; pRecord *parents[2]; bool foundParents[2][MAX_PARENTS]; bool ignore_query; int secondary_mode; + ParentHashAlgorithm selected_algorithm; + uint64_t hash_seed0; + uint64_t hash_seed1; public: static const int PRIMARY = 0; diff --git a/include/proxy/ParentSelection.h b/include/proxy/ParentSelection.h index 09726f50366..5960cfda189 100644 --- a/include/proxy/ParentSelection.h +++ b/include/proxy/ParentSelection.h @@ -35,11 +35,13 @@ #include "proxy/ControlMatcher.h" #include "records/RecProcess.h" #include "tscore/ConsistentHash.h" +#include "tscore/Hash.h" #include "tscore/Tokenizer.h" #include "tscore/ink_apidefs.h" #include "proxy/HostStatus.h" #include +#include #include #define MAX_PARENTS 64 @@ -73,6 +75,8 @@ enum class ParentRetry_t { BOTH = 3 }; +enum class ParentHashAlgorithm { SIPHASH24 = 0, SIPHASH13 }; + struct UnavailableServerResponseCodes { UnavailableServerResponseCodes(char *val); ~UnavailableServerResponseCodes(){}; @@ -163,6 +167,10 @@ class ParentRecord : public ControlBase int max_unavailable_server_retries = 1; int secondary_mode = 1; bool ignore_self_detect = false; + ParentHashAlgorithm consistent_hash_algorithm = ParentHashAlgorithm::SIPHASH24; + uint64_t consistent_hash_seed0 = 0; + uint64_t consistent_hash_seed1 = 0; + int consistent_hash_replicas = 1024; // Number of virtual nodes per host (int to match ATSConsistentHash constructor) }; // If the parent was set by the external customer api, @@ -444,6 +452,10 @@ struct ParentConfig { // Helper Functions ParentRecord *createDefaultParent(char *val); +// Hash utility functions +ParentHashAlgorithm parseHashAlgorithm(std::string_view name); +std::unique_ptr createHashInstance(ParentHashAlgorithm algo, uint64_t seed0, uint64_t seed1); + // Unit Test Functions void show_result(ParentResult *aParentResult); void br(HttpRequestData *h, const char *os_hostname, sockaddr const *dest_ip = nullptr); // short for build request diff --git a/include/proxy/http/remap/NextHopConsistentHash.h b/include/proxy/http/remap/NextHopConsistentHash.h index 7f3c29e8e1f..12d0ca9b5dd 100644 --- a/include/proxy/http/remap/NextHopConsistentHash.h +++ b/include/proxy/http/remap/NextHopConsistentHash.h @@ -46,8 +46,12 @@ class NextHopConsistentHash : public NextHopSelectionStrategy uint64_t getHashKey(uint64_t sm_id, const HttpRequestData &hrdata, ATSHash64 *h); public: - NHHashKeyType hash_key = NHHashKeyType::PATH_HASH_KEY; - NHHashUrlType hash_url = NHHashUrlType::REQUEST; + NHHashKeyType hash_key = NHHashKeyType::PATH_HASH_KEY; + NHHashUrlType hash_url = NHHashUrlType::REQUEST; + std::string hash_algorithm = "siphash24"; // Default hash algorithm name + uint64_t hash_seed0 = 0; // First 64 bits of hash seed + uint64_t hash_seed1 = 0; // Second 64 bits of hash seed + int hash_replicas = 1024; // Number of virtual nodes per host (int to match ATSConsistentHash constructor) NextHopConsistentHash() = delete; NextHopConsistentHash(const std::string_view name, const NHPolicyType &policy, ts::Yaml::Map &n); diff --git a/include/tscore/HashSip.h b/include/tscore/HashSip.h index ce94404dc83..a26d3c10577 100644 --- a/include/tscore/HashSip.h +++ b/include/tscore/HashSip.h @@ -23,22 +23,145 @@ #include "tscore/Hash.h" #include +#include /* - Siphash is a Hash Message Authentication Code and can take a key. + SipHash is a Hash Message Authentication Code and can take a key. If you don't care about MAC use the void constructor and it will use a zero key for you. - */ -struct ATSHash64Sip24 : ATSHash64 { - ATSHash64Sip24(); - ATSHash64Sip24(const unsigned char key[16]); - ATSHash64Sip24(std::uint64_t key0, std::uint64_t key1); - void update(const void *data, std::size_t len) override; - void final() override; - std::uint64_t get() const override; - void clear() override; + Template parameters: + - c_rounds: number of compression rounds per message block + - d_rounds: number of finalization rounds +*/ + +#define SIP_BLOCK_SIZE 8 + +#define ROTL64(a, b) (((a) << (b)) | ((a) >> (64 - b))) + +static inline std::uint64_t +U8TO64_LE(const std::uint8_t *p) +{ + std::uint64_t result; + std::memcpy(&result, p, sizeof(result)); + return result; +} + +#define SIPCOMPRESS(x0, x1, x2, x3) \ + x0 += x1; \ + x2 += x3; \ + x1 = ROTL64(x1, 13); \ + x3 = ROTL64(x3, 16); \ + x1 ^= x0; \ + x3 ^= x2; \ + x0 = ROTL64(x0, 32); \ + x2 += x1; \ + x0 += x3; \ + x1 = ROTL64(x1, 17); \ + x3 = ROTL64(x3, 21); \ + x1 ^= x2; \ + x3 ^= x0; \ + x2 = ROTL64(x2, 32); + +template struct ATSHashSip : ATSHash64 { + ATSHashSip() { this->clear(); } + + ATSHashSip(const unsigned char key[16]) : k0(U8TO64_LE(key)), k1(U8TO64_LE(key + sizeof(k0))) { this->clear(); } + + ATSHashSip(std::uint64_t key0, std::uint64_t key1) : k0(key0), k1(key1) { this->clear(); } + + void + update(const void *data, std::size_t len) override + { + std::size_t i, blocks; + unsigned char *m; + std::uint64_t mi; + std::uint8_t block_off = 0; + + if (!finalized) { + m = (unsigned char *)data; + total_len += len; + + if (len + block_buffer_len < SIP_BLOCK_SIZE) { + std::memcpy(block_buffer + block_buffer_len, m, len); + block_buffer_len += len; + } else { + if (block_buffer_len > 0) { + block_off = SIP_BLOCK_SIZE - block_buffer_len; + std::memcpy(block_buffer + block_buffer_len, m, block_off); + + mi = U8TO64_LE(block_buffer); + v3 ^= mi; + for (int r = 0; r < c_rounds; r++) { + SIPCOMPRESS(v0, v1, v2, v3); + } + v0 ^= mi; + } + + for (i = block_off, blocks = ((len - block_off) & ~(SIP_BLOCK_SIZE - 1)); i < blocks; i += SIP_BLOCK_SIZE) { + mi = U8TO64_LE(m + i); + v3 ^= mi; + for (int r = 0; r < c_rounds; r++) { + SIPCOMPRESS(v0, v1, v2, v3); + } + v0 ^= mi; + } + + block_buffer_len = (len - block_off) & (SIP_BLOCK_SIZE - 1); + std::memcpy(block_buffer, m + block_off + blocks, block_buffer_len); + } + } + } + + void + final() override + { + std::uint64_t last7; + int i; + + if (!finalized) { + last7 = static_cast(total_len & 0xff) << 56; + + for (i = block_buffer_len - 1; i >= 0; i--) { + last7 |= static_cast(block_buffer[i]) << (i * 8); + } + + v3 ^= last7; + for (int r = 0; r < c_rounds; r++) { + SIPCOMPRESS(v0, v1, v2, v3); + } + v0 ^= last7; + v2 ^= 0xff; + for (int r = 0; r < d_rounds; r++) { + SIPCOMPRESS(v0, v1, v2, v3); + } + hfinal = v0 ^ v1 ^ v2 ^ v3; + finalized = true; + } + } + + std::uint64_t + get() const override + { + if (finalized) { + return hfinal; + } else { + return 0; + } + } + + void + clear() override + { + v0 = k0 ^ 0x736f6d6570736575ull; + v1 = k1 ^ 0x646f72616e646f6dull; + v2 = k0 ^ 0x6c7967656e657261ull; + v3 = k1 ^ 0x7465646279746573ull; + finalized = false; + total_len = 0; + block_buffer_len = 0; + } private: unsigned char block_buffer[8] = {0}; @@ -53,3 +176,7 @@ struct ATSHash64Sip24 : ATSHash64 { std::size_t total_len = 0; bool finalized = false; }; + +// Standard SipHash variants +using ATSHash64Sip24 = ATSHashSip<2, 4>; +using ATSHash64Sip13 = ATSHashSip<1, 3>; diff --git a/src/proxy/CMakeLists.txt b/src/proxy/CMakeLists.txt index f145034fb67..93c16697845 100644 --- a/src/proxy/CMakeLists.txt +++ b/src/proxy/CMakeLists.txt @@ -55,4 +55,8 @@ if(TS_USE_QUIC) add_subdirectory(http3) endif() +if(BUILD_TESTING) + add_subdirectory(unit_tests) +endif() + clang_tidy_check(proxy) diff --git a/src/proxy/ParentConsistentHash.cc b/src/proxy/ParentConsistentHash.cc index a9dab9a1d3a..ce0cc46c216 100644 --- a/src/proxy/ParentConsistentHash.cc +++ b/src/proxy/ParentConsistentHash.cc @@ -23,6 +23,7 @@ #include #include "proxy/HostStatus.h" #include "proxy/ParentConsistentHash.h" +#include "tscore/HashSip.h" namespace { @@ -38,21 +39,26 @@ ParentConsistentHash::ParentConsistentHash(ParentRecord *parent_record) parents[SECONDARY] = parent_record->secondary_parents; ignore_query = parent_record->ignore_query; secondary_mode = parent_record->secondary_mode; + selected_algorithm = parent_record->consistent_hash_algorithm; + hash_seed0 = parent_record->consistent_hash_seed0; + hash_seed1 = parent_record->consistent_hash_seed1; ink_zero(foundParents); - chash[PRIMARY] = std::make_unique(); + hash[PRIMARY] = createHashInstance(selected_algorithm, hash_seed0, hash_seed1); + chash[PRIMARY] = std::make_unique(parent_record->consistent_hash_replicas); for (i = 0; i < parent_record->num_parents; i++) { - chash[PRIMARY]->insert(&(parent_record->parents[i]), parent_record->parents[i].weight, (ATSHash64 *)&hash[PRIMARY]); + chash[PRIMARY]->insert(&(parent_record->parents[i]), parent_record->parents[i].weight, hash[PRIMARY].get()); } if (parent_record->num_secondary_parents > 0) { Dbg(dbg_ctl_parent_select, "ParentConsistentHash(): initializing the secondary parents hash."); - chash[SECONDARY] = std::make_unique(); + hash[SECONDARY] = createHashInstance(selected_algorithm, hash_seed0, hash_seed1); + chash[SECONDARY] = std::make_unique(parent_record->consistent_hash_replicas); for (i = 0; i < parent_record->num_secondary_parents; i++) { chash[SECONDARY]->insert(&(parent_record->secondary_parents[i]), parent_record->secondary_parents[i].weight, - (ATSHash64 *)&hash[SECONDARY]); + hash[SECONDARY].get()); } } else { chash[SECONDARY] = nullptr; @@ -110,8 +116,8 @@ ParentConsistentHash::getPathHash(HttpRequestData *hrdata, ATSHash64 *h) // Helper function to abstract calling ATSConsistentHash lookup_by_hashval() vs lookup(). static pRecord * -chash_lookup(ATSConsistentHash *fhash, uint64_t path_hash, ATSConsistentHashIter *chashIter, bool *wrap_around, - ATSHash64Sip24 *hash, bool *chash_init, bool *mapWrapped) +chash_lookup(ATSConsistentHash *fhash, uint64_t path_hash, ATSConsistentHashIter *chashIter, bool *wrap_around, ATSHash64 *hash, + bool *chash_init, bool *mapWrapped) { pRecord *prtmp; @@ -134,18 +140,18 @@ void ParentConsistentHash::selectParent(bool first_call, ParentResult *result, RequestData *rdata, unsigned int /* fail_threshold ATS_UNUSED */, unsigned int retry_time) { - ATSHash64Sip24 hash; - ATSConsistentHash *fhash; - HttpRequestData *request_info = static_cast(rdata); - bool firstCall = first_call; - bool parentRetry = false; - bool wrap_around[2] = {false, false}; - int lookups = 0; - uint64_t path_hash = 0; - uint32_t last_lookup; - pRecord *prtmp = nullptr, *pRec = nullptr; - HostStatus &pStatus = HostStatus::instance(); - TSHostStatus host_stat = TSHostStatus::TS_HOST_STATUS_INIT; + std::unique_ptr hash = createHashInstance(selected_algorithm, hash_seed0, hash_seed1); + ATSConsistentHash *fhash; + HttpRequestData *request_info = static_cast(rdata); + bool firstCall = first_call; + bool parentRetry = false; + bool wrap_around[2] = {false, false}; + int lookups = 0; + uint64_t path_hash = 0; + uint32_t last_lookup; + pRecord *prtmp = nullptr, *pRec = nullptr; + HostStatus &pStatus = HostStatus::instance(); + TSHostStatus host_stat = TSHostStatus::TS_HOST_STATUS_INIT; Dbg(dbg_ctl_parent_select, "ParentConsistentHash::%s(): Using a consistent hash parent selection strategy.", __func__); ink_assert(numParents(result) > 0 || result->rec->go_direct == true); @@ -193,10 +199,10 @@ ParentConsistentHash::selectParent(bool first_call, ParentResult *result, Reques } // Do the initial parent look-up. - path_hash = getPathHash(request_info, (ATSHash64 *)&hash); + path_hash = getPathHash(request_info, hash.get()); fhash = chash[last_lookup].get(); do { // search until we've selected a different parent if !firstCall - prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup], &wrap_around[last_lookup], &hash, + prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup], &wrap_around[last_lookup], hash.get(), &result->chash_init[last_lookup], &result->mapWrapped[last_lookup]); lookups++; if (prtmp) { @@ -283,7 +289,7 @@ ParentConsistentHash::selectParent(bool first_call, ParentResult *result, Reques } } fhash = chash[last_lookup].get(); - prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup], &wrap_around[last_lookup], &hash, + prtmp = chash_lookup(fhash, path_hash, &result->chashIter[last_lookup], &wrap_around[last_lookup], hash.get(), &result->chash_init[last_lookup], &result->mapWrapped[last_lookup]); lookups++; if (prtmp) { diff --git a/src/proxy/ParentSelection.cc b/src/proxy/ParentSelection.cc index a83f0f2a728..0870bcb9bf1 100644 --- a/src/proxy/ParentSelection.cc +++ b/src/proxy/ParentSelection.cc @@ -56,6 +56,33 @@ DbgCtl ParentResult::dbg_ctl_parent_select{"parent_select"}; static DbgCtl &dbg_ctl_parent_select{ParentResult::dbg_ctl_parent_select}; static DbgCtl dbg_ctl_parent_config{"parent_config"}; +ParentHashAlgorithm +parseHashAlgorithm(std::string_view name) +{ + if (name == "siphash24") { + return ParentHashAlgorithm::SIPHASH24; + } else if (name == "siphash13") { + return ParentHashAlgorithm::SIPHASH13; + } else { + Warning("Unknown hash algorithm '%.*s', defaulting to siphash24", static_cast(name.size()), name.data()); + return ParentHashAlgorithm::SIPHASH24; + } +} + +std::unique_ptr +createHashInstance(ParentHashAlgorithm algo, uint64_t seed0, uint64_t seed1) +{ + switch (algo) { + case ParentHashAlgorithm::SIPHASH24: + return std::make_unique(seed0, seed1); + case ParentHashAlgorithm::SIPHASH13: + return std::make_unique(seed0, seed1); + default: + Warning("Unknown hash algorithm %d, using SipHash-2-4", static_cast(algo)); + return std::make_unique(seed0, seed1); + } +} + ParentSelectionPolicy::ParentSelectionPolicy() { int32_t retry_time = 0; @@ -647,6 +674,22 @@ ParentRecord::Init(matcher_line *line_info) self_detect = static_cast(rec_self_detect.value()); } + if (auto rec_hash_algo{RecGetRecordStringAlloc("proxy.config.http.parent_proxy.consistent_hash_algorithm")}; rec_hash_algo) { + consistent_hash_algorithm = parseHashAlgorithm(rec_hash_algo.value()); + } + + if (auto rec_hash_seed0{RecGetRecordInt("proxy.config.http.parent_proxy.consistent_hash_seed0")}; rec_hash_seed0) { + consistent_hash_seed0 = static_cast(rec_hash_seed0.value()); + } + + if (auto rec_hash_seed1{RecGetRecordInt("proxy.config.http.parent_proxy.consistent_hash_seed1")}; rec_hash_seed1) { + consistent_hash_seed1 = static_cast(rec_hash_seed1.value()); + } + + if (auto rec_hash_replicas{RecGetRecordInt("proxy.config.http.parent_proxy.consistent_hash_replicas")}; rec_hash_replicas) { + consistent_hash_replicas = static_cast(rec_hash_replicas.value()); + } + for (int i = 0; i < MATCHER_MAX_TOKENS; i++) { used = false; label = line_info->line[0][i]; @@ -754,6 +797,23 @@ ParentRecord::Init(matcher_line *line_info) ignore_self_detect = false; } used = true; + } else if (strcasecmp(label, "hash_algorithm") == 0) { + consistent_hash_algorithm = parseHashAlgorithm(val); + used = true; + } else if (strcasecmp(label, "hash_seed0") == 0) { + consistent_hash_seed0 = static_cast(strtoull(val, nullptr, 10)); + used = true; + } else if (strcasecmp(label, "hash_seed1") == 0) { + consistent_hash_seed1 = static_cast(strtoull(val, nullptr, 10)); + used = true; + } else if (strcasecmp(label, "hash_replicas") == 0) { + int v = atoi(val); + if (v > 0) { + consistent_hash_replicas = v; + used = true; + } else { + errPtr = "invalid argument to hash_replicas directive. Argument must be greater than 0."; + } } // Report errors generated by ProcessParents(); if (errPtr != nullptr) { diff --git a/src/proxy/http/remap/NextHopConsistentHash.cc b/src/proxy/http/remap/NextHopConsistentHash.cc index 855655063d1..848e78a751f 100644 --- a/src/proxy/http/remap/NextHopConsistentHash.cc +++ b/src/proxy/http/remap/NextHopConsistentHash.cc @@ -27,6 +27,7 @@ #include "iocore/utils/Machine.h" #include "tsutil/YamlCfg.h" #include "proxy/http/remap/NextHopConsistentHash.h" +#include "proxy/ParentSelection.h" // hash_key strings. constexpr std::string_view hash_key_url = "url"; @@ -57,17 +58,17 @@ std::shared_ptr NextHopConsistentHash::chashLookup(const std::shared_ptr &ring, uint32_t cur_ring, ParentResult &result, HttpRequestData &request_info, bool *wrapped, uint64_t sm_id) { - uint64_t hash_key = 0; - ATSHash64Sip24 hash; - HostRecord *host_rec = nullptr; - ATSConsistentHashIter *iter = &result.chashIter[cur_ring]; + uint64_t hash_key = 0; + std::unique_ptr hash = createHashInstance(parseHashAlgorithm(hash_algorithm), hash_seed0, hash_seed1); + HostRecord *host_rec = nullptr; + ATSConsistentHashIter *iter = &result.chashIter[cur_ring]; if (result.chash_init[cur_ring] == false) { - hash_key = getHashKey(sm_id, request_info, &hash); + hash_key = getHashKey(sm_id, request_info, hash.get()); host_rec = static_cast(ring->lookup_by_hashval(hash_key, iter, wrapped)); result.chash_init[cur_ring] = true; } else { - host_rec = static_cast(ring->lookup(nullptr, iter, wrapped, &hash)); + host_rec = static_cast(ring->lookup(nullptr, iter, wrapped, hash.get())); } bool wrap_around = *wrapped; *wrapped = (result.mapWrapped[cur_ring] && *wrapped) ? true : false; @@ -91,7 +92,7 @@ NextHopConsistentHash::~NextHopConsistentHash() NextHopConsistentHash::NextHopConsistentHash(const std::string_view name, const NHPolicyType &policy, ts::Yaml::Map &n) : NextHopSelectionStrategy(name, policy, n) { - ATSHash64Sip24 hash; + std::unique_ptr hash; try { if (n["hash_url"]) { @@ -140,9 +141,61 @@ NextHopConsistentHash::NextHopConsistentHash(const std::string_view name, const "', this strategy will be ignored."); } + // Parse hash_algorithm + try { + if (n["hash_algorithm"]) { + hash_algorithm = n["hash_algorithm"].Scalar(); + if (hash_algorithm != "siphash24" && hash_algorithm != "siphash13") { + NH_Note("Invalid 'hash_algorithm' value, '%s', for the strategy named '%s', using default 'siphash24'.", + hash_algorithm.c_str(), strategy_name.c_str()); + hash_algorithm = "siphash24"; + } + } + } catch (std::exception &ex) { + throw std::invalid_argument("Error parsing the strategy named '" + strategy_name + "' due to '" + ex.what() + + "', this strategy will be ignored."); + } + + // Parse hash_seed0 + try { + if (n["hash_seed0"]) { + hash_seed0 = n["hash_seed0"].as(); + } + } catch (std::exception &ex) { + throw std::invalid_argument("Error parsing the strategy named '" + strategy_name + "' due to '" + ex.what() + + "', this strategy will be ignored."); + } + + // Parse hash_seed1 + try { + if (n["hash_seed1"]) { + hash_seed1 = n["hash_seed1"].as(); + } + } catch (std::exception &ex) { + throw std::invalid_argument("Error parsing the strategy named '" + strategy_name + "' due to '" + ex.what() + + "', this strategy will be ignored."); + } + + // Parse hash_replicas + try { + if (n["hash_replicas"]) { + hash_replicas = n["hash_replicas"].as(); + if (hash_replicas <= 0) { + NH_Note("Invalid 'hash_replicas' value, %d, for the strategy named '%s', must be > 0, using default 1024.", hash_replicas, + strategy_name.c_str()); + hash_replicas = 1024; + } + } + } catch (std::exception &ex) { + throw std::invalid_argument("Error parsing the strategy named '" + strategy_name + "' due to '" + ex.what() + + "', this strategy will be ignored."); + } + + hash = createHashInstance(parseHashAlgorithm(hash_algorithm), hash_seed0, hash_seed1); + // load up the hash rings. for (uint32_t i = 0; i < groups; i++) { - std::shared_ptr hash_ring = std::make_shared(); + std::shared_ptr hash_ring = std::make_shared(hash_replicas); for (uint32_t j = 0; j < host_groups[i].size(); j++) { // ATSConsistentHash needs the raw pointer. HostRecord *p = host_groups[i][j].get(); @@ -154,11 +207,11 @@ NextHopConsistentHash::NextHopConsistentHash(const std::string_view name, const } p->group_index = host_groups[i][j]->group_index; p->host_index = host_groups[i][j]->host_index; - hash_ring->insert(p, p->weight, &hash); + hash_ring->insert(p, p->weight, hash.get()); NH_Dbg(NH_DBG_CTL, "Loading hash rings - ring: %d, host record: %d, name: %s, hostname: %s, strategy: %s", i, j, p->name, p->hostname.c_str(), strategy_name.c_str()); } - hash.clear(); + hash->clear(); rings.push_back(std::move(hash_ring)); } } diff --git a/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc b/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc index 9302ca14470..d5945d30c6c 100644 --- a/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc +++ b/src/proxy/http/remap/unit-tests/nexthop_test_stubs.cc @@ -209,3 +209,31 @@ HostStatus::setHostStatus(const std::string_view host, TSHostStatus status, unsi NH_Dbg(DbgCtl{"next_hop"}, "setting host status for '%.*s' to %s", static_cast(host.size()), host.data(), HostStatusNames[status]); } + +// Stub implementations for Parent Selection hash utilities +#include "proxy/ParentSelection.h" +#include "tscore/Hash.h" +#include "tscore/HashSip.h" + +ParentHashAlgorithm +parseHashAlgorithm(std::string_view name) +{ + if (name == "siphash13") { + return ParentHashAlgorithm::SIPHASH13; + } else { + return ParentHashAlgorithm::SIPHASH24; + } +} + +std::unique_ptr +createHashInstance(ParentHashAlgorithm algo, uint64_t seed0, uint64_t seed1) +{ + switch (algo) { + case ParentHashAlgorithm::SIPHASH24: + return std::make_unique(seed0, seed1); + case ParentHashAlgorithm::SIPHASH13: + return std::make_unique(seed0, seed1); + default: + return std::make_unique(seed0, seed1); + } +} diff --git a/src/proxy/unit_tests/CMakeLists.txt b/src/proxy/unit_tests/CMakeLists.txt new file mode 100644 index 00000000000..b21fb327155 --- /dev/null +++ b/src/proxy/unit_tests/CMakeLists.txt @@ -0,0 +1,28 @@ +####################### +# +# 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. +# +####################### + +add_executable( + test_proxy main.cc test_ParentHashConfig.cc "${PROJECT_SOURCE_DIR}/src/iocore/net/libinknet_stub.cc" stub.cc +) + +target_link_libraries(test_proxy PRIVATE Catch2::Catch2WithMain ts::http ts::proxy ts::tscore ts::records ts::inkevent) + +if(NOT APPLE) + target_link_options(test_proxy PRIVATE -Wl,--allow-multiple-definition) +endif() + +add_catch2_test(NAME test_proxy COMMAND $) diff --git a/src/proxy/unit_tests/main.cc b/src/proxy/unit_tests/main.cc new file mode 100644 index 00000000000..a4b3b9453c2 --- /dev/null +++ b/src/proxy/unit_tests/main.cc @@ -0,0 +1,51 @@ +/** @file + + The main file for proxy unit tests + + @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 +#include +#include + +#include "tscore/Layout.h" +#include "iocore/eventsystem/EventSystem.h" +#include "records/RecordsConfig.h" +#include "iocore/utils/diags.i" + +struct DiagnosticsListener : Catch::EventListenerBase { + using EventListenerBase::EventListenerBase; + + void + testRunStarting(Catch::TestRunInfo const & /* testRunInfo */) override + { + Layout::create(); + init_diags("", nullptr); + RecProcessInit(); + LibRecordsConfigInit(); + } + + void + testRunEnded(Catch::TestRunStats const & /* testRunStats */) override + { + } +}; + +CATCH_REGISTER_LISTENER(DiagnosticsListener); diff --git a/src/proxy/unit_tests/stub.cc b/src/proxy/unit_tests/stub.cc new file mode 100644 index 00000000000..a1a95fe8ccb --- /dev/null +++ b/src/proxy/unit_tests/stub.cc @@ -0,0 +1,26 @@ +/** @file + + Stub file for test_proxy + + @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 "proxy/IPAllow.h" + +uint8_t IpAllow::subjects[IpAllow::Subject::MAX_SUBJECTS]; diff --git a/src/proxy/unit_tests/test_ParentHashConfig.cc b/src/proxy/unit_tests/test_ParentHashConfig.cc new file mode 100644 index 00000000000..31e9ab1496c --- /dev/null +++ b/src/proxy/unit_tests/test_ParentHashConfig.cc @@ -0,0 +1,62 @@ +/** @file + + Unit tests for Parent Selection hash algorithm configuration + + @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 "proxy/ParentSelection.h" +#include + +// Helper function to test parseHashAlgorithm (normally static, exposed here for testing) +extern ParentHashAlgorithm parseHashAlgorithm(std::string_view name); + +TEST_CASE("parseHashAlgorithm - Valid inputs", "[ParentSelection]") +{ + REQUIRE(parseHashAlgorithm("siphash24") == ParentHashAlgorithm::SIPHASH24); + REQUIRE(parseHashAlgorithm("siphash13") == ParentHashAlgorithm::SIPHASH13); +} + +TEST_CASE("parseHashAlgorithm - Invalid inputs fallback to default", "[ParentSelection]") +{ + REQUIRE(parseHashAlgorithm("invalid") == ParentHashAlgorithm::SIPHASH24); + REQUIRE(parseHashAlgorithm("") == ParentHashAlgorithm::SIPHASH24); + REQUIRE(parseHashAlgorithm("SIPHASH24") == ParentHashAlgorithm::SIPHASH24); // case sensitive + REQUIRE(parseHashAlgorithm("xxh3") == ParentHashAlgorithm::SIPHASH24); // not yet implemented + REQUIRE(parseHashAlgorithm("md5") == ParentHashAlgorithm::SIPHASH24); +} + +TEST_CASE("parseHashAlgorithm - Case sensitivity", "[ParentSelection]") +{ + // Should be case-sensitive - uppercase should fall back to default + REQUIRE(parseHashAlgorithm("SipHash24") == ParentHashAlgorithm::SIPHASH24); +} + +TEST_CASE("ParentHashAlgorithm - Backward compatibility", "[ParentSelection]") +{ + // Verify default is siphash24 for backward compatibility + REQUIRE(parseHashAlgorithm("siphash24") == ParentHashAlgorithm::SIPHASH24); + + // Verify enum default value is SIPHASH24 + ParentHashAlgorithm default_algo = ParentHashAlgorithm::SIPHASH24; + REQUIRE(static_cast(default_algo) == 0); + + // Verify that unrecognized values fall back to siphash24 + REQUIRE(parseHashAlgorithm("unknown") == ParentHashAlgorithm::SIPHASH24); +} diff --git a/src/records/RecordsConfig.cc b/src/records/RecordsConfig.cc index fbc2d3eb653..ed54615f124 100644 --- a/src/records/RecordsConfig.cc +++ b/src/records/RecordsConfig.cc @@ -436,6 +436,8 @@ static constexpr RecordElement RecordsConfig[] = , {RECT_CONFIG, "proxy.config.http.parent_proxy.disable_parent_markdowns", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_INT, "[0-1]", RECA_NULL} , + {RECT_CONFIG, "proxy.config.http.parent_proxy.consistent_hash_algorithm", RECD_STRING, "siphash24", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL} + , {RECT_CONFIG, "proxy.config.http.forward.proxy_auth_to_parent", RECD_INT, "0", RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL} , diff --git a/src/tscore/CMakeLists.txt b/src/tscore/CMakeLists.txt index d5d1e2b85d1..66576d06b58 100644 --- a/src/tscore/CMakeLists.txt +++ b/src/tscore/CMakeLists.txt @@ -44,7 +44,6 @@ add_library( FrequencyCounter.cc Hash.cc HashFNV.cc - HashSip.cc HostLookup.cc InkErrno.cc JeMiAllocator.cc @@ -155,6 +154,7 @@ if(BUILD_TESTING) unit_tests/test_Extendible.cc unit_tests/test_Encoding.cc unit_tests/test_FrequencyCounter.cc + unit_tests/test_HashAlgorithms.cc unit_tests/test_HKDF.cc unit_tests/test_Histogram.cc unit_tests/test_History.cc diff --git a/src/tscore/HashSip.cc b/src/tscore/HashSip.cc deleted file mode 100644 index c2487255fe9..00000000000 --- a/src/tscore/HashSip.cc +++ /dev/null @@ -1,141 +0,0 @@ -/** - -Algorithm Info: -https://131002.net/siphash/ - -Based off of implementation: -https://github.com/floodyberry/siphash - - */ - -#include "tscore/HashSip.h" -#include - -using namespace std; - -#define SIP_BLOCK_SIZE 8 - -#define ROTL64(a, b) (((a) << (b)) | ((a) >> (64 - b))) - -#define U8TO64_LE(p) *(const uint64_t *)(p) - -#define SIPCOMPRESS(x0, x1, x2, x3) \ - x0 += x1; \ - x2 += x3; \ - x1 = ROTL64(x1, 13); \ - x3 = ROTL64(x3, 16); \ - x1 ^= x0; \ - x3 ^= x2; \ - x0 = ROTL64(x0, 32); \ - x2 += x1; \ - x0 += x3; \ - x1 = ROTL64(x1, 17); \ - x3 = ROTL64(x3, 21); \ - x1 ^= x2; \ - x3 ^= x0; \ - x2 = ROTL64(x2, 32); - -ATSHash64Sip24::ATSHash64Sip24() -{ - this->clear(); -} - -ATSHash64Sip24::ATSHash64Sip24(const unsigned char key[16]) : k0(U8TO64_LE(key)), k1(U8TO64_LE(key + sizeof(k0))) -{ - this->clear(); -} - -ATSHash64Sip24::ATSHash64Sip24(uint64_t key0, uint64_t key1) : k0(key0), k1(key1) -{ - this->clear(); -} - -void -ATSHash64Sip24::update(const void *data, size_t len) -{ - size_t i, blocks; - unsigned char *m; - uint64_t mi; - uint8_t block_off = 0; - - if (!finalized) { - m = (unsigned char *)data; - total_len += len; - - if (len + block_buffer_len < SIP_BLOCK_SIZE) { - memcpy(block_buffer + block_buffer_len, m, len); - block_buffer_len += len; - } else { - if (block_buffer_len > 0) { - block_off = SIP_BLOCK_SIZE - block_buffer_len; - memcpy(block_buffer + block_buffer_len, m, block_off); - - mi = U8TO64_LE(block_buffer); - v3 ^= mi; - SIPCOMPRESS(v0, v1, v2, v3); - SIPCOMPRESS(v0, v1, v2, v3); - v0 ^= mi; - } - - for (i = block_off, blocks = ((len - block_off) & ~(SIP_BLOCK_SIZE - 1)); i < blocks; i += SIP_BLOCK_SIZE) { - mi = U8TO64_LE(m + i); - v3 ^= mi; - SIPCOMPRESS(v0, v1, v2, v3); - SIPCOMPRESS(v0, v1, v2, v3); - v0 ^= mi; - } - - block_buffer_len = (len - block_off) & (SIP_BLOCK_SIZE - 1); - memcpy(block_buffer, m + block_off + blocks, block_buffer_len); - } - } -} - -void -ATSHash64Sip24::final() -{ - uint64_t last7; - int i; - - if (!finalized) { - last7 = static_cast(total_len & 0xff) << 56; - - for (i = block_buffer_len - 1; i >= 0; i--) { - last7 |= static_cast(block_buffer[i]) << (i * 8); - } - - v3 ^= last7; - SIPCOMPRESS(v0, v1, v2, v3); - SIPCOMPRESS(v0, v1, v2, v3); - v0 ^= last7; - v2 ^= 0xff; - SIPCOMPRESS(v0, v1, v2, v3); - SIPCOMPRESS(v0, v1, v2, v3); - SIPCOMPRESS(v0, v1, v2, v3); - SIPCOMPRESS(v0, v1, v2, v3); - hfinal = v0 ^ v1 ^ v2 ^ v3; - finalized = true; - } -} - -uint64_t -ATSHash64Sip24::get() const -{ - if (finalized) { - return hfinal; - } else { - return 0; - } -} - -void -ATSHash64Sip24::clear() -{ - v0 = k0 ^ 0x736f6d6570736575ull; - v1 = k1 ^ 0x646f72616e646f6dull; - v2 = k0 ^ 0x6c7967656e657261ull; - v3 = k1 ^ 0x7465646279746573ull; - finalized = false; - total_len = 0; - block_buffer_len = 0; -} diff --git a/src/tscore/unit_tests/test_HashAlgorithms.cc b/src/tscore/unit_tests/test_HashAlgorithms.cc new file mode 100644 index 00000000000..7c69648b027 --- /dev/null +++ b/src/tscore/unit_tests/test_HashAlgorithms.cc @@ -0,0 +1,155 @@ +/** @file + + Unit tests for hash algorithms (SipHash-1-3, SipHash-2-4) + + @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 "tscore/HashSip.h" +#include +#include +#include + +TEST_CASE("HashSip13 - Deterministic output", "[libts][HashSip13]") +{ + ATSHash64Sip13 hash1, hash2; + const char *input = "test"; + + hash1.update(input, 4); + hash1.final(); + + hash2.update(input, 4); + hash2.final(); + + REQUIRE(hash1.get() == hash2.get()); + REQUIRE(hash1.get() != 0); +} + +TEST_CASE("HashSip13 - Empty input", "[libts][HashSip13]") +{ + ATSHash64Sip13 hash; + hash.update("", 0); + hash.final(); + REQUIRE(hash.get() != 0); +} + +TEST_CASE("HashSip13 - Single byte", "[libts][HashSip13]") +{ + ATSHash64Sip13 hash; + hash.update("a", 1); + hash.final(); + REQUIRE(hash.get() != 0); +} + +TEST_CASE("HashSip13 - Block boundaries", "[libts][HashSip13]") +{ + std::vector sizes = {7, 8, 9, 16, 17, 31, 32, 33}; + std::string input(64, 'x'); + + for (auto size : sizes) { + ATSHash64Sip13 hash; + hash.update(input.c_str(), size); + hash.final(); + REQUIRE(hash.get() != 0); + } +} + +TEST_CASE("HashSip13 - Incremental vs single update", "[libts][HashSip13]") +{ + ATSHash64Sip13 hash1, hash2; + + hash1.update("hello", 5); + hash1.update(" world", 6); + hash1.final(); + + hash2.update("hello world", 11); + hash2.final(); + + REQUIRE(hash1.get() == hash2.get()); +} + +TEST_CASE("HashSip13 - Typical URL paths", "[libts][HashSip13]") +{ + std::vector urls = {"/", "/index.html", "/api/v1/users/123", "/images/photos/vacation/beach/2024/photo_12345.jpg"}; + + for (const auto &url : urls) { + ATSHash64Sip13 hash; + hash.update(url.c_str(), url.size()); + hash.final(); + REQUIRE(hash.get() != 0); + } +} + +TEST_CASE("HashSip13 - Long URLs with query strings", "[libts][HashSip13]") +{ + std::string long_url = "/search?"; + for (int i = 0; i < 200; i++) { + long_url += "parameter" + std::to_string(i) + "=some_longer_value" + std::to_string(i) + "&"; + } + + ATSHash64Sip13 hash; + hash.update(long_url.c_str(), long_url.size()); + hash.final(); + REQUIRE(hash.get() != 0); + REQUIRE(long_url.size() > 2000); +} + +TEST_CASE("HashSip13 - Different inputs produce different hashes", "[libts][HashSip13]") +{ + ATSHash64Sip13 hash1, hash2; + + hash1.update("parent1", 7); + hash1.final(); + + hash2.update("parent2", 7); + hash2.final(); + + REQUIRE(hash1.get() != hash2.get()); +} + +TEST_CASE("HashSip13 - Clear and reuse", "[libts][HashSip13]") +{ + ATSHash64Sip13 hash; + + hash.update("first", 5); + hash.final(); + uint64_t first_result = hash.get(); + + hash.clear(); + hash.update("first", 5); + hash.final(); + + REQUIRE(hash.get() == first_result); +} + +TEST_CASE("HashSip13 - Comparison with SipHash-2-4", "[libts][HashSip13]") +{ + ATSHash64Sip13 hash13; + ATSHash64Sip24 hash24; + const char *input = "test"; + + hash13.update(input, 4); + hash13.final(); + + hash24.update(input, 4); + hash24.final(); + + REQUIRE(hash13.get() != 0); + REQUIRE(hash24.get() != 0); +}