Update on ICAP integration proposal: Working implementation of SSLproxy (icap branch) & icapsuricata (libsuricata service)

Hi All,

Following up on my ICAP integration proposal from earlier this year—specifically the architectural shift discussed in this topic on the Suricata forum—I wanted to share a major update regarding working software implementations for both components.

Over the last few months, I have moved this from a theoretical proposal to working prototypes proving the validity of the ICAP model for inline decryption and deep inspection.

Here is the current status:

  1. SSLproxy (icap branch): The icap branch of SSLproxy now features a full, functional ICAP client implementation. It has been validated against standard ecosystem services including c-icap (echo/ex206), squidclamav, E2Guardian, and my newly developed custom Suricata service plugin.
  2. icapsuricata (c-icap service module): To bridge the gap between the proxy layer and the engine, I have launched icapsuricata. This is a custom c-icap service module running Suricata in library mode (libsuricata). Rather than passive interface capture, it interacts inline directly with the proxy stream. The module processes c-icap data blocks, dynamically constructs sequentially aligned, in-memory TCP/IP packet streams, and injects them straight into libsuricata for real-time app-layer tracking and signature detection. To ensure high performance and zero heap memory fragmentation, it utilizes a custom single-writer, dual-reader circular ring buffer.

Please note that both codebases are actively in a Work-In-Progress (WIP) phase and are intended as architectural proofs-of-concept rather than production-ready artifacts. However, they serve as a concrete, working implementation of the proposed ICAP paradigm.

I would highly value your technical feedback, comments, or suggestions on this approach.

Best,
Soner

Very cool @Soner_Tari :slight_smile:

This should also help us more properly define a public libsuricata API. Are there any clear pain points in the libsuricata handling so far?

Thanks Victor for the reply.

Overall, my experience developing icapsuricata against libsuricata has been very positive. The integration was smooth, and the library mode behaves predictably. That said, since this is a fresh proof-of-concept and hasn’t faced high-throughput production stress yet, my feedback should be taken with a grain of salt.

To answer your question regarding pain points or structural hurdles, the most notable observation relates to data ingestion styles:

The Packet Emulation Requirement vs. Stream Ingestion
When I first approached libsuricata for purely inline content inspection (rather than L3/L4 network anomaly detection), I was initially hoping I could bypass low-level packet emulation entirely and feed raw application-layer proxy stream chunks straight into the engine.

However, because Suricata’s stream reassembly, protocol parsing, and detection matrices are tightly coupled to the state transitions of the TCP engine, packet emulation turned out to be strictly necessary. To keep StreamTcp aligned, I have to synthesize IPv4/TCP headers and manually manage sequence/acknowledgment tracking spaces.

Furthermore, I discovered that to prevent app-layer blindspots (due to internal optimizations like skipping frame inspection on non-state-changing payload updates), I have to systematically inject synthetic cross-direction ACK packets mid-stream (currently 4KB chunks) to act as evaluation/flushing triggers, alongside a final sequence-incremented FIN|ACK sweep to cleanly finalize the flow.

While this requires some emulation overhead, I realized it is an inherent requirement of the current architecture. It ensures that Suricata can perfectly handle stream reassembly, especially when signature payloads (like an HTTP response body match) happen to cross the boundaries of multiple contiguous ICAP chunks.

If a future public libsuricata API eventually exposes a purely stream-oriented or transaction-oriented ingestion interface (bypassing the need to fake a wire tap), it would be a massive win for proxy integrations. But the current packet-level injection is absolutely viable.

As a quick side note regarding configuration management: I noticed the ongoing forum discussions about libsuricata config handling, but I haven’t yet implemented or tested live-reloading the Yaml configuration or dynamically updating IDS rules from within icapsuricata. For now, it’s a static load at initialization, so I don’t have any feedback on that front just yet.

Next Steps & Deep Testing Challenges
The next items on my roadmap will provide a much more thorough test of libsuricata’s boundary limits:

  • Ecosystem Integration: I am currently integrating the SSLproxy + icapsuricata combination into my UTM firewall project (UTMFW) for extensive, multi-client HTTP/1 verification.
  • Service Chaining: I plan to test SSLproxy running multiple concurrent ICAP services in series (such as passing traffic through E2Guardian and icapsuricata sequentially).
  • The HTTP/2 and HTTP/3 Frontier: This will be the real trial. Extending packet emulation and stream tracking to accommodate HTTP/2 multiplexed streams and HTTP/3 QUIC frames within an asynchronous ICAP loop is going to be complex. Seeing how libsuricata’s app-layer parsers coordinate with emulated network frames under heavy stream multiplexing will provide excellent data for refining the public API.
  • Protocol Expansion: Down the road, I want to expand this past HTTP to see how other proxy-handled protocols adapt to this model.

