Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/admin-guide/logging/formatting.en.rst
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,10 @@ ppd Proxy Protocol Destination IP received via Proxy Protocol context from the
Dest IP to the |TS|
ppa Proxy Protocol The Authority TLV from Proxy Protocol context from the LB
Authority to the |TS|
pptc Proxy Protocol The TLS cipher from Proxy Protocol context from the LB
TLS Cipher to the |TS|
pptv Proxy Protocol The TLS version from Proxy Protocol context from the LB
TLS version to the |TS|
===== ============== ==========================================================

.. note::
Expand Down
1 change: 1 addition & 0 deletions example/plugins/c-api/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ add_atsplugin(statistic ./statistic/statistic.cc)
add_atsplugin(protocol_stack ./protocol_stack/protocol_stack.cc)
add_atsplugin(client_context_dump ./client_context_dump/client_context_dump.cc)
target_link_libraries(client_context_dump PRIVATE OpenSSL::SSL libswoc::libswoc)
add_atsplugin(custom_logfield ./custom_logfield/custom_logfield.cc)
71 changes: 71 additions & 0 deletions example/plugins/c-api/custom_logfield/custom_logfield.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/** @file

This plugin counts the number of times every header has appeared.
Maintains separate counts for client and origin headers.

@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 <ts/ts.h>

DbgCtl dbg_ctl{"custom_logfield"};

char PLUGIN_NAME[] = "header_freq";
char VENDOR_NAME[] = "Apache Software Foundation";
char SUPPORT_EMAIL[] = "dev@trafficserver.apache.org";

int
marshal_function(TSHttpTxn txnp, char *)
{
Dbg(dbg_ctl, "Marshaling a custom field");
TSAssert(txnp);
return 0;
}

int
unmarshal_function(char **, char *, int)
{
Dbg(dbg_ctl, "Unarshaling a custom field");
return 0;
}

int
lifecycle_event_handler(TSCont /* contp ATS_UNUSED */, TSEvent event, void * /* edata ATS_UNUSED */)
{
TSAssert(event == TS_EVENT_LIFECYCLE_LOG_INITIAZLIED);

Dbg(dbg_ctl, "Registering a custom field");
TSLogFieldRegister("custom log field", "cstm", TS_LOG_TYPE_STRING, marshal_function, unmarshal_function);

return TS_SUCCESS;
}

void
TSPluginInit(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */)
{
Dbg(dbg_ctl, "Initializing plugin");

TSPluginRegistrationInfo info = {PLUGIN_NAME, VENDOR_NAME, SUPPORT_EMAIL};
if (TSPluginRegister(&info) != TS_SUCCESS) {
TSError("[%s](%s) Plugin registration failed. \n", PLUGIN_NAME, __FUNCTION__);
}

TSCont cont = TSContCreate(lifecycle_event_handler, nullptr);
TSLifecycleHookAdd(TS_LIFECYCLE_LOG_INITIAZLIED_HOOK, cont);
}
4 changes: 4 additions & 0 deletions include/iocore/net/ProxyProtocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ class ProxyProtocol
void set_ipv6_addrs(const in6_addr &src_addr, uint16_t src_port, const in6_addr &dst_addr, uint16_t dst_port);

std::optional<std::string_view> get_tlv(const uint8_t tlvCode) const;
std::optional<std::string_view> get_tlv_ssl_version() const;
std::optional<std::string_view> get_tlv_ssl_cipher() const;

ProxyProtocolVersion version = ProxyProtocolVersion::UNDEFINED;
uint16_t ip_family = AF_UNSPEC;
Expand Down Expand Up @@ -134,6 +136,8 @@ class ProxyProtocol

private:
std::string additional_data;

std::optional<std::string_view> _get_tlv_ssl_subtype(uint8_t subtype) const;
};

const size_t PPv1_CONNECTION_HEADER_LEN_MAX = 108;
Expand Down
1 change: 1 addition & 0 deletions include/proxy/logging/Log.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ class Log
// main interface
static void init(int configFlags = 0);
static void init_fields();
static void init_plugin_fields();

static bool
transaction_logging_enabled()
Expand Down
7 changes: 7 additions & 0 deletions include/proxy/logging/LogAccess.h
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ class LogAccess
int marshal_proxy_protocol_src_ip(char *); // STR
int marshal_proxy_protocol_dst_ip(char *); // STR
int marshal_proxy_protocol_authority(char *); // STR
int marshal_proxy_protocol_tls_cipher(char *); // STR
int marshal_proxy_protocol_tls_version(char *); // STR

// named fields from within a http header
//
Expand Down Expand Up @@ -303,6 +305,11 @@ class LogAccess
int marshal_milestone_fmt_ms(TSMilestonesType ms, char *buf);
int marshal_milestone_diff(TSMilestonesType ms1, TSMilestonesType ms2, char *buf);
void set_http_header_field(LogField::Container container, char *field, char *buf, int len);

