Skip to content
Merged
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
2 changes: 1 addition & 1 deletion tests/tools/plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#
#######################

add_autest_plugin(client_packet_mark client_packet_mark.cc)
add_autest_plugin(client_packet_mark client_packet_mark.cc packet_mark_common.cc)
add_autest_plugin(conf_remap_stripped conf_remap_stripped.cc)
add_autest_plugin(continuations_verify continuations_verify.cc)
add_autest_plugin(cont_schedule cont_schedule.cc)
Expand Down
118 changes: 14 additions & 104 deletions tests/tools/plugins/client_packet_mark.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,121 +27,31 @@
limitations under the License.
*/

#include <ts/ts.h>
#include "packet_mark_common.h"

extern "C" {
#include <sys/socket.h>
}
#include <ts/ts.h>

#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <optional>
#include <string>
#include <string_view>

namespace
{
constexpr std::string_view PLUGIN_NAME = "client_packet_mark";
constexpr std::string_view MARK_HEADER = "X-Set-Mark";
constexpr std::string_view ECHO_HEADER = "X-Client-Packet-Mark";

DbgCtl dbg_ctl{PLUGIN_NAME.data()};

/** Read a header field and interpret its value as a 32-bit unsigned quantity.

Values are parsed with strtoul (base 0), so "0x0000000A" and "10" are both
accepted. Returns std::nullopt if the header is absent. */
std::optional<uint32_t>
get_uint_header(TSMBuffer bufp, TSMLoc hdr_loc, std::string_view header)
{
TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, header.data(), static_cast<int>(header.length()));
if (field_loc == TS_NULL_MLOC) {
return std::nullopt;
}
constexpr char PLUGIN_NAME[] = "client_packet_mark";
constexpr char MARK_HEADER[] = "X-Set-Mark";
constexpr char ECHO_HEADER[] = "X-Client-Packet-Mark";

int value_len = 0;
const char *value_str = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, -1, &value_len);
uint32_t result = 0;
if (value_str != nullptr && value_len > 0) {
std::string value(value_str, value_len);
result = static_cast<uint32_t>(strtoul(value.c_str(), nullptr, 0));
}
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
return result;
}

/** Create the echo header on the response with the value formatted as 0x%08x. */
void
set_echo_header(TSMBuffer bufp, TSMLoc hdr_loc, uint32_t value)
{
// 0x + 8 hex digits for a uint32_t + NUL = 11 bytes; 16 is comfortably enough.
char formatted[16];
std::snprintf(formatted, sizeof(formatted), "0x%08x", value);

TSMLoc field_loc = TS_NULL_MLOC;
if (TSMimeHdrFieldCreateNamed(bufp, hdr_loc, ECHO_HEADER.data(), static_cast<int>(ECHO_HEADER.length()), &field_loc) ==
TS_SUCCESS) {
// -1 length lets the API strlen the null-terminated buffer, so we do not
// rely on snprintf's return value (which is the would-be length, not the
// truncated length) as a byte count.
TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, formatted, -1);
TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc);
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
}
}
DbgCtl dbg_ctl{PLUGIN_NAME};

