blob: 49967a507175069cbf95e4496d901c2031c9dedb [file]
/** @file
Outbound connection tracking support.
@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 "P_Net.h" // For Metrics.
#include "iocore/net/ConnectionTracker.h"
#include "records/RecCore.h"
#include "swoc/IPAddr.h"
#include <algorithm>
#include <array>
using namespace std::literals;
ConnectionTracker::TableSingleton ConnectionTracker::_inbound_table;
ConnectionTracker::TableSingleton ConnectionTracker::_outbound_table;
ConnectionTracker::GlobalConfig *ConnectionTracker::_global_config{nullptr};
const MgmtConverter ConnectionTracker::MAX_SERVER_CONV(
[](const void *data) -> MgmtInt { return static_cast<MgmtInt>(*static_cast<const decltype(TxnConfig::server_max) *>(data)); },
[](void *data, MgmtInt i) -> void {
*static_cast<decltype(TxnConfig::server_max) *>(data) = static_cast<decltype(TxnConfig::server_max)>(i);
});
const MgmtConverter ConnectionTracker::MIN_SERVER_CONV(
[](const void *data) -> MgmtInt { return static_cast<MgmtInt>(*static_cast<const decltype(TxnConfig::server_min) *>(data)); },
[](void *data, MgmtInt i) -> void {
*static_cast<decltype(TxnConfig::server_min) *>(data) = static_cast<decltype(TxnConfig::server_min)>(i);
});
// Do integer and string conversions.
const MgmtConverter ConnectionTracker::SERVER_MATCH_CONV{
[](const void *data) -> MgmtInt { return static_cast<MgmtInt>(*static_cast<const decltype(TxnConfig::server_match) *>(data)); },
[](void *data, MgmtInt i) -> void {
auto const value = std::clamp(i, static_cast<MgmtInt>(MATCH_IP), static_cast<MgmtInt>(MATCH_BOTH));
*static_cast<decltype(TxnConfig::server_match) *>(data) = static_cast<decltype(TxnConfig::server_match)>(value);
},
nullptr,
nullptr,
[](const void *data) -> std::string_view {
auto t = *static_cast<const ConnectionTracker::MatchType *>(data);
return t < 0 || t > ConnectionTracker::MATCH_BOTH ? "Invalid"sv : ConnectionTracker::MATCH_TYPE_NAME[t];
},
[](void *data, std::string_view src) -> void {
ConnectionTracker::MatchType t;
if (ConnectionTracker::lookup_match_type(src, t)) {
*static_cast<ConnectionTracker::MatchType *>(data) = t;
} else {
ConnectionTracker::Warning_Bad_Match_Type(src);
}
}};
// Neither of these clamps, for the same reason as SERVER_MATCH_CONV above: the InkAPITest
// regression test requires an arbitrary integer to round trip through the setter and getter. The
// records paths do the range checking instead -- records.yaml validates the value and the reload
// callbacks below clamp -- so an out of range value is only reachable by a plugin that sets one
// deliberately. Both settings degrade safely if that happens: any non-zero metric_enabled enables
// metrics, and any metric_aggregate outside 0..3 publishes everything, the same as
// AGGREGATE_GROUP.
const MgmtConverter ConnectionTracker::METRIC_ENABLED_CONV{
[](const void *data) -> MgmtInt { return static_cast<MgmtInt>(*static_cast<const decltype(TxnConfig::metric_enabled) *>(data)); },
[](void *data, MgmtInt i) -> void {
*static_cast<decltype(TxnConfig::metric_enabled) *>(data) = static_cast<decltype(TxnConfig::metric_enabled)>(i);
}};
const MgmtConverter ConnectionTracker::METRIC_AGGREGATE_CONV{
[](const void *data) -> MgmtInt { return static_cast<MgmtInt>(*static_cast<const ConnectionTracker::MetricAggregate *>(data)); },
[](void *data, MgmtInt i) -> void {
*static_cast<ConnectionTracker::MetricAggregate *>(data) = static_cast<ConnectionTracker::MetricAggregate>(i);
}};
const std::array<std::string_view, static_cast<int>(ConnectionTracker::MATCH_BOTH) + 1> ConnectionTracker::MATCH_TYPE_NAME{
{"ip"sv, "port"sv, "host"sv, "both"sv}
};
// Make sure the clock is millisecond resolution or finer.
static_assert(ConnectionTracker::Group::Clock::period::num == 1);
static_assert(ConnectionTracker::Group::Clock::period::den >= 1000);
// Configuration callback functions.
namespace
{
bool
Config_Update_Conntrack_Min(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::TxnConfig *>(cookie);
if (RECD_INT == dtype) {
config->server_min = data.rec_int;
return true;
}
return false;
}
bool
Config_Update_Conntrack_Max(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::TxnConfig *>(cookie);
if (RECD_INT == dtype) {
config->server_max = data.rec_int;
return true;
}
return false;
}
bool
Config_Update_Conntrack_Match(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::TxnConfig *>(cookie);
if (RECD_STRING == dtype) {
ConnectionTracker::MatchType match_type;
std::string_view tag{data.rec_string};
if (ConnectionTracker::lookup_match_type(tag, match_type)) {
config->server_match = match_type;
return true;
} else {
ConnectionTracker::Warning_Bad_Match_Type(tag);
}
} else {
Warning("Invalid type for '%s' - must be 'INT'", ConnectionTracker::CONFIG_SERVER_VAR_MATCH.data());
}
return false;
}
bool
Config_Update_Conntrack_Server_Alert_Delay_Helper(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data,
void * /* cookie ATS_UNUSED */, std::chrono::seconds &alert_delay)
{
if (RECD_INT == dtype && data.rec_int >= 0) {
alert_delay = std::chrono::seconds(data.rec_int);
return true;
}
return false;
}
bool
Config_Update_Conntrack_Server_Alert_Delay(const char *name, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::GlobalConfig *>(cookie);
return Config_Update_Conntrack_Server_Alert_Delay_Helper(name, dtype, data, cookie, config->server_alert_delay);
}
bool
Config_Update_Conntrack_Client_Alert_Delay(const char *name, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::GlobalConfig *>(cookie);
return Config_Update_Conntrack_Server_Alert_Delay_Helper(name, dtype, data, cookie, config->client_alert_delay);
}
bool
Config_Update_Conntrack_Metric_Enabled(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::TxnConfig *>(cookie);
if (RECD_INT == dtype) {
config->metric_enabled = std::clamp(static_cast<int>(data.rec_int), 0, 1);
return true;
}
return false;
}
bool
Config_Update_Conntrack_Metric_Aggregate(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::TxnConfig *>(cookie);
if (RECD_INT == dtype) {
auto level = std::clamp(static_cast<int>(data.rec_int), static_cast<int>(ConnectionTracker::AGGREGATE_NONE),
static_cast<int>(ConnectionTracker::AGGREGATE_SUM));
config->metric_aggregate = static_cast<ConnectionTracker::MetricAggregate>(level);
return true;
}
return false;
}
bool
Config_Update_Conntrack_Metric_Prefix(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie)
{
auto config = static_cast<ConnectionTracker::GlobalConfig *>(cookie);
if (RECD_STRING == dtype) {
config->metric_prefix = data.rec_string;
return true;
}
return false;
}
// // helper function to build up a json string from the passed conn groups.
std::string
Groups_To_JSON(std::vector<std::shared_ptr<ConnectionTracker::Group const>> const &groups)
{
std::string text;
size_t extent = 0;
static const swoc::bwf::Format header_fmt{R"({{"count": {}, "list": [ )"};
static const swoc::bwf::Format item_fmt{
R"( {{"type": "{}", "ip": "{}", "fqdn": "{}", "current": {}, "max": {}, "blocked": {}, "alert": {}}},
)"};
static const std::string_view trailer{" \n]}"};
static const auto printer = [](swoc::BufferWriter &w, ConnectionTracker::Group const *g) -> swoc::BufferWriter & {
w.print(item_fmt, g->_match_type, g->_addr, g->_fqdn, g->_count.load(), g->_count_max.load(), g->_blocked.load(),
g->get_last_alert_epoch_time());
return w;
};
swoc::FixedBufferWriter null_bw{nullptr}; // Empty buffer for sizing work.
null_bw.print(header_fmt, groups.size()).extent();
for (auto const &g : groups) {
printer(null_bw, g.get());
}
extent = null_bw.extent() + trailer.size() - 2; // 2 for the trailing comma newline that will get clipped.
text.resize(extent);
swoc::FixedBufferWriter w(const_cast<char *>(text.data()), text.size());
w.restrict(trailer.size());
w.print(header_fmt, groups.size());
for (auto const &g : groups) {
printer(w, g.get());
}
w.restore(trailer.size());
w.write(trailer);
return text;
}
bool
Config_Update_Conntrack_Client_Exempt_List(const char * /* name ATS_UNUSED */, RecDataT dtype, RecData data, void *cookie)
{
if (RECD_STRING != dtype) {
Warning("Invalid type for '%s' - must be 'STRING'", ConnectionTracker::CONFIG_CLIENT_VAR_EXEMPT_LIST.data());
return false;
}
auto *config = static_cast<ConnectionTracker::GlobalConfig *>(cookie);
ink_release_assert(config != nullptr);
if (data.rec_string == nullptr) {
// There is no exempt list configured. Ensure that our exempt list is empty.
std::lock_guard<ts::bravo::shared_mutex> lock(config->client_exempt_list_mutex);
config->client_exempt_list.clear();
return true;
}
std::string_view exempt_list_string{data.rec_string};
// Parse the comma-separated list of IP ranges into a temporary set first.
// This ensures we don't lose the previous configuration if parsing fails.
swoc::IPRangeSet new_exempt_list;
swoc::TextView ranges{exempt_list_string};
while (!ranges.empty()) {
swoc::TextView range_sv = ranges.take_prefix_at(',');
range_sv.trim_if(&isspace);
if (!range_sv.empty()) {
swoc::IPRange range;
if (!range.load(range_sv)) {
Warning("%s: '%.*s' is not a valid IP range in configuration '%s'", ConnectionTracker::CONFIG_CLIENT_VAR_EXEMPT_LIST.data(),
static_cast<int>(range_sv.size()), range_sv.data(), ConnectionTracker::CONFIG_CLIENT_VAR_EXEMPT_LIST.data());
return false;
}
new_exempt_list.mark(range);
}
}
// Parsing succeeded. Now acquire the lock and replace the global exempt list.
std::lock_guard<ts::bravo::shared_mutex> lock(config->client_exempt_list_mutex);
config->client_exempt_list.clear();
for (auto const &ip_range : new_exempt_list) {
config->client_exempt_list.mark(ip_range);
}
return true;
}
} // anonymous namespace
ConnectionTracker::GlobalConfig::GlobalConfig(GlobalConfig const &other)
{
this->client_alert_delay = other.client_alert_delay;
this->server_alert_delay = other.server_alert_delay;
this->metric_prefix = other.metric_prefix;
// Lock the source to safely copy the exempt list.
// Note: the mutex itself is not copied; it's default-constructed.
ts::bravo::shared_lock<ts::bravo::shared_mutex> lock(other.client_exempt_list_mutex);
this->client_exempt_list.clear();
for (auto const &ip_range : other.client_exempt_list) {
this->client_exempt_list.mark(ip_range);
}
}
ConnectionTracker::GlobalConfig &
ConnectionTracker::GlobalConfig::operator=(GlobalConfig const &other)
{
if (this != &other) {
this->client_alert_delay = other.client_alert_delay;
this->server_alert_delay = other.server_alert_delay;
this->metric_prefix = other.metric_prefix;
// Lock both source and destination to safely copy the exempt list.
// Lock in a consistent order to avoid deadlock (lock 'other' first, then 'this').
ts::bravo::shared_lock<ts::bravo::shared_mutex> lock_src(other.client_exempt_list_mutex);
std::lock_guard<ts::bravo::shared_mutex> lock_dst(this->client_exempt_list_mutex);
this->client_exempt_list.clear();
for (auto const &ip_range : other.client_exempt_list) {
this->client_exempt_list.mark(ip_range);
}
}
return *this;
}
void
ConnectionTracker::config_init(GlobalConfig *global, TxnConfig *txn, RecConfigUpdateCb const &config_cb)
{
_global_config = global; // remember this for later retrieval.
// Per transaction lookup must be done at call time because it changes.
Enable_Config_Var(CONFIG_CLIENT_VAR_ALERT_DELAY, &Config_Update_Conntrack_Client_Alert_Delay, config_cb, global);
Enable_Config_Var(CONFIG_CLIENT_VAR_EXEMPT_LIST, &Config_Update_Conntrack_Client_Exempt_List, config_cb, global);
Enable_Config_Var(CONFIG_SERVER_VAR_MIN, &Config_Update_Conntrack_Min, config_cb, txn);
Enable_Config_Var(CONFIG_SERVER_VAR_MAX, &Config_Update_Conntrack_Max, config_cb, txn);
Enable_Config_Var(CONFIG_SERVER_VAR_MATCH, &Config_Update_Conntrack_Match, config_cb, txn);
Enable_Config_Var(CONFIG_SERVER_VAR_ALERT_DELAY, &Config_Update_Conntrack_Server_Alert_Delay, config_cb, global);
Enable_Config_Var(CONFIG_SERVER_VAR_METRIC_ENABLED, &Config_Update_Conntrack_Metric_Enabled, config_cb, txn);
Enable_Config_Var(CONFIG_SERVER_VAR_METRIC_AGGREGATE, &Config_Update_Conntrack_Metric_Aggregate, config_cb, txn);
Enable_Config_Var(CONFIG_SERVER_VAR_METRIC_PREFIX, &Config_Update_Conntrack_Metric_Prefix, config_cb, global);
}
bool
ConnectionTracker::set_client_exempt_list(swoc::IPRangeSet const &ip_ranges)
{
if (_global_config == nullptr) {
Warning("ConnectionTracker::set_client_exempt_list called before config_init");
return false;
}
// Acquire exclusive lock and replace the exempt list.
std::lock_guard<ts::bravo::shared_mutex> lock(_global_config->client_exempt_list_mutex);
_global_config->client_exempt_list.clear();
for (auto const &ip_range : ip_ranges) {
_global_config->client_exempt_list.mark(ip_range);
}
return true;
}
bool
ConnectionTracker::add_client_exempt_range(swoc::IPRange const &ip_range)
{
if (_global_config == nullptr) {
Warning("ConnectionTracker::add_client_exempt_range called before config_init");
return false;
}
// Acquire exclusive lock and add the new range to the existing exempt list.
std::lock_guard<ts::bravo::shared_mutex> lock(_global_config->client_exempt_list_mutex);
_global_config->client_exempt_list.mark(ip_range);
return true;
}
bool
ConnectionTracker::remove_client_exempt_range(swoc::IPRange const &ip_range)
{
if (_global_config == nullptr) {
Warning("ConnectionTracker::remove_client_exempt_range called before config_init");
return false;
}
// Acquire exclusive lock and remove the range from the existing exempt list.
std::lock_guard<ts::bravo::shared_mutex> lock(_global_config->client_exempt_list_mutex);
_global_config->client_exempt_list.erase(ip_range);
return true;
}
void
ConnectionTracker::clear_client_exempt_list()
{
if (_global_config == nullptr) {
Warning("ConnectionTracker::clear_client_exempt_list called before config_init");
return;
}
// Acquire exclusive lock and clear all ranges from the exempt list.
std::lock_guard<ts::bravo::shared_mutex> lock(_global_config->client_exempt_list_mutex);
_global_config->client_exempt_list.clear();
}
ConnectionTracker::TxnState
ConnectionTracker::obtain_inbound(IpEndpoint const &addr)
{
TxnState zret;
// Check if the address is in the exempt list with shared (read) lock.
{
ts::bravo::shared_lock<ts::bravo::shared_mutex> lock(_global_config->client_exempt_list_mutex);
if (_global_config->client_exempt_list.contains(swoc::IPAddr{addr})) {
// This short-circuits all our connection throttling logic. Save time by
// just setting the flag for the caller to see that connections are exempt
// this address.
zret._exempt_p = true;
return zret;
}
}
CryptoHash hash;
Group::Key key{addr, hash, MatchType::MATCH_IP};
std::lock_guard<std::mutex> lock(_inbound_table._mutex); // Table lock
auto loc = _inbound_table._table.find(key);
if (loc != _inbound_table._table.end()) {
zret._g = loc->second;
} else {
zret._g = std::make_shared<Group>(Group::DirectionType::INBOUND, key, "", 0);
// Note that we must use zret._g's key, not the above key, because Key's
// members are references to the Group's members. Thus the above key's
// members are invalid after this function.
_inbound_table._table.insert(std::make_pair(zret._g->_key, zret._g));
}
return zret;
}
ConnectionTracker::TxnState
ConnectionTracker::obtain_outbound(TxnConfig const &txn_cnf, std::string_view fqdn, IpEndpoint const &addr)
{
TxnState zret;
CryptoHash hash;
CryptoContext().hash_immediate(hash, fqdn.data(), fqdn.size());
Group::Key key{addr, hash, txn_cnf.server_match};
std::lock_guard<std::mutex> lock(_outbound_table._mutex); // Table lock
auto loc = _outbound_table._table.find(key);
if (loc != _outbound_table._table.end()) {
zret._g = loc->second;
} else {
zret._g = std::make_shared<Group>(Group::DirectionType::OUTBOUND, key, fqdn, txn_cnf.server_min, txn_cnf.metric_enabled,
txn_cnf.metric_aggregate);
// Note that we must use zret._g's key, not the above key, because Key's
// members are references to the Group's members. Thus the above key's
// members are invalid after this function.
_outbound_table._table.insert(std::make_pair(zret._g->_key, zret._g));
}
return zret;
}
ConnectionTracker::Group::Group(DirectionType direction, Key const &key, std::string_view fqdn, int min_keep_alive,
int metric_enabled, MetricAggregate metric_aggregate)
: _direction{direction},
_hash(key._hash),
_match_type(key._match_type),
_min_keep_alive_conns(min_keep_alive),
_key{_addr, _hash, _match_type},
_alert_delay{direction == DirectionType::INBOUND ? _global_config->client_alert_delay : _global_config->server_alert_delay}
{
Metrics::Gauge::increment(net_rsb.connection_tracker_table_size);
// only add metrics for server connections
if (metric_enabled && direction == DirectionType::OUTBOUND) {
std::string _metric_name = metric_name(key, fqdn, _global_config->metric_prefix);
// Per group metrics always live in the hidden store. metric_aggregate controls what is
// published from them (see MetricAggregate), not whether they exist.
_count_metric = Metrics::Gauge::createHiddenPtr("proxy.process.http.per_server.current_connection.", _metric_name);
_count_total_metric = Metrics::Counter::createHiddenPtr("proxy.process.http.per_server.total_connection.", _metric_name);
_blocked_metric = Metrics::Counter::createHiddenPtr("proxy.process.http.per_server.blocked_connection.", _metric_name);
// Only MATCH_BOTH groups have siblings sharing a hostname to aggregate across.
std::string _host_metric_name = host_metric_name(key, fqdn, _global_config->metric_prefix);
bool const has_aggregate = !_host_metric_name.empty();
// A plugin can set an out of range value through the overridable config, see
// METRIC_AGGREGATE_CONV. Anything unrecognized publishes everything.
if (metric_aggregate < AGGREGATE_NONE || metric_aggregate > AGGREGATE_SUM) {
metric_aggregate = AGGREGATE_GROUP;
}
// See MetricAggregate for the table these three implement. A group with no hostname to
// aggregate under keeps its own metrics whatever the setting says, since suppressing them would
// report nothing at all for that upstream.
bool const publish_sums = has_aggregate && (metric_aggregate == AGGREGATE_GROUP || metric_aggregate == AGGREGATE_SUM);
bool const publish_max = has_aggregate && metric_aggregate != AGGREGATE_NONE;
bool const publish_group = !has_aggregate || metric_aggregate == AGGREGATE_NONE || metric_aggregate == AGGREGATE_GROUP;
std::array<std::string, 3> const sum_names{
"proxy.process.http.per_server.current_connection." + _host_metric_name,
"proxy.process.http.per_server.total_connection." + _host_metric_name,
"proxy.process.http.per_server.blocked_connection." + _host_metric_name,
};
std::array<std::string, 3> const group_names{
"proxy.process.http.per_server.current_connection." + _metric_name,
"proxy.process.http.per_server.total_connection." + _metric_name,
"proxy.process.http.per_server.blocked_connection." + _metric_name,
};
std::string const max_name = "proxy.process.http.per_server.current_connection.max." + _host_metric_name;
// metric_aggregate is dynamic and overridable, so this group may well have published a name
// under an earlier value. A published name is never removed from the store, so without
// withdrawing it here it would report for the life of the process no matter what the setting
// says. Re-registering a source republishes it if the setting changes back.
if (publish_sums) {
Metrics::Derived::add_source(sum_names[0], Metrics::MetricType::GAUGE, _count_metric, Metrics::Derived::Op::SUM);
Metrics::Derived::add_source(sum_names[1], Metrics::MetricType::COUNTER, _count_total_metric, Metrics::Derived::Op::SUM);
Metrics::Derived::add_source(sum_names[2], Metrics::MetricType::COUNTER, _blocked_metric, Metrics::Derived::Op::SUM);
} else if (has_aggregate) {
// Stop contributing rather than unlist: every group of this hostname shares these names, so
// one that does not want them must not remove a name another is still publishing.
Metrics::Derived::remove_source(sum_names[0], _count_metric);
Metrics::Derived::remove_source(sum_names[1], _count_total_metric);
Metrics::Derived::remove_source(sum_names[2], _blocked_metric);
}
if (publish_max) {
// The largest current count among this hostname's groups, sampled. Deliberately taken over
// the instantaneous gauge rather than each group's all time peak, so the value falls again
// and a maximum over time can be computed by whatever scrapes it.
Metrics::Derived::add_source(max_name, Metrics::MetricType::GAUGE, _count_metric, Metrics::Derived::Op::MAX);
} else if (has_aggregate) {
Metrics::Derived::remove_source(max_name, _count_metric);
}
if (publish_group) {
// Mirror the per group metrics into the published store under their own name. A single
// source SUM combines nothing, but the published value is still a sample: it is whatever
// the last derived tick read, and it reads 0 from creation until that first tick.
Metrics::Derived::add_source(group_names[0], Metrics::MetricType::GAUGE, _count_metric, Metrics::Derived::Op::SUM);
Metrics::Derived::add_source(group_names[1], Metrics::MetricType::COUNTER, _count_total_metric, Metrics::Derived::Op::SUM);
Metrics::Derived::add_source(group_names[2], Metrics::MetricType::COUNTER, _blocked_metric, Metrics::Derived::Op::SUM);
} else {
// Same mechanism as the aggregates above, though these names have only this group as a
// source. It leaves nothing behind for the derived pass to keep recomputing into a name that
// is no longer published.
Metrics::Derived::remove_source(group_names[0], _count_metric);
Metrics::Derived::remove_source(group_names[1], _count_total_metric);
Metrics::Derived::remove_source(group_names[2], _blocked_metric);
}
if (dbg_ctl.on()) {
swoc::LocalBufferWriter<256> w;
w.print("Registered per_server_connection.{}\0", _metric_name);
DbgPrint(dbg_ctl, "%s", w.data());
}
}
// store the host name if relevant.
if (MATCH_HOST == _match_type || MATCH_BOTH == _match_type) {
_fqdn.assign(fqdn);
}
// store the IP address if relevant.
if (MATCH_HOST == _match_type) {
_addr.setToAnyAddr(AF_INET);
} else {
ats_ip_copy(_addr, key._addr);
}
}
ConnectionTracker::Group::~Group()
{
Metrics::Gauge::decrement(net_rsb.connection_tracker_table_size);
}
bool
ConnectionTracker::Group::equal(const Key &lhs, const Key &rhs)
{
bool zret = false;
if (lhs._match_type == rhs._match_type) {
switch (lhs._match_type) {
case MATCH_IP:
zret = ats_ip_addr_eq(&lhs._addr.sa, &rhs._addr.sa);
break;
case MATCH_PORT:
zret = ats_ip_addr_port_eq(&lhs._addr.sa, &rhs._addr.sa);
break;
case MATCH_HOST:
zret = lhs._hash == rhs._hash;
break;
case MATCH_BOTH:
zret = (lhs._hash == rhs._hash && ats_ip_addr_port_eq(&lhs._addr.sa, &rhs._addr.sa));
break;
}
}
if (dbg_ctl.on()) {
swoc::LocalBufferWriter<256> w;
w.print("Comparing {} to {} -> {}\0", lhs, rhs, zret ? "match" : "fail");
DbgPrint(dbg_ctl, "%s", w.data());
}
return zret;
}
bool
ConnectionTracker::Group::should_alert(std::time_t *lat)
{
bool zret = false;
// This is a bit clunky because the goal is to store just the tick count as an atomic.
// Might check to see if an atomic time_point is really atomic and avoid this.
Ticker last_tick{_last_alert}; // Load the most recent alert time in ticks.
TimePoint last{TimePoint::duration{last_tick}}; // Most recent alert time in a time_point.
TimePoint now = Clock::now(); // Current time_point.
if (last + _alert_delay <= now) {
// it's been long enough, swap out our time for the last time. The winner of this swap
// does the actual alert, leaving its current time as the last alert time.
zret = _last_alert.compare_exchange_strong(last_tick, now.time_since_epoch().count());
if (zret && lat) {
*lat = Clock::to_time_t(last);
}
}
return zret;
}
void
ConnectionTracker::Group::release()
{
// @a _count is always the authoritative count; the metric, if enabled, only mirrors it.
if (_count > 0) {
auto count = --_count;
if (_count_metric != nullptr) {
ts::Metrics::Gauge::decrement(_count_metric);
}
if (count == 0) {
TableSingleton &table = _direction == DirectionType::INBOUND ? _inbound_table : _outbound_table;
std::lock_guard<std::mutex> lock(table._mutex); // Table lock
if (_count > 0) {
// Someone else grabbed the Group between our last check and taking the
// lock.
return;
}
table._table.erase(_key);
}
} else {
// A bit dubious, as there's no guarantee it's still negative, but even that would be interesting to know.
Error("Number of tracked connections should be greater than or equal to zero: %d", _count.load());
}
}
std::time_t
ConnectionTracker::Group::get_last_alert_epoch_time() const
{
return Clock::to_time_t(TimePoint{TimePoint::duration{Ticker{_last_alert}}});
}
void
ConnectionTracker::get_outbound_groups(std::vector<std::shared_ptr<Group const>> &groups)
{
std::lock_guard<std::mutex> lock(_outbound_table._mutex); // TABLE LOCK
groups.resize(0);
groups.reserve(_outbound_table._table.size());
for (auto &&[key, group] : _outbound_table._table) {
groups.push_back(group);
}
}
void
ConnectionTracker::get_inbound_groups(std::vector<std::shared_ptr<Group const>> &groups)
{
std::lock_guard<std::mutex> lock(_inbound_table._mutex); // TABLE LOCK
groups.resize(0);
groups.reserve(_inbound_table._table.size());
for (auto &&[key, group] : _inbound_table._table) {
groups.push_back(group);
}
}
std::string
ConnectionTracker::inbound_to_json_string()
{
std::vector<std::shared_ptr<Group const>> groups;
self_type::get_inbound_groups(groups);
return Groups_To_JSON(groups);
}
std::string
ConnectionTracker::outbound_to_json_string()
{
std::vector<std::shared_ptr<Group const>> groups;
self_type::get_outbound_groups(groups);
return Groups_To_JSON(groups);
}
void
ConnectionTracker::dump_outbound(FILE *f)
{
std::vector<std::shared_ptr<Group const>> groups;
self_type::get_outbound_groups(groups);
if (groups.size()) {
fprintf(f, "\n[O] Peer Connection Tracking\n%7s | %5s | %24s | %33s | %8s |\n", "Current", "Block", "Address", "Hostname Hash",
"Match");
fprintf(f, "------|-------|--------------------------|-----------------------------------|----------|\n");
for (std::shared_ptr<Group const> const &g : groups) {
swoc::LocalBufferWriter<128> w;
w.print("{:7} | {:5} | {:24} | {:33} | {:8} |\n", g->_count.load(), g->_blocked.load(), g->_addr, g->_hash, g->_match_type);
fwrite(w.data(), w.size(), 1, f);
}
fprintf(f, "------|-------|--------------------------|-----------------------------------|----------|\n");
}
}
void
ConnectionTracker::dump(FILE *f)
{
dump_outbound(f);
dump_inbound(f);
}
void
ConnectionTracker::dump_inbound(FILE *f)
{
std::vector<std::shared_ptr<Group const>> groups;
self_type::get_inbound_groups(groups);
if (groups.size()) {
fprintf(f, "\n[I] Peer Connection Tracking\n%7s | %5s | %24s | %33s | %8s |\n", "Current", "Block", "Address", "Hostname Hash",
"Match");
fprintf(f, "------|-------|--------------------------|-----------------------------------|----------|\n");
for (std::shared_ptr<Group const> const &g : groups) {
swoc::LocalBufferWriter<128> w;
w.print("{:7} | {:5} | {:24} | {:33} | {:8} |\n", g->_count.load(), g->_blocked.load(), g->_addr, g->_hash, g->_match_type);
fwrite(w.data(), w.size(), 1, f);
}
fprintf(f, "------|-------|--------------------------|-----------------------------------|----------|\n");
}
}
bool
ConnectionTracker::lookup_match_type(std::string_view tag, ConnectionTracker::MatchType &type)
{
// Search the array for the tag.
for (ConnectionTracker::MatchType idx :
{ConnectionTracker::MATCH_IP, ConnectionTracker::MATCH_PORT, ConnectionTracker::MATCH_HOST, ConnectionTracker::MATCH_BOTH}) {
if (tag == MATCH_TYPE_NAME[idx]) {
type = idx;
return true;
}
}
return false;
}
void
ConnectionTracker::Warning_Bad_Match_Type(std::string_view tag)
{
swoc::LocalBufferWriter<256> w;
w.print("Invalid value '{}' for '{}' - must be one of", tag, CONFIG_SERVER_VAR_MATCH);
for (auto n : MATCH_TYPE_NAME) {
w.write(" '"sv);
w.write(n);
w.write("',"sv);
}
w.aux_data()[-1] = '\0'; // clip trailing comma and null terminate.
Warning("%s", w.data());
}
void
ConnectionTracker::TxnState::Note_Unblocked(const TxnConfig *config, int count, sockaddr const *addr)
{
time_t lat; // last alert time (epoch seconds)
if (_g->_blocked > 0 && _g->should_alert(&lat)) {
auto blocked = _g->_blocked.exchange(0);
swoc::LocalBufferWriter<256> w;
w.print("Peer unblocked: [{}] count={} limit={} group=({}) blocked={} peer={}\0", swoc::bwf::Date(lat, "%b %d %H:%M:%S"sv),
count, config->server_max, *_g, blocked, addr);
Dbg(dbg_ctl, "%s", w.data());
Note("%s", w.data());
}
}
void
ConnectionTracker::TxnState::Warn_Blocked(int max_connections, int64_t id, int count, sockaddr const *addr, DbgCtl *dbg_ctl_p)
{
bool alert_p = _g->should_alert();
auto blocked = alert_p ? _g->_blocked.exchange(0) : _g->_blocked.load();
if (alert_p || dbg_ctl_p) {
swoc::LocalBufferWriter<256> w;
if (id > 1) {
w.print("[{}] ", id);
}
w.print("too many connections: count={} limit={} group=({}) blocked={} peer={}\0", count, max_connections, *_g, blocked, addr);
if (dbg_ctl_p) {
Dbg(*dbg_ctl_p, "%s", w.data());
}
if (alert_p) {
Warning("%s", w.data());
}
}
}
namespace swoc
{
BufferWriter &
bwformat(BufferWriter &w, bwf::Spec const &spec, ConnectionTracker::MatchType type)
{
if (spec.has_numeric_type()) {
bwformat(w, spec, static_cast<unsigned int>(type));
} else {
bwformat(w, spec, ConnectionTracker::MATCH_TYPE_NAME[type]);
}
return w;
}
BufferWriter &
bwformat(BufferWriter &w, bwf::Spec const & /* spec ATS_UNUSED */, ConnectionTracker::Group::Key const &key)
{
switch (key._match_type) {
case ConnectionTracker::MATCH_BOTH:
w.print("{:s} {},{}", key._match_type, key._addr, key._hash);
break;
case ConnectionTracker::MATCH_HOST:
w.print("{:s} {}", key._match_type, key._hash);
break;
case ConnectionTracker::MATCH_PORT:
w.print("{:s} {}", key._match_type, key._addr);
break;
case ConnectionTracker::MATCH_IP:
w.print("{:s} {::a}", key._match_type, key._addr);
break;
}
return w;
}
BufferWriter &
bwformat(BufferWriter &w, bwf::Spec const & /* spec ATS_UNUSED */, ConnectionTracker::Group const &g)
{
switch (g._match_type) {
case ConnectionTracker::MATCH_BOTH:
w.print("{:s} {},{}", g._match_type, g._addr, g._fqdn);
break;
case ConnectionTracker::MATCH_HOST:
w.print("{:s} {}", g._match_type, g._fqdn);
break;
case ConnectionTracker::MATCH_PORT:
w.print("{:s} {}", g._match_type, g._addr);
break;
case ConnectionTracker::MATCH_IP:
w.print("{:s} {::a}", g._match_type, g._addr);
break;
}
return w;
}
} // namespace swoc