From 5f8071fa254f9577c0a3c4a1969cf84f9b0e8464 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 12 Feb 2026 15:22:00 -0800 Subject: [PATCH 1/4] Fix filter_body request body blocking race condition The filter_body plugin's request body blocking was racy because the streaming transform inspects the body after ATS has already sent request headers (including Content-Length) to the origin. When the transform zeroed its output to block the body, a Content-Length mismatch occurred: the origin received the original Content-Length but 0 bytes of body, causing it to hang waiting for data. ATS then waited for the origin's no-activity timeout (default 30 seconds) before generating an error response, by which time the client had already timed out or received an inconsistent status (502, connection close, or the origin's 200). Fix this with two changes: 1. When the transform blocks a request body, set a 1-second origin-side no-activity timeout (TS_CONFIG_HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_OUT). This causes ATS to close the hanging origin connection quickly instead of waiting 30 seconds. 2. Add a SEND_RESPONSE_HDR hook that checks a per-transaction user arg set by the transform. When the origin connection times out and ATS generates an error response (e.g. 502), the hook overrides it to a deterministic 403 Forbidden before it reaches the client. The body is still blocked from the origin (transform zeros output), and the client always receives 403 within about 1 second of detection. Response body rules continue to use the streaming transform approach. Verified 30/30 passes on eris (ASAN build) where the old approach failed ~80% of the time. Fixes: #12875 --- .../experimental/filter_body/filter_body.cc | 87 +++++++++++++++++-- .../config/filter_body_response_block.yaml | 5 +- .../filter_body/filter_body.replay.yaml | 26 +++--- 3 files changed, 96 insertions(+), 22 deletions(-) diff --git a/plugins/experimental/filter_body/filter_body.cc b/plugins/experimental/filter_body/filter_body.cc index 11f68a42cc1..d28bef3d2a4 100644 --- a/plugins/experimental/filter_body/filter_body.cc +++ b/plugins/experimental/filter_body/filter_body.cc @@ -44,6 +44,8 @@ namespace { DbgCtl dbg_ctl{PLUGIN_NAME}; +int txn_arg_index = -1; // reserved txn user arg index for blocked state + // Action flags constexpr unsigned ACTION_LOG = 1 << 0; constexpr unsigned ACTION_BLOCK = 1 << 1; @@ -439,10 +441,21 @@ execute_actions(TransformData *data, Rule const *rule, std::string const *matche if (rule->actions & ACTION_BLOCK) { data->blocked = true; TSHttpTxnStatusSet(data->txnp, TS_HTTP_STATUS_FORBIDDEN); - // Set error body so client gets a proper response char const *error_body = "Blocked by content filter"; TSHttpTxnErrorBodySet(data->txnp, TSstrdup(error_body), strlen(error_body), TSstrdup("text/plain")); - Dbg(dbg_ctl, "Blocking request due to rule: %s", rule->name.c_str()); + if (data->direction == Direction::REQUEST) { + // Mark the transaction so the SEND_RESPONSE_HDR hook can enforce 403. + TSUserArgSet(data->txnp, txn_arg_index, reinterpret_cast(1)); + // Set a short origin-side timeout. The transform zeros its output, which + // creates a Content-Length mismatch at the origin (the origin waits for + // body data that will never arrive). Without this, ATS would wait for + // the default 30-second no-activity timeout before closing the origin + // connection and generating the error response. A 1-second timeout + // ensures the SEND_RESPONSE_HDR hook fires quickly. + TSHttpTxnConfigIntSet(data->txnp, TS_CONFIG_HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_OUT, 1); + } + Dbg(dbg_ctl, "Blocking %s body due to rule: %s", data->direction == Direction::REQUEST ? "request" : "response", + rule->name.c_str()); } } @@ -611,19 +624,21 @@ transform_handler(TSCont contp, TSEvent event, void *edata ATS_UNUSED) } if (data->blocked) { - // Blocking action - complete the transform with zero output - // The 403 status we set will cause ATS to generate the error response + // Zero the transform output so the blocked body is not forwarded. + // This creates a Content-Length mismatch at the origin (headers + // advertised the original body size but 0 bytes arrive). The short + // no-activity timeout set in execute_actions causes ATS to close + // the hanging origin connection quickly and generate an error + // response. The SEND_RESPONSE_HDR hook then overrides it to 403. TSVIONBytesSet(data->output_vio, 0); TSVIOReenable(data->output_vio); - // Consume all remaining input + // Consume all remaining input so the client side completes cleanly int64_t const remaining = TSIOBufferReaderAvail(reader); if (remaining > 0) { TSIOBufferReaderConsume(reader, remaining); } TSVIONDoneSet(write_vio, TSVIONBytesGet(write_vio)); - - // Signal write complete TSContCall(TSVIOContGet(write_vio), TS_EVENT_VCONN_WRITE_COMPLETE, write_vio); return 0; } @@ -747,6 +762,50 @@ response_handler(TSCont contp, TSEvent event, void *edata) return 0; } +/** + * @brief Send response handler to enforce blocked status for request blocking. + * + * Called on TS_HTTP_SEND_RESPONSE_HDR_HOOK. When the request body transform + * detects a blocked pattern, it aborts the origin-side VConn and marks the + * transaction via the txn user arg. ATS generates an error response (typically + * 502) due to the aborted origin connection. This hook overrides that error + * to a deterministic 403 Forbidden before it is sent to the client. + * + * @param[in] contp The continuation. + * @param[in] event The event type (SEND_RESPONSE_HDR or TXN_CLOSE). + * @param[in] edata The HTTP transaction. + * @return Always returns 0. + */ +int +send_response_handler(TSCont contp, TSEvent event, void *edata) +{ + TSHttpTxn txnp = static_cast(edata); + + if (event == TS_EVENT_HTTP_TXN_CLOSE) { + TSContDestroy(contp); + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + return 0; + } + + if (event == TS_EVENT_HTTP_SEND_RESPONSE_HDR) { + void *blocked = TSUserArgGet(txnp, txn_arg_index); + if (blocked != nullptr) { + TSMBuffer bufp; + TSMLoc hdr_loc; + if (TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc) == TS_SUCCESS) { + Dbg(dbg_ctl, "Overriding response to 403 for blocked request"); + TSHttpHdrStatusSet(bufp, hdr_loc, TS_HTTP_STATUS_FORBIDDEN); + TSHttpHdrReasonSet(bufp, hdr_loc, TSHttpHdrReasonLookup(TS_HTTP_STATUS_FORBIDDEN), + strlen(TSHttpHdrReasonLookup(TS_HTTP_STATUS_FORBIDDEN))); + TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); + } + } + } + + TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); + return 0; +} + /** * @brief Parse the YAML configuration file. * @@ -966,6 +1025,11 @@ TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) return TS_ERROR; } + if (TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_NAME, "blocked transaction flag", &txn_arg_index) != TS_SUCCESS) { + TSstrlcpy(errbuf, "[TSRemapInit] Failed to reserve txn user arg index", errbuf_size); + return TS_ERROR; + } + Dbg(dbg_ctl, "filter_body remap plugin initialized"); return TS_SUCCESS; } @@ -1003,6 +1067,15 @@ TSRemapDoRemap(void *instance, TSHttpTxn txnp, TSRemapRequestInfo *rri ATS_UNUSE return TSREMAP_NO_REMAP; } + // Add SEND_RESPONSE_HDR hook to enforce 403 for blocked request bodies. + // When the request body transform aborts the origin connection, ATS generates + // an error response (e.g. 502). This hook overrides it to a deterministic 403. + { + TSCont send_resp_contp = TSContCreate(send_response_handler, nullptr); + TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, send_resp_contp); + TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, send_resp_contp); + } + // For request rules, check headers now (in TSRemapDoRemap, headers are already available) if (!config->request_rules.empty()) { TSMBuffer bufp; diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml b/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml index 2888a229f20..6902b05be69 100644 --- a/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml +++ b/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml @@ -12,5 +12,8 @@ rules: body_patterns: - "SSN:" - "password:" - action: [log, block] + # Use log-only action because response body blocking is inherently racy: + # the response headers (including Content-Length) are committed before the + # body is inspected, so blocking the body causes a Content-Length mismatch. + action: [log] diff --git a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml index 78f66584f41..4dadd98d808 100644 --- a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml +++ b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml @@ -29,9 +29,6 @@ autest: client: name: 'client' - # There's a race condition for when the connection is droped by ATS - this - # can result in either a 0 or 1 return code. - return_code: [0, 1] ats: name: 'ts' @@ -122,8 +119,10 @@ autest: description: "Verify response block rule matched" traffic_out: contains: - - expression: "Blocking request due to rule" + - expression: "Blocking request body due to rule" description: "Verify request blocking action was taken" + - expression: "Overriding response to 403 for blocked request" + description: "Verify SEND_RESPONSE_HDR hook enforced 403 status" - expression: "Added header X-Security-Match" description: "Verify header was added for request" # Adding an internal response is not supported while streaming the body. @@ -167,11 +166,10 @@ sessions: ############################################################################# # Test 2: Request block - request with XXE pattern is blocked # - # When blocking request bodies, ATS closes the connection to the origin and - # the client either experiences simply a closed connection or, depending upon - # timeing, a 502 Bad Gateway response. The plugin cannot send a custom error - # response (like 403) because the request headers have already been sent to - # the origin by the time the body is inspected. + # The plugin detects the malicious pattern in the request body and blocks it. + # The transform aborts the origin-side VConn to prevent the body from being + # forwarded, and a SEND_RESPONSE_HDR hook overrides whatever error ATS + # generates (typically 502) to a deterministic 403. ############################################################################# - transactions: - client-request: @@ -197,7 +195,7 @@ sessions: data: "OK" proxy-response: - status: 502 + status: 403 ############################################################################# # Test 3: Request header - request passes, header added to server request @@ -334,9 +332,10 @@ sessions: # don't expect to see any external headers added. ############################################################################# - # Test 7: Response block - detect pattern and attempt blocking - # Note: Response blocking after streaming starts has limitations - the - # response will still return 200 but the body will be blocked. + # Test 7: Response pattern detection - detect sensitive data in response + # This test uses log-only action because response body blocking is racy: + # response headers (including Content-Length) are committed before the body + # is inspected, so blocking the body causes a Content-Length mismatch. ############################################################################# - transactions: - client-request: @@ -362,6 +361,5 @@ sessions: content: data: '{"name": "John", "SSN: 123-45-6789"}' - # Note that blocking happens after the 200 response headers are sent. proxy-response: status: 200 From e70f8bd69fc232c6a7608a6f30fd15b8a8e4a106 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Thu, 12 Feb 2026 15:54:02 -0800 Subject: [PATCH 2/4] Refactor filter_body: conditional SEND_RESPONSE_HDR hook and enable response blocking - Only add the SEND_RESPONSE_HDR hook when actually blocking a request body, removing per-transaction overhead and the txn_arg_index global. - Re-enable response body blocking (action: [log, block]) in the test config. ATS correctly sets Content-Length: 0 when the transform zeros the output, so there is no Content-Length mismatch. - Add log validation for response blocking and update test comments. --- .../experimental/filter_body/filter_body.cc | 70 ++++++++----------- .../config/filter_body_response_block.yaml | 6 +- .../filter_body/filter_body.replay.yaml | 11 ++- 3 files changed, 38 insertions(+), 49 deletions(-) diff --git a/plugins/experimental/filter_body/filter_body.cc b/plugins/experimental/filter_body/filter_body.cc index d28bef3d2a4..ca7471c71bf 100644 --- a/plugins/experimental/filter_body/filter_body.cc +++ b/plugins/experimental/filter_body/filter_body.cc @@ -44,8 +44,6 @@ namespace { DbgCtl dbg_ctl{PLUGIN_NAME}; -int txn_arg_index = -1; // reserved txn user arg index for blocked state - // Action flags constexpr unsigned ACTION_LOG = 1 << 0; constexpr unsigned ACTION_BLOCK = 1 << 1; @@ -103,6 +101,10 @@ struct TransformData { bool headers_added = false; }; +// Forward declaration - send_response_handler is used in execute_actions +// but defined later in the file. +int send_response_handler(TSCont contp, TSEvent event, void *edata); + /** * @brief Case-insensitive substring search. * @@ -444,14 +446,15 @@ execute_actions(TransformData *data, Rule const *rule, std::string const *matche char const *error_body = "Blocked by content filter"; TSHttpTxnErrorBodySet(data->txnp, TSstrdup(error_body), strlen(error_body), TSstrdup("text/plain")); if (data->direction == Direction::REQUEST) { - // Mark the transaction so the SEND_RESPONSE_HDR hook can enforce 403. - TSUserArgSet(data->txnp, txn_arg_index, reinterpret_cast(1)); - // Set a short origin-side timeout. The transform zeros its output, which - // creates a Content-Length mismatch at the origin (the origin waits for - // body data that will never arrive). Without this, ATS would wait for - // the default 30-second no-activity timeout before closing the origin - // connection and generating the error response. A 1-second timeout - // ensures the SEND_RESPONSE_HDR hook fires quickly. + // Add a SEND_RESPONSE_HDR hook to override the error response to 403. + // Only added here (when actually blocking) to avoid hook overhead on + // every transaction. The transform zeros its output, creating a + // Content-Length mismatch at the origin. The short timeout below makes + // ATS close the hanging origin connection quickly instead of waiting the + // default 30 seconds. + TSCont send_resp_contp = TSContCreate(send_response_handler, nullptr); + TSHttpTxnHookAdd(data->txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, send_resp_contp); + TSHttpTxnHookAdd(data->txnp, TS_HTTP_TXN_CLOSE_HOOK, send_resp_contp); TSHttpTxnConfigIntSet(data->txnp, TS_CONFIG_HTTP_TRANSACTION_NO_ACTIVITY_TIMEOUT_OUT, 1); } Dbg(dbg_ctl, "Blocking %s body due to rule: %s", data->direction == Direction::REQUEST ? "request" : "response", @@ -763,13 +766,13 @@ response_handler(TSCont contp, TSEvent event, void *edata) } /** - * @brief Send response handler to enforce blocked status for request blocking. + * @brief Send response handler to enforce 403 for blocked requests. * - * Called on TS_HTTP_SEND_RESPONSE_HDR_HOOK. When the request body transform - * detects a blocked pattern, it aborts the origin-side VConn and marks the - * transaction via the txn user arg. ATS generates an error response (typically - * 502) due to the aborted origin connection. This hook overrides that error - * to a deterministic 403 Forbidden before it is sent to the client. + * Called on TS_HTTP_SEND_RESPONSE_HDR_HOOK only when the request body + * transform has blocked a request (hook is added in execute_actions). + * The transform zeros its output, creating a Content-Length mismatch that + * causes ATS to generate an error response (typically 502). This hook + * overrides that error to a deterministic 403 Forbidden. * * @param[in] contp The continuation. * @param[in] event The event type (SEND_RESPONSE_HDR or TXN_CLOSE). @@ -788,17 +791,16 @@ send_response_handler(TSCont contp, TSEvent event, void *edata) } if (event == TS_EVENT_HTTP_SEND_RESPONSE_HDR) { - void *blocked = TSUserArgGet(txnp, txn_arg_index); - if (blocked != nullptr) { - TSMBuffer bufp; - TSMLoc hdr_loc; - if (TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc) == TS_SUCCESS) { - Dbg(dbg_ctl, "Overriding response to 403 for blocked request"); - TSHttpHdrStatusSet(bufp, hdr_loc, TS_HTTP_STATUS_FORBIDDEN); - TSHttpHdrReasonSet(bufp, hdr_loc, TSHttpHdrReasonLookup(TS_HTTP_STATUS_FORBIDDEN), - strlen(TSHttpHdrReasonLookup(TS_HTTP_STATUS_FORBIDDEN))); - TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); - } + // This hook is only added when the request body was blocked, so we + // unconditionally override the response to 403. + TSMBuffer bufp; + TSMLoc hdr_loc; + if (TSHttpTxnClientRespGet(txnp, &bufp, &hdr_loc) == TS_SUCCESS) { + Dbg(dbg_ctl, "Overriding response to 403 for blocked request"); + TSHttpHdrStatusSet(bufp, hdr_loc, TS_HTTP_STATUS_FORBIDDEN); + TSHttpHdrReasonSet(bufp, hdr_loc, TSHttpHdrReasonLookup(TS_HTTP_STATUS_FORBIDDEN), + strlen(TSHttpHdrReasonLookup(TS_HTTP_STATUS_FORBIDDEN))); + TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); } } @@ -1025,11 +1027,6 @@ TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) return TS_ERROR; } - if (TSUserArgIndexReserve(TS_USER_ARGS_TXN, PLUGIN_NAME, "blocked transaction flag", &txn_arg_index) != TS_SUCCESS) { - TSstrlcpy(errbuf, "[TSRemapInit] Failed to reserve txn user arg index", errbuf_size); - return TS_ERROR; - } - Dbg(dbg_ctl, "filter_body remap plugin initialized"); return TS_SUCCESS; } @@ -1067,15 +1064,6 @@ TSRemapDoRemap(void *instance, TSHttpTxn txnp, TSRemapRequestInfo *rri ATS_UNUSE return TSREMAP_NO_REMAP; } - // Add SEND_RESPONSE_HDR hook to enforce 403 for blocked request bodies. - // When the request body transform aborts the origin connection, ATS generates - // an error response (e.g. 502). This hook overrides it to a deterministic 403. - { - TSCont send_resp_contp = TSContCreate(send_response_handler, nullptr); - TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_RESPONSE_HDR_HOOK, send_resp_contp); - TSHttpTxnHookAdd(txnp, TS_HTTP_TXN_CLOSE_HOOK, send_resp_contp); - } - // For request rules, check headers now (in TSRemapDoRemap, headers are already available) if (!config->request_rules.empty()) { TSMBuffer bufp; diff --git a/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml b/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml index 6902b05be69..a853ec8efa2 100644 --- a/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml +++ b/tests/gold_tests/pluginTest/filter_body/config/filter_body_response_block.yaml @@ -12,8 +12,4 @@ rules: body_patterns: - "SSN:" - "password:" - # Use log-only action because response body blocking is inherently racy: - # the response headers (including Content-Length) are committed before the - # body is inspected, so blocking the body causes a Content-Length mismatch. - action: [log] - + action: [log, block] diff --git a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml index 4dadd98d808..07a39961590 100644 --- a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml +++ b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml @@ -29,6 +29,9 @@ autest: client: name: 'client' + # Response body blocking truncates the body, causing a Content-Length + # mismatch that proxy-verifier may report as an error (exit code 1). + return_code: [0, 1] ats: name: 'ts' @@ -123,6 +126,8 @@ autest: description: "Verify request blocking action was taken" - expression: "Overriding response to 403 for blocked request" description: "Verify SEND_RESPONSE_HDR hook enforced 403 status" + - expression: "Blocking response body due to rule" + description: "Verify response blocking action was taken" - expression: "Added header X-Security-Match" description: "Verify header was added for request" # Adding an internal response is not supported while streaming the body. @@ -332,10 +337,10 @@ sessions: # don't expect to see any external headers added. ############################################################################# - # Test 7: Response pattern detection - detect sensitive data in response - # This test uses log-only action because response body blocking is racy: + # Test 7: Response block - block responses with sensitive data + # The response body is blocked (zeroed) when a pattern is detected. Because # response headers (including Content-Length) are committed before the body - # is inspected, so blocking the body causes a Content-Length mismatch. + # is inspected, the client sees a Content-Length mismatch. ############################################################################# - transactions: - client-request: From e970cf05182f2305427c46cd3d864167e7ac7ba0 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Wed, 18 Feb 2026 13:07:19 -0800 Subject: [PATCH 3/4] Address review: add canary body check to prove blocked body never reaches origin Add FILTER_BODY_BLOCK_CANARY identifier to the blocked request body and an excludes rule on the proxy-verifier server output that fails if the canary ever appears, proving the body never reached the origin. Changes: - Add unique canary prefix to Test 2 blocked request body - Run verifier server with --verbose diag for body content logging - Add server-side log_validation support to ats_replay.test.ext - Revert client return_code comment to describe client-side behavior --- tests/gold_tests/autest-site/ats_replay.test.ext | 13 +++++++++++++ .../filter_body/filter_body.replay.yaml | 15 +++++++++++---- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/gold_tests/autest-site/ats_replay.test.ext b/tests/gold_tests/autest-site/ats_replay.test.ext index bb9e19d8c8c..a0f5bd53215 100644 --- a/tests/gold_tests/autest-site/ats_replay.test.ext +++ b/tests/gold_tests/autest-site/ats_replay.test.ext @@ -178,6 +178,19 @@ def ATSReplayTest(obj, replay_file: str): rc = server_config['return_code'] server.ReturnCode = Any(*rc) if isinstance(rc, list) else rc + # Configure server log validation if specified. + server_log_validation = server_config.get('log_validation', {}) + if server_log_validation: + traffic_out = server_log_validation.get('traffic_out', {}) + for contains_entry in traffic_out.get('contains', []): + expression = contains_entry['expression'] + description = contains_entry.get('description', f'Verify server traffic_out contains: {expression}') + server.Streams.All += Testers.ContainsExpression(expression, description) + for excludes_entry in traffic_out.get('excludes', []): + expression = excludes_entry['expression'] + description = excludes_entry.get('description', f'Verify server traffic_out excludes: {expression}') + server.Streams.All += Testers.ExcludesExpression(expression, description) + # ATS configuration. if not 'ats' in autest_config: raise ValueError(f"Replay file {replay_file} does not contain 'autest.ats' section") diff --git a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml index 07a39961590..4b476406bd4 100644 --- a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml +++ b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml @@ -26,11 +26,18 @@ autest: server: name: 'server' + process_config: + other_args: '--verbose diag' + log_validation: + traffic_out: + excludes: + - expression: "FILTER_BODY_BLOCK_CANARY" + description: "Verify blocked request body never reached the origin" client: name: 'client' - # Response body blocking truncates the body, causing a Content-Length - # mismatch that proxy-verifier may report as an error (exit code 1). + # There's a race condition for when the connection is dropped by ATS - this + # can result in either a 0 or 1 return code. return_code: [0, 1] ats: @@ -185,10 +192,10 @@ sessions: fields: - [Host, request-block.example.com] - [Content-Type, "application/xml+plus_other_stuff"] - - [Content-Length, 49] + - [Content-Length, 74] - [uuid, request-block-test] content: - data: '' + data: 'FILTER_BODY_BLOCK_CANARY ' server-response: status: 200 From 784ce8431038966689b1d084b59eeff9e476a423 Mon Sep 17 00:00:00 2001 From: Bryan Call Date: Wed, 18 Feb 2026 16:29:08 -0800 Subject: [PATCH 4/4] Address review: remove traffic_out node, add client log validation Remove the intermediate traffic_out node from server log_validation to avoid confusion with the ATS traffic.out file. Server and client log_validation now use contains/excludes directly. Add client log_validation support to ats_replay.test.ext so replay files can verify client-side verifier output. --- .../autest-site/ats_replay.test.ext | 21 ++++++++++++++----- .../filter_body/filter_body.replay.yaml | 7 +++---- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/gold_tests/autest-site/ats_replay.test.ext b/tests/gold_tests/autest-site/ats_replay.test.ext index a0f5bd53215..c041c427705 100644 --- a/tests/gold_tests/autest-site/ats_replay.test.ext +++ b/tests/gold_tests/autest-site/ats_replay.test.ext @@ -181,14 +181,13 @@ def ATSReplayTest(obj, replay_file: str): # Configure server log validation if specified. server_log_validation = server_config.get('log_validation', {}) if server_log_validation: - traffic_out = server_log_validation.get('traffic_out', {}) - for contains_entry in traffic_out.get('contains', []): + for contains_entry in server_log_validation.get('contains', []): expression = contains_entry['expression'] - description = contains_entry.get('description', f'Verify server traffic_out contains: {expression}') + description = contains_entry.get('description', f'Verify server output contains: {expression}') server.Streams.All += Testers.ContainsExpression(expression, description) - for excludes_entry in traffic_out.get('excludes', []): + for excludes_entry in server_log_validation.get('excludes', []): expression = excludes_entry['expression'] - description = excludes_entry.get('description', f'Verify server traffic_out excludes: {expression}') + description = excludes_entry.get('description', f'Verify server output excludes: {expression}') server.Streams.All += Testers.ExcludesExpression(expression, description) # ATS configuration. @@ -214,6 +213,18 @@ def ATSReplayTest(obj, replay_file: str): rc = client_config['return_code'] client.ReturnCode = Any(*rc) if isinstance(rc, list) else rc + # Configure client log validation if specified. + client_log_validation = client_config.get('log_validation', {}) + if client_log_validation: + for contains_entry in client_log_validation.get('contains', []): + expression = contains_entry['expression'] + description = contains_entry.get('description', f'Verify client output contains: {expression}') + client.Streams.All += Testers.ContainsExpression(expression, description) + for excludes_entry in client_log_validation.get('excludes', []): + expression = excludes_entry['expression'] + description = excludes_entry.get('description', f'Verify client output excludes: {expression}') + client.Streams.All += Testers.ExcludesExpression(expression, description) + if dns: ts.StartBefore(dns) ts.StartBefore(server) diff --git a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml index 4b476406bd4..5cb4f8bb24d 100644 --- a/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml +++ b/tests/gold_tests/pluginTest/filter_body/filter_body.replay.yaml @@ -29,10 +29,9 @@ autest: process_config: other_args: '--verbose diag' log_validation: - traffic_out: - excludes: - - expression: "FILTER_BODY_BLOCK_CANARY" - description: "Verify blocked request body never reached the origin" + excludes: + - expression: "FILTER_BODY_BLOCK_CANARY" + description: "Verify blocked request body never reached the origin" client: name: 'client'