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

Reply via email to