// Plugin
int marshal_custom_field(char *buf, LogField::CustomMarshalFunc plugin_marshal_func);
static int unmarshal_custom_field(char **buf, char *dest, int len, LogField::CustomUnmarshalFunc plugin_unmarshal_func);

//
// unmarshalling routines
//
Expand Down
6 changes: 6 additions & 0 deletions include/proxy/logging/LogField.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ class LogField
using UnmarshalFuncWithSlice = int (*)(char **, char *, int, LogSlice *, LogEscapeType);
using UnmarshalFuncWithMap = int (*)(char **, char *, int, const Ptr<LogFieldAliasMap> &);
using SetFunc = void (LogAccess::*)(char *, int);
using CustomMarshalFunc = int (*)(void *, char *);
using CustomUnmarshalFunc = int (*)(char **, char *, int);

using VarUnmarshalFuncSliceOnly = std::variant<UnmarshalFunc, UnmarshalFuncWithSlice>;
using VarUnmarshalFunc = std::variant<decltype(nullptr), UnmarshalFunc, UnmarshalFuncWithSlice, UnmarshalFuncWithMap>;
Expand Down Expand Up @@ -132,6 +134,8 @@ class LogField
LogField(const char *name, const char *symbol, Type type, MarshalFunc marshal, UnmarshalFuncWithMap unmarshal,
const Ptr<LogFieldAliasMap> &map, SetFunc _setFunc = nullptr);

LogField(const char *name, const char *symbol, Type type, CustomMarshalFunc custom_marshal, CustomUnmarshalFunc custom_unmarshal);

LogField(const char *field, Container container);
LogField(const LogField &rhs);
~LogField();
Expand Down Expand Up @@ -207,6 +211,8 @@ class LogField
SetFunc m_set_func;
TSMilestonesType milestone_from_m_name();
int milestones_from_m_name(TSMilestonesType *m1, TSMilestonesType *m2);
CustomMarshalFunc m_custom_martial_func;
CustomUnmarshalFunc m_custom_unmartial_func;