int
handle_send_response(TSCont /* contp ATS_UNUSED */, TSEvent event, void *edata)
{
TSHttpTxn txnp = static_cast<TSHttpTxn>(edata);

if (event != TS_EVENT_HTTP_SEND_RESPONSE_HDR) {
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return 0;
}

TSMBuffer req_bufp = nullptr;
TSMLoc req_loc = TS_NULL_MLOC;
if (TSHttpTxnClientReqGet(txnp, &req_bufp, &req_loc) != TS_SUCCESS) {
TSError("[%s] Failed to get client request headers", PLUGIN_NAME.data());
TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return 0;
}

std::optional<uint32_t> mark = get_uint_header(req_bufp, req_loc, MARK_HEADER);
TSHandleMLocRelease(req_bufp, TS_NULL_MLOC, req_loc);

if (mark.has_value()) {
Dbg(dbg_ctl, "Setting client packet mark to 0x%08x", *mark);
TSHttpTxnClientPacketMarkSet(txnp, static_cast<int>(*mark));
}

uint32_t observed = 0;
#if defined(SO_MARK)
int client_fd = -1;
if (TSHttpTxnClientFdGet(txnp, &client_fd) == TS_SUCCESS && client_fd >= 0) {
socklen_t optlen = sizeof(observed);
if (getsockopt(client_fd, SOL_SOCKET, SO_MARK, &observed, &optlen) != 0) {
TSError("[%s] getsockopt(SO_MARK) failed on fd %d", PLUGIN_NAME.data(), client_fd);
}
} else {
TSError("[%s] Failed to obtain client fd", PLUGIN_NAME.data());
}
#else
// SO_MARK is Linux-only. On other platforms the accompanying AuTest is skipped
// via Test.SkipUnless, so this readback path is never exercised; keep it
// compilable so the plugin still builds everywhere.
TSError("[%s] SO_MARK is not supported on this platform", PLUGIN_NAME.data());
#endif

TSMBuffer resp_bufp = nullptr;
TSMLoc resp_loc = TS_NULL_MLOC;
if (TSHttpTxnClientRespGet(txnp, &resp_bufp, &resp_loc) == TS_SUCCESS) {
set_echo_header(resp_bufp, resp_loc, observed);
TSHandleMLocRelease(resp_bufp, TS_NULL_MLOC, resp_loc);
} else {
TSError("[%s] Failed to get client response headers", PLUGIN_NAME.data());
if (event == TS_EVENT_HTTP_SEND_RESPONSE_HDR) {
// The client connection is live here; this applies the mark to it and reads
// it back off the client socket.
packet_mark::LogContext log{PLUGIN_NAME, dbg_ctl};
packet_mark::apply_client_mark(log, txnp, MARK_HEADER);
packet_mark::echo_client_mark(log, txnp, ECHO_HEADER);
}

TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
Expand All @@ -154,12 +64,12 @@ void
TSPluginInit(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */)
{
TSPluginRegistrationInfo info;
info.plugin_name = PLUGIN_NAME.data();
info.plugin_name = PLUGIN_NAME;
info.vendor_name = "Apache Software Foundation";
info.support_email = "dev@trafficserver.apache.org";

if (TSPluginRegister(&info) != TS_SUCCESS) {
TSError("[%s] Plugin registration failed", PLUGIN_NAME.data());
TSError("[%s] Plugin registration failed", PLUGIN_NAME);
return;
}

Expand Down
187 changes: 187 additions & 0 deletions tests/tools/plugins/packet_mark_common.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/** @file

Shared helpers for the packet-mark test plugins.

@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 "packet_mark_common.h"

extern "C" {
#include <sys/socket.h>
}

#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <optional>
#include <string>

namespace packet_mark
{
namespace
{
// The tsapi setter and getters are bound as non-type template parameters below,
// which pins the correct client/server trio at compile time. These must be raw
// function-pointer types, not std::function: a non-type template parameter has
// to be a structural type, and std::function is a runtime type-erasure wrapper,
// so template <std::function<...> Setter> is ill-formed.
using MarkSetter = TSReturnCode (*)(TSHttpTxn, int);
using FdGetter = TSReturnCode (*)(TSHttpTxn, int *);
using RespGetter = TSReturnCode (*)(TSHttpTxn, TSMBuffer *, TSMLoc *);

std::optional<uint32_t>
get_uint_header(TSMBuffer bufp, TSMLoc hdr_loc, std::string_view header)
{
// Values are parsed with strtoul (base 0), so "0x0000000A" and "10" are both
// accepted. Returns std::nullopt if the header is absent, empty, or not a valid
// number -- a malformed value is a test-harness error, not a silent 0.
TSMLoc field_loc{TSMimeHdrFieldFind(bufp, hdr_loc, header.data(), static_cast<int>(header.length()))};
if (field_loc == TS_NULL_MLOC) {
return std::nullopt;
}

int value_len{0};
char const *value_str{TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, -1, &value_len)};

std::optional<uint32_t> result{std::nullopt};
if (value_str != nullptr && value_len > 0) {
std::string value{value_str, static_cast<std::size_t>(value_len)};
char *end{nullptr};
errno = 0;
unsigned long const parsed{std::strtoul(value.c_str(), &end, 0)};
// Reject empty, partially-numeric, or out-of-range values: a malformed
// header is a test-harness error, not a silent 0.
if (errno == 0 && end == value.c_str() + value.size() && parsed <= UINT32_MAX) {
result = static_cast<uint32_t>(parsed);
}
}
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
return result;
}

void
set_echo_header(TSMBuffer bufp, TSMLoc hdr_loc, std::string_view header, uint32_t value)
{
// 0x + 8 hex digits for a uint32_t + NUL = 11 bytes; 16 is comfortably enough.
char formatted[16];
std::snprintf(formatted, sizeof(formatted), "0x%08x", value);

TSMLoc field_loc{TS_NULL_MLOC};
if (TSMimeHdrFieldCreateNamed(bufp, hdr_loc, header.data(), static_cast<int>(header.length()), &field_loc) == TS_SUCCESS) {
// -1 length lets the API strlen the null-terminated buffer, so we do not
// rely on snprintf's return value (which is the would-be length, not the
// truncated length) as a byte count.
TSMimeHdrFieldValueStringSet(bufp, hdr_loc, field_loc, -1, formatted, -1);
TSMimeHdrFieldAppend(bufp, hdr_loc, field_loc);
TSHandleMLocRelease(bufp, hdr_loc, field_loc);
}
}

std::optional<uint32_t>
get_so_mark([[maybe_unused]] int fd)
{
#if defined(SO_MARK)
if (fd < 0) {
return std::nullopt;
}

uint32_t observed{0};
socklen_t optlen{sizeof(observed)};
if (getsockopt(fd, SOL_SOCKET, SO_MARK, &observed, &optlen) != 0) {
return std::nullopt;
}
return observed;
#else
// SO_MARK is Linux-only. On other platforms the accompanying AuTest is
// skipped via Test.SkipUnless, so this readback path is never exercised;
// keep it compilable so the plugins still build everywhere.
return std::nullopt;
#endif
}

// Parameterized on the exact tsapi function and kept private to this file,
// driven only by the named entry points below. The public API is split by
// client/server rather than taking the function as an argument so each plugin
// links against exactly the tsapi trio it exercises.
template <MarkSetter Setter>
void
apply_mark_from_header(const LogContext &log, TSHttpTxn txnp, std::string_view header)
{
TSMBuffer req_bufp{nullptr};
TSMLoc req_loc{TS_NULL_MLOC};
if (TSHttpTxnClientReqGet(txnp, &req_bufp, &req_loc) != TS_SUCCESS) {
TSError("[%.*s] Failed to get client request headers", static_cast<int>(log.plugin_name.length()), log.plugin_name.data());
return;
}

std::optional<uint32_t> const mark{get_uint_header(req_bufp, req_loc, header)};
TSHandleMLocRelease(req_bufp, TS_NULL_MLOC, req_loc);

if (mark.has_value()) {
Dbg(log.dbg_ctl, "Setting packet mark to 0x%08x (via %.*s)", *mark, static_cast<int>(header.length()), header.data());
if (Setter(txnp, static_cast<int>(*mark)) != TS_SUCCESS) {
TSError("[%.*s] Failed to set packet mark 0x%08x", static_cast<int>(log.plugin_name.length()), log.plugin_name.data(),
*mark);
}
}
}

template <FdGetter FdGet, RespGetter RespGet>
void
echo_observed_mark(const LogContext &log, TSHttpTxn txnp, std::string_view echo_header)
{
int fd{-1};
if (FdGet(txnp, &fd) != TS_SUCCESS || fd < 0) {
TSError("[%.*s] Failed to obtain socket fd", static_cast<int>(log.plugin_name.length()), log.plugin_name.data());
return;
}

std::optional<uint32_t> const observed{get_so_mark(fd)};
if (!observed.has_value()) {
TSError("[%.*s] Failed to read SO_MARK on fd %d", static_cast<int>(log.plugin_name.length()), log.plugin_name.data(), fd);
return;
}

TSMBuffer resp_bufp{nullptr};
TSMLoc resp_loc{TS_NULL_MLOC};
if (RespGet(txnp, &resp_bufp, &resp_loc) != TS_SUCCESS) {
TSError("[%.*s] Failed to get response headers", static_cast<int>(log.plugin_name.length()), log.plugin_name.data());
return;
}

set_echo_header(resp_bufp, resp_loc, echo_header, *observed);
TSHandleMLocRelease(resp_bufp, TS_NULL_MLOC, resp_loc);
}
} // anonymous namespace

void
apply_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view header)
{
apply_mark_from_header<TSHttpTxnClientPacketMarkSet>(log, txnp, header);
}

void
echo_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view echo_header)
{
echo_observed_mark<TSHttpTxnClientFdGet, TSHttpTxnClientRespGet>(log, txnp, echo_header);
}

} // namespace packet_mark
47 changes: 47 additions & 0 deletions tests/tools/plugins/packet_mark_common.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/** @file

Shared helpers for the packet-mark test plugins.

The plugin reads a target mark out of a request header, applies it to a
connection via the tsapi under test, reads the applied mark back off the
relevant socket with getsockopt(SO_MARK), and echoes the observed value into a
response header for the accompanying AuTest to assert on. Everything except
the tsapi call and the fd getter is identical, so it lives here.

@section license License

Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

#pragma once

#include <ts/ts.h>

#include <string_view>

namespace packet_mark
{
struct LogContext {
std::string_view plugin_name;
const DbgCtl &dbg_ctl;
};

void apply_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view header);

void echo_client_mark(const LogContext &log, TSHttpTxn txnp, std::string_view echo_header);

} // namespace packet_mark