I’d like to hear your thoughts on how the current engine expects stream flushes, or if there are cleaner ways to signal transaction progress through libsuricata without faking many ACKs!

Quick reply for now: the ACK flushes should only be needed in the (default) IDS mode of stream operations. I think for this usecase the stream engine should be put in IPS/inline mode. In that case segments are processed immediately w/o a wait for an ACK.

See


bool StreamTcpInlineMode(void)
{   
    return (stream_config.flags & STREAMTCP_INIT_FLAG_INLINE); 
}           

Some other places may use EngineModeIsIPS.

First API issue with libsuricata: When including <suricata/stream-tcp.h> (to call StreamTcpInlineMode()) alongside standard system networking headers (or frameworks like c-icap that pull them in implicitly), the compiler throws hard redeclaration errors.

Specifically, the TcpState enum in stream-tcp-private.h redefines globally scoped symbols that clash directly with standard definitions in <netinet/tcp.h>:

/* Conflicts with /usr/include/netinet/tcp.h */
enum TcpState {
    TCP_NONE = 0,
    // TCP_LISTEN = 1,
    TCP_SYN_SENT = 2,
    TCP_SYN_RECV = 3,
    TCP_ESTABLISHED = 4,
    ...
};

Because these are exposed in the flat namespace, it prevents external proxy or server applications from compiling cleanly if they manage network sockets directly.

As a temporary workaround in icapsuricata, I’ve managed to bypass this by sandboxing the header inclusion with preprocessor macro masking right before pulling in the Suricata headers:

// Temporarily rename conflicting symbols to protect them from netinet/tcp.h
#define TCP_SYN_SENT    SURI_TCP_SYN_SENT
#define TCP_SYN_RECV    SURI_TCP_SYN_RECV
#define TCP_ESTABLISHED SURI_TCP_ESTABLISHED
#define TCP_FIN_WAIT1   SURI_TCP_FIN_WAIT1
#define TCP_FIN_WAIT2   SURI_TCP_FIN_WAIT2
#define TCP_TIME_WAIT   SURI_TCP_TIME_WAIT
#define TCP_LAST_ACK    SURI_TCP_LAST_ACK
#define TCP_CLOSE_WAIT  SURI_TCP_CLOSE_WAIT
#define TCP_CLOSING     SURI_TCP_CLOSING
#define TCP_CLOSED      SURI_TCP_CLOSED

#include <suricata/stream-tcp.h>

// Restore standard definitions for the rest of the codebase, not used in icapsuricata
#undef TCP_SYN_SENT
#undef TCP_SYN_RECV
#undef TCP_ESTABLISHED
#undef TCP_FIN_WAIT1
#undef TCP_FIN_WAIT2
#undef TCP_TIME_WAIT
#undef TCP_LAST_ACK
#undef TCP_CLOSE_WAIT
#undef TCP_CLOSING
#undef TCP_CLOSED

Moving forward with a public libsuricata API definition, it would be amazing if internal state enums like this were either hidden entirely from the public-facing headers, prefixed (e.g., SURICATA_TCP_ESTABLISHED), or guarded with an #ifndef _NETINET_TCP_H check to ensure frictionless embedding! (P.S.: I have Suricata 8.0.4)

Thanks Victor for pointing me to StreamTcpInlineMode().

I have just pushed the changes:

Do not ACK flush in inline mode and add ACKwindow config option

ACK flushing is not needed if Suricata is in inline mode. The user can
set inline mode in suricata.yaml.
Also, we let the user set the ACK window size via the new ACKwindow
config option (0-65535 bytes) in c-icap.conf. Setting ACKwindow to 0
disables ACK flushing in IDS mode too.

I would just make this hard coded. The main diff when dealing with packets is that in IDS mode we can still get retransmissions with different data that we have to account for. In IDS mode we try to handle that like the destination OS. In IPS we accept the first data and then rewrite packets on the write to match that if there is a retransmission with different data. I don’t think in your use case you’ll have to deal with this, as this is already handled by the OS? So I think just forcing it to use the inline mode is fine.

As the header suggests, this was meant to be private. We’ll have to see how it gets to be public anyway.