public:
LINK(LogField, link);
Expand Down
17 changes: 14 additions & 3 deletions include/ts/apidefs.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ enum TSEvent {
TS_EVENT_LIFECYCLE_MSG = 60105,
TS_EVENT_LIFECYCLE_TASK_THREADS_READY = 60106,
TS_EVENT_LIFECYCLE_SHUTDOWN = 60107,
TS_EVENT_LIFECYCLE_LOG_INITIAZLIED = 60108,

TS_EVENT_INTERNAL_60200 = 60200,
TS_EVENT_INTERNAL_60201 = 60201,
Expand Down Expand Up @@ -578,6 +579,7 @@ enum TSLifecycleHookID {
TS_LIFECYCLE_TASK_THREADS_READY_HOOK,
TS_LIFECYCLE_SHUTDOWN_HOOK,
TS_LIFECYCLE_SSL_SECRET_HOOK,
TS_LIFECYCLE_LOG_INITIAZLIED_HOOK,
TS_LIFECYCLE_LAST_HOOK,
};

Expand Down Expand Up @@ -1083,9 +1085,11 @@ using TSRemapPluginInfo = struct tsapi_remap_plugin_info *;

using TSFetchSM = struct tsapi_fetchsm *;

using TSThreadFunc = void *(*)(void *data);
using TSEventFunc = int (*)(TSCont contp, TSEvent event, void *edata);
using TSConfigDestroyFunc = void (*)(void *data);
using TSThreadFunc = void *(*)(void *data);
using TSEventFunc = int (*)(TSCont contp, TSEvent event, void *edata);
using TSConfigDestroyFunc = void (*)(void *data);
using TSLogMarshalCallback = int (*)(TSHttpTxn, char *);
using TSLogUnmarshalCallback = int (*)(char **, char *, int);

struct TSFetchEvent {
int success_event_id;
Expand Down Expand Up @@ -1570,6 +1574,13 @@ struct TSResponseAction {
bool no_cache;
};

enum TSLogType {
TS_LOG_TYPE_S_INT,
TS_LOG_TYPE_D_INT,
TS_LOG_TYPE_STRING,
TS_LOG_TYPE_IP,
};

/* --------------------------------------------------------------------------
Init */

Expand Down
3 changes: 3 additions & 0 deletions include/ts/ts.h
Original file line number Diff line number Diff line change
Expand Up @@ -3224,3 +3224,6 @@ TSReturnCode TSVConnPPInfoGet(TSVConn vconn, uint16_t key, const char **value, i

*/
TSReturnCode TSVConnPPInfoIntGet(TSVConn vconn, uint16_t key, TSMgmtInt *value);

TSReturnCode TSLogFieldRegister(std::string_view name, std::string_view symbol, TSLogType type, TSLogMarshalCallback marshal_cb,
TSLogUnmarshalCallback unmarshal_cb);
12 changes: 12 additions & 0 deletions src/api/InkAPI.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8996,3 +8996,15 @@ TSConnectionLimitExemptListClear()
{
ConnectionTracker::clear_client_exempt_list();
}

TSReturnCode
TSLogFieldRegister(std::string_view name, std::string_view symbol, TSLogType type, TSLogMarshalCallback marshal_cb,
TSLogUnmarshalCallback unmarshal_cb)
{
LogField *field = new LogField(name.data(), symbol.data(), static_cast<LogField::Type>(type),
reinterpret_cast<LogField::CustomMarshalFunc>(marshal_cb), unmarshal_cb);
Log::global_field_list.add(field, false);
Log::field_symbol_hash.emplace(symbol.data(), field);

return TS_SUCCESS;
}
78 changes: 78 additions & 0 deletions src/iocore/net/ProxyProtocol.cc
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,84 @@ ProxyProtocol::get_tlv(const uint8_t tlvCode) const
return std::nullopt;
}

/*
* PP2_TYPE_SSL
* struct pp2_tlv_ssl {
* uint8_t client;
* uint32_t verify;
* struct pp2_tlv sub_tlv[0];
* };
*/

std::optional<std::string_view>
ProxyProtocol::_get_tlv_ssl_subtype(uint8_t subtype) const
{
if (auto v = tlv.find(PP2_TYPE_SSL); v != tlv.end() && v->second.length() != 0) {
auto ssl = v->second;

// Is the client connected over TLS
if ((ssl.data()[0] & 0x01) == 0) {
// Not over TLS
return std::nullopt;
}

if (ssl.length() < 5) {
return std::nullopt;
}

// Find the given subtype
uint16_t len = ssl.length();
const char *p = ssl.data() + 5; // Skip client (uint8_t) + verify (uint32_t)
Comment on lines 562 to 577
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_tlv_ssl_subtype() reads ssl.data()[0] and skips 5 bytes without verifying the TLV value is at least 5 bytes long. If PP2_TYPE_SSL is present but truncated/malformed, this will access beyond the string_view. Add a ssl.size() >= 5 (and preferably >= 5 + 3 before parsing sub-TLVs) guard before dereferencing and parsing.

Copilot uses AI. Check for mistakes.
const char *end = ssl.data() + len;
while (p != end) {
Comment on lines 575 to 579
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_get_tlv_ssl_subtype() computes the end pointer incorrectly (it uses end = p + len + 1 after already advancing p by 5). This makes the loop read past the end of the PP2_TYPE_SSL TLV value and can cause out-of-bounds reads/undefined behavior. Compute end from the start of the SSL value (e.g., ssl.data() + ssl.size()) and iterate while p < end (not p != end).

Suggested change
// Find the given subtype
uint16_t len = ssl.length();
const char *p = ssl.data() + 5; // Skip client (uint8_t) + verify (uint32_t)
const char *end = p + len + 1;
while (p != end) {
// The SSL TLV must contain at least the client (uint8_t) and verify (uint32_t) fields.
if (ssl.length() < 5) {
Dbg(dbg_ctl_proxyprotocol_v2, "SSL TLV too short: %zu bytes (expected at least 5)", static_cast<size_t>(ssl.length()));
return std::nullopt;
}
// Find the given subtype
uint16_t len = ssl.length();
const char *p = ssl.data() + 5; // Skip client (uint8_t) + verify (uint32_t)
const char *end = ssl.data() + len; // End of the SSL TLV value
while (p < end) {

Copilot uses AI. Check for mistakes.
if (end - p < 3) {
// The size of a sub TLV entry must be 3 bytes or more
Dbg(dbg_ctl_proxyprotocol_v2, "Remaining data (%ld bytes) is not enough for a sub TLV field", end - p);
return std::nullopt;
}

// Type
uint8_t type = *p;
p += 1;

// Length
uint16_t length = ntohs(*reinterpret_cast<const uint16_t *>(p));
p += 2;

// Value
if (end - p < length) {
// Does not have enough data
Dbg(dbg_ctl_proxyprotocol_v2, "Remaining data (%ld bytes) is not enough for a TLV field (ID:%u LEN:%hu)", end - p, type,
length);
return std::nullopt;
}

// Found it?
if (type == subtype) {
Dbg(dbg_ctl_proxyprotocol, "TLV: ID=%u LEN=%hu", type, length);
return std::string_view(p, length);
}

p += length;
}
}
return std::nullopt;
}

std::optional<std::string_view>
ProxyProtocol::get_tlv_ssl_version() const
{
// The specification only says "the US-ASCII string representation of the TLS version".
// HAProxy sends a string returned by SSL_get_version.
return this->_get_tlv_ssl_subtype(PP2_SUBTYPE_SSL_VERSION);
}

std::optional<std::string_view>
ProxyProtocol::get_tlv_ssl_cipher() const
{
return this->_get_tlv_ssl_subtype(PP2_SUBTYPE_SSL_CIPHER);
}

int
ProxyProtocol::set_additional_data(std::string_view data)
{
Expand Down
10 changes: 8 additions & 2 deletions src/iocore/net/unit_tests/test_ProxyProtocol.cc
Original file line number Diff line number Diff line change
Expand Up @@ -303,13 +303,16 @@ TEST_CASE("PROXY Protocol v2 Parser", "[ProxyProtocol][ProxyProtocolv2]")
0x55, 0x49, 0x54, 0x0A, ///<
0x21, ///< version & command
0x11, ///< protocol & family
0x00, 0x17, ///< len
0x00, 0x2B, ///< len
0xC0, 0x00, 0x02, 0x01, ///< src_addr
0xC6, 0x33, 0x64, 0x01, ///< dst_addr
0xC3, 0x50, ///< src_port
0x01, 0xBB, ///< dst_port
0x01, 0x00, 0x02, 0x68, 0x32, /// PP2_TYPE_ALPN (h2)
0x02, 0x00, 0x03, 0x61, 0x62, 0x63 /// PP2_TYPE_AUTHORITY (abc)
0x02, 0x00, 0x03, 0x61, 0x62, 0x63, /// PP2_TYPE_AUTHORITY (abc)
0x20, 0x00, 0x11, 0x01, 0x00, 0x00, 0x00, 0x00, /// PP2_TYPE_SSL (client=0x01, verify=0)
0x23, 0x00, 0x03, 0x58, 0x59, 0x5A, /// PP2_SUBTYPE_SSL_CIPHER (XYZ)
0x21, 0x00, 0x03, 0x54, 0x4C, 0x53, /// PP2_SUBTYPE_SSL_VERSION (TLS)
};

swoc::TextView tv(reinterpret_cast<char *>(raw_data), sizeof(raw_data));
Expand All @@ -327,6 +330,9 @@ TEST_CASE("PROXY Protocol v2 Parser", "[ProxyProtocol][ProxyProtocolv2]")

CHECK(pp_info.tlv[PP2_TYPE_ALPN] == "h2");
CHECK(pp_info.tlv[PP2_TYPE_AUTHORITY] == "abc");

CHECK(pp_info.get_tlv_ssl_cipher() == "XYZ");
CHECK(pp_info.get_tlv_ssl_version() == "TLS");
}

SECTION("TLVs with extra data")
Expand Down
24 changes: 24 additions & 0 deletions src/proxy/logging/Log.cc
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@

#include "tscore/MgmtDefs.h"

#include <api/LifecycleAPIHooks.h>

#define PERIODIC_TASKS_INTERVAL_FALLBACK 5

// Log global objects
Expand Down Expand Up @@ -1034,6 +1036,16 @@ Log::init_fields()
global_field_list.add(field, false);
field_symbol_hash.emplace("ppa", field);

field = new LogField("proxy_protocol_tls_cipher", "pptc", LogField::STRING, &LogAccess::marshal_proxy_protocol_tls_cipher,
&LogAccess::unmarshal_str);
global_field_list.add(field, false);
field_symbol_hash.emplace("pptc", field);

field = new LogField("proxy_protocol_tls_version", "pptv", LogField::STRING, &LogAccess::marshal_proxy_protocol_tls_version,
&LogAccess::unmarshal_str);
global_field_list.add(field, false);
field_symbol_hash.emplace("pptv", field);

field = new LogField("version_build_number", "vbn", LogField::STRING, &LogAccess::marshal_version_build_number,
&LogAccess::unmarshal_str);
global_field_list.add(field, false);
Expand All @@ -1043,9 +1055,21 @@ Log::init_fields()
global_field_list.add(field, false);
field_symbol_hash.emplace("vs", field);

Log::init_plugin_fields();

init_status |= FIELDS_INITIALIZED;
}

void
Log::init_plugin_fields()
{
APIHook *hook = g_lifecycle_hooks->get(TS_LIFECYCLE_LOG_INITIAZLIED_HOOK);
while (hook) {
hook->invoke(TS_EVENT_LIFECYCLE_LOG_INITIAZLIED, nullptr);
hook = hook->next();
}
}

/*-------------------------------------------------------------------------

Initialization functions
Expand Down
Loading