This is an automated email from the ASF dual-hosted git repository.
JosiahWI pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficserver.git
The following commit(s) were added to refs/heads/master by this push:
new 05c958a8db Improve testing and documentation for client firewall marks
(#13383)
05c958a8db is described below
commit 05c958a8db881288c9288e6a78dcff67818761d5
Author: JosiahWI <[email protected]>
AuthorDate: Wed Jul 15 07:21:14 2026 -0500
Improve testing and documentation for client firewall marks (#13383)
* Improve docs for `TSHttpTxnClientPacketMarkSet`
* Add AuTest for `TSHttpTxnClientPacketMarkSet`
* Make changes requested by Brian Neradt
Allow PACKET_MARK in sock_option_flag_in
Mention `CAP_NET_RAW` in docstring
---
.../functions/TSHttpTxnClientPacketMarkSet.en.rst | 17 ++-
include/ts/ts.h | 15 +-
.../client_packet_mark/client_packet_mark.test.py | 103 +++++++++++++
tests/tools/plugins/CMakeLists.txt | 1 +
tests/tools/plugins/client_packet_mark.cc | 168 +++++++++++++++++++++
5 files changed, 299 insertions(+), 5 deletions(-)
diff --git
a/doc/developer-guide/api/functions/TSHttpTxnClientPacketMarkSet.en.rst
b/doc/developer-guide/api/functions/TSHttpTxnClientPacketMarkSet.en.rst
index 4cdbc5c990..e0752c2dcf 100644
--- a/doc/developer-guide/api/functions/TSHttpTxnClientPacketMarkSet.en.rst
+++ b/doc/developer-guide/api/functions/TSHttpTxnClientPacketMarkSet.en.rst
@@ -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
========
diff --git a/include/ts/ts.h b/include/ts/ts.h
index 5631bee0f6..2a4eacfd4d 100644
--- a/include/ts/ts.h
+++ b/include/ts/ts.h
@@ -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);
diff --git
a/tests/gold_tests/pluginTest/client_packet_mark/client_packet_mark.test.py
b/tests/gold_tests/pluginTest/client_packet_mark/client_packet_mark.test.py
new file mode 100644
index 0000000000..796e6d5d44
--- /dev/null
+++ b/tests/gold_tests/pluginTest/client_packet_mark/client_packet_mark.test.py
@@ -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.
+ """
+ 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,
+ '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()
diff --git a/tests/tools/plugins/CMakeLists.txt
b/tests/tools/plugins/CMakeLists.txt
index bbdc5866cd..a658ba9aa2 100644
--- a/tests/tools/plugins/CMakeLists.txt
+++ b/tests/tools/plugins/CMakeLists.txt
@@ -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)
diff --git a/tests/tools/plugins/client_packet_mark.cc
b/tests/tools/plugins/client_packet_mark.cc
new file mode 100644
index 0000000000..72acab439f
--- /dev/null
+++ b/tests/tools/plugins/client_packet_mark.cc
@@ -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 = "[email protected]";
+
+ 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);
+}