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
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,24 @@ Synopsis
Description
===========

Change packet firewall :arg:`mark` for the client side connection.
Change the packet firewall :arg:`mark` for the client side connection. The
entire firewall mark is replaced with :arg:`mark`, which is interpreted as a
32-bit unsigned bit pattern.

Returns :const:`TS_SUCCESS` when the client connection was modified, and
:const:`TS_ERROR` when there is no client connection to modify.

.. note::

The firewall mark is only honored on platforms whose OS supports it,
specifically Linux via ``SO_MARK``. On platforms without ``SO_MARK`` support
the call still returns :const:`TS_SUCCESS` when a client connection is
present, but setting the mark has no effect at the OS layer (it is a safe
no-op).

.. note::

Changes take effect immediately.
The change takes effect immediately on the live client connection.

See Also
========
Expand Down
15 changes: 12 additions & 3 deletions include/ts/ts.h
Original file line number Diff line number Diff line change
Expand Up @@ -1585,10 +1585,19 @@ TSReturnCode TSHttpSsnClientFdGet(TSHttpSsn ssnp, int *fdp);
/* TS-1008 END */

/** Change packet firewall mark for the client side connection
*
@note The change takes effect immediately

@return TS_SUCCESS if the client connection was modified
Sets the entire client-side packet firewall mark to @a mark; the whole mark is replaced. @a mark
is interpreted as a 32-bit unsigned bit pattern.

@note The firewall mark is only honored on platforms whose OS supports it, specifically Linux via
@c SO_MARK. On platforms without @c SO_MARK support the call still returns TS_SUCCESS when a
client connection is present, but setting the mark has no effect at the OS layer (it is a safe
no-op).

@note The change takes effect immediately on the live client connection

@return TS_SUCCESS if the client connection was modified, TS_ERROR if there is no client
connection to modify
*/
TSReturnCode TSHttpTxnClientPacketMarkSet(TSHttpTxn txnp, int mark);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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.

import os
import socket

Test.Summary = '''
Verify TSHttpTxnClientPacketMarkSet sets the client-side firewall mark to the
supplied value, using a test plugin that reads the applied mark back off the
client socket.
'''


def _can_set_so_mark() -> bool:
"""Probe whether SO_MARK can actually be set on this host.

Setting SO_MARK is Linux-only and requires CAP_NET_ADMIN or CAP_NET_RAW.
On any host that lacks the capability (or the platform), setsockopt raises,
and the applied value would be unobservable -- so the test is skipped
rather than failed.
"""
Comment thread
JosiahWI marked this conversation as resolved.
if not hasattr(socket, "SO_MARK"):
return False
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
probe.setsockopt(socket.SOL_SOCKET, socket.SO_MARK, 0x1)
return True
except (OSError, PermissionError):
return False


Test.SkipUnless(
Condition.IsPlatform("linux"),
Condition(_can_set_so_mark, "Setting SO_MARK requires Linux with CAP_NET_ADMIN or CAP_NET_RAW", True),
)


class ClientPacketMarkTest:
"""Drive TSHttpTxnClientPacketMarkSet through a test plugin and assert on the
firewall mark read back off the client socket.

The starting mark is seeded per process via
proxy.config.net.sock_packet_mark_in, applied at accept time.
"""

# Value the plugin sets; the mark is expected to become exactly this.
SET_MARK = 0x0000000A

def __init__(self):
self._server = self._make_server()
self._ts = self._make_ats("ts", seed_mark=0x0000FF00)

def _make_server(self) -> 'Process':
server = Test.MakeOriginServer("server")
request_header = {"headers": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
response_header = {"headers": "HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n", "timestamp": "1469733493.993", "body": ""}
server.addResponse("sessionlog.json", request_header, response_header)
return server

def _make_ats(self, name: str, seed_mark: int) -> 'Process':
ts = Test.MakeATSProcess(name, enable_cache=False)
ts.Disk.records_config.update(
{
'proxy.config.net.sock_packet_mark_in': seed_mark,
Comment thread
JosiahWI marked this conversation as resolved.
'proxy.config.net.sock_option_flag_in': 0x11,
'proxy.config.diags.debug.enabled': 1,
'proxy.config.diags.debug.tags': 'http|client_packet_mark',
'proxy.config.url_remap.remap_required': 0,
# Keep ATS running as the invoking user inside sudo (no privilege drop).
'proxy.config.admin.user_id': '#-1',
})
ts.Disk.remap_config.AddLine(f"map / http://127.0.0.1:{self._server.Variables.Port}")
Test.PrepareTestPlugin(os.path.join(Test.Variables.AtsTestPluginsDir, 'client_packet_mark.so'), ts)
return ts

def run(self):
# The mark is set to the supplied value, regardless of the seeded
# starting mark.
tr = Test.AddTestRun("TSHttpTxnClientPacketMarkSet sets the mark")
tr.Processes.Default.StartBefore(self._server)
tr.Processes.Default.StartBefore(self._ts)
tr.MakeCurlCommand(
f'--verbose --ipv4 --header "X-Set-Mark: 0x{self.SET_MARK:08x}" http://localhost:{self._ts.Variables.port}/',
ts=self._ts)
tr.Processes.Default.ReturnCode = 0
tr.Processes.Default.Streams.All += Testers.ContainsExpression(
f"X-Client-Packet-Mark: 0x{self.SET_MARK:08x}", f"Observed client packet mark should be 0x{self.SET_MARK:08x}")


ClientPacketMarkTest().run()
1 change: 1 addition & 0 deletions tests/tools/plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#
#######################

add_autest_plugin(client_packet_mark client_packet_mark.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
168 changes: 168 additions & 0 deletions tests/tools/plugins/client_packet_mark.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/** @file

Test plugin for the TSHttpTxnClientPacketMarkSet API.

On each request it reads a target mark from request headers, applies it to the
client-side connection via TSHttpTxnClientPacketMarkSet, then reads the mark
back off the client socket with getsockopt(SO_MARK) and echoes the observed
value into the X-Client-Packet-Mark response header for the AuTest to assert
on.

@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>

extern "C" {
#include <sys/socket.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;
}

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);
}
}

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());
}

TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE);
return 0;
}

} // anonymous namespace

void
TSPluginInit(int /* argc ATS_UNUSED */, const char ** /* argv ATS_UNUSED */)
{
TSPluginRegistrationInfo info;
info.plugin_name = PLUGIN_NAME.data();
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());
return;
}

TSCont contp = TSContCreate(handle_send_response, nullptr);
TSHttpHookAdd(TS_HTTP_SEND_RESPONSE_HDR_HOOK, contp);
}