Project

General

Profile

Actions

Bug #8377

open
AD SS

OPNsense Suricata divert-to mode causes aborted HTTPS response streams (Axios / Node.js) – works with responseType: stream

Bug #8377: OPNsense Suricata divert-to mode causes aborted HTTPS response streams (Axios / Node.js) – works with responseType: stream

Added by ABC DEF 6 months ago. Updated about 1 hour ago.

Status:
In Progress
Priority:
Normal
Target version:
Affected Versions:
Effort:
Difficulty:
Label:

Description

Describe the bug

When using Suricata with the new divert-to mode in OPNsense, some HTTPS requests made with Node.js (Axios) fail with response stream aborted.

The same request works normally when:

  • `divert-to` is disabled (`none`)
  • Axios is configured with `responseType: "stream"`
  • requesting a static file (e.g. `/robots.txt`) instead of an HTML page

Issue in OPNsense: https://github.com/opnsense/core/issues/9956

Network Setup

Cloud VM (uptime-kuma)

Internet

OPNsense (Suricata IPS enabled, divert-to mode)

Reverse Proxy

Backend Application

To Reproduce

The request is sent from an external cloud VM to a public service behind OPNsense.

  1. Deploy a service behind OPNsense that is reachable via HTTPS through a reverse proxy.
  2. Enable Suricata IPS on the WAN interface using divert-to mode.
  3. From an external system (e.g. cloud VM), run the following Node.js request:
node -e ' const axios = require("axios"); const https = require("https"); const http = require("http"); const httpsAgent = new https.Agent({ rejectUnauthorized: true, maxCachedSessions: 0 }); const httpAgent = new http.Agent({ maxCachedSessions: 0 }); axios({ url: "https://example.com", method: "get", timeout: 10000, maxRedirects: 10, httpAgent, httpsAgent, headers: { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } }) .then(res => { console.log("STATUS", res.status); }) .catch(err => { console.error("ERROR", err.message); }); '

Output: ERROR response stream aborted

Suricata disabled

When i set divert-to to None it works as expected:

node -e ' const axios = require("axios"); const https = require("https"); const http = require("http"); const httpsAgent = new https.Agent({ rejectUnauthorized: true, maxCachedSessions: 0 }); const httpAgent = new http.Agent({ maxCachedSessions: 0 }); axios({ url: "https://example.com", method: "get", timeout: 10000, maxRedirects: 10, httpAgent, httpsAgent, headers: { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } }) .then(res => { console.log("STATUS", res.status); }) .catch(err => { console.error("ERROR", err.message); }); '

Output: STATUS 200

Response Type: stream

If set 'responseType: "stream",' the request works as expected even with suricata divert-to enabled:

node -e ' const axios = require("axios"); const https = require("https"); const http = require("http"); const httpsAgent = new https.Agent({ rejectUnauthorized: true, maxCachedSessions: 0 }); const httpAgent = new http.Agent({ maxCachedSessions: 0 }); axios({ url: "https://example.com", method: "get", responseType: "stream", timeout: 10000, maxRedirects: 10, httpAgent, httpsAgent, headers: { "A
ccept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } }) .then(res => { console.log("STATUS", res.status); }) .catch(err => { console.error("ERROR", err.message); }); '

Output: STATUS 200

Expected behavior

I would expect STATUS 200 with suricata divert-to enabled.

Relevant log files

There are no relevant logs and no alerts.

Environment

  • OPNsense version: 26.1.4
  • Suricata version: 8.0.3
  • Client: Node.js + Axios
  • Test system: containerized uptime-kuma

VJ Updated by Victor Julien 6 months ago Actions #1

Can you share any Suricata logs generated for this traffic? Would also be good record a pcap to see if that gives any clues.

AD Updated by ABC DEF 6 months ago Actions #2

Hello Victor,

I sent you the requested packet captures via email (I don’t want to share them publicly). I’m not sure which Suricata logs might be helpful. As far as I could see when looking through them, there wasn’t really anything that stood out or seems related to this request.

AD Updated by ABC DEF 6 months ago Actions #3

This issue only occurs when divert-to is used on a WAN-side pf rule, i.e., when the passing traffic is still encrypted.
If I add the divert-to setting to the rule from the reverse proxy to the web server (where traffic is unencrypted) instead, the request works correctly and the eve logging shows:

{"timestamp":"2026-03-14T09:47:41.161486+0100","flow_id":1411451134062175,"event_type":"http","src_ip":"ip.from.reverse.proxy","src_port":54814,"dest_ip":"ip.from.web.server","dest_port":8080,"proto":"TCP","ip_v":4,"pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"example.com","url":"/","http_user_agent":"axios/0.30.3","xff":"::ffff:ip.from.requesting.host","http_content_type":"text/html","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":123929,"request_headers":[{"name":"accept","value":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},{"name":"user-agent","value":"axios/0.30.3"},{"name":"host","value":"example.com"},{"name":"x-forwarded-proto","value":"https"},{"name":"x-forwarded-for","value":"::ffff:ip.from.requesting.host"}],"response_headers":[{"name":"Date","value":"Sat, 14 Mar 2026 08:47:41 GMT"},{"name":"Server","value":"Apache/2.4.66 (Debian)"},{"name":"X-Powered-By","value":"PHP/8.3.30"},{"name":"Link","value":"<https://example.com/wp-json/>; rel=\"https://api.w.org/\""},{"name":"Vary","value":"Accept-Encoding"},{"name":"Transfer-Encoding","value":"chunked"},{"name":"Content-Type","value":"text/html; charset=UTF-8"}]}}
{"timestamp":"2026-03-14T09:47:48.431785+0100","flow_id":1133209534922269,"event_type":"http","src_ip":"ip.from.reverse.proxy","src_port":35892,"dest_ip":"ip.from.web.server","dest_port":8080,"proto":"TCP","ip_v":4,"pkt_src":"wire/pcap","tx_id":0,"http":{"hostname":"example.com","url":"/robots.txt","http_user_agent":"Uptime-Kuma/2.2.1","xff":"::ffff:ip.from.requesting.host","http_content_type":"text/plain","http_method":"GET","protocol":"HTTP/1.1","status":200,"length":168,"request_headers":[{"name":"accept","value":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"},{"name":"user-agent","value":"Uptime-Kuma/2.2.1"},{"name":"host","value":"example.com"},{"name":"x-forwarded-proto","value":"https"},{"name":"x-forwarded-for","value":"::ffff:ip.from.requesting.host"}],"response_headers":[{"name":"Date","value":"Sat, 14 Mar 2026 08:47:48 GMT"},{"name":"Server","value":"Apache/2.4.66 (Debian)"},{"name":"X-Powered-By","value":"PHP/8.3.30"},{"name":"X-Robots-Tag","value":"noindex, follow"},{"name":"Link","value":"<https://example.com/wp-json/>; rel=\"https://api.w.org/\""},{"name":"Vary","value":"Accept-Encoding"},{"name":"Content-Length","value":"168"},{"name":"Content-Type","value":"text/plain; charset=utf-8"}]}}

SS Updated by Samaresh Kumar singh 4 months ago Actions #4

  • Assignee set to Samaresh Kumar singh

VJ Updated by Victor Julien 4 months ago Actions #5

  • Status changed from New to In Review

https://github.com/OISF/suricata/pull/15396

@asfsdfsdf are you able to test this PR?

JI Updated by Jason Ish 14 days ago Actions #6

The PR above puts sendto into a loop, however its not confirmed if that is the actual issue. If sendto was failing, Suricata should log a warning:

SCLogWarning("Write to ipfw divert socket failed: %s", strerror(r));

Another theory is that the socket buffer, for receiving packets into Suricata is not large enough for bursts of traffic, which I think defaults just a bit over 64k.

JI Updated by Jason Ish 13 days ago Actions #7

I gave my robot a Proxmox lab to deploy and test OPNsense end-to-end. It confirmed my suspicion that sendto is not the issue, however still needs further review.

On OPNsense, PF RDR/NAT and the FreeBSD divert path can present the two directions of one TCP connection to Suricata under different 5-tuples. The request is seen with the translated internal destination, while the response is seen with the public address as its source. The divert API does not provide the original PF tuple or a PF state identifier, so Suricata cannot safely associate the packets as one bidirectional flow. The response therefore reaches the TCP stream engine as a one-sided flow without handshake or session state.

A sessionless ACK packet containing payload is normally allowed, but adding FIN to that same packet sends it through the fin_but_no_session error path. In inline mode with stream.drop-invalid enabled, Suricata does not reinject the packet, truncating the response when the final body data and FIN are coalesced. This was reproduced with the untouched OPNsense configuration. Direct routing without NAT passed, PF RDR passed when diversion was disabled, and enabling midstream with async-oneside did not resolve the failure.

The smallest tested fix retains the fin_but_no_session event but avoids treating the packet as a stream error when it has ACK, contains payload, has no SYN, and differs from an otherwise accepted sessionless data packet only by also carrying FIN. Pure FIN, SYN+FIN, and RST packets remain on the existing error path. The change passed a base-failure -> candidate-success -> base-failure -> candidate-success sequence on OPNsense.

diff --git a/src/stream-tcp.c b/src/stream-tcp.c
index 47ca5a7bd..7245a4f73 100644
--- a/src/stream-tcp.c
+++ b/src/stream-tcp.c
@@ -1209,6 +1209,15 @@ static int StreamTcpPacketStateNone(
         if (!stream_config.midstream || p->payload_len == 0) {
             StreamTcpSetEvent(p, STREAM_FIN_BUT_NO_SESSION);
             SCLogDebug("FIN packet received, no session setup");
+
+            /* A no-session ACK carrying payload is already allowed through this state. In inline
+             * mode, do not turn the same untracked payload into a stream-error drop solely because
+             * it also closes the sender side. Keep pure FIN and SYN+FIN packets on the existing
+             * error path. */
+            if (!stream_config.midstream && StreamTcpInlineDropInvalid() &&
+                    (tcph->th_flags & TH_ACK) && !(tcph->th_flags & TH_SYN) && p->payload_len > 0) {
+                return 0;
+            }
             return -1;
         }

PA Updated by Philippe Antoine about 1 hour ago Actions #8

  • Status changed from In Review to In Progress
Actions

Also available in: PDF Atom