diff --git a/Makefile b/Makefile index 670fda1c5..59ae15c55 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Tempesta FW # # Copyright (C) 2014 NatSys Lab. (info@natsys-lab.com). -# Copyright (C) 2015-2022 Tempesta Technologies, Inc. +# Copyright (C) 2015-2024 Tempesta Technologies, Inc. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -158,7 +158,7 @@ ifdef ERROR endif ifndef AVX2 $(warning !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!) - $(warning WARNING: YOUR PLATFORM IS TOO OLD AND IS NOT UNSUPPORTED) + $(warning WARNING: YOUR PLATFORM IS TOO OLD AND IS NOT SUPPORTED) $(warning WARNING: THIS AFFECT PERFORMANCE AND MIGHT AFFECT SECURITY) $(warning WARNING: PLEASE DO NOT USE THE BUILD IN PRODUCTION) $(warning !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!) diff --git a/etc/tempesta_fw.conf b/etc/tempesta_fw.conf index e681ab45f..d69dc73eb 100644 --- a/etc/tempesta_fw.conf +++ b/etc/tempesta_fw.conf @@ -1421,3 +1421,77 @@ # Example: # access_log dmesg mmap mmap_host=localhost mmap_log=access.log; # + +# TAG: ja5t +# +# Specifies TLS filtering behaviour: which Ja5t hashes and how to filter. +# +# Syntax: +# ja5t storage_size= { +# hash ; +# ... +# hash ; +# } +# +# +# STORAGE_SIZE is the size of the storage holding ja5t hashes to be monitored +# by filtering code. Hashes are evicted by LRU algorithm. The value must be multiple +# of 2^21. Defalut: 25 * 2^21 +# +# HASH_STRING is a string value of a ja5t hash calculated from the Client Hello. +# You can find these values in the access log or in ClickHouse-based +# analytics. +# +# CONNECTIONS_PER_SEC is a number of allowed connections per second for +# clients identified by HASH_STRING. +# +# TLS_RECORDS_PER_SECOND is a number of allowed TLS records per second for +# clients identified by HASH_STRING. +# +# Examples: +# ja5t storage_size=2097152 { +# hash deadbeef12345678 10 1000; +# ... +# hash 1234abcdeeaabbcc 0 0; +# } +# +# Default: +# No TLS filtering applied. +# + +# TAG: ja5h +# +# Specifies HTTP filtering behaviour: which Ja5h hashes and how to filter. +# +# Syntax: +# ja5h storage_size= { +# hash ; +# ... +# hash ; +# } +# +# +# STORAGE_SIZE is the size of the storage holding ja5h hashes to be monitored +# by filtering code. Hashes are evicted by LRU algorithm. The value MUST be multiple +# of 2^21. Defalut: 25 * 2^21 +# +# HASH_STRING is a string value of a ja5h hash calculated from the HTTP request. +# You can find these values in the access log or in ClickHouse-based +# analytics. +# +# CONNECTIONS_PER_SEC is a number of allowed connections per second for +# clients identified by HASH_STRING. +# +# HTTP_REQUESTS_PER_SECOND is a number of allowed HTTP requests per second for +# the clients identified by HASH_STRING. +# +# Examples: +# ja5h storage_size=2097152 { +# hash deadbeef12345678 10 1000; +# ... +# hash 1234abcdeeaabbcc 0 0; +# } +# +# Default: +# No HTTP filtering applied. +# diff --git a/fw/cfg.c b/fw/cfg.c index 7fb9f69f7..cad98248b 100644 --- a/fw/cfg.c +++ b/fw/cfg.c @@ -1393,6 +1393,16 @@ tfw_cfg_parse_long(const char *s, long *out_long) return kstrtol(s, base, out_long); } +int +tfw_cfg_parse_ulonglong(const char *s, unsigned long long *out_ull) +{ + int base = detect_base(&s); + + if (!base) + return -EINVAL; + return kstrtoull(s, base, out_ull); +} + int tfw_cfg_parse_uint(const char *s, unsigned int *out_uint) { diff --git a/fw/cfg.h b/fw/cfg.h index 0d16c7de7..029d2207d 100644 --- a/fw/cfg.h +++ b/fw/cfg.h @@ -188,32 +188,77 @@ typedef struct { (k) = (idx < (e)->attr_n ? (e)->attrs[(idx)].key : NULL), \ (v) = (idx < (e)->attr_n ? (e)->attrs[(idx)].val : NULL)) -#define TFW_CFG_ENTRY_FOR_EACH_VAL(e, idx, v) \ - for ((idx) = 0, (v) = (e)->vals[0]; \ - (idx) < (e)->val_n; \ - (idx)++, \ +#define TFW_CFG_ENTRY_FOR_EACH_VAL(e, idx, v) \ + for ((idx) = 0, (v) = (e)->vals[0]; \ + (idx) < (e)->val_n; \ + (idx)++, \ (v) = (idx < (e)->val_n ? (e)->vals[(idx)] : NULL)) -#define TFW_CFG_CHECK_NO_ATTRS(spec, entry) \ - if ((entry)->attr_n) { \ - T_ERR_NL("%s: Arguments may not have the '=' sign\n", \ - (spec)->name); \ - return -EINVAL; \ - } - -#define TFW_CFG_CHECK_VAL_N(op, req, spec, entry) \ - if (! ((entry)->val_n op (req)) ) { \ - T_ERR_NL("%s: Invalid number of arguments: %zu, must " \ - "be %s %d\n", (spec)->name, (entry)->val_n, \ - #op, (req)); \ - return -EINVAL; \ - } - -#define TFW_CFG_CHECK_VAL_DUP(name, val_was_set, code) \ - if (val_was_set) { \ - T_ERR_NL("Duplicate argument: '%s'\n", name); \ - code; \ - } \ +#define TFW_CFG_CHECK_NO_ATTRS(spec, entry) \ + do { \ + if ((entry)->attr_n) { \ + T_ERR_NL("%s: Arguments may not have " \ + "the '=' sign\n", (spec)->name);\ + return -EINVAL; \ + } \ + } while (0) + +#define TFW_CFG_CHECK_ATTR_N(op, req, spec, entry) \ +do { \ + if (!((entry)->attr_n op (req))) { \ + T_ERR_NL("%s: Invalid number of attributes: " \ + "%zu, must be %s %d\n", \ + (spec)->name, (entry)->attr_n, \ + #op, (req)); \ + return -EINVAL; \ + } \ +} while (0) + +#define TFW_CFG_CHECK_ATTR_EQ_N(req, spec, entry) \ +do { \ + if (!((entry)->attr_n == (req))) { \ + T_ERR_NL("%s: Invalid number of attributes: " \ + "%zu, must be queal %d\n", \ + (spec)->name, (entry)->attr_n, (req)); \ + return -EINVAL; \ + } \ +} while (0) + +#define TFW_CFG_CHECK_ATTR_LE_N(req, spec, entry) \ +do { \ + if (!((entry)->attr_n <= (req))) { \ + T_ERR_NL("%s: Invalid number of attributes: " \ + "%zu, must be less or equal %d\n", \ + (spec)->name, (entry)->attr_n, (req)); \ + return -EINVAL; \ + } \ +} while (0) + +#define TFW_CFG_CHECK_VAL_N(op, req, spec, entry) \ +do { \ + if (!((entry)->val_n op (req))) { \ + T_ERR_NL("%s: Invalid number of arguments: " \ + "%zu, must be %s %d\n", \ + (spec)->name, (entry)->val_n, #op, (req)); \ + return -EINVAL; \ + } \ +} while (0) + +#define TFW_CFG_CHECK_VAL_EQ_N(req, spec, entry) \ +do { \ + if (!((entry)->val_n == (req))) { \ + T_ERR_NL("%s: Invalid number of arguments: " \ + "%zu, must be equal %d\n", \ + (spec)->name, (entry)->val_n, (req)); \ + return -EINVAL; \ + } \ +} while (0) + +#define TFW_CFG_CHECK_VAL_DUP(name, val_was_set, code) \ + if (val_was_set) { \ + T_ERR_NL("Duplicate argument: '%s'\n", name); \ + code; \ + } \ val_was_set = true; /** @@ -451,6 +496,7 @@ int tfw_cfg_check_val_n(const TfwCfgEntry *e, int val_n); int tfw_cfg_check_single_val(const TfwCfgEntry *e); int tfw_cfg_parse_int(const char *s, int *out_int); int tfw_cfg_parse_long(const char *s, long *out_long); +int tfw_cfg_parse_ulonglong(const char *s, unsigned long long *out_ull); int tfw_cfg_parse_uint(const char *s, unsigned int *out_uint); int tfw_cfg_parse_bool(const char *in_str, bool *out_bool); int tfw_cfg_parse_intvl(const char *s, unsigned long *i0, unsigned long *i1); diff --git a/fw/ja5_conf.c b/fw/ja5_conf.c new file mode 100644 index 000000000..39d029dcb --- /dev/null +++ b/fw/ja5_conf.c @@ -0,0 +1,249 @@ +/** + * Tempesta FW + * + * Transport Layer Security (TLS) interfaces to Tempesta TLS. + * + * Copyright (C) 2024-2025 Tempesta Technologies, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, + * or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#include +#include +#include + +#include "hash.h" +#include "ja5_conf.h" +#include "log.h" +#include "tempesta_fw.h" + +/* Define default size as multiple of TDB extent size */ +#define TLS_JA5_DEFAULT_STORAGE_SIZE ((1 << 21) * 25) +#define TLS_JA5_HASHTABLE_BITS 10 + +typedef struct { + struct hlist_node hlist; + refcount_t refcnt; + /* TODO: make an unified macro for different types of ja5 hashes */ + TlsJa5t ja5_hash; + u64 conns_per_sec; + u64 records_per_sec; +} TlsJa5HashEntry; + +typedef struct { + u64 storage_size; + DECLARE_HASHTABLE(hashes, TLS_JA5_HASHTABLE_BITS); +} TlsJa5FilterCfg; + +static TlsJa5FilterCfg __rcu *tls_filter_cfg; +static TlsJa5FilterCfg *tls_filter_cfg_reconfig; + +static TlsJa5HashEntry* +tls_get_ja5_hash_entry(TlsJa5t fingerprint) +{ + u64 key; + TlsJa5HashEntry *entry = NULL; + TlsJa5FilterCfg *cfg; + + if (!tls_filter_cfg) + return NULL; + + key = hash_calc((char *)&fingerprint, sizeof(fingerprint)); + + rcu_read_lock_bh(); + cfg = rcu_dereference_bh(tls_filter_cfg); + hash_for_each_possible(cfg->hashes, entry, hlist, key) { + if (!memcmp(&fingerprint, &entry->ja5_hash, + sizeof(fingerprint))) + { + refcount_inc(&entry->refcnt); + break; + } + } + rcu_read_unlock_bh(); + + return entry; +} + +static void +tls_put_ja5_hash_entry(TlsJa5HashEntry *entry) +{ + if (entry && refcount_dec_and_test(&entry->refcnt)) + kfree(entry); +} + +u64 +tls_get_ja5_conns_limit(TlsJa5t fingerprint) +{ + u64 res = U64_MAX; + TlsJa5HashEntry *e = tls_get_ja5_hash_entry(fingerprint); + + if (e) { + res = e->conns_per_sec; + tls_put_ja5_hash_entry(e); + } + + return res; +} + +u64 +tls_get_ja5_recs_limit(TlsJa5t fingerprint) +{ + u64 res = U64_MAX; + TlsJa5HashEntry *e = tls_get_ja5_hash_entry(fingerprint); + + if (e) { + res = e->records_per_sec; + tls_put_ja5_hash_entry(e); + } + + return res; +} + +u64 +tls_get_ja5_storage_size(void) +{ + u64 res = 0; + + if (tls_filter_cfg) { + rcu_read_lock_bh(); + res = rcu_dereference_bh(tls_filter_cfg)->storage_size; + rcu_read_unlock_bh(); + } + + return res; +} + +int +ja5_cfgop_handle_hash_entry(TfwCfgSpec *cs, TfwCfgEntry *ce) +{ + TlsJa5t hash; + u32 conns_per_sec; + u32 recs_per_sec; + TlsJa5HashEntry *he; + u64 key; + + BUILD_BUG_ON(sizeof(hash) > sizeof(u64)); + TFW_CFG_CHECK_VAL_EQ_N(3, cs, ce); + TFW_CFG_CHECK_NO_ATTRS(cs, ce); + + if (tfw_cfg_parse_uint(ce->vals[1], &conns_per_sec)) { + T_ERR_NL("Failed to parse hash entry in ja5 section: " + "invalid connections per second value %s", ce->vals[1]); + return -EINVAL; + } + + if (tfw_cfg_parse_uint(ce->vals[2], &recs_per_sec)) { + T_ERR_NL("Failed to parse hash entry in ja5 section: " + "invalid records per second value %s", ce->vals[2]); + return -EINVAL; + } + + if (kstrtou64(ce->vals[0], 16, (u64 *)&hash)) { + T_ERR_NL("Failed to parse hash entry in ja5 section: " + "invalid hash value %s", ce->vals[0]); + return -EINVAL; + } + + if (!(he = kmalloc(sizeof(TlsJa5HashEntry), GFP_KERNEL))) + return -ENOMEM; + + he->ja5_hash = hash; + he->conns_per_sec = conns_per_sec; + he->records_per_sec = recs_per_sec; + INIT_HLIST_NODE(&he->hlist); + refcount_set(&he->refcnt, 1); + + key = hash_calc((char *)&hash, sizeof(hash)); + hash_add(tls_filter_cfg_reconfig->hashes, &he->hlist, key); + + return 0; +} + +int +ja5_cfgop_begin(TfwCfgSpec *cs, TfwCfgEntry *ce) +{ + BUG_ON(tls_filter_cfg_reconfig); + TFW_CFG_CHECK_VAL_EQ_N(0, cs, ce); + TFW_CFG_CHECK_ATTR_LE_N(1, cs, ce); + + if (!(tls_filter_cfg_reconfig = + kzalloc(sizeof(TlsJa5FilterCfg), GFP_KERNEL))) + return -ENOMEM; + + if (ce->attr_n == 1) { + if (strcasecmp(ce->attrs[0].key, "storage_size")) { + T_ERR_NL("Failed to parse ja5 section: " + "invalid attribute %s", ce->attrs[0].key); + return -EINVAL; + } + + if (tfw_cfg_parse_ulonglong(ce->attrs[0].val, + &tls_filter_cfg_reconfig->storage_size)) { + T_ERR_NL("Failed to parse ja5 section: " + "invalid storage_size value"); + return -EINVAL; + } + } else { + tls_filter_cfg_reconfig->storage_size = + TLS_JA5_DEFAULT_STORAGE_SIZE; + } + + return 0; +} + +static void +free_cfg(TlsJa5FilterCfg *cfg) +{ + u32 bkt_i; + struct hlist_node *tmp; + TlsJa5HashEntry *entry; + + if (!cfg) + return; + + hash_for_each_safe(cfg->hashes, bkt_i, tmp, entry, hlist) + tls_put_ja5_hash_entry(entry); + + kfree(cfg); +} + +int +ja5_cfgop_finish(TfwCfgSpec *cs) +{ + TlsJa5FilterCfg *prev = tls_filter_cfg; + + BUG_ON(!tls_filter_cfg_reconfig); + + rcu_assign_pointer(tls_filter_cfg, tls_filter_cfg_reconfig); + synchronize_rcu(); + free_cfg(prev); + tls_filter_cfg_reconfig = NULL; + + return 0; +} + +void +ja5_cfgop_cleanup(TfwCfgSpec *cs) +{ + free_cfg(tls_filter_cfg_reconfig); + tls_filter_cfg_reconfig = NULL; + + if (!tfw_runstate_is_reconfig()) { + TlsJa5FilterCfg *prev = tls_filter_cfg; + rcu_assign_pointer(tls_filter_cfg, NULL); + synchronize_rcu(); + free_cfg(prev); + } +} diff --git a/fw/ja5_conf.h b/fw/ja5_conf.h new file mode 100644 index 000000000..cc940ca3c --- /dev/null +++ b/fw/ja5_conf.h @@ -0,0 +1,32 @@ +/** + * Tempesta FW + * + * Transport Layer Security (TLS) interfaces to Tempesta TLS. + * + * Copyright (C) 2024 Tempesta Technologies, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, + * or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#include "lib/ja5.h" +#include "cfg.h" + +u64 tls_get_ja5_storage_size(void); +u64 tls_get_ja5_conns_limit(TlsJa5t fingerprint); +u64 tls_get_ja5_recs_limit(TlsJa5t fingerprint); + +int ja5_cfgop_handle_hash_entry(TfwCfgSpec *cs, TfwCfgEntry *ce); +int ja5_cfgop_begin(TfwCfgSpec *cs, TfwCfgEntry *ce); +int ja5_cfgop_finish(TfwCfgSpec *cs); +void ja5_cfgop_cleanup(TfwCfgSpec *cs); diff --git a/fw/ja5_filter.h b/fw/ja5_filter.h new file mode 100644 index 000000000..ebc372065 --- /dev/null +++ b/fw/ja5_filter.h @@ -0,0 +1,260 @@ +/** + * Tempesta FW + * + * Copyright (C) 2024-2025 Tempesta Technologies, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, + * or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#include + +#include "db/core/tdb.h" +#include "lib/str.h" +#include "log.h" + + +#define JA5_FILTER_TIME_SLOTS_POW 3 +#define JA5_FILTER_TIME_SLOTS_CNT (1 << JA5_FILTER_TIME_SLOTS_POW) +#define JA5_FILTER_TIME_SLOTS_MASK (JA5_FILTER_TIME_SLOTS_CNT - 1) +#define DB_RECS_TO_FREE_CNT 32 + +/** + * Holds time slot's @counter for timestamp @ts + */ +typedef struct { + u32 counter; + u32 ts; +} TimeSlot; + +/** + * Holds connections and data records rates for a particular fingerprint + * + * @param list_node - required for LRU list + * @param conns - array of connections counters for timeslots for the last second + * @param conns_lock - lock for @conns + * @param recs - array of records counters for timeslots for the last second + * @param recs_lock - lock for @recs + */ +typedef struct { + /** Keep @list_node first for easy type casts */ + struct list_head list_node; + TimeSlot conns[JA5_FILTER_TIME_SLOTS_CNT]; + spinlock_t conns_lock; + TimeSlot recs[JA5_FILTER_TIME_SLOTS_CNT]; + spinlock_t recs_lock; +} Rates; + +/** + * Rates storage for a particular fingerprints types. This file(ja5_filter.h) + * is supposed to be included into a .c file responsible for that fingerprints + * processing. + * + * @param tdb - TDB instance keeping rates by fingerprint value + * @param lru_list - LRU list used for eviction of outdates fingerprints + * @param lru_list_lock - lock for @lru_list + */ +static struct { + TDB *tdb; + struct list_head lru_list; + spinlock_t lru_list_lock; +} storage; + +static bool +get_alloc_ctx_eq_rec(TdbRec *, void *) +{ + return true; +} + +static void +put_fingerprint_rates(Rates *rates) +{ + tdb_rec_put(storage.tdb, (char *)rates - sizeof(TdbRec)); +} + +static void +get_alloc_ctx_init_rec(TdbRec *rec, void *) +{ + Rates *rates = (Rates *)rec->data; + + bzero_fast(rates, sizeof(Rates)); + INIT_LIST_HEAD(&rates->list_node); + spin_lock_init(&rates->conns_lock); + spin_lock_init(&rates->recs_lock); + tdb_rec_keep(rec); +} + +/** + * Finds @Rates object by fingerprint in the storage if it exists and adds it + * to the storage otherwise. Remove outdates rates from the storage if it's + * full by RLU algorithm. + */ +static Rates* +get_fingerprint_rates(u64 fingerprint) +{ + TdbGetAllocCtx get_alloc_ctx = { + .eq_rec = get_alloc_ctx_eq_rec, + .ctx = NULL, + .precreate_rec = NULL, + .init_rec = get_alloc_ctx_init_rec, + .len = sizeof(Rates)}; + const u64 key = fingerprint; + TdbRec *rec; + Rates *rates; + + /* Try to remove DB_RECS_TO_FREE_CNT records from DB if it's full */ + while (!(rec = tdb_rec_get_alloc(storage.tdb, key, &get_alloc_ctx))) { + struct list_head *pos, *tmp, tail_to_delete; + u32 cnt = DB_RECS_TO_FREE_CNT; + + INIT_LIST_HEAD(&tail_to_delete); + + /* Cut off DB_RECS_TO_FREE_CNT entries from the LRU list */ + spin_lock(&storage.lru_list_lock); + list_for_each_prev_safe(pos, tmp, &storage.lru_list) { + list_move(pos, &tail_to_delete); + if (!--cnt) + break; + } + spin_unlock(&storage.lru_list_lock); + + list_for_each_safe(pos, tmp, &tail_to_delete) { + u64 key = ((TdbRec *)pos - 1)->key; + // TODO: remove directly by record bypassing search by key + tdb_entry_remove(storage.tdb, key, NULL, NULL, true); + } + + /** + * Protect from low probable case where all records + * were deleted but are held by references + */ + if (cnt == DB_RECS_TO_FREE_CNT) + return NULL; + } + + rates = (Rates *)rec->data; + + spin_lock(&storage.lru_list_lock); + /* The record still was not added to the LRU list */ + if (list_empty(&rates->list_node)) + list_add_tail(&rates->list_node, &storage.lru_list); + spin_unlock(&storage.lru_list_lock); + + return rates; +} + +/** + * Inintializes the storage with it's max size + * + * @param max_storage_size storage size + * @return true if storage's been successfully initialized or is already + * initialized + * @return false otherwise + */ +static bool +init_filter(size_t max_storage_size) +{ + /** + * Initialize storage only once during whole uptime. + * Storage size reconfiguration is not supported. + */ + if (storage.tdb) + return true; + + INIT_LIST_HEAD(&storage.lru_list); + spin_lock_init(&storage.lru_list_lock); + + return (storage.tdb = tdb_open( + "/tmp/ja5t_flt.tdb", max_storage_size, sizeof(Rates), 0)); +} + +static u32 +ja5_calc_rate(TimeSlot slots[], spinlock_t *lock) +{ + u32 sum = 0; + u64 ts = jiffies * JA5_FILTER_TIME_SLOTS_CNT / HZ; + u8 slot_num = ts & JA5_FILTER_TIME_SLOTS_MASK; + TimeSlot *current_slot = &slots[slot_num]; + + spin_lock(lock); + + if (current_slot->ts != ts) { + current_slot->ts = ts; + current_slot->counter = 0; + } + current_slot->counter++; + + for (slot_num = 0; slot_num < JA5_FILTER_TIME_SLOTS_CNT; slot_num++) + if (slots[slot_num].ts + JA5_FILTER_TIME_SLOTS_CNT >= ts) + sum += slots[slot_num].counter; + + spin_unlock(lock); + + return sum; +} + +/** + * Returns the last second's connections number for the specified fingerprint + * + * @param fingerprint fingerprint connections rates to look for + */ +static u32 +ja5_get_conns_rate(u64 fingerprint) +{ + u32 res; + Rates *rates; + + if (!storage.tdb) + return 0; + + if (!(rates = get_fingerprint_rates(fingerprint))) + /* Allow connection if DB is full */ + return 0; + + res = ja5_calc_rate(rates->conns, &rates->conns_lock); + + put_fingerprint_rates(rates); + + T_DBG("JA5 Fingerprint %08llx: connections/sec %d", + *(u64 *)&fingerprint, res); + + return res; +} + +/** + * Returns the last second's records number for the specified fingerprint + * + * @param fingerprint a fingerprint records rates to look for + */ +static u32 +ja5_get_records_rate(u64 fingerprint) +{ + u32 res; + Rates *rates; + + if (!storage.tdb) + return 0; + + if (!(rates = get_fingerprint_rates(fingerprint))) + /* Allow record if DB is full */ + return 0; + + res = ja5_calc_rate(rates->recs, &rates->recs_lock); + + put_fingerprint_rates(rates); + + T_DBG("JA5 Fingerprint %08llx: records/sec %d", + *(u64 *)&fingerprint, res); + + return res; +} diff --git a/fw/ja5t_filter.c b/fw/ja5t_filter.c new file mode 100644 index 000000000..7317dca5b --- /dev/null +++ b/fw/ja5t_filter.c @@ -0,0 +1,43 @@ +/** + * Tempesta FW + * + * Copyright (C) 2024 Tempesta Technologies, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, + * or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#include "ja5t_filter.h" +#include "ja5_filter.h" + +bool +ja5t_init_filter(size_t max_storage_size) +{ + return init_filter(max_storage_size); +} + +u32 +ja5t_get_conns_rate(TlsJa5t fingerprint) +{ + BUILD_BUG_ON(sizeof(fingerprint) != sizeof(u64)); + + return ja5_get_conns_rate(*(u64 *)&fingerprint); +} + +u32 +ja5t_get_records_rate(TlsJa5t fingerprint) +{ + BUILD_BUG_ON(sizeof(fingerprint) != sizeof(u64)); + + return ja5_get_records_rate(*(u64 *)&fingerprint); +} diff --git a/fw/ja5t_filter.h b/fw/ja5t_filter.h new file mode 100644 index 000000000..a53006c54 --- /dev/null +++ b/fw/ja5t_filter.h @@ -0,0 +1,25 @@ +/** + * Tempesta FW + * + * Copyright (C) 2024 Tempesta Technologies, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, + * or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +#include "log.h" +#include "lib/ja5.h" + +bool ja5t_init_filter(size_t max_storage_size); +u32 ja5t_get_conns_rate(TlsJa5t fingerprint); +u32 ja5t_get_records_rate(TlsJa5t fingerprint); diff --git a/fw/pool.c b/fw/pool.c index a9efa22d6..17407a500 100644 --- a/fw/pool.c +++ b/fw/pool.c @@ -74,7 +74,7 @@ tfw_pool_alloc_pages(unsigned int order) unsigned long pg_res; gfp_t flags; - preempt_disable(); + local_bh_disable(); pgn = this_cpu_ptr(&pg_next); @@ -82,11 +82,11 @@ tfw_pool_alloc_pages(unsigned int order) --*pgn; pg_res = ((unsigned long *)this_cpu_ptr(pg_cache))[*pgn]; - preempt_enable(); + local_bh_enable(); return pg_res; } - preempt_enable(); + local_bh_enable(); flags = order > 0 ? GFP_ATOMIC | __GFP_COMP : GFP_ATOMIC; return __get_free_pages(flags, order); @@ -98,7 +98,7 @@ tfw_pool_free_pages(unsigned long addr, unsigned int order) unsigned int *pgn; int refcnt; - preempt_disable(); + local_bh_disable(); pgn = this_cpu_ptr(&pg_next); refcnt = page_count(virt_to_page(addr)); @@ -107,11 +107,11 @@ tfw_pool_free_pages(unsigned long addr, unsigned int order) ((unsigned long *)this_cpu_ptr(pg_cache))[*pgn] = addr; ++*pgn; - preempt_enable(); + local_bh_enable(); return; } - preempt_enable(); + local_bh_enable(); put_page(compound_head(virt_to_page(addr))); } diff --git a/fw/tls.c b/fw/tls.c index c32447a2c..f15d778a2 100644 --- a/fw/tls.c +++ b/fw/tls.c @@ -36,6 +36,8 @@ #include "tls.h" #include "vhost.h" #include "tcp.h" +#include "ja5_conf.h" +#include "ja5t_filter.h" /* Common tls configuration for all vhosts. */ static TlsCfg tfw_tls_cfg; @@ -1010,6 +1012,24 @@ tfw_tls_alpn_match(const TlsCtx *tls, const ttls_alpn_proto *alpn) return false; } +static bool +tfw_ja5t_limit_conn(TlsJa5t fingerprint) +{ + u64 limit = tls_get_ja5_conns_limit(fingerprint); + u64 rate = ja5t_get_conns_rate(fingerprint); + + return rate > limit; +} + +static bool +tfw_ja5t_limit_rec(TlsJa5t fingerprint) +{ + u64 limit = tls_get_ja5_recs_limit(fingerprint); + u64 rate = ja5t_get_records_rate(fingerprint); + + return rate > limit; +} + /* * ------------------------------------------------------------------------ * TLS library configuration. @@ -1158,8 +1178,13 @@ tfw_tls_cfgend(void) static int tfw_tls_start(void) { + u64 storage_size = tls_get_ja5_storage_size(); + tfw_tls_allow_any_sni = allow_any_sni_reconfig; + if (storage_size && !ja5t_init_filter(storage_size)) + return -ENOMEM; + return 0; } @@ -1169,7 +1194,33 @@ tfw_tls_get_allow_any_sni_reconfig(void) return allow_any_sni_reconfig; } +static TfwCfgSpec tfw_tls_hash_specs[] = { + { + .name = "hash", + .deflt = NULL, + .handler = ja5_cfgop_handle_hash_entry, + .allow_none = true, + .allow_repeat = true, + .allow_reconfig = true, + }, + { 0 } +}; + static TfwCfgSpec tfw_tls_specs[] = { + { + .name = "ja5t", + .deflt = NULL, + .handler = tfw_cfg_handle_children, + .cleanup = ja5_cfgop_cleanup, + .dest = tfw_tls_hash_specs, + .spec_ext = &(TfwCfgSpecChild) { + .begin_hook = ja5_cfgop_begin, + .finish_hook = ja5_cfgop_finish + }, + .allow_none = true, + .allow_repeat = false, + .allow_reconfig = true, + }, { 0 } }; @@ -1197,7 +1248,8 @@ tfw_tls_init(void) return -EINVAL; ttls_register_callbacks(tfw_tls_send, tfw_tls_sni, tfw_tls_over, - ttls_cli_id, tfw_tls_alpn_match); + ttls_cli_id, tfw_tls_alpn_match, + tfw_ja5t_limit_conn, tfw_ja5t_limit_rec); if ((r = tfw_h2_init())) goto err_h2; diff --git a/pkg/scripts/tempesta_installer.sh b/pkg/scripts/tempesta_installer.sh index 1c57cf6b6..328b69328 100755 --- a/pkg/scripts/tempesta_installer.sh +++ b/pkg/scripts/tempesta_installer.sh @@ -29,7 +29,7 @@ declare -r GITHUB_USER="tempesta-tech" declare -r GITHUB_REPO_TEMPESTA="tempesta" declare -r GITHUB_REPO_LINUX="linux-5.10.35-tfw" -#TODO: currently Ubuntu 20 is the only supported distribution, other +#TODO: currently Ubuntu 24 is the only supported distribution, other # distributions may have other names. # Don't install packages with debug symbols. declare -a FILES_LINUX=("linux-image-[\d.]+\.tfw-[\da-f]+" @@ -92,10 +92,10 @@ tfw_download() log "INFO" "https://github.com/$GITHUB_USER/$GITHUB_REPO_LINUX" #TODO: show next line only if received 403 status code. log "INFO" "Or may be Github API rate limit exceeded. Fallback download from repo instead github.com" - fall_links=("http://tempesta-vm.cloud.cherryservers.net:8081/repository/tempesta/pool/l/linux-headers-5.10.35.tfw-4c9ba16/linux-headers-5.10.35.tfw-4c9ba16_5.10.35.tfw-4c9ba16-1_amd64.deb" - "http://tempesta-vm.cloud.cherryservers.net:8081/repository/tempesta/pool/l/linux-image-5.10.35.tfw-4c9ba16/linux-image-5.10.35.tfw-4c9ba16_5.10.35.tfw-4c9ba16-1_amd64.deb" - "http://tempesta-vm.cloud.cherryservers.net:8081/repository/tempesta/pool/l/linux-libc-dev/linux-libc-dev_5.10.35.tfw-4c9ba16-1_amd64.deb" - "http://tempesta-vm.cloud.cherryservers.net:8081/repository/tempesta/pool/t/tempesta-fw-dkms/tempesta-fw-dkms_0.7.1_amd64.deb -O tempesta-fw-dkms.deb") + fall_links=("http://172.240.91.52:8081/repository/tempesta/pool/l/linux-headers-5.10.35.tfw-4c9ba16/linux-headers-5.10.35.tfw-4c9ba16_5.10.35.tfw-4c9ba16-1_amd64.deb" + "http://172.240.91.52:8081/repository/tempesta/pool/l/linux-image-5.10.35.tfw-4c9ba16/linux-image-5.10.35.tfw-4c9ba16_5.10.35.tfw-4c9ba16-1_amd64.deb" + "http://172.240.91.52:8081/repository/tempesta/pool/l/linux-libc-dev/linux-libc-dev_5.10.35.tfw-4c9ba16-1_amd64.deb" + "http://172.240.91.52:8081/repository/tempesta/pool/t/tempesta-fw-dkms/tempesta-fw-dkms_0.7.1_amd64.deb -O tempesta-fw-dkms.deb") for file in ${fall_links[@]} do @@ -117,7 +117,7 @@ tfw_install_packages() files=("${@}") case $DISTRO in - "ubuntu-22") + "ubuntu-24") repo="" log "INFO" "Downloading latest packages from github.com/$GITHUB_USER/$repo ..." mkdir -p $DOWNLOAD_DIR/$repo @@ -145,16 +145,16 @@ tfw_install_deps() APT_OPTS= case $DISTRO in - "ubuntu-22") + "ubuntu-24") echo "" - log "INFO" "Installation on Ubuntu 22 LTS requires updating system from jessie-backports repository before installing TempestaFW." - log "INFO" "Updating system from jammy repository for Ubuntu 22 LTS." + log "INFO" "Installation on Ubuntu 24 LTS requires updating system from jessie-backports repository before installing TempestaFW." + log "INFO" "Updating system from jammy repository for Ubuntu 24 LTS." tfw_confirm echo "deb http://ru.archive.ubuntu.com/ubuntu " \ "jammy main" >> /etc/apt/sources.list - apt-get update || log "ERROR" "Failed to update package lists for Ubuntu 22 LTS." - apt-get dist-upgrade -y || log "ERROR" "Failed to dist-upgrade on Ubuntu 22 LTS." + apt-get update || log "ERROR" "Failed to update package lists for Ubuntu 24 LTS." + apt-get dist-upgrade -y || log "ERROR" "Failed to dist-upgrade on Ubuntu 24 LTS." ;; *) log "ERROR" "Unsupported distribution: $DISTRO" @@ -291,8 +291,8 @@ tfw_try_distro() log "INFO" "Detected distribution name: $d_name" case $d_name in - Ubuntu[[:space:]]22*) - DISTRO="ubuntu-22" + Ubuntu[[:space:]]24*) + DISTRO="ubuntu-24" ;; *) log "ERROR" "Installer does not support $d_name distro!" diff --git a/tls/tls_srv.c b/tls/tls_srv.c index 31a27797a..2c99b7b31 100644 --- a/tls/tls_srv.c +++ b/tls/tls_srv.c @@ -33,6 +33,7 @@ ttls_sni_cb_t *ttls_sni_cb; ttls_hs_over_cb_t *ttls_hs_over_cb; ttls_alpn_match_t *ttls_alpn_match_cb; +ttls_ja5t_limit_conn_cb_t *ttls_ja5t_limit_conn_cb; static int ttls_parse_servername_ext(TlsCtx *tls, const unsigned char *buf, size_t len) @@ -2243,6 +2244,10 @@ ttls_handshake_server_step(TlsCtx *tls, unsigned char *buf, size_t len, r = ttls_parse_client_hello(tls, buf, len, hh_len, read); if (r) return r; + + if (ttls_ja5t_limit_conn_cb(tls->sess.ja5t)) + return T_BLOCK_WITH_RST; + tls->state = TTLS_SERVER_HELLO; fallthrough; } diff --git a/tls/ttls.c b/tls/ttls.c index 492cd1e7c..b26ee0343 100644 --- a/tls/ttls.c +++ b/tls/ttls.c @@ -62,11 +62,13 @@ MODULE_LICENSE("GPL"); static DEFINE_PER_CPU(struct aead_request *, g_req) ____cacheline_aligned; static struct kmem_cache *ttls_hs_cache = NULL; +static ttls_ja5t_limit_rec_cb_t *ttls_ja5t_limit_rec_cb; static ttls_send_cb_t *ttls_send_cb; extern ttls_sni_cb_t *ttls_sni_cb; extern ttls_hs_over_cb_t *ttls_hs_over_cb; extern ttls_cli_id_t *ttls_cli_id_cb; extern ttls_alpn_match_t *ttls_alpn_match_cb; +extern ttls_ja5t_limit_conn_cb_t *ttls_ja5t_limit_conn_cb; static inline size_t ttls_max_ciphertext_len(const TlsXfrm *xfrm) @@ -253,13 +255,17 @@ ttls_skb_extract_alert(TlsIOCtx *io, TlsXfrm *xfrm) void ttls_register_callbacks(ttls_send_cb_t *send_cb, ttls_sni_cb_t *sni_cb, ttls_hs_over_cb_t *hs_over_cb, ttls_cli_id_t *cli_id_cb, - ttls_alpn_match_t *alpn_match_cb) + ttls_alpn_match_t *alpn_match_cb, + ttls_ja5t_limit_conn_cb_t *ja5t_limit_conn_cb, + ttls_ja5t_limit_rec_cb_t *ja5t_limit_rec_cb) { ttls_send_cb = send_cb; ttls_sni_cb = sni_cb; ttls_hs_over_cb = hs_over_cb; ttls_cli_id_cb = cli_id_cb; ttls_alpn_match_cb = alpn_match_cb; + ttls_ja5t_limit_conn_cb = ja5t_limit_conn_cb; + ttls_ja5t_limit_rec_cb = ja5t_limit_rec_cb; } EXPORT_SYMBOL(ttls_register_callbacks); @@ -2312,6 +2318,10 @@ ttls_recv(void *tls_data, unsigned char *buf, unsigned int len, unsigned int *re io->rlen += len; return T_POSTPONE; } + + if (ttls_ja5t_limit_rec_cb(tls->sess.ja5t)) + return T_BLOCK_WITH_RST; + *read += io->msglen - io->rlen; if ((r = ttls_decrypt(tls, NULL))) { TTLS_WARN(tls, "TLS cannot decrypt msg on state %s, ret=%d%s\n", diff --git a/tls/ttls.h b/tls/ttls.h index b92a50330..af9d6e856 100644 --- a/tls/ttls.h +++ b/tls/ttls.h @@ -574,6 +574,8 @@ typedef int ttls_send_cb_t(TlsCtx *tls, struct sg_table *sgt); typedef int ttls_sni_cb_t(TlsCtx *tls, const unsigned char *data, size_t len); typedef unsigned long ttls_cli_id_t(TlsCtx *tls, unsigned long hash); typedef bool ttls_alpn_match_t(const TlsCtx *tls, const ttls_alpn_proto *alpn); +typedef bool ttls_ja5t_limit_conn_cb_t(TlsJa5t fingerprint); +typedef bool ttls_ja5t_limit_rec_cb_t(TlsJa5t fingerprint); enum { TTLS_HS_CB_FINISHED_NEW, @@ -591,7 +593,9 @@ void *ttls_alloc_crypto_req(unsigned int extra_size, unsigned int *rsz); void ttls_register_callbacks(ttls_send_cb_t *send_cb, ttls_sni_cb_t *sni_cb, ttls_hs_over_cb_t *hs_over_cb, ttls_cli_id_t *cli_id_cb, - ttls_alpn_match_t *alpn_match_cb); + ttls_alpn_match_t *alpn_match_cb, + ttls_ja5t_limit_conn_cb_t *ja5t_limit_conn_cb, + ttls_ja5t_limit_rec_cb_t *ja5t_limit_rec_cb); const char *ttls_get_ciphersuite_name(const int ciphersuite